merge: game-perf (zero-copy game path)
82 files · +9455 -1931
M .mockery.yml +7 -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: | ||
| @@ -30,6 +30,11 @@ packages: | |||||
| 30 | dir: internal/adapters/wayland | 30 | dir: internal/adapters/wayland | ||
| 31 | pkgname: wayland | 31 | pkgname: wayland | ||
| 32 | filename: proc_env_mock_test.go | 32 | filename: proc_env_mock_test.go | ||
| 33 | + | syncobjDevice: | |||
| 34 | + | config: | |||
| 35 | + | dir: internal/adapters/wayland | |||
| 36 | + | pkgname: wayland | |||
| 37 | + | filename: syncobj_device_mock_test.go | |||
| 33 | github.com/bnema/nefertty/internal/adapters/statefile: | 38 | github.com/bnema/nefertty/internal/adapters/statefile: | ||
| 34 | interfaces: | 39 | interfaces: | ||
| 35 | procTree: | 40 | procTree: | ||
M README.md +3 -1
| @@ -6,8 +6,10 @@ Requires Linux and Go 1.27. | |||||
| 6 | 6 | ||||
| 7 | ## Hardware support | 7 | ## Hardware support | ||
| 8 | 8 | ||||
| 9 | + | NeferTTY needs a GPU and kernel driver with atomic KMS and a Vulkan 1.3 device that can export images as dmabufs; without them it stops at startup with an error. Frames are composed on the GPU only: no CPU copy and no CPU wait for the GPU before a page flip. | |||
| 10 | + | ||||
| 9 | - **AMD GPUs:** developed and tested on AMD (radv). | 11 | - **AMD GPUs:** developed and tested on AMD (radv). | ||
| 10 | - | - **NVIDIA GPUs:** untested. NVIDIA drivers need explicit sync (`linux-drm-syncobj`), which NeferTTY does not implement yet. Testers with NVIDIA hardware are welcome to report issues or contribute. | 12 | + | - **NVIDIA GPUs:** untested. Explicit sync (`linux-drm-syncobj`), which NVIDIA drivers need, is implemented. Testers with NVIDIA hardware are welcome to report issues or contribute. |
| 11 | - **Intel GPUs:** untested. | 13 | - **Intel GPUs:** untested. | ||
| 12 | 14 | ||||
| 13 | Build: `make build`. | 15 | Build: `make build`. | ||
M cmd/nefertty/main.go +1 -1
| @@ -119,7 +119,7 @@ func run() error { | |||||
| 119 | noTerminal := flags.Bool("no-terminal", false, "skip initial terminal") | 119 | noTerminal := flags.Bool("no-terminal", false, "skip initial terminal") | ||
| 120 | noXwayland := flags.Bool("no-xwayland", false, "no X11 display for X11 apps") | 120 | noXwayland := flags.Bool("no-xwayland", false, "no X11 display for X11 apps") | ||
| 121 | timeout := flags.Duration("timeout", 0, "duration before exit (0 disables timeout)") | 121 | timeout := flags.Duration("timeout", 0, "duration before exit (0 disables timeout)") | ||
| 122 | - | debugFlag := flags.String("debug", "", "debug components (comma-separated or all)") | 122 | + | debugFlag := flags.String("debug", "", "debug components (comma-separated or all; input-motion logs every pointer motion)") |
| 123 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | 123 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | ||
| 124 | session := flags.Bool("session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services") | 124 | session := flags.Bool("session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services") | ||
| 125 | if err := flags.Parse(os.Args[1:]); err != nil { | 125 | if err := flags.Parse(os.Args[1:]); err != nil { | ||
M go.mod +1 -1
| @@ -4,7 +4,7 @@ go 1.27 | |||||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/bnema/purego v0.11.0-bnema.4 | 6 | github.com/bnema/purego v0.11.0-bnema.4 | ||
| 7 | - | github.com/bnema/purego-libwayland v0.1.1 | 7 | + | github.com/bnema/purego-libwayland v0.2.0 |
| 8 | github.com/bnema/purego-vulkan v0.4.1 | 8 | github.com/bnema/purego-vulkan v0.4.1 | ||
| 9 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 10 | github.com/bnema/zerowrap v1.4.1 | 10 | github.com/bnema/zerowrap v1.4.1 | ||
M go.sum +2 -8 generated file: collapsed
A internal/adapters/drm/atomic.go +372 -0
| @@ -0,0 +1,372 @@ | |||||
| 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 { | |||
| 92 | + | id, possible uint32 | |||
| 93 | + | // formats is the plane's format list (GETPLANE): used, all linear, | |||
| 94 | + | // when the plane has no IN_FORMATS (drivers without modifiers). | |||
| 95 | + | formats []uint32 | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // atomicReq collects (object, property, value) triples in insertion order. | |||
| 99 | + | type atomicReq struct { | |||
| 100 | + | objs []uint32 | |||
| 101 | + | props [][]propValue | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | type propValue struct { | |||
| 105 | + | prop uint32 | |||
| 106 | + | val uint64 | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // set adds a property; unknown properties (id 0) are skipped. | |||
| 110 | + | func (r *atomicReq) set(obj, prop uint32, v uint64) { | |||
| 111 | + | if prop == 0 || obj == 0 { | |||
| 112 | + | return | |||
| 113 | + | } | |||
| 114 | + | i := slices.Index(r.objs, obj) | |||
| 115 | + | if i < 0 { | |||
| 116 | + | r.objs = append(r.objs, obj) | |||
| 117 | + | r.props = append(r.props, nil) | |||
| 118 | + | i = len(r.objs) - 1 | |||
| 119 | + | } | |||
| 120 | + | for j, p := range r.props[i] { | |||
| 121 | + | if p.prop == prop { | |||
| 122 | + | r.props[i][j].val = v | |||
| 123 | + | return | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | r.props[i] = append(r.props[i], propValue{prop, v}) | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // value returns a property set on an object. | |||
| 130 | + | func (r *atomicReq) value(obj, prop uint32) (uint64, bool) { | |||
| 131 | + | i := slices.Index(r.objs, obj) | |||
| 132 | + | if i < 0 { | |||
| 133 | + | return 0, false | |||
| 134 | + | } | |||
| 135 | + | for _, p := range r.props[i] { | |||
| 136 | + | if p.prop == prop { | |||
| 137 | + | return p.val, true | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | return 0, false | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // kmsDevice drives a card fd. | |||
| 144 | + | type kmsDevice struct { | |||
| 145 | + | fd int | |||
| 146 | + | // gemMu serialises GEM handle import and close: handles are shared by | |||
| 147 | + | // every import of a buffer on the fd, and outputs run on their own | |||
| 148 | + | // goroutines. modifiers is DRM_CAP_ADDFB2_MODIFIERS. | |||
| 149 | + | gemMu *sync.Mutex | |||
| 150 | + | modifiers bool | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | func (k kmsDevice) commit(req *atomicReq, flags uint32, userData uint64) error { | |||
| 154 | + | if len(req.objs) == 0 { | |||
| 155 | + | return errors.New("empty atomic commit") | |||
| 156 | + | } | |||
| 157 | + | counts := make([]uint32, len(req.objs)) | |||
| 158 | + | var props []uint32 | |||
| 159 | + | var vals []uint64 | |||
| 160 | + | for i, ps := range req.props { | |||
| 161 | + | counts[i] = uint32(len(ps)) | |||
| 162 | + | for _, p := range ps { | |||
| 163 | + | props = append(props, p.prop) | |||
| 164 | + | vals = append(vals, p.val) | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | a := modeAtomic{flags: flags, countObjs: uint32(len(req.objs)), userData: userData, | |||
| 168 | + | objs: uint64(uintptr(unsafe.Pointer(&req.objs[0]))), countProps: uint64(uintptr(unsafe.Pointer(&counts[0]))), | |||
| 169 | + | props: uint64(uintptr(unsafe.Pointer(&props[0]))), values: uint64(uintptr(unsafe.Pointer(&vals[0])))} | |||
| 170 | + | err := ioctl(k.fd, ioctlAtomic, unsafe.Pointer(&a)) | |||
| 171 | + | runtime.KeepAlive(req) | |||
| 172 | + | runtime.KeepAlive(counts) | |||
| 173 | + | runtime.KeepAlive(props) | |||
| 174 | + | runtime.KeepAlive(vals) | |||
| 175 | + | return err | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | func (k kmsDevice) createBlob(data []byte) (uint32, error) { | |||
| 179 | + | b := createBlob{data: uint64(uintptr(unsafe.Pointer(&data[0]))), length: uint32(len(data))} | |||
| 180 | + | err := ioctl(k.fd, ioctlCreateBlob, unsafe.Pointer(&b)) | |||
| 181 | + | runtime.KeepAlive(data) | |||
| 182 | + | return b.blobID, err | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func (k kmsDevice) destroyBlob(id uint32) error { | |||
| 186 | + | v := id | |||
| 187 | + | return ioctl(k.fd, ioctlDestroyBlob, unsafe.Pointer(&v)) | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | func (k kmsDevice) getBlob(id uint32) ([]byte, error) { | |||
| 191 | + | b := getBlob{blobID: id} | |||
| 192 | + | if err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)); err != nil { | |||
| 193 | + | return nil, err | |||
| 194 | + | } | |||
| 195 | + | if b.length == 0 { | |||
| 196 | + | return nil, nil | |||
| 197 | + | } | |||
| 198 | + | data := make([]byte, b.length) | |||
| 199 | + | b.data = uint64(uintptr(unsafe.Pointer(&data[0]))) | |||
| 200 | + | err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)) | |||
| 201 | + | runtime.KeepAlive(data) | |||
| 202 | + | return data[:min(int(b.length), len(data))], err | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | func (k kmsDevice) planes() ([]planeRes, error) { | |||
| 206 | + | var r getPlaneRes | |||
| 207 | + | if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil { | |||
| 208 | + | return nil, err | |||
| 209 | + | } | |||
| 210 | + | if r.count == 0 { | |||
| 211 | + | return nil, nil | |||
| 212 | + | } | |||
| 213 | + | ids := make([]uint32, r.count) | |||
| 214 | + | r.planeIDs = uint64(uintptr(unsafe.Pointer(&ids[0]))) | |||
| 215 | + | if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil { | |||
| 216 | + | return nil, err | |||
| 217 | + | } | |||
| 218 | + | runtime.KeepAlive(ids) | |||
| 219 | + | var out []planeRes | |||
| 220 | + | for _, id := range ids[:min(int(r.count), len(ids))] { | |||
| 221 | + | p := getPlane{planeID: id} | |||
| 222 | + | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil { | |||
| 223 | + | continue | |||
| 224 | + | } | |||
| 225 | + | res := planeRes{id: id, possible: p.possibleCrtcs} | |||
| 226 | + | if p.countFormats > 0 { | |||
| 227 | + | fs := make([]uint32, p.countFormats) | |||
| 228 | + | p.formats = uint64(uintptr(unsafe.Pointer(&fs[0]))) | |||
| 229 | + | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) == nil { | |||
| 230 | + | res.formats = fs[:min(int(p.countFormats), len(fs))] | |||
| 231 | + | } | |||
| 232 | + | runtime.KeepAlive(fs) | |||
| 233 | + | } | |||
| 234 | + | out = append(out, res) | |||
| 235 | + | } | |||
| 236 | + | return out, nil | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | func (k kmsDevice) objProps(obj, typ uint32) (map[string][2]uint64, error) { | |||
| 240 | + | return objProps(k.fd, obj, typ) | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | func (k kmsDevice) rmFB(id uint32) error { | |||
| 244 | + | v := id | |||
| 245 | + | return ioctl(k.fd, ioctlRmFB, unsafe.Pointer(&v)) | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | // enableAtomic turns on atomic modesetting and checks the caps outputs rely on. | |||
| 249 | + | func enableAtomic(fd int) error { | |||
| 250 | + | for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} { | |||
| 251 | + | v := setClientCap{capability: c, value: 1} | |||
| 252 | + | if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil { | |||
| 253 | + | return err | |||
| 254 | + | } | |||
| 255 | + | } | |||
| 256 | + | if !hasCap(fd, capTimestampMonotonic) { | |||
| 257 | + | return errors.New("flip timestamps are not CLOCK_MONOTONIC") | |||
| 258 | + | } | |||
| 259 | + | return nil | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | func hasCap(fd int, c uint64) bool { | |||
| 263 | + | v := getCap{capability: c} | |||
| 264 | + | return ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value == 1 | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // plane is a KMS plane an output drives, with its property IDs by name. | |||
| 268 | + | type plane struct { | |||
| 269 | + | id, typ uint32 | |||
| 270 | + | props map[string]uint32 | |||
| 271 | + | crtc uint32 // CRTC_ID when read | |||
| 272 | + | formats []ports.DMABufFormat | |||
| 273 | + | // zposValue is the plane's zpos when read (its "zpos" property). | |||
| 274 | + | zposValue uint64 | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | func (p *plane) prop(name string) uint32 { | |||
| 278 | + | if p == nil { | |||
| 279 | + | return 0 | |||
| 280 | + | } | |||
| 281 | + | return p.props[name] | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | // readPlanes returns the planes that can show on the CRTC at index pipe. | |||
| 285 | + | func readPlanes(k kms, pipe int) ([]*plane, error) { | |||
| 286 | + | res, err := k.planes() | |||
| 287 | + | if err != nil { | |||
| 288 | + | return nil, err | |||
| 289 | + | } | |||
| 290 | + | var out []*plane | |||
| 291 | + | for _, r := range res { | |||
| 292 | + | if r.possible&(1<<pipe) == 0 { | |||
| 293 | + | continue | |||
| 294 | + | } | |||
| 295 | + | props, err := k.objProps(r.id, objPlane) | |||
| 296 | + | if err != nil { | |||
| 297 | + | continue | |||
| 298 | + | } | |||
| 299 | + | p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1]), zposValue: props["zpos"][1]} | |||
| 300 | + | for name, v := range props { | |||
| 301 | + | p.props[name] = uint32(v[0]) | |||
| 302 | + | } | |||
| 303 | + | if blob := uint32(props["IN_FORMATS"][1]); blob != 0 { | |||
| 304 | + | if data, err := k.getBlob(blob); err == nil { | |||
| 305 | + | p.formats = parseInFormats(data) | |||
| 306 | + | } | |||
| 307 | + | } | |||
| 308 | + | if p.formats == nil { | |||
| 309 | + | for _, f := range r.formats { | |||
| 310 | + | p.formats = append(p.formats, ports.DMABufFormat{Format: f}) | |||
| 311 | + | } | |||
| 312 | + | } | |||
| 313 | + | out = append(out, p) | |||
| 314 | + | } | |||
| 315 | + | return out, nil | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // parseInFormats decodes an IN_FORMATS blob (struct drm_format_modifier_blob) | |||
| 319 | + | // into format and modifier pairs. | |||
| 320 | + | func parseInFormats(b []byte) []ports.DMABufFormat { | |||
| 321 | + | if len(b) < 24 { | |||
| 322 | + | return nil | |||
| 323 | + | } | |||
| 324 | + | le := binary.LittleEndian | |||
| 325 | + | nf, fo := int(le.Uint32(b[8:])), int(le.Uint32(b[12:])) | |||
| 326 | + | nm, mo := int(le.Uint32(b[16:])), int(le.Uint32(b[20:])) | |||
| 327 | + | if fo < 0 || fo+nf*4 > len(b) || mo < 0 || mo+nm*24 > len(b) { | |||
| 328 | + | return nil | |||
| 329 | + | } | |||
| 330 | + | var out []ports.DMABufFormat | |||
| 331 | + | for m := range nm { | |||
| 332 | + | e := b[mo+m*24:] | |||
| 333 | + | mask, offset, mod := le.Uint64(e), int(le.Uint32(e[8:])), le.Uint64(e[16:]) | |||
| 334 | + | for bit := range 64 { | |||
| 335 | + | i := offset + bit | |||
| 336 | + | if mask&(1<<bit) == 0 || i >= nf { | |||
| 337 | + | continue | |||
| 338 | + | } | |||
| 339 | + | out = append(out, ports.DMABufFormat{Format: le.Uint32(b[fo+i*4:]), Modifier: mod}) | |||
| 340 | + | } | |||
| 341 | + | } | |||
| 342 | + | return out | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | // flipEvent is a completed commit on a CRTC: when is its CLOCK_MONOTONIC | |||
| 346 | + | // time, seq the CRTC's vblank counter, user the commit's userData. | |||
| 347 | + | type flipEvent struct { | |||
| 348 | + | crtc uint32 | |||
| 349 | + | user uint64 | |||
| 350 | + | when time.Duration | |||
| 351 | + | seq uint32 | |||
| 352 | + | } | |||
| 353 | + | ||||
| 354 | + | // parseFlips parses DRM events read from the card fd. | |||
| 355 | + | func parseFlips(buf []byte) []flipEvent { | |||
| 356 | + | var out []flipEvent | |||
| 357 | + | le := binary.LittleEndian | |||
| 358 | + | for len(buf) >= 8 { | |||
| 359 | + | typ := le.Uint32(buf) | |||
| 360 | + | length := int(le.Uint32(buf[4:])) | |||
| 361 | + | if length < 8 || length > len(buf) { | |||
| 362 | + | break | |||
| 363 | + | } | |||
| 364 | + | // struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id. | |||
| 365 | + | if typ == eventFlipDone && length >= 32 { | |||
| 366 | + | sec, usec := le.Uint32(buf[16:]), le.Uint32(buf[20:]) | |||
| 367 | + | 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:])}) | |||
| 368 | + | } | |||
| 369 | + | buf = buf[length:] | |||
| 370 | + | } | |||
| 371 | + | return out | |||
| 372 | + | } | |||
M internal/adapters/drm/card.go +63 -27
| @@ -6,15 +6,16 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "slices" | 7 | "slices" | ||
| 8 | "sync" | 8 | "sync" | ||
| 9 | - | "unsafe" | 9 | + | "sync/atomic" |
| 10 | 10 | ||||
| 11 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | "github.com/bnema/zerowrap" | 12 | "github.com/bnema/zerowrap" | ||
| 12 | "golang.org/x/sys/unix" | 13 | "golang.org/x/sys/unix" | ||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| 15 | // Card is one DRM device and the outputs it drives. Scan finds connected | 16 | // Card is one DRM device and the outputs it drives. Scan finds connected | ||
| 16 | - | // connectors; each gets its own Output (CRTC, buffers, cursor) run by one | 17 | + | // 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 | 18 | + | // goroutine. One reader goroutine reads commit events for all of them |
| 18 | // and hands each to its output by CRTC. | 19 | // and hands each to its output by CRTC. | ||
| 19 | type Card struct { | 20 | type Card struct { | ||
| 20 | fd int | 21 | fd int | ||
| @@ -24,35 +25,54 @@ type Card struct { | |||||
| 24 | crtcs []uint32 | 25 | crtcs []uint32 | ||
| 25 | // outputs by connector name; owned by the goroutine that calls Scan. | 26 | // outputs by connector name; owned by the goroutine that calls Scan. | ||
| 26 | outputs map[string]*Output | 27 | outputs map[string]*Output | ||
| 27 | - | flips map[uint32]chan int // by CRTC; set before the reader starts | 28 | + | 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 | 29 | + | k kmsDevice |
| 29 | - | // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS. | 30 | + | // taken are planes driven by an output, by plane ID. |
| 30 | - | gemMu sync.Mutex | 31 | + | taken map[uint32]bool |
| 31 | - | modifiers bool | 32 | + | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. |
| 32 | - | // async is DRM_CAP_ASYNC_PAGE_FLIP. | |||
| 33 | async bool | 33 | async bool | ||
| 34 | + | // serials numbers the commits of every output (event userData). | |||
| 35 | + | serials atomic.Uint64 | |||
| 36 | + | // formats receives each output's direct scanout formats (nil: none | |||
| 37 | + | // sent); set before the first Scan. | |||
| 38 | + | formats chan<- ports.OutputFormats | |||
| 34 | } | 39 | } | ||
| 35 | 40 | ||||
| 36 | - | // OpenCard reads the card's CRTCs. fd stays owned by the caller. | 41 | + | // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd |
| 42 | + | // 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) { | 43 | func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) { | ||
| 44 | + | if err := enableAtomic(fd); err != nil { | |||
| 45 | + | return nil, fmt.Errorf("drm: atomic modesetting unsupported by %s: %w", path, err) | |||
| 46 | + | } | |||
| 38 | crtcs, _, err := resources(fd) | 47 | crtcs, _, err := resources(fd) | ||
| 39 | if err != nil { | 48 | if err != nil { | ||
| 40 | return nil, fmt.Errorf("%s: %w", path, err) | 49 | return nil, fmt.Errorf("%s: %w", path, err) | ||
| 41 | } | 50 | } | ||
| 42 | - | flips := make(map[uint32]chan int, len(crtcs)) | 51 | + | flips := make(map[uint32]chan flipEvent, len(crtcs)) |
| 43 | for _, c := range crtcs { | 52 | for _, c := range crtcs { | ||
| 44 | - | flips[c] = make(chan int, 4) | 53 | + | flips[c] = make(chan flipEvent, 4) |
| 45 | } | 54 | } | ||
| 46 | - | cp := getCap{capability: capAddFB2Modifiers} | 55 | + | k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)} |
| 47 | - | mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1 | 56 | + | return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil |
| 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 | } | 57 | } | ||
| 52 | 58 | ||||
| 53 | // Path is the device path, e.g. /dev/dri/card1. | 59 | // Path is the device path, e.g. /dev/dri/card1. | ||
| 54 | func (c *Card) Path() string { return c.path } | 60 | func (c *Card) Path() string { return c.path } | ||
| 55 | 61 | ||||
| 62 | + | // Device is the card's dev_t, the KMS device clients allocate scanout | |||
| 63 | + | // buffers for (0 when unknown). | |||
| 64 | + | func (c *Card) Device() uint64 { | |||
| 65 | + | var st unix.Stat_t | |||
| 66 | + | if unix.Fstat(c.fd, &st) != nil { | |||
| 67 | + | return 0 | |||
| 68 | + | } | |||
| 69 | + | return st.Rdev | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // SetFormats sets where outputs report their direct scanout formats. | |||
| 73 | + | // Call before the first Scan. | |||
| 74 | + | func (c *Card) SetFormats(ch chan<- ports.OutputFormats) { c.formats = ch } | |||
| 75 | + | ||||
| 56 | // SetWant replaces the connector config; the next Scan applies it. | 76 | // SetWant replaces the connector config; the next Scan applies it. | ||
| 57 | func (c *Card) SetWant(w Want) { c.want = w } | 77 | func (c *Card) SetWant(w Want) { c.want = w } | ||
| 58 | 78 | ||||
| @@ -103,9 +123,16 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) { | |||||
| 103 | return added, removed, replaced, nil | 123 | return added, removed, replaced, nil | ||
| 104 | } | 124 | } | ||
| 105 | 125 | ||||
| 106 | - | // Release forgets an output closed by its goroutine, freeing its CRTC for the | 126 | + | // Release forgets an output closed by its goroutine, freeing its CRTC and |
| 107 | - | // next Scan. | 127 | + | // planes for the next Scan. |
| 108 | - | func (c *Card) Release(name string) { delete(c.outputs, name) } | 128 | + | func (c *Card) Release(name string) { |
| 129 | + | if o := c.outputs[name]; o != nil { | |||
| 130 | + | for _, p := range o.owned() { | |||
| 131 | + | delete(c.taken, p.id) | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | delete(c.outputs, name) | |||
| 135 | + | } | |||
| 109 | 136 | ||||
| 110 | // Outputs returns the driven outputs. | 137 | // Outputs returns the driven outputs. | ||
| 111 | func (c *Card) Outputs() []*Output { | 138 | func (c *Card) Outputs() []*Output { | ||
| @@ -131,11 +158,19 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) { | |||||
| 131 | if err != nil { | 158 | if err != nil { | ||
| 132 | return nil, err | 159 | return nil, err | ||
| 133 | } | 160 | } | ||
| 134 | - | return newOutput(c, conn, mode, crtc) | 161 | + | o, err := newOutput(c, conn, mode, crtc) |
| 162 | + | if err != nil { | |||
| 163 | + | return nil, err | |||
| 164 | + | } | |||
| 165 | + | for _, p := range o.owned() { | |||
| 166 | + | c.taken[p.id] = true | |||
| 167 | + | } | |||
| 168 | + | return o, nil | |||
| 135 | } | 169 | } | ||
| 136 | 170 | ||||
| 137 | - | // ReadEvents reads page-flip events until ctx ends and routes them to the | 171 | + | // ReadEvents reads commit events until ctx ends and routes them to the |
| 138 | - | // output of their CRTC. Run it once per card. | 172 | + | // output of their CRTC. Run it once per card. An event is never dropped: |
| 173 | + | // the output owes it a pending commit. | |||
| 139 | func (c *Card) ReadEvents(ctx context.Context) error { | 174 | func (c *Card) ReadEvents(ctx context.Context) error { | ||
| 140 | buf := make([]byte, 1024) | 175 | buf := make([]byte, 1024) | ||
| 141 | for ctx.Err() == nil { | 176 | for ctx.Err() == nil { | ||
| @@ -154,14 +189,15 @@ func (c *Card) ReadEvents(ctx context.Context) error { | |||||
| 154 | } | 189 | } | ||
| 155 | return fmt.Errorf("drm read: %w", err) | 190 | return fmt.Errorf("drm read: %w", err) | ||
| 156 | } | 191 | } | ||
| 157 | - | for _, crtc := range flipCrtcs(buf[:m]) { | 192 | + | for _, ev := range parseFlips(buf[:m]) { |
| 158 | - | ch := c.flips[crtc] | 193 | + | ch := c.flips[ev.crtc] |
| 159 | if ch == nil { | 194 | if ch == nil { | ||
| 160 | continue | 195 | continue | ||
| 161 | } | 196 | } | ||
| 162 | select { | 197 | select { | ||
| 163 | - | case ch <- 1: | 198 | + | case ch <- ev: |
| 164 | - | default: // the output is busy; the flip is counted on its next event | 199 | + | case <-ctx.Done(): |
| 200 | + | return nil | |||
| 165 | } | 201 | } | ||
| 166 | } | 202 | } | ||
| 167 | } | 203 | } | ||
A internal/adapters/drm/commit_test.go +410 -0
| @@ -0,0 +1,410 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // An event queued before a modeset (or for an earlier output on the CRTC) | |||
| 16 | + | // must not complete the commit that follows. | |||
| 17 | + | func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) { | |||
| 18 | + | o, _, commits := testOutput(t) | |||
| 19 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 20 | + | t.Fatal(err) | |||
| 21 | + | } | |||
| 22 | + | old := eventOf((*commits)[0]) | |||
| 23 | + | o.pending = false // a modeset dropped the commit; its event is queued | |||
| 24 | + | if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil { | |||
| 25 | + | t.Fatal(err) | |||
| 26 | + | } | |||
| 27 | + | if o.completed(old, nil) || !o.pending { | |||
| 28 | + | t.Fatal("stale event completed the new commit") | |||
| 29 | + | } | |||
| 30 | + | if !o.completed(eventOf((*commits)[1]), nil) || o.pending { | |||
| 31 | + | t.Fatal("own event did not complete the commit") | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // After EBUSY no commit of ours is known: the next event of the CRTC ends | |||
| 36 | + | // the wait, then the frame commits. | |||
| 37 | + | func TestBusyFrameCommitsAfterEvent(t *testing.T) { | |||
| 38 | + | o, _, commits := testOutput(t, unix.EBUSY) | |||
| 39 | + | enabled := true | |||
| 40 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) { | |||
| 41 | + | t.Fatalf("EBUSY not handled: %v", err) | |||
| 42 | + | } | |||
| 43 | + | if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.pending { | |||
| 44 | + | t.Fatal("EBUSY wait not ended by the CRTC's event") | |||
| 45 | + | } | |||
| 46 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.pending { | |||
| 47 | + | t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits)) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // A cursor image change writes only an image the plane neither shows nor | |||
| 52 | + | // is about to show; with both busy it waits for the event. | |||
| 53 | + | func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |||
| 54 | + | o, _, _ := testOutput(t) | |||
| 55 | + | c := o.cursor | |||
| 56 | + | r := portsmocks.NewMockRenderer(t) | |||
| 57 | + | var written []int | |||
| 58 | + | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |||
| 59 | + | written = append(written, i) | |||
| 60 | + | return nil | |||
| 61 | + | }) | |||
| 62 | + | px := []byte{1, 2, 3, 4} | |||
| 63 | + | // Image 0 on screen: two changes before a commit both go to image 1. | |||
| 64 | + | c.screen, c.cur, c.image = 90, 0, true | |||
| 65 | + | for range 2 { | |||
| 66 | + | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil { | |||
| 67 | + | t.Fatal(err) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | if len(written) != 2 || written[0] != 1 || written[1] != 1 { | |||
| 71 | + | t.Fatalf("written %v", written) | |||
| 72 | + | } | |||
| 73 | + | // Image 1 in flight, image 0 on screen: nothing is free. | |||
| 74 | + | c.committed(cursorState{on: true, fb: 91}) | |||
| 75 | + | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil { | |||
| 76 | + | t.Fatalf("wrote a busy image: %v %v", err, written) | |||
| 77 | + | } | |||
| 78 | + | // The flip frees image 0. | |||
| 79 | + | c.landed() | |||
| 80 | + | if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { | |||
| 81 | + | t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // KMS refusing the cursor plane turns the hardware cursor off and the | |||
| 86 | + | // frame commits without it; the output keeps running. | |||
| 87 | + | func TestCursorRefusedFrameCommitsWithout(t *testing.T) { | |||
| 88 | + | o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 89 | + | o.cursor.image = true | |||
| 90 | + | o.cursor.Move(5, 5) | |||
| 91 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 92 | + | t.Fatal(err) | |||
| 93 | + | } | |||
| 94 | + | if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly { | |||
| 95 | + | t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits)) | |||
| 96 | + | } | |||
| 97 | + | if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 { | |||
| 98 | + | t.Fatalf("retry shows cursor fb %d", v) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // A refused cursor is not blamed on the client buffer. | |||
| 103 | + | func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) { | |||
| 104 | + | o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 105 | + | o.cursor.image = true | |||
| 106 | + | o.cursor.Move(5, 5) | |||
| 107 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |||
| 108 | + | direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{}) | |||
| 109 | + | if err != nil || !direct || o.clientFBs[9].failed != "" { | |||
| 110 | + | t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // showImages validates the images with a TEST_ONLY modeset and falls back | |||
| 115 | + | // to linear images when KMS refuses the driver's. | |||
| 116 | + | func TestShowImagesTestsBeforeModeset(t *testing.T) { | |||
| 117 | + | o, k, commits := testOutput(t, unix.EINVAL) | |||
| 118 | + | o.cursor = nil | |||
| 119 | + | r := portsmocks.NewMockRenderer(t) | |||
| 120 | + | buf := func() ports.DMABuf { | |||
| 121 | + | f, w, _ := os.Pipe() | |||
| 122 | + | w.Close() | |||
| 123 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 124 | + | } | |||
| 125 | + | r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 126 | + | r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 127 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 128 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |||
| 129 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |||
| 130 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 131 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 132 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 133 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 134 | + | t.Fatal(err) | |||
| 135 | + | } | |||
| 136 | + | want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes} | |||
| 137 | + | if len(*commits) != len(want) { | |||
| 138 | + | t.Fatalf("%d commits", len(*commits)) | |||
| 139 | + | } | |||
| 140 | + | for i, f := range want { | |||
| 141 | + | if (*commits)[i].flags != f { | |||
| 142 | + | t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f) | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | if o.kind != imagesLinear { | |||
| 146 | + | t.Fatalf("kind %d", o.kind) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // Enabling an output whose images were not made yet (started switched | |||
| 151 | + | // away) tests them and modesets once. | |||
| 152 | + | func TestEnableAfterStartAwayModesetsOnce(t *testing.T) { | |||
| 153 | + | o, k, commits, commitMu := testOutputMu(t) | |||
| 154 | + | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |||
| 155 | + | r := portsmocks.NewMockRenderer(t) | |||
| 156 | + | buf := func() ports.DMABuf { | |||
| 157 | + | f, w, _ := os.Pipe() | |||
| 158 | + | w.Close() | |||
| 159 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 160 | + | } | |||
| 161 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 162 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 163 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |||
| 164 | + | r.EXPECT().Close().Return().Once() | |||
| 165 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 166 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 167 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 168 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 169 | + | active := make(chan bool, 1) | |||
| 170 | + | active <- false | |||
| 171 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 172 | + | done := make(chan error, 1) | |||
| 173 | + | go func() { | |||
| 174 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1)) | |||
| 175 | + | }() | |||
| 176 | + | count := func(flags uint32) int { | |||
| 177 | + | commitMu.Lock() | |||
| 178 | + | defer commitMu.Unlock() | |||
| 179 | + | n := 0 | |||
| 180 | + | for _, c := range *commits { | |||
| 181 | + | if c.flags == flags { | |||
| 182 | + | n++ | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | return n | |||
| 186 | + | } | |||
| 187 | + | time.Sleep(20 * time.Millisecond) | |||
| 188 | + | if n := count(atomicAllowModes); n != 0 { | |||
| 189 | + | t.Fatalf("%d modesets while away", n) | |||
| 190 | + | } | |||
| 191 | + | active <- true | |||
| 192 | + | waitFor(t, func() bool { return count(atomicAllowModes) == 1 }) | |||
| 193 | + | cancel() | |||
| 194 | + | if err := <-done; err != nil { | |||
| 195 | + | t.Fatal(err) | |||
| 196 | + | } | |||
| 197 | + | if n := count(atomicTestOnly | atomicAllowModes); n != 1 { | |||
| 198 | + | t.Fatalf("%d test modesets", n) | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | // Close without a mode to restore turns the CRTC off. | |||
| 203 | + | func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) { | |||
| 204 | + | o, k, commits := testOutput(t) | |||
| 205 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 206 | + | o.Close() | |||
| 207 | + | c := (*commits)[0] | |||
| 208 | + | for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} { | |||
| 209 | + | if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 { | |||
| 210 | + | t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok) | |||
| 211 | + | } | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // Close restores the saved mode and gives the connector its CRTC back. | |||
| 216 | + | func TestCloseRestoresSavedMode(t *testing.T) { | |||
| 217 | + | o, k, commits := testOutput(t) | |||
| 218 | + | o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}} | |||
| 219 | + | k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once() | |||
| 220 | + | k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once() | |||
| 221 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 222 | + | o.Close() | |||
| 223 | + | c := (*commits)[0] | |||
| 224 | + | if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc { | |||
| 225 | + | t.Fatalf("connector crtc %d", v) | |||
| 226 | + | } | |||
| 227 | + | if v, _ := c.req.value(tPrimary, pFB); v != 5 { | |||
| 228 | + | t.Fatalf("primary fb %d", v) | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | // With the cursor off no image is loaded (no warning per change). | |||
| 233 | + | func TestCursorOffSkipsLoads(t *testing.T) { | |||
| 234 | + | o, _, _ := testOutput(t) | |||
| 235 | + | o.cursor.off = true | |||
| 236 | + | loaded := false | |||
| 237 | + | o.setCursor(nil, func(ports.CursorChange, float64, int) (ports.CursorImage, error) { | |||
| 238 | + | loaded = true | |||
| 239 | + | return ports.CursorImage{}, nil | |||
| 240 | + | }, ports.CursorChange{}, 1) | |||
| 241 | + | if loaded || o.cursor.desired().on { | |||
| 242 | + | t.Fatal("cursor used while off") | |||
| 243 | + | } | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // Content read while only a cursor commit is in flight is reported when | |||
| 247 | + | // that commit's event arrives, not held until a later frame. | |||
| 248 | + | func TestRunReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 249 | + | o, k, commits, commitMu := testOutputMu(t) | |||
| 250 | + | o.tearing, o.cursor.fbs = false, [2]uint32{} | |||
| 251 | + | flips := make(chan flipEvent, 1) | |||
| 252 | + | o.flipped = flips | |||
| 253 | + | r := portsmocks.NewMockRenderer(t) | |||
| 254 | + | buf := func() ports.DMABuf { | |||
| 255 | + | f, w, _ := os.Pipe() | |||
| 256 | + | w.Close() | |||
| 257 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 258 | + | } | |||
| 259 | + | r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 260 | + | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil) | |||
| 261 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 262 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 263 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |||
| 264 | + | r.EXPECT().Close().Return().Once() | |||
| 265 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once() | |||
| 266 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once() | |||
| 267 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 268 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 269 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |||
| 270 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 271 | + | load := func(ports.CursorChange, float64, int) (ports.CursorImage, error) { | |||
| 272 | + | return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil | |||
| 273 | + | } | |||
| 274 | + | last := func(kind uint64) (commitRec, bool) { | |||
| 275 | + | commitMu.Lock() | |||
| 276 | + | defer commitMu.Unlock() | |||
| 277 | + | for i := len(*commits) - 1; i >= 0; i-- { | |||
| 278 | + | if c := (*commits)[i]; c.user&3 == kind { | |||
| 279 | + | return c, true | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | return commitRec{}, false | |||
| 283 | + | } | |||
| 284 | + | scenes := make(chan ports.Scene, 1) | |||
| 285 | + | contents := make(chan ports.SurfaceContent, 1) | |||
| 286 | + | presented := make(chan ports.OutputPresented, 8) | |||
| 287 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 288 | + | done := make(chan error, 1) | |||
| 289 | + | go func() { | |||
| 290 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented) | |||
| 291 | + | }() | |||
| 292 | + | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |||
| 293 | + | var frame commitRec | |||
| 294 | + | waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok }) | |||
| 295 | + | flips <- eventOf(frame) | |||
| 296 | + | <-presented // the frame's flip | |||
| 297 | + | o.cursor.Move(3, 3) | |||
| 298 | + | var cur commitRec | |||
| 299 | + | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | |||
| 300 | + | // A window this output does not show commits while the cursor flies. | |||
| 301 | + | contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}} | |||
| 302 | + | time.Sleep(20 * time.Millisecond) | |||
| 303 | + | flips <- eventOf(cur) | |||
| 304 | + | select { | |||
| 305 | + | case p := <-presented: | |||
| 306 | + | if p.Seen[7] != 2 { | |||
| 307 | + | t.Fatalf("report %+v", p) | |||
| 308 | + | } | |||
| 309 | + | case <-time.After(time.Second): | |||
| 310 | + | t.Fatal("Seen not reported after the cursor commit") | |||
| 311 | + | } | |||
| 312 | + | cancel() | |||
| 313 | + | if err := <-done; err != nil { | |||
| 314 | + | t.Fatal(err) | |||
| 315 | + | } | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // Frame fences are closed on every path: flipped, EBUSY (re-rendered | |||
| 319 | + | // after the event) and lost master (re-rendered after the modeset). The | |||
| 320 | + | // fd count is unchanged after many frames. | |||
| 321 | + | func TestRunClosesFrameFences(t *testing.T) { | |||
| 322 | + | fds := func() int { | |||
| 323 | + | ents, err := os.ReadDir("/proc/self/fd") | |||
| 324 | + | if err != nil { | |||
| 325 | + | t.Skip(err) | |||
| 326 | + | } | |||
| 327 | + | return len(ents) | |||
| 328 | + | } | |||
| 329 | + | const rounds = 30 | |||
| 330 | + | // Commit results: modeset test and modeset, then per round | |||
| 331 | + | // EBUSY + flip, or lost master + modeset + flip, or a plain flip. | |||
| 332 | + | errs := []error{nil, nil} | |||
| 333 | + | for i := range rounds { | |||
| 334 | + | switch i % 3 { | |||
| 335 | + | case 0: | |||
| 336 | + | errs = append(errs, unix.EBUSY, nil) | |||
| 337 | + | case 1: | |||
| 338 | + | errs = append(errs, unix.EACCES, nil, nil) | |||
| 339 | + | default: | |||
| 340 | + | errs = append(errs, nil) | |||
| 341 | + | } | |||
| 342 | + | } | |||
| 343 | + | o, k, commits, commitMu := testOutputMu(t, errs...) | |||
| 344 | + | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |||
| 345 | + | flips := make(chan flipEvent, 1) | |||
| 346 | + | o.flipped = flips | |||
| 347 | + | r := portsmocks.NewMockRenderer(t) | |||
| 348 | + | buf := func() ports.DMABuf { | |||
| 349 | + | f, w, _ := os.Pipe() | |||
| 350 | + | w.Close() | |||
| 351 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 352 | + | } | |||
| 353 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 354 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 355 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 356 | + | f, w, _ := os.Pipe() | |||
| 357 | + | _, _ = w.Write([]byte{1}) | |||
| 358 | + | w.Close() | |||
| 359 | + | return f, nil | |||
| 360 | + | }) | |||
| 361 | + | r.EXPECT().Close().Return().Once() | |||
| 362 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 363 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 364 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 365 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 366 | + | before := fds() | |||
| 367 | + | active := make(chan bool, 1) | |||
| 368 | + | scenes := make(chan ports.Scene, 1) | |||
| 369 | + | presented := make(chan ports.OutputPresented, 4*rounds) | |||
| 370 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 371 | + | done := make(chan error, 1) | |||
| 372 | + | go func() { | |||
| 373 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented) | |||
| 374 | + | }() | |||
| 375 | + | count := func() int { | |||
| 376 | + | commitMu.Lock() | |||
| 377 | + | defer commitMu.Unlock() | |||
| 378 | + | return len(*commits) | |||
| 379 | + | } | |||
| 380 | + | last := func() commitRec { | |||
| 381 | + | commitMu.Lock() | |||
| 382 | + | defer commitMu.Unlock() | |||
| 383 | + | return (*commits)[len(*commits)-1] | |||
| 384 | + | } | |||
| 385 | + | waitFor(t, func() bool { return count() == 2 }) | |||
| 386 | + | for i := range rounds { | |||
| 387 | + | n := count() | |||
| 388 | + | scenes <- ports.Scene{Seq: uint64(i)} | |||
| 389 | + | switch i % 3 { | |||
| 390 | + | case 0: | |||
| 391 | + | waitFor(t, func() bool { return count() == n+1 }) | |||
| 392 | + | flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait | |||
| 393 | + | waitFor(t, func() bool { return count() == n+2 }) | |||
| 394 | + | case 1: | |||
| 395 | + | waitFor(t, func() bool { return count() == n+1 }) | |||
| 396 | + | active <- true | |||
| 397 | + | waitFor(t, func() bool { return count() == n+3 }) | |||
| 398 | + | default: | |||
| 399 | + | waitFor(t, func() bool { return count() == n+1 }) | |||
| 400 | + | } | |||
| 401 | + | flips <- eventOf(last()) | |||
| 402 | + | } | |||
| 403 | + | cancel() | |||
| 404 | + | if err := <-done; err != nil { | |||
| 405 | + | t.Fatal(err) | |||
| 406 | + | } | |||
| 407 | + | if after := fds(); after > before { | |||
| 408 | + | t.Fatalf("fds %d -> %d", before, after) | |||
| 409 | + | } | |||
| 410 | + | } | |||
M internal/adapters/drm/cursor.go +184 -224
| @@ -1,281 +1,241 @@ | |||||
| 1 | package drm | 1 | package drm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "fmt" | 4 | + | "errors" |
| 5 | + | "slices" | |||
| 5 | "sync" | 6 | "sync" | ||
| 6 | - | "time" | 7 | + | |
| 7 | - | "unsafe" | 8 | + | "github.com/bnema/nefertty/internal/ports" |
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| 10 | - | // Hardware cursor through the legacy cursor ioctls: the GPU overlays a small | 11 | + | // 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. | 12 | + | // input goroutine: they only record the position and wake the output, |
| 13 | + | // whose goroutine puts the cursor plane in its next atomic commit. A move | |||
| 14 | + | // while a commit is pending waits for it, so the cursor moves at most once | |||
| 15 | + | // per flip and never blocks input. | |||
| 12 | 16 | ||||
| 13 | const ( | 17 | const ( | ||
| 14 | - | ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap) | 18 | + | ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap) |
| 15 | - | ioctlCursor2 = 0xC02464BB // DRM_IOWR('d', 0xBB, struct drm_mode_cursor2) | 19 | + | capCursorW = 0x8 |
| 16 | - | ioctlVblank = 0xC018643A // DRM_IOWR('d', 0x3A, union drm_wait_vblank) | 20 | + | 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 | ) | 21 | ) | ||
| 25 | 22 | ||||
| 26 | type getCap struct{ capability, value uint64 } | 23 | type getCap struct{ capability, value uint64 } | ||
| 27 | 24 | ||||
| 28 | - | // waitVblank is union drm_wait_vblank (request and reply share 24 bytes). | 25 | + | // Cursor is the hardware cursor of one output. |
| 29 | - | type waitVblank struct { | 26 | + | type Cursor struct { |
| 30 | - | typ, sequence uint32 | 27 | + | mu sync.Mutex // guards x, y, away and moves |
| 31 | - | sec, usec int64 | 28 | + | x, y int // physical position of the hotspot |
| 29 | + | away bool // Hide was called and no Move since | |||
| 30 | + | moves int | |||
| 31 | + | wake chan struct{} | |||
| 32 | + | ||||
| 33 | + | // Owned by the output goroutine. | |||
| 34 | + | plane *plane | |||
| 35 | + | size int | |||
| 36 | + | fbs [2]uint32 // the two images; cur is on screen or next | |||
| 37 | + | cur int | |||
| 38 | + | image bool // an image is loaded | |||
| 39 | + | hotX, hotY int | |||
| 40 | + | applied cursorState // what the last commit put on the plane | |||
| 41 | + | commits int | |||
| 42 | + | // screen is the image the plane scans out (0: none); flight is what | |||
| 43 | + | // the pending commit puts on it when flying. An image is written only | |||
| 44 | + | // when it is neither, so the plane never shows a half-written image. | |||
| 45 | + | screen uint32 | |||
| 46 | + | flight cursorState | |||
| 47 | + | flying bool | |||
| 48 | + | // later is an image that waits for a free slot (both on screen or in | |||
| 49 | + | // flight); off is set when KMS refused the cursor plane. | |||
| 50 | + | later *laterImage | |||
| 51 | + | off bool | |||
| 32 | } | 52 | } | ||
| 33 | 53 | ||||
| 34 | - | // cursorPlane is the kernel side of the hardware cursor. | 54 | + | type laterImage struct { |
| 35 | - | type cursorPlane interface { | 55 | + | pixels []byte |
| 36 | - | // Set issues DRM_IOCTL_MODE_CURSOR2. | 56 | + | w, h, hotX, hotY int |
| 37 | - | Set(v *modeCursor2) error | |||
| 38 | - | // WaitVblank blocks until the next vblank of the cursor's CRTC. | |||
| 39 | - | WaitVblank() error | |||
| 40 | } | 57 | } | ||
| 41 | 58 | ||||
| 42 | - | // kmsCursorPlane drives the cursor of the CRTC at index pipe. | 59 | + | // cursorState is the cursor plane as committed: off, or fb at x, y. |
| 43 | - | type kmsCursorPlane struct{ fd, pipe int } | 60 | + | type cursorState struct { |
| 44 | - | 61 | + | on bool | |
| 45 | - | func (p kmsCursorPlane) Set(v *modeCursor2) error { | 62 | + | fb uint32 |
| 46 | - | return ioctl(p.fd, ioctlCursor2, unsafe.Pointer(v)) | 63 | + | x, y int |
| 47 | } | 64 | } | ||
| 48 | 65 | ||||
| 49 | - | func (p kmsCursorPlane) WaitVblank() error { | 66 | + | // CursorStats counts cursor work since the last TakeStats. |
| 50 | - | v := waitVblank{typ: vblankRel | uint32(p.pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1} | 67 | + | type CursorStats struct { |
| 51 | - | return ioctl(p.fd, ioctlVblank, unsafe.Pointer(&v)) | 68 | + | Moves int // Move calls |
| 69 | + | Commits int // commits that moved or changed the cursor | |||
| 52 | } | 70 | } | ||
| 53 | 71 | ||||
| 54 | - | type modeCursor2 struct { | 72 | + | func newCursor(p *plane, size int) *Cursor { |
| 55 | - | flags, crtcID uint32 | 73 | + | return &Cursor{plane: p, size: size, wake: make(chan struct{}, 1)} |
| 56 | - | x, y int32 | |||
| 57 | - | width, height uint32 | |||
| 58 | - | handle uint32 | |||
| 59 | - | hotX, hotY int32 | |||
| 60 | } | 74 | } | ||
| 61 | 75 | ||||
| 62 | - | // Cursor is the hardware cursor of one output. Move is safe to call from the | 76 | + | // TakeStats returns and resets the counters. Output goroutine only. |
| 63 | - | // input goroutine while the output goroutine changes the image. | 77 | + | func (c *Cursor) TakeStats() CursorStats { |
| 64 | - | // | 78 | + | c.mu.Lock() |
| 65 | - | // Move only records the position. A worker waits for the next vblank and then | 79 | + | s := CursorStats{Moves: c.moves, Commits: c.commits} |
| 66 | - | // applies the latest one, so the cursor moves at most once per frame, between | 80 | + | c.moves, c.commits = 0, 0 |
| 67 | - | // scanouts: moving it mid-scanout tears it into doubled images, and input is | 81 | + | c.mu.Unlock() |
| 68 | - | // never throttled by the ioctl. | 82 | + | return s |
| 69 | - | type Cursor struct { | 83 | + | } |
| 70 | - | io sync.Mutex // serialises cursor ioctls and guards the fields below | |||
| 71 | - | fd int | |||
| 72 | - | crtc uint32 | |||
| 73 | - | buf *dumbBuffer | |||
| 74 | - | size int // buffer side | |||
| 75 | - | hotX, hotY int | |||
| 76 | - | shown bool | |||
| 77 | - | hidden bool // the plane is off because the pointer left this output | |||
| 78 | - | plane cursorPlane | |||
| 79 | 84 | ||||
| 80 | - | mu sync.Mutex // guards x, y, away and stats; never held across an ioctl | 85 | + | // Limit is the largest image side the cursor plane accepts. |
| 81 | - | x, y int // physical position of the hotspot | 86 | + | func (c *Cursor) Limit() int { return c.size } |
| 82 | - | away bool // Hide was called and no Move since | |||
| 83 | - | stats CursorStats | |||
| 84 | 87 | ||||
| 85 | - | wake chan struct{} | 88 | + | // Move places the hotspot at physical (x, y). It never blocks. |
| 86 | - | stop chan struct{} | 89 | + | func (c *Cursor) Move(x, y float64) { |
| 87 | - | done chan struct{} | 90 | + | c.mu.Lock() |
| 91 | + | c.x, c.y = int(x), int(y) | |||
| 92 | + | c.away = false | |||
| 93 | + | c.moves++ | |||
| 94 | + | c.mu.Unlock() | |||
| 95 | + | c.poke() | |||
| 88 | } | 96 | } | ||
| 89 | 97 | ||||
| 90 | - | // CursorStats counts cursor work since the last TakeStats. | 98 | + | // Hide removes the cursor from the output until the next move (the pointer |
| 91 | - | type CursorStats struct { | 99 | + | // is on another output). |
| 92 | - | Moves int // Move calls | 100 | + | func (c *Cursor) Hide() { |
| 93 | - | Ioctls int // move ioctls issued by the worker | 101 | + | c.mu.Lock() |
| 94 | - | MaxIoctl time.Duration // slowest move ioctl | 102 | + | c.away = true |
| 103 | + | c.mu.Unlock() | |||
| 104 | + | c.poke() | |||
| 95 | } | 105 | } | ||
| 96 | 106 | ||||
| 97 | - | // TakeStats returns and resets the counters. | 107 | + | func (c *Cursor) poke() { |
| 98 | - | func (c *Cursor) TakeStats() CursorStats { | 108 | + | select { |
| 99 | - | c.mu.Lock() | 109 | + | case c.wake <- struct{}{}: |
| 100 | - | defer c.mu.Unlock() | 110 | + | default: // a wake is queued; the output reads the latest state |
| 101 | - | s := c.stats | 111 | + | } |
| 102 | - | c.stats = CursorStats{} | |||
| 103 | - | return s | |||
| 104 | } | 112 | } | ||
| 105 | 113 | ||||
| 106 | - | // newCursor allocates a cursor buffer at the driver's preferred size (64 when | 114 | + | // setup allocates the two cursor images and their framebuffers. |
| 107 | - | // the driver does not say). | 115 | + | func (c *Cursor) setup(k kms, r ports.Renderer) error { |
| 108 | - | func newCursor(fd int, crtc uint32, pipe int) (*Cursor, error) { | 116 | + | bufs, err := r.CursorBuffers(c.size) |
| 109 | - | // The plane is square: the smaller of the width and height caps. | 117 | + | if err != nil { |
| 110 | - | size := 0 | 118 | + | return err |
| 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 | } | ||
| 119 | - | if size == 0 { | 120 | + | for i := range bufs { |
| 120 | - | size = 64 | 121 | + | if err == nil { |
| 122 | + | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) | |||
| 123 | + | } | |||
| 124 | + | for _, p := range bufs[i].Planes { | |||
| 125 | + | p.File.Close() | |||
| 126 | + | } | |||
| 121 | } | 127 | } | ||
| 122 | - | buf, err := newDumb(fd, size, size) | |||
| 123 | if err != nil { | 128 | if err != nil { | ||
| 124 | - | return nil, err | 129 | + | c.free(k) |
| 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 | } | 130 | } | ||
| 131 | - | c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size, plane: kmsCursorPlane{fd: fd, pipe: pipe}} | 131 | + | return err |
| 132 | - | c.start() | |||
| 133 | - | return c, nil | |||
| 134 | - | } | |||
| 135 | - | ||||
| 136 | - | // start runs the worker that applies coalesced moves. | |||
| 137 | - | func (c *Cursor) start() { | |||
| 138 | - | c.wake = make(chan struct{}, 1) | |||
| 139 | - | c.stop = make(chan struct{}) | |||
| 140 | - | c.done = make(chan struct{}) | |||
| 141 | - | go func() { | |||
| 142 | - | defer close(c.done) | |||
| 143 | - | for { | |||
| 144 | - | select { | |||
| 145 | - | case <-c.stop: | |||
| 146 | - | return | |||
| 147 | - | case <-c.wake: | |||
| 148 | - | // Errors (VT switched away) fall through: the move | |||
| 149 | - | // fails too and Reapply restores the cursor. | |||
| 150 | - | _ = c.plane.WaitVblank() | |||
| 151 | - | c.io.Lock() | |||
| 152 | - | c.mu.Lock() | |||
| 153 | - | away := c.away | |||
| 154 | - | c.mu.Unlock() | |||
| 155 | - | if away { | |||
| 156 | - | // A move queued before Hide: the pointer is elsewhere. | |||
| 157 | - | } else if c.hidden { | |||
| 158 | - | // A move shows it again, even before its first image. | |||
| 159 | - | c.hidden = false | |||
| 160 | - | _ = c.apply() | |||
| 161 | - | } else if c.shown && c.buf != nil { | |||
| 162 | - | x, y := c.position() | |||
| 163 | - | v := modeCursor2{flags: cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY)} | |||
| 164 | - | // Errors while another session owns the card (VT switched | |||
| 165 | - | // away) are ignored; Reapply restores the cursor. | |||
| 166 | - | start := time.Now() | |||
| 167 | - | _ = c.plane.Set(&v) | |||
| 168 | - | d := time.Since(start) | |||
| 169 | - | c.mu.Lock() | |||
| 170 | - | c.stats.Ioctls++ | |||
| 171 | - | c.stats.MaxIoctl = max(c.stats.MaxIoctl, d) | |||
| 172 | - | c.mu.Unlock() | |||
| 173 | - | } | |||
| 174 | - | c.io.Unlock() | |||
| 175 | - | } | |||
| 176 | - | } | |||
| 177 | - | }() | |||
| 178 | } | 132 | } | ||
| 179 | 133 | ||||
| 180 | - | func (c *Cursor) position() (int, int) { | 134 | + | // setImage loads premultiplied ARGB8888 pixels into an image neither on |
| 181 | - | c.mu.Lock() | 135 | + | // screen nor in flight; when there is none it waits for the next commit |
| 182 | - | defer c.mu.Unlock() | 136 | + | // event (flushLater). An empty image hides the cursor (a client asked for |
| 183 | - | return c.x, c.y | 137 | + | // none). |
| 184 | - | } | 138 | + | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { |
| 185 | - | 139 | + | c.later = nil | |
| 186 | - | // Limit is the largest image side the cursor plane accepts. | 140 | + | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { |
| 187 | - | func (c *Cursor) Limit() int { return c.size } | 141 | + | c.image = false |
| 188 | - | ||||
| 189 | - | // SetImage shows premultiplied ARGB8888 pixels (w×h, w*4 per row) with the | |||
| 190 | - | // click point at (hotX, hotY). Larger images are cropped to the plane; an | |||
| 191 | - | // empty image hides the cursor (a client asked for none). | |||
| 192 | - | func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | |||
| 193 | - | c.io.Lock() | |||
| 194 | - | defer c.io.Unlock() | |||
| 195 | - | if c.buf == nil { | |||
| 196 | return nil | 142 | return nil | ||
| 197 | } | 143 | } | ||
| 198 | - | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | 144 | + | if c.fbs[0] == 0 { |
| 199 | - | wasShown := c.shown | 145 | + | return errors.New("no cursor images") |
| 200 | - | c.shown = false | 146 | + | } |
| 201 | - | if !wasShown || c.hidden { | 147 | + | next := c.freeSlot() |
| 202 | - | return nil | 148 | + | if next < 0 { |
| 203 | - | } | 149 | + | c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY} |
| 204 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | 150 | + | return nil |
| 205 | - | return c.plane.Set(&v) | |||
| 206 | } | 151 | } | ||
| 207 | - | pitch := int(c.buf.pitch) | 152 | + | if err := r.WriteCursor(next, pixels, w, h); err != nil { |
| 208 | - | clear(c.buf.mem) | 153 | + | return err |
| 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 | } | 154 | } | ||
| 213 | - | c.hotX, c.hotY, c.shown = hotX, hotY, true | 155 | + | c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY |
| 214 | - | return c.apply() | 156 | + | return nil |
| 215 | } | 157 | } | ||
| 216 | 158 | ||||
| 217 | - | // apply (re)attaches the buffer at the current position. Callers hold c.io. | 159 | + | // freeSlot is an image the plane neither shows nor is about to show, -1 |
| 218 | - | func (c *Cursor) apply() error { | 160 | + | // when both are busy. |
| 219 | - | if !c.shown || c.hidden || c.buf == nil { | 161 | + | func (c *Cursor) freeSlot() int { |
| 220 | - | return nil | 162 | + | for i, fb := range c.fbs { |
| 163 | + | if fb != c.screen && !(c.flying && c.flight.on && c.flight.fb == fb) { | |||
| 164 | + | return i | |||
| 165 | + | } | |||
| 221 | } | 166 | } | ||
| 222 | - | x, y := c.position() | 167 | + | return -1 |
| 223 | - | v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)} | |||
| 224 | - | return c.plane.Set(&v) | |||
| 225 | } | 168 | } | ||
| 226 | 169 | ||||
| 227 | - | // Move places the hotspot at physical (x, y). It never blocks on the GPU: | 170 | + | // flushLater loads an image that waited for a free slot. It reports |
| 228 | - | // the worker applies the latest position. | 171 | + | // whether the cursor changed. |
| 229 | - | func (c *Cursor) Move(x, y float64) { | 172 | + | func (c *Cursor) flushLater(r ports.Renderer) (bool, error) { |
| 230 | - | c.mu.Lock() | 173 | + | l := c.later |
| 231 | - | c.x, c.y = int(x), int(y) | 174 | + | if l == nil || c.freeSlot() < 0 { |
| 232 | - | c.away = false | 175 | + | return false, nil |
| 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 | } | 176 | } | ||
| 177 | + | return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY) | |||
| 239 | } | 178 | } | ||
| 240 | 179 | ||||
| 241 | - | // Hide removes the cursor from the output until the next move (the pointer | 180 | + | // committed records that a commit carrying state s is in flight. |
| 242 | - | // is on another output). | 181 | + | func (c *Cursor) committed(s cursorState) { |
| 243 | - | func (c *Cursor) Hide() { | 182 | + | if s != c.applied { |
| 244 | - | c.io.Lock() | 183 | + | c.mu.Lock() |
| 245 | - | defer c.io.Unlock() | 184 | + | c.commits++ |
| 246 | - | c.mu.Lock() | 185 | + | c.mu.Unlock() |
| 247 | - | c.away = true | |||
| 248 | - | c.mu.Unlock() | |||
| 249 | - | if c.buf == nil || c.hidden { | |||
| 250 | - | return | |||
| 251 | } | 186 | } | ||
| 252 | - | c.hidden = true | 187 | + | c.applied, c.flight, c.flying = s, s, true |
| 253 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | |||
| 254 | - | _ = c.plane.Set(&v) | |||
| 255 | } | 188 | } | ||
| 256 | 189 | ||||
| 257 | - | // Reapply shows the cursor again after a modeset (VT switch back). | 190 | + | // landed records that the pending commit reached the screen. |
| 258 | - | func (c *Cursor) Reapply() error { | 191 | + | func (c *Cursor) landed() { |
| 259 | - | c.io.Lock() | 192 | + | if c.flying { |
| 260 | - | defer c.io.Unlock() | 193 | + | c.screen, c.flying = 0, false |
| 261 | - | return c.apply() | 194 | + | if c.flight.on { |
| 195 | + | c.screen = c.flight.fb | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 262 | } | 198 | } | ||
| 263 | 199 | ||||
| 264 | - | // close stops the worker, hides the cursor and frees its buffer. | 200 | + | // desired is what the cursor plane should show now. |
| 265 | - | func (c *Cursor) close() { | 201 | + | func (c *Cursor) desired() cursorState { |
| 266 | - | if c.stop != nil { | 202 | + | c.mu.Lock() |
| 267 | - | close(c.stop) | 203 | + | x, y, away := c.x, c.y, c.away |
| 268 | - | <-c.done | 204 | + | c.mu.Unlock() |
| 269 | - | c.stop = nil | 205 | + | if !c.image || away || c.off || c.fbs[c.cur] == 0 { |
| 206 | + | return cursorState{} | |||
| 270 | } | 207 | } | ||
| 271 | - | c.io.Lock() | 208 | + | return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY} |
| 272 | - | defer c.io.Unlock() | 209 | + | } |
| 273 | - | if c.buf == nil { | 210 | + | |
| 211 | + | // props adds the cursor plane in state s to req. | |||
| 212 | + | func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) { | |||
| 213 | + | p := c.plane | |||
| 214 | + | if !s.on { | |||
| 215 | + | req.set(p.id, p.prop("FB_ID"), 0) | |||
| 216 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |||
| 274 | return | 217 | return | ||
| 275 | } | 218 | } | ||
| 276 | - | // Best effort: at exit the card may already belong to another session. | 219 | + | size := uint64(c.size) |
| 277 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | 220 | + | req.set(p.id, p.prop("FB_ID"), uint64(s.fb)) |
| 278 | - | _ = c.plane.Set(&v) | 221 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(crtc)) |
| 279 | - | c.buf.destroy(c.fd) | 222 | + | req.set(p.id, p.prop("SRC_X"), 0) |
| 280 | - | c.buf, c.shown = nil, false | 223 | + | req.set(p.id, p.prop("SRC_Y"), 0) |
| 224 | + | req.set(p.id, p.prop("SRC_W"), size<<16) | |||
| 225 | + | req.set(p.id, p.prop("SRC_H"), size<<16) | |||
| 226 | + | req.set(p.id, p.prop("CRTC_X"), uint64(int64(s.x))) | |||
| 227 | + | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(s.y))) | |||
| 228 | + | req.set(p.id, p.prop("CRTC_W"), size) | |||
| 229 | + | req.set(p.id, p.prop("CRTC_H"), size) | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | func (c *Cursor) free(k kms) { | |||
| 233 | + | for i, fb := range c.fbs { | |||
| 234 | + | if fb != 0 { | |||
| 235 | + | _ = k.rmFB(fb) | |||
| 236 | + | c.fbs[i] = 0 | |||
| 237 | + | } | |||
| 238 | + | } | |||
| 239 | + | c.image, c.later = false, nil | |||
| 240 | + | c.screen, c.flying = 0, false | |||
| 281 | } | 241 | } | ||
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 +7 -109
| @@ -1,12 +1,12 @@ | |||||
| 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" | ||
| 9 | 8 | ||||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 10 | "golang.org/x/sys/unix" | 10 | "golang.org/x/sys/unix" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| @@ -14,18 +14,10 @@ import ( | |||||
| 14 | const ( | 14 | const ( | ||
| 15 | ioctlGetResources = 0xC04064A0 | 15 | ioctlGetResources = 0xC04064A0 | ||
| 16 | ioctlGetCrtc = 0xC06864A1 | 16 | ioctlGetCrtc = 0xC06864A1 | ||
| 17 | - | ioctlSetCrtc = 0xC06864A2 | |||
| 18 | ioctlGetEncoder = 0xC01464A6 | 17 | ioctlGetEncoder = 0xC01464A6 | ||
| 19 | ioctlGetConnector = 0xC05064A7 | 18 | ioctlGetConnector = 0xC05064A7 | ||
| 20 | - | ioctlAddFB = 0xC01C64AE | |||
| 21 | ioctlRmFB = 0xC00464AF | 19 | ioctlRmFB = 0xC00464AF | ||
| 22 | - | ioctlPageFlip = 0xC01864B0 | |||
| 23 | - | ioctlCreateDumb = 0xC02064B2 | |||
| 24 | - | ioctlMapDumb = 0xC01064B3 | |||
| 25 | - | ioctlDestroyDumb = 0xC00464B4 | |||
| 26 | 20 | ||||
| 27 | - | pageFlipEvent = 1 | |||
| 28 | - | pageFlipAsync = 2 | |||
| 29 | eventFlipDone = 2 | 21 | eventFlipDone = 2 | ||
| 30 | modeTypePrefered = 1 << 3 | 22 | modeTypePrefered = 1 << 3 | ||
| 31 | connected = 1 | 23 | connected = 1 | ||
| @@ -72,24 +64,6 @@ type modeCrtc struct { | |||||
| 72 | mode modeInfo | 64 | mode modeInfo | ||
| 73 | } | 65 | } | ||
| 74 | 66 | ||||
| 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 { | 67 | func ioctl(fd int, req uintptr, arg unsafe.Pointer) error { | ||
| 94 | for { | 68 | for { | ||
| 95 | _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)) | 69 | _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)) | ||
| @@ -126,6 +100,11 @@ type Want struct { | |||||
| 126 | NoTearing bool | 100 | NoTearing bool | ||
| 127 | // NoVRR keeps variable refresh off (render.vrr = off). | 101 | // NoVRR keeps variable refresh off (render.vrr = off). | ||
| 128 | NoVRR bool | 102 | NoVRR bool | ||
| 103 | + | // Sampled are the dmabuf formats the renderer composes: direct | |||
| 104 | + | // scanout is offered only for those, so a refused buffer still draws. | |||
| 105 | + | Sampled []ports.DMABufFormat | |||
| 106 | + | // Device is the KMS device (dev_t) offered in scanout tranches. | |||
| 107 | + | Device uint64 | |||
| 129 | } | 108 | } | ||
| 130 | 109 | ||||
| 131 | // usable reports whether a connector should be driven. | 110 | // usable reports whether a connector should be driven. | ||
| @@ -261,89 +240,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) | 240 | return 0, fmt.Errorf("no free CRTC for %s", c.name) | ||
| 262 | } | 241 | } | ||
| 263 | 242 | ||||
| 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) { | 243 | func getCrtc(fd int, crtc uint32) (modeCrtc, error) { | ||
| 319 | c := modeCrtc{crtcID: crtc} | 244 | c := modeCrtc{crtcID: crtc} | ||
| 320 | err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c)) | 245 | err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c)) | ||
| 321 | return c, err | 246 | return c, err | ||
| 322 | } | 247 | } | ||
| 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 +56 -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,22 @@ 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}, | |||
| 35 | + | {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob}, | |||
| 29 | } { | 36 | } { | ||
| 30 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | 37 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | ||
| 31 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | 38 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | ||
| @@ -88,19 +95,53 @@ func TestParseEDID(t *testing.T) { | |||||
| 88 | } | 95 | } | ||
| 89 | } | 96 | } | ||
| 90 | 97 | ||||
| 91 | - | func TestFlipCrtcs(t *testing.T) { | 98 | + | func TestParseFlips(t *testing.T) { |
| 92 | - | ev := func(typ, crtc uint32) []byte { | 99 | + | ev := func(typ, crtc uint32, user uint64, sec, usec, seq uint32) []byte { |
| 93 | b := make([]byte, 32) | 100 | b := make([]byte, 32) | ||
| 94 | - | binary.LittleEndian.PutUint32(b, typ) | 101 | + | le := binary.LittleEndian |
| 95 | - | binary.LittleEndian.PutUint32(b[4:], 32) | 102 | + | le.PutUint32(b, typ) |
| 96 | - | binary.LittleEndian.PutUint32(b[28:], crtc) | 103 | + | le.PutUint32(b[4:], 32) |
| 104 | + | le.PutUint64(b[8:], user) | |||
| 105 | + | le.PutUint32(b[16:], sec) | |||
| 106 | + | le.PutUint32(b[20:], usec) | |||
| 107 | + | le.PutUint32(b[24:], seq) | |||
| 108 | + | le.PutUint32(b[28:], crtc) | |||
| 97 | return b | 109 | return b | ||
| 98 | } | 110 | } | ||
| 99 | - | buf := append(append(ev(eventFlipDone, 41), ev(1, 7)...), ev(eventFlipDone, 42)...) | 111 | + | 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 { | 112 | + | got := parseFlips(buf) |
| 101 | - | t.Fatalf("got %v", got) | 113 | + | want := []flipEvent{{crtc: 41, user: userFrame, when: 3*time.Second + 250*time.Microsecond, seq: 7}, {crtc: 42, user: userState, when: time.Microsecond, seq: 8}} |
| 114 | + | if !slices.Equal(got, want) { | |||
| 115 | + | t.Fatalf("got %+v, want %+v", got, want) | |||
| 102 | } | 116 | } | ||
| 103 | - | if got := flipCrtcs(buf[:20]); len(got) != 0 { | 117 | + | if got := parseFlips(buf[:20]); len(got) != 0 { |
| 104 | t.Fatalf("truncated: got %v", got) | 118 | t.Fatalf("truncated: got %v", got) | ||
| 105 | } | 119 | } | ||
| 106 | } | 120 | } | ||
| 121 | + | ||||
| 122 | + | func TestParseInFormats(t *testing.T) { | |||
| 123 | + | le := binary.LittleEndian | |||
| 124 | + | // 3 formats at 24, 2 modifiers at 40. | |||
| 125 | + | b := make([]byte, 40+2*24) | |||
| 126 | + | le.PutUint32(b, 1) | |||
| 127 | + | le.PutUint32(b[8:], 3) | |||
| 128 | + | le.PutUint32(b[12:], 24) | |||
| 129 | + | le.PutUint32(b[16:], 2) | |||
| 130 | + | le.PutUint32(b[20:], 40) | |||
| 131 | + | for i, f := range []uint32{fourccXRGB, fourccARGB, 0x3432564e} { | |||
| 132 | + | le.PutUint32(b[24+i*4:], f) | |||
| 133 | + | } | |||
| 134 | + | // linear on formats 0 and 2, a tiled modifier on format 1. | |||
| 135 | + | le.PutUint64(b[40:], 0b101) | |||
| 136 | + | le.PutUint64(b[56:], 0) | |||
| 137 | + | le.PutUint64(b[64:], 0b010) | |||
| 138 | + | le.PutUint64(b[80:], 0x0200000000000001) | |||
| 139 | + | got := parseInFormats(b) | |||
| 140 | + | want := []ports.DMABufFormat{{Format: fourccXRGB}, {Format: 0x3432564e}, {Format: fourccARGB, Modifier: 0x0200000000000001}} | |||
| 141 | + | if !slices.Equal(got, want) { | |||
| 142 | + | t.Fatalf("got %+v, want %+v", got, want) | |||
| 143 | + | } | |||
| 144 | + | if parseInFormats(b[:30]) != nil { | |||
| 145 | + | t.Fatal("truncated blob parsed") | |||
| 146 | + | } | |||
| 147 | + | } | |||
M internal/adapters/drm/output.go +974 -247
| @@ -5,82 +5,371 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "maps" | 7 | "maps" | ||
| 8 | + | "os" | |||
| 8 | "slices" | 9 | "slices" | ||
| 10 | + | "sync/atomic" | |||
| 9 | "time" | 11 | "time" | ||
| 10 | "unsafe" | 12 | "unsafe" | ||
| 11 | 13 | ||||
| 14 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |||
| 12 | "github.com/bnema/nefertty/internal/ports" | 15 | "github.com/bnema/nefertty/internal/ports" | ||
| 13 | "github.com/bnema/zerowrap" | 16 | "github.com/bnema/zerowrap" | ||
| 14 | "golang.org/x/sys/unix" | 17 | "golang.org/x/sys/unix" | ||
| 15 | ) | 18 | ) | ||
| 16 | 19 | ||||
| 17 | - | // Output is one KMS connector flipping between two images. One goroutine | 20 | + | // Output is one KMS connector on a CRTC, driven by atomic commits (ADR |
| 18 | - | // runs it (Run) and closes it. | 21 | + | // 015). One goroutine runs it (Run) and closes it. Every commit that |
| 22 | + | // changes the screen asks for an event and leaves the output pending until | |||
| 23 | + | // it arrives: the next commit waits for it. The kernel may still refuse | |||
| 24 | + | // it with EBUSY (it retries soon), and an event that never comes is | |||
| 25 | + | // bounded (stuckTimeout, then a modeset). | |||
| 19 | type Output struct { | 26 | type Output struct { | ||
| 20 | - | fd int | 27 | + | k kms |
| 21 | - | flipped <-chan int // page flips of this CRTC, from Card.ReadEvents | 28 | + | flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents |
| 22 | crtc uint32 | 29 | crtc uint32 | ||
| 23 | conn connector | 30 | conn connector | ||
| 24 | mode modeInfo | 31 | mode modeInfo | ||
| 25 | saved modeCrtc | 32 | saved modeCrtc | ||
| 26 | - | // fbs are the two images frames alternate between, back the one the | 33 | + | monitor Monitor |
| 27 | - | // next frame draws into. They are renderer images exported as dmabufs | 34 | + | log zerowrap.Logger |
| 28 | - | // (ADR 014: zero copy), or dumb buffers filled by the CPU when the GPU | 35 | + | // Properties: CRTC and connector property IDs by name. |
| 29 | - | // cannot export a scanout-capable image (logged fallback). | 36 | + | crtcProps map[string]uint32 |
| 30 | - | fbs [2]uint32 | 37 | + | connCrtc uint32 // the connector's CRTC_ID property |
| 31 | - | bufs [2]*dumbBuffer // fallback only | 38 | + | modeBlob uint32 |
| 32 | - | back int | 39 | + | // primary shows frames; cursor is the cursor plane (nil: none); |
| 33 | - | pending bool | 40 | + | // stray are other planes that can show on this CRTC, turned off at |
| 34 | - | flipStart time.Time | 41 | + | // every modeset when a previous master left them on it. |
| 35 | - | flips int | 42 | + | primary *plane |
| 36 | - | monitor Monitor | 43 | + | cursor *Cursor |
| 37 | - | cursor *Cursor | 44 | + | stray []*plane |
| 38 | - | log zerowrap.Logger | 45 | + | // overlay shows one window's buffer above the composed frame |
| 39 | - | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | 46 | + | // (overlay.go); overlayOn is the buffer on it in the pending or last |
| 40 | - | // and the buffer on screen and queued (0: the composed image). | 47 | + | // frame (0: off), overlayReason why it is off. |
| 41 | - | card *Card | 48 | + | overlay *plane |
| 49 | + | overlayOn uint64 | |||
| 50 | + | overlayReason string | |||
| 51 | + | // fbs are the two renderer images frames alternate between, back the | |||
| 52 | + | // one the next frame draws into (ADR 014: zero copy). | |||
| 53 | + | fbs [2]uint32 | |||
| 54 | + | back int | |||
| 55 | + | // pending: a commit waits for its event; pendingFrame when it flips a | |||
| 56 | + | // new frame (else it only changed the cursor or VRR). | |||
| 57 | + | pending bool | |||
| 58 | + | pendingFrame pendingFrame | |||
| 59 | + | // serials numbers commits per card, so an event queued before a | |||
| 60 | + | // modeset, or for an earlier output on this CRTC, never completes a | |||
| 61 | + | // newer commit; pendingSerial is the pending commit's. | |||
| 62 | + | serials *atomic.Uint64 | |||
| 63 | + | pendingSerial, nextSerial uint64 | |||
| 64 | + | flipStart time.Time | |||
| 65 | + | flips int | |||
| 66 | + | // A commit whose event does not come by stuckAt is abandoned with a | |||
| 67 | + | // modeset once its fences signalled (stuckAfter, default | |||
| 68 | + | // stuckTimeout; fenceWarned once it waits for a fence). A commit | |||
| 69 | + | // refused with EBUSY is retried after busyRetry; busySince is when | |||
| 70 | + | // the EBUSY run began, lastBusy the latest EBUSY. | |||
| 71 | + | stuckAfter time.Duration | |||
| 72 | + | stuckAt time.Time | |||
| 73 | + | fenceWarned bool | |||
| 74 | + | busySince, lastBusy time.Time | |||
| 75 | + | // readFences are fences of frames rendered but not committed: the | |||
| 76 | + | // GPU may still read client buffers until they signal, so what was | |||
| 77 | + | // seen is reported only then (or after a later frame flipped). | |||
| 78 | + | readFences []*os.File | |||
| 79 | + | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | |||
| 80 | + | // screen and queued (0: the composed image). | |||
| 42 | scanout bool | 81 | scanout bool | ||
| 43 | clientFBs map[uint64]*clientFB | 82 | clientFBs map[uint64]*clientFB | ||
| 44 | shown, queued uint64 | 83 | shown, queued uint64 | ||
| 45 | reason string // why the last frame was composed ("" = scanout) | 84 | reason string // why the last frame was composed ("" = scanout) | ||
| 46 | - | unsent ports.OutputPresented | 85 | + | unsent []ports.OutputPresented |
| 47 | - | // Tearing (ADR 006): async flips of scanned-out buffers that ask for | 86 | + | // 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 | 87 | + | // 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. | 88 | + | // is set when an async commit may carry IN_FENCE_FD (probed once). |
| 50 | - | tearing bool | 89 | + | tearing bool |
| 51 | - | vrrProp uint32 | 90 | + | asyncFence bool |
| 52 | - | vrrOn, async bool | 91 | + | asyncProbed bool |
| 53 | - | // composedSince is when composition took over from scanout. | 92 | + | async bool |
| 93 | + | // VRR: the CRTC's VRR_ENABLED property (0: none), on while a buffer is | |||
| 94 | + | // scanned out, set inside frame commits. | |||
| 95 | + | vrrProp uint32 | |||
| 96 | + | vrrOn bool | |||
| 54 | composedSince time.Time | 97 | composedSince time.Time | ||
| 55 | - | // kind is how the images were made; validated once a modeset took them. | 98 | + | // kind is how the images were made. |
| 56 | - | kind imageKind | 99 | + | kind imageKind |
| 57 | - | validated bool | 100 | + | // formats receives the direct scanout formats after each modeset; |
| 101 | + | // sampled and device are what they are built from (Want). | |||
| 102 | + | formats chan<- ports.OutputFormats | |||
| 103 | + | sampled []ports.DMABufFormat | |||
| 104 | + | device uint64 | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // pendingFrame is what the pending frame commit shows. | |||
| 108 | + | type pendingFrame struct { | |||
| 109 | + | frame bool | |||
| 110 | + | queued uint64 | |||
| 111 | + | zeroCopy ports.WindowID // window shown without composition | |||
| 112 | + | async bool | |||
| 113 | + | shows map[ports.WindowID]uint64 | |||
| 114 | + | // fences are duplicates of the fences the commit waits on, owned by | |||
| 115 | + | // the output until the commit ends. composed is set when the frame | |||
| 116 | + | // was rendered (its fence is the renderer's). | |||
| 117 | + | fences []*os.File | |||
| 118 | + | composed bool | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // closeFences closes the fences of f. | |||
| 122 | + | func (f *pendingFrame) closeFences() { | |||
| 123 | + | for _, fd := range f.fences { | |||
| 124 | + | fd.Close() | |||
| 125 | + | } | |||
| 126 | + | f.fences = nil | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // dupFences duplicates the non-nil fences. | |||
| 130 | + | func dupFences(fs ...*os.File) []*os.File { | |||
| 131 | + | var out []*os.File | |||
| 132 | + | for _, f := range fs { | |||
| 133 | + | if d := dupFence(f); d != nil { | |||
| 134 | + | out = append(out, d) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | return out | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | // signalled reports whether every fence signalled (readable); an fd that | |||
| 141 | + | // cannot be polled counts as signalled. | |||
| 142 | + | func signalled(fs []*os.File) bool { | |||
| 143 | + | if len(fs) == 0 { | |||
| 144 | + | return true | |||
| 145 | + | } | |||
| 146 | + | pfds := make([]unix.PollFd, len(fs)) | |||
| 147 | + | for i, f := range fs { | |||
| 148 | + | pfds[i] = unix.PollFd{Fd: int32(f.Fd()), Events: unix.POLLIN} | |||
| 149 | + | } | |||
| 150 | + | if _, err := unix.Poll(pfds, 0); err != nil { | |||
| 151 | + | return true | |||
| 152 | + | } | |||
| 153 | + | for _, p := range pfds { | |||
| 154 | + | if p.Revents == 0 { | |||
| 155 | + | return false | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | return true | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // holdRead keeps the fences of a frame that was not committed. | |||
| 162 | + | func (o *Output) holdRead(fs ...*os.File) { | |||
| 163 | + | o.readFences = append(o.readFences, dupFences(fs...)...) | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // readDone reports whether no uncommitted frame may still read client | |||
| 167 | + | // buffers, closing the fences that signalled. | |||
| 168 | + | func (o *Output) readDone() bool { | |||
| 169 | + | kept := o.readFences[:0] | |||
| 170 | + | for _, f := range o.readFences { | |||
| 171 | + | if signalled([]*os.File{f}) { | |||
| 172 | + | f.Close() | |||
| 173 | + | continue | |||
| 174 | + | } | |||
| 175 | + | kept = append(kept, f) | |||
| 176 | + | } | |||
| 177 | + | clear(o.readFences[len(kept):]) | |||
| 178 | + | o.readFences = kept | |||
| 179 | + | return len(kept) == 0 | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // dropRead closes the fences of uncommitted frames. | |||
| 183 | + | func (o *Output) dropRead() { | |||
| 184 | + | for _, f := range o.readFences { | |||
| 185 | + | f.Close() | |||
| 186 | + | } | |||
| 187 | + | o.readFences = nil | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // endPending ends the pending commit, closing its fences. | |||
| 191 | + | func (o *Output) endPending() { | |||
| 192 | + | o.pendingFrame.closeFences() | |||
| 193 | + | o.pending, o.pendingFrame = false, pendingFrame{} | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | // Commit event userData: the commit serial above userKindBits, the kind | |||
| 197 | + | // of commit below. | |||
| 198 | + | const ( | |||
| 199 | + | userFrame = 1 | |||
| 200 | + | userState = 2 | |||
| 201 | + | userKindBits = 2 | |||
| 202 | + | ) | |||
| 203 | + | ||||
| 204 | + | // userData takes a new serial for the next commit of kind and returns | |||
| 205 | + | // what the commit carries. | |||
| 206 | + | func (o *Output) userData(kind uint64) uint64 { | |||
| 207 | + | o.nextSerial = o.serials.Add(1) | |||
| 208 | + | return o.nextSerial<<userKindBits | kind | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // begin marks the commit made with the last userData pending. | |||
| 212 | + | func (o *Output) begin(f pendingFrame) { | |||
| 213 | + | o.pendingSerial = o.nextSerial | |||
| 214 | + | o.pendingFrame.closeFences() | |||
| 215 | + | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | |||
| 216 | + | o.stuckAt, o.fenceWarned = o.flipStart.Add(o.stuckLimit()), false | |||
| 217 | + | o.busySince = time.Time{} | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | // Bounds on waiting for a commit event: no path may leave an output | |||
| 221 | + | // pending forever. | |||
| 222 | + | const ( | |||
| 223 | + | stuckTimeout = time.Second | |||
| 224 | + | busyRetry = 50 * time.Millisecond | |||
| 225 | + | ) | |||
| 226 | + | ||||
| 227 | + | func (o *Output) stuckLimit() time.Duration { | |||
| 228 | + | if o.stuckAfter > 0 { | |||
| 229 | + | return o.stuckAfter | |||
| 230 | + | } | |||
| 231 | + | return stuckTimeout | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | // expire handles a pending commit whose event did not come by stuckAt. | |||
| 235 | + | // A commit still waiting for a fence (a long GPU job) keeps waiting. | |||
| 236 | + | // Else the commit is abandoned so the next can go; it reports whether | |||
| 237 | + | // the output needs a modeset: an event is missing (the kernel state is | |||
| 238 | + | // unknown) or EBUSY lasted past the limit. | |||
| 239 | + | func (o *Output) expire() bool { | |||
| 240 | + | now := time.Now() | |||
| 241 | + | age := now.Sub(o.flipStart) | |||
| 242 | + | if o.pendingSerial != 0 && !signalled(o.pendingFrame.fences) { | |||
| 243 | + | if !o.fenceWarned { | |||
| 244 | + | o.fenceWarned = true | |||
| 245 | + | o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Dur("age", age).Msg("frame waits for GPU fence") | |||
| 246 | + | } | |||
| 247 | + | o.stuckAt = now.Add(o.stuckLimit()) | |||
| 248 | + | return false | |||
| 249 | + | } | |||
| 250 | + | kind := "state" | |||
| 251 | + | if o.pendingFrame.frame { | |||
| 252 | + | kind = "frame" | |||
| 253 | + | } | |||
| 254 | + | o.endPending() | |||
| 255 | + | if o.pendingSerial == 0 { | |||
| 256 | + | busy := now.Sub(o.busySince) | |||
| 257 | + | if o.busySince.IsZero() || busy < o.stuckLimit() { | |||
| 258 | + | return false | |||
| 259 | + | } | |||
| 260 | + | o.log.Warn().Str("connector", o.conn.name).Str("kind", "busy").Dur("age", busy).Msg("commits refused as busy; modeset") | |||
| 261 | + | return true | |||
| 262 | + | } | |||
| 263 | + | o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Str("kind", kind).Dur("age", age).Msg("commit event missing; modeset") | |||
| 264 | + | return true | |||
| 58 | } | 265 | } | ||
| 59 | 266 | ||||
| 60 | // CursorLoader returns the image of a cursor at an output scale, at most | 267 | // 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. | 268 | // 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) | 269 | type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) | ||
| 63 | 270 | ||||
| 64 | - | // newOutput prepares the cursor and saves the CRTC for a connector on crtc. | 271 | + | // 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) { | 272 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 66 | log := card.log | 273 | 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"} | 274 | + | pipe := slices.Index(card.crtcs, crtc) |
| 275 | + | o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, 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 | 276 | var err error | ||
| 69 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 277 | 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") | 278 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| 71 | } | 279 | } | ||
| 280 | + | if err := o.readProps(pipe, card.taken, cursorSize(card.fd)); err != nil { | |||
| 281 | + | return nil, err | |||
| 282 | + | } | |||
| 72 | o.tearing = card.async && !card.want.NoTearing | 283 | o.tearing = card.async && !card.want.NoTearing | ||
| 73 | if !card.want.NoVRR { | 284 | if !card.want.NoVRR { | ||
| 74 | - | o.vrrProp = vrrProperty(card.fd, c.id, crtc) | 285 | + | o.vrrProp = vrrProperty(o.k, c.id, crtc) |
| 75 | - | } | |||
| 76 | - | if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil { | |||
| 77 | - | log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor") | |||
| 78 | } | 286 | } | ||
| 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") | 287 | + | log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Uint32("primary", o.primary.id).Bool("cursor", o.cursor != nil).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output") |
| 80 | return o, nil | 288 | return o, nil | ||
| 81 | } | 289 | } | ||
| 82 | 290 | ||||
| 83 | - | // Cursor is the hardware cursor, or nil when the driver has none. | 291 | + | // readProps finds the output's planes and property IDs. taken are planes |
| 292 | + | // other outputs drive. | |||
| 293 | + | func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) error { | |||
| 294 | + | cp, err := o.k.objProps(o.crtc, objCrtc) | |||
| 295 | + | if err != nil { | |||
| 296 | + | return fmt.Errorf("crtc properties: %w", err) | |||
| 297 | + | } | |||
| 298 | + | o.crtcProps = map[string]uint32{} | |||
| 299 | + | for name, v := range cp { | |||
| 300 | + | o.crtcProps[name] = uint32(v[0]) | |||
| 301 | + | } | |||
| 302 | + | np, err := o.k.objProps(o.conn.id, objConnector) | |||
| 303 | + | if err != nil { | |||
| 304 | + | return fmt.Errorf("connector properties: %w", err) | |||
| 305 | + | } | |||
| 306 | + | o.connCrtc = uint32(np["CRTC_ID"][0]) | |||
| 307 | + | if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 { | |||
| 308 | + | return errors.New("crtc or connector lacks atomic properties") | |||
| 309 | + | } | |||
| 310 | + | planes, err := readPlanes(o.k, pipe) | |||
| 311 | + | if err != nil { | |||
| 312 | + | return fmt.Errorf("planes: %w", err) | |||
| 313 | + | } | |||
| 314 | + | pick := func(typ uint32) *plane { | |||
| 315 | + | var best *plane | |||
| 316 | + | for _, p := range planes { | |||
| 317 | + | if p.typ != typ || taken[p.id] { | |||
| 318 | + | continue | |||
| 319 | + | } | |||
| 320 | + | // Prefer the plane already on this CRTC. | |||
| 321 | + | if best == nil || p.crtc == o.crtc && best.crtc != o.crtc { | |||
| 322 | + | best = p | |||
| 323 | + | } | |||
| 324 | + | } | |||
| 325 | + | return best | |||
| 326 | + | } | |||
| 327 | + | if o.primary = pick(planePrimary); o.primary == nil { | |||
| 328 | + | return errors.New("no free primary plane") | |||
| 329 | + | } | |||
| 330 | + | if p := pick(planeCursor); p != nil { | |||
| 331 | + | o.cursor = newCursor(p, cursorSide) | |||
| 332 | + | } | |||
| 333 | + | for _, p := range planes { | |||
| 334 | + | if p != o.primary && (o.cursor == nil || p != o.cursor.plane) && !taken[p.id] { | |||
| 335 | + | o.stray = append(o.stray, p) | |||
| 336 | + | } | |||
| 337 | + | } | |||
| 338 | + | o.pickOverlay() | |||
| 339 | + | return nil | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | // owned are the planes this output drives. | |||
| 343 | + | func (o *Output) owned() []*plane { | |||
| 344 | + | out := []*plane{o.primary} | |||
| 345 | + | if o.cursor != nil { | |||
| 346 | + | out = append(out, o.cursor.plane) | |||
| 347 | + | } | |||
| 348 | + | if o.overlay != nil { | |||
| 349 | + | out = append(out, o.overlay) | |||
| 350 | + | } | |||
| 351 | + | return out | |||
| 352 | + | } | |||
| 353 | + | ||||
| 354 | + | // cursorSize is the cursor plane side: the smaller of the driver's width | |||
| 355 | + | // and height caps, 64 when it does not say. | |||
| 356 | + | func cursorSize(fd int) int { | |||
| 357 | + | size := 0 | |||
| 358 | + | for _, c := range []uint64{capCursorW, capCursorH} { | |||
| 359 | + | v := getCap{capability: c} | |||
| 360 | + | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 { | |||
| 361 | + | if size == 0 || int(v.value) < size { | |||
| 362 | + | size = int(v.value) | |||
| 363 | + | } | |||
| 364 | + | } | |||
| 365 | + | } | |||
| 366 | + | if size == 0 { | |||
| 367 | + | size = 64 | |||
| 368 | + | } | |||
| 369 | + | return size | |||
| 370 | + | } | |||
| 371 | + | ||||
| 372 | + | // Cursor is the hardware cursor, or nil when the CRTC has no cursor plane. | |||
| 84 | func (o *Output) Cursor() *Cursor { return o.cursor } | 373 | func (o *Output) Cursor() *Cursor { return o.cursor } | ||
| 85 | 374 | ||||
| 86 | // Info describes the output for core and clients. Unknown EDID fields are empty. | 375 | // Info describes the output for core and clients. Unknown EDID fields are empty. | ||
| @@ -97,52 +386,309 @@ func (o *Output) Info() ports.OutputInfo { | |||||
| 97 | func (o *Output) Width() int { return int(o.mode.HDisplay) } | 386 | func (o *Output) Width() int { return int(o.mode.HDisplay) } | ||
| 98 | func (o *Output) Height() int { return int(o.mode.VDisplay) } | 387 | func (o *Output) Height() int { return int(o.mode.VDisplay) } | ||
| 99 | 388 | ||||
| 100 | - | // modeset shows the front buffer; needed at start and after every VT resume. | 389 | + | // primaryProps shows fb full screen on the primary plane. |
| 390 | + | func (o *Output) primaryProps(req *atomicReq, fb uint32) { | |||
| 391 | + | p := o.primary | |||
| 392 | + | w, h := uint64(o.Width()), uint64(o.Height()) | |||
| 393 | + | req.set(p.id, p.prop("FB_ID"), uint64(fb)) | |||
| 394 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |||
| 395 | + | req.set(p.id, p.prop("SRC_X"), 0) | |||
| 396 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |||
| 397 | + | req.set(p.id, p.prop("SRC_W"), w<<16) | |||
| 398 | + | req.set(p.id, p.prop("SRC_H"), h<<16) | |||
| 399 | + | req.set(p.id, p.prop("CRTC_X"), 0) | |||
| 400 | + | req.set(p.id, p.prop("CRTC_Y"), 0) | |||
| 401 | + | req.set(p.id, p.prop("CRTC_W"), w) | |||
| 402 | + | req.set(p.id, p.prop("CRTC_H"), h) | |||
| 403 | + | } | |||
| 404 | + | ||||
| 405 | + | // modeset shows the front image with the output's mode, turning off every | |||
| 406 | + | // other plane on the CRTC; needed at start and after every VT resume. | |||
| 407 | + | // It blocks until the kernel applied it. | |||
| 101 | func (o *Output) modeset() error { | 408 | func (o *Output) modeset() error { | ||
| 102 | - | o.pending = false | 409 | + | blob, err := o.modeBlobFor(o.mode) |
| 410 | + | if err != nil { | |||
| 411 | + | return err | |||
| 412 | + | } | |||
| 413 | + | if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil { | |||
| 414 | + | _ = o.k.destroyBlob(blob) | |||
| 415 | + | return fmt.Errorf("modeset: %w", err) | |||
| 416 | + | } | |||
| 417 | + | // Only now is nothing of ours pending or on screen: on failure the | |||
| 418 | + | // previous buffers may still show. | |||
| 419 | + | o.endPending() | |||
| 420 | + | o.busySince = time.Time{} | |||
| 103 | o.shown, o.queued = 0, 0 | 421 | 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 { | 422 | + | if o.modeBlob != 0 { |
| 105 | - | return fmt.Errorf("set crtc: %w", err) | 423 | + | _ = o.k.destroyBlob(o.modeBlob) |
| 424 | + | } | |||
| 425 | + | o.modeBlob = blob | |||
| 426 | + | o.vrrOn, o.overlayOn = false, 0 | |||
| 427 | + | if o.cursor != nil { | |||
| 428 | + | o.cursor.applied = cursorState{} | |||
| 429 | + | o.cursor.screen, o.cursor.flying = 0, false | |||
| 106 | } | 430 | } | ||
| 107 | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | 431 | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | ||
| 108 | - | // Another master may have left VRR on: start from off. | 432 | + | o.testCursor() |
| 109 | - | o.vrrOn = true | 433 | + | o.sendFormats() |
| 110 | - | o.setVRR(false) | 434 | + | return nil |
| 435 | + | } | |||
| 436 | + | ||||
| 437 | + | // sendFormats reports the formats clients may allocate for direct | |||
| 438 | + | // scanout on this output (none when scanout is off). It never blocks: | |||
| 439 | + | // wayland keeps the latest per output, a dropped report is resent at the | |||
| 440 | + | // next modeset. | |||
| 441 | + | func (o *Output) sendFormats() { | |||
| 442 | + | if o.formats == nil { | |||
| 443 | + | return | |||
| 444 | + | } | |||
| 445 | + | f := ports.OutputFormats{Output: o.conn.name, Device: o.device} | |||
| 446 | + | if o.scanout { | |||
| 447 | + | f.Formats = o.scanoutFormats(o.sampled) | |||
| 448 | + | } | |||
| 449 | + | select { | |||
| 450 | + | case o.formats <- f: | |||
| 451 | + | default: | |||
| 452 | + | o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy") | |||
| 453 | + | } | |||
| 454 | + | } | |||
| 455 | + | ||||
| 456 | + | // testModeset asks KMS whether it would take the modeset of the output | |||
| 457 | + | // images without applying it. | |||
| 458 | + | func (o *Output) testModeset() error { | |||
| 459 | + | blob, err := o.modeBlobFor(o.mode) | |||
| 460 | + | if err != nil { | |||
| 461 | + | return err | |||
| 462 | + | } | |||
| 463 | + | defer func() { _ = o.k.destroyBlob(blob) }() | |||
| 464 | + | if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil { | |||
| 465 | + | return fmt.Errorf("modeset test: %w", err) | |||
| 466 | + | } | |||
| 111 | return nil | 467 | return nil | ||
| 112 | } | 468 | } | ||
| 113 | 469 | ||||
| 114 | - | // present queues a page flip to the frame just drawn into the back image | 470 | + | func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) { |
| 115 | - | // (copied into it by the CPU on the dumb-buffer fallback). | 471 | + | blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))) |
| 116 | - | func (o *Output) present(r ports.Renderer) error { | 472 | + | if err != nil { |
| 117 | - | if b := o.bufs[o.back]; b != nil { | 473 | + | return 0, fmt.Errorf("mode blob: %w", err) |
| 118 | - | r.CopyBGRX(b.mem, int(b.pitch)) | 474 | + | } |
| 475 | + | return blob, nil | |||
| 476 | + | } | |||
| 477 | + | ||||
| 478 | + | // testCursor turns the hardware cursor off when KMS refuses its images on | |||
| 479 | + | // the cursor plane (e.g. a pitch the plane cannot scan out). | |||
| 480 | + | func (o *Output) testCursor() { | |||
| 481 | + | c := o.cursor | |||
| 482 | + | if c == nil || c.off || c.fbs[0] == 0 { | |||
| 483 | + | return | |||
| 484 | + | } | |||
| 485 | + | req := &atomicReq{} | |||
| 486 | + | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) | |||
| 487 | + | if err := o.k.commit(req, atomicTestOnly, 0); refused(err) { | |||
| 488 | + | o.cursorOff(err) | |||
| 489 | + | } | |||
| 490 | + | } | |||
| 491 | + | ||||
| 492 | + | // cursorOff stops using the cursor plane for the rest of the output's life. | |||
| 493 | + | func (o *Output) cursorOff(err error) { | |||
| 494 | + | o.cursor.off = true | |||
| 495 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off") | |||
| 496 | + | } | |||
| 497 | + | ||||
| 498 | + | // modesetReq is the modeset of the front image with mode blob. | |||
| 499 | + | func (o *Output) modesetReq(blob uint32) *atomicReq { | |||
| 500 | + | req := &atomicReq{} | |||
| 501 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | |||
| 502 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |||
| 503 | + | req.set(o.crtc, o.vrrProp, 0) | |||
| 504 | + | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | |||
| 505 | + | o.primaryProps(req, o.fbs[1-o.back]) | |||
| 506 | + | if o.cursor != nil { | |||
| 507 | + | o.cursor.props(req, o.crtc, cursorState{}) | |||
| 508 | + | } | |||
| 509 | + | o.overlayProps(req, overlayWin{}) | |||
| 510 | + | for _, p := range o.stray { | |||
| 511 | + | // Only planes a previous master left on this CRTC. | |||
| 512 | + | props, err := o.k.objProps(p.id, objPlane) | |||
| 513 | + | if err != nil || uint32(props["CRTC_ID"][1]) != o.crtc { | |||
| 514 | + | continue | |||
| 515 | + | } | |||
| 516 | + | req.set(p.id, p.prop("FB_ID"), 0) | |||
| 517 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |||
| 518 | + | } | |||
| 519 | + | return req | |||
| 520 | + | } | |||
| 521 | + | ||||
| 522 | + | // probeAsync checks once whether async commits may carry IN_FENCE_FD. | |||
| 523 | + | func (o *Output) probeAsync(r ports.Renderer) { | |||
| 524 | + | if o.asyncProbed || !o.tearing { | |||
| 525 | + | return | |||
| 526 | + | } | |||
| 527 | + | o.asyncProbed = true | |||
| 528 | + | r.UseTarget(o.back) | |||
| 529 | + | done, err := r.Render(ports.Scene{Background: "#000000"}, nil) | |||
| 530 | + | if err != nil { | |||
| 531 | + | return | |||
| 532 | + | } | |||
| 533 | + | req := &atomicReq{} | |||
| 534 | + | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back])) | |||
| 535 | + | if done != nil { | |||
| 536 | + | defer done.Close() | |||
| 537 | + | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd())) | |||
| 538 | + | } | |||
| 539 | + | err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0) | |||
| 540 | + | o.asyncFence = err == nil && done != nil | |||
| 541 | + | o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe") | |||
| 542 | + | } | |||
| 543 | + | ||||
| 544 | + | // wantVRR is the VRR state the next frame commit sets: on while a buffer | |||
| 545 | + | // is scanned out, off after composing for vrrHold (each toggle may | |||
| 546 | + | // flicker, so short compositions keep it). | |||
| 547 | + | func (o *Output) wantVRR(direct bool) bool { | |||
| 548 | + | if o.vrrProp == 0 { | |||
| 549 | + | return false | |||
| 550 | + | } | |||
| 551 | + | if direct { | |||
| 552 | + | o.composedSince = time.Time{} | |||
| 553 | + | return true | |||
| 119 | } | 554 | } | ||
| 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() { | 555 | if o.composedSince.IsZero() { | ||
| 123 | o.composedSince = time.Now() | 556 | o.composedSince = time.Now() | ||
| 124 | - | } else if time.Since(o.composedSince) > vrrHold { | |||
| 125 | - | o.setVRR(false) | |||
| 126 | } | 557 | } | ||
| 127 | - | o.setAsync(false) | 558 | + | return o.vrrOn && time.Since(o.composedSince) <= vrrHold |
| 128 | - | if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil { | 559 | + | } |
| 560 | + | ||||
| 561 | + | // commitFrame flips fb in one commit with the cursor and VRR. An async | |||
| 562 | + | // commit carries only the primary plane (kernel rule): a frame that must | |||
| 563 | + | // also move the cursor or change VRR flips at vblank. fence is the | |||
| 564 | + | // frame's GPU fence (nil: none); the caller keeps and closes it. | |||
| 565 | + | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | |||
| 566 | + | return o.commitWith(fb, fence, async, vrr, f, overlayWin{}) | |||
| 567 | + | } | |||
| 568 | + | ||||
| 569 | + | // commitWith is commitFrame with ov on the overlay plane (zero: off). | |||
| 570 | + | func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error { | |||
| 571 | + | cur := cursorState{} | |||
| 572 | + | if o.cursor != nil { | |||
| 573 | + | cur = o.cursor.desired() | |||
| 574 | + | } | |||
| 575 | + | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) { | |||
| 576 | + | async = false | |||
| 577 | + | } | |||
| 578 | + | req := &atomicReq{} | |||
| 579 | + | flags := uint32(atomicNonblock | flipEventFlag) | |||
| 580 | + | if async { | |||
| 581 | + | flags |= flipAsyncFlag | |||
| 582 | + | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb)) | |||
| 583 | + | } else { | |||
| 584 | + | o.primaryProps(req, fb) | |||
| 585 | + | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |||
| 586 | + | if o.cursor != nil { | |||
| 587 | + | o.cursor.props(req, o.crtc, cur) | |||
| 588 | + | } | |||
| 589 | + | if ov.buf != 0 || o.overlayOn != 0 { | |||
| 590 | + | o.overlayProps(req, ov) | |||
| 591 | + | } | |||
| 592 | + | } | |||
| 593 | + | if fence != nil { | |||
| 594 | + | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |||
| 595 | + | } | |||
| 596 | + | // The kernel takes its own reference on the fence. | |||
| 597 | + | if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil { | |||
| 598 | + | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |||
| 599 | + | return o.commitWith(fb, fence, false, vrr, f, ov) | |||
| 600 | + | } | |||
| 601 | + | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |||
| 602 | + | // Retry without turning VRR on: if that passes, the driver | |||
| 603 | + | // refuses VRR on this output. | |||
| 604 | + | if o.commitFrame(fb, fence, false, false, f) == nil { | |||
| 605 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | |||
| 606 | + | o.vrrProp = 0 | |||
| 607 | + | return nil | |||
| 608 | + | } | |||
| 609 | + | } | |||
| 610 | + | return err | |||
| 611 | + | } | |||
| 612 | + | if !async { | |||
| 613 | + | if o.vrrOn != vrr { | |||
| 614 | + | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |||
| 615 | + | } | |||
| 616 | + | o.vrrOn = vrr | |||
| 617 | + | } | |||
| 618 | + | if o.cursor != nil { | |||
| 619 | + | // An async commit leaves the cursor plane as applied. | |||
| 620 | + | o.cursor.committed(cur) | |||
| 621 | + | } | |||
| 622 | + | if !async { | |||
| 623 | + | o.overlayOn = ov.buf | |||
| 624 | + | } | |||
| 625 | + | f.frame, f.async = true, async | |||
| 626 | + | f.fences = dupFences(fence, ov.acquire) | |||
| 627 | + | o.setAsync(async) | |||
| 628 | + | o.begin(f) | |||
| 629 | + | return nil | |||
| 630 | + | } | |||
| 631 | + | ||||
| 632 | + | // cursorRefused reports, after a commit carrying the cursor failed with | |||
| 633 | + | // EINVAL, whether the cursor plane was the cause; then the cursor is off | |||
| 634 | + | // and the commit can be retried without it. | |||
| 635 | + | func (o *Output) cursorRefused() bool { | |||
| 636 | + | o.testCursor() | |||
| 637 | + | return o.cursor.off | |||
| 638 | + | } | |||
| 639 | + | ||||
| 640 | + | // commitState commits a cursor or VRR change without a new frame. | |||
| 641 | + | func (o *Output) commitState(vrr bool) error { | |||
| 642 | + | req := &atomicReq{} | |||
| 643 | + | cur := cursorState{} | |||
| 644 | + | if o.cursor != nil { | |||
| 645 | + | cur = o.cursor.desired() | |||
| 646 | + | if cur != o.cursor.applied { | |||
| 647 | + | o.cursor.props(req, o.crtc, cur) | |||
| 648 | + | } | |||
| 649 | + | } | |||
| 650 | + | if vrr != o.vrrOn { | |||
| 651 | + | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |||
| 652 | + | } | |||
| 653 | + | if len(req.objs) == 0 { | |||
| 654 | + | return nil | |||
| 655 | + | } | |||
| 656 | + | if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil { | |||
| 657 | + | if o.overlayConflict(err, o.overlayOn) { | |||
| 658 | + | // The next frame is composed without the overlay. | |||
| 659 | + | return errOverlayDropped | |||
| 660 | + | } | |||
| 661 | + | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |||
| 662 | + | return o.commitState(vrr) | |||
| 663 | + | } | |||
| 129 | return err | 664 | return err | ||
| 130 | } | 665 | } | ||
| 131 | - | o.pending, o.flipStart = true, time.Now() | 666 | + | if o.cursor != nil { |
| 132 | - | o.queued = 0 | 667 | + | o.cursor.committed(cur) |
| 133 | - | o.back = 1 - o.back | 668 | + | } |
| 669 | + | if vrr != o.vrrOn { | |||
| 670 | + | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |||
| 671 | + | } | |||
| 672 | + | o.vrrOn = vrr | |||
| 673 | + | o.begin(pendingFrame{}) | |||
| 134 | return nil | 674 | return nil | ||
| 135 | } | 675 | } | ||
| 136 | 676 | ||||
| 137 | - | // tryScanout flips a fullscreen client buffer directly. It reports false, | 677 | + | func boolValue(b bool) uint64 { |
| 138 | - | // with the reason logged on change, when the frame must be composed. | 678 | + | if b { |
| 139 | - | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | 679 | + | return 1 |
| 680 | + | } | |||
| 681 | + | return 0 | |||
| 682 | + | } | |||
| 683 | + | ||||
| 684 | + | // scanoutFrame picks a fullscreen client buffer to flip directly. It | |||
| 685 | + | // returns fb 0, with the reason logged on change, when the frame must be | |||
| 686 | + | // composed. | |||
| 687 | + | func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent) { | |||
| 140 | reason := "disabled" | 688 | reason := "disabled" | ||
| 141 | - | var c ports.SurfaceContent | |||
| 142 | if o.scanout { | 689 | if o.scanout { | ||
| 143 | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | 690 | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | ||
| 144 | } | 691 | } | ||
| 145 | - | var fb uint32 | |||
| 146 | if reason == "" { | 692 | if reason == "" { | ||
| 147 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | 693 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | ||
| 148 | } | 694 | } | ||
| @@ -151,71 +697,51 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||||
| 151 | o.reason = reason | 697 | o.reason = reason | ||
| 152 | } | 698 | } | ||
| 153 | if reason != "" { | 699 | if reason != "" { | ||
| 154 | - | return false, nil | 700 | + | 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 | } | 701 | } | ||
| 702 | + | return fb, c | |||
| 703 | + | } | |||
| 704 | + | ||||
| 705 | + | // commitScanout flips a client buffer. It reports false when KMS refused | |||
| 706 | + | // the buffer: the frame must be composed. | |||
| 707 | + | func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) { | |||
| 708 | + | cfb := o.clientFBs[c.DMABuf.ID] | |||
| 160 | // Drivers refuse async flips that change the format or modifier: the | 709 | // Drivers refuse async flips that change the format or modifier: the | ||
| 161 | // flip from the composed image into scanout waits for vblank. | 710 | // 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 | 711 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | ||
| 164 | - | err := flip(o.fd, o.crtc, fb, async) | 712 | + | vrr := o.wantVRR(true) |
| 713 | + | f.queued, f.zeroCopy = c.DMABuf.ID, c.ID | |||
| 714 | + | // Explicit sync: KMS waits on the client's acquire fence. Wayland may | |||
| 715 | + | // close its file at any time: commit a duplicate taken while it is | |||
| 716 | + | // held open (the kernel keeps its own reference). | |||
| 717 | + | fence := dupFence(c.Acquire) | |||
| 718 | + | if fence != nil { | |||
| 719 | + | defer fence.Close() | |||
| 720 | + | } | |||
| 721 | + | err := o.commitFrame(fb, fence, async, vrr, f) | |||
| 165 | if err != nil && async && errors.Is(err, unix.EINVAL) { | 722 | if err != nil && async && errors.Is(err, unix.EINVAL) { | ||
| 166 | - | // Refused (e.g. a layout change, or not a fast update): this | 723 | + | // Refused (e.g. not a fast update): this buffer flips at vblank |
| 167 | - | // buffer flips at vblank from now on. | 724 | + | // from now on. |
| 168 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 725 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 169 | - | cfb.noAsync, async = true, false | 726 | + | cfb.noAsync = true |
| 170 | - | err = flip(o.fd, o.crtc, fb, false) | 727 | + | err = o.commitFrame(fb, fence, false, vrr, f) |
| 171 | } | 728 | } | ||
| 172 | - | if err == nil { | 729 | + | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { |
| 173 | - | o.setAsync(async) | 730 | + | // KMS refuses this buffer on the plane: compose it instead. |
| 174 | - | o.setVRR(true) | 731 | + | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") |
| 175 | - | o.composedSince = time.Time{} | 732 | + | cfb.failed, o.reason = "flip_refused", "flip_refused" |
| 733 | + | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |||
| 734 | + | return false, nil | |||
| 176 | } | 735 | } | ||
| 177 | - | if err != nil { | 736 | + | if err == nil { |
| 178 | - | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | 737 | + | o.queued = c.DMABuf.ID |
| 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 | } | 738 | } | ||
| 188 | - | o.pending, o.flipStart = true, time.Now() | 739 | + | return true, err |
| 189 | - | o.queued = c.DMABuf.ID | |||
| 190 | - | return true, nil | |||
| 191 | } | 740 | } | ||
| 192 | 741 | ||||
| 193 | // vrrHold is how long composition runs before VRR turns off. | 742 | // vrrHold is how long composition runs before VRR turns off. | ||
| 194 | const vrrHold = 500 * time.Millisecond | 743 | const vrrHold = 500 * time.Millisecond | ||
| 195 | 744 | ||||
| 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. | 745 | // setAsync logs when the output starts or stops tearing. | ||
| 220 | func (o *Output) setAsync(on bool) { | 746 | func (o *Output) setAsync(on bool) { | ||
| 221 | if on != o.async { | 747 | if on != o.async { | ||
| @@ -227,30 +753,77 @@ func (o *Output) setAsync(on bool) { | |||||
| 227 | // Close restores the CRTC state found at startup, when it had a mode, and | 753 | // Close restores the CRTC state found at startup, when it had a mode, and | ||
| 228 | // frees buffers. | 754 | // frees buffers. | ||
| 229 | func (o *Output) Close() { | 755 | func (o *Output) Close() { | ||
| 756 | + | req := &atomicReq{} | |||
| 230 | if o.cursor != nil { | 757 | if o.cursor != nil { | ||
| 231 | - | o.cursor.close() | 758 | + | o.cursor.props(req, o.crtc, cursorState{}) |
| 232 | } | 759 | } | ||
| 233 | - | o.setVRR(false) | 760 | + | o.overlayProps(req, overlayWin{}) |
| 234 | - | if o.saved.crtcID != 0 { | 761 | + | req.set(o.crtc, o.vrrProp, 0) |
| 235 | - | ids := []uint32{o.conn.id} | 762 | + | s := o.saved |
| 236 | - | s := o.saved | 763 | + | var blob uint32 |
| 237 | - | if s.modeValid != 0 { | 764 | + | p := o.primary |
| 238 | - | _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode) | 765 | + | restored := false |
| 766 | + | if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 { | |||
| 767 | + | if b, err := o.modeBlobFor(s.mode); err == nil { | |||
| 768 | + | blob, restored = b, true | |||
| 769 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b)) | |||
| 770 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |||
| 771 | + | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | |||
| 772 | + | req.set(p.id, p.prop("FB_ID"), uint64(s.fbID)) | |||
| 773 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |||
| 774 | + | req.set(p.id, p.prop("SRC_X"), 0) | |||
| 775 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |||
| 776 | + | req.set(p.id, p.prop("SRC_W"), uint64(s.mode.HDisplay)<<16) | |||
| 777 | + | req.set(p.id, p.prop("SRC_H"), uint64(s.mode.VDisplay)<<16) | |||
| 778 | + | req.set(p.id, p.prop("CRTC_X"), 0) | |||
| 779 | + | req.set(p.id, p.prop("CRTC_Y"), 0) | |||
| 780 | + | req.set(p.id, p.prop("CRTC_W"), uint64(s.mode.HDisplay)) | |||
| 781 | + | req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay)) | |||
| 239 | } | 782 | } | ||
| 240 | } | 783 | } | ||
| 784 | + | if !restored { | |||
| 785 | + | // Nothing to give back: leave the CRTC off. | |||
| 786 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 0) | |||
| 787 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], 0) | |||
| 788 | + | req.set(p.id, p.prop("FB_ID"), 0) | |||
| 789 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |||
| 790 | + | req.set(o.conn.id, o.connCrtc, 0) | |||
| 791 | + | } | |||
| 792 | + | // Best effort: at exit the card may already belong to another session. | |||
| 793 | + | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | |||
| 794 | + | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("restore crtc") | |||
| 795 | + | } | |||
| 796 | + | if blob != 0 { | |||
| 797 | + | _ = o.k.destroyBlob(blob) | |||
| 798 | + | } | |||
| 799 | + | if o.modeBlob != 0 { | |||
| 800 | + | _ = o.k.destroyBlob(o.modeBlob) | |||
| 801 | + | o.modeBlob = 0 | |||
| 802 | + | } | |||
| 803 | + | if o.cursor != nil { | |||
| 804 | + | o.cursor.free(o.k) | |||
| 805 | + | } | |||
| 241 | o.freeImages() | 806 | o.freeImages() | ||
| 807 | + | o.endPending() | |||
| 808 | + | o.dropRead() | |||
| 242 | o.shown, o.queued = 0, 0 | 809 | o.shown, o.queued = 0, 0 | ||
| 243 | o.dropClientFBs(time.Now(), true) | 810 | o.dropClientFBs(time.Now(), true) | ||
| 244 | } | 811 | } | ||
| 245 | 812 | ||||
| 246 | - | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | 813 | + | // Run renders scenes and commits them until ctx ends. active reports seat |
| 247 | - | // Each completed page flip is reported on presented (dropped when full). | 814 | + | // 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 { | 815 | 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()) | 816 | r, err := newRenderer(o.Width(), o.Height()) | ||
| 250 | if err != nil { | 817 | if err != nil { | ||
| 251 | return fmt.Errorf("create renderer: %w", err) | 818 | return fmt.Errorf("create renderer: %w", err) | ||
| 252 | } | 819 | } | ||
| 253 | defer r.Close() | 820 | defer r.Close() | ||
| 821 | + | if o.cursor != nil { | |||
| 822 | + | if err := o.cursor.setup(o.k, r); err != nil { | |||
| 823 | + | o.cursor.off = true | |||
| 824 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") | |||
| 825 | + | } | |||
| 826 | + | } | |||
| 254 | // The seat sends its state first; while switched away (e.g. a monitor | 827 | // 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. | 828 | // plugged in on another VT) the modeset waits for the enable. | ||
| 256 | enabled := true | 829 | enabled := true | ||
| @@ -258,15 +831,13 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 258 | case enabled = <-active: | 831 | case enabled = <-active: | ||
| 259 | default: | 832 | default: | ||
| 260 | } | 833 | } | ||
| 261 | - | // Output images, from the best to the fallback (ADR 014). While | 834 | + | // Output images, from the best to the linear fallback (ADR 014). |
| 262 | - | // switched away they are validated by the first modeset on enable. | 835 | + | // While switched away they are made on the first enable. |
| 263 | if enabled { | 836 | if enabled { | ||
| 264 | - | err = o.showImages(r, imagesDriver, nil) | 837 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { |
| 265 | - | } else { | 838 | + | return err |
| 266 | - | o.kind, err = o.setupImages(r, imagesDriver, nil) | 839 | + | } |
| 267 | - | } | 840 | + | o.probeAsync(r) |
| 268 | - | if err != nil { | |||
| 269 | - | return err | |||
| 270 | } | 841 | } | ||
| 271 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | 842 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | ||
| 272 | var scene ports.Scene | 843 | var scene ports.Scene | ||
| @@ -276,91 +847,113 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 276 | frame := 0 | 847 | frame := 0 | ||
| 277 | stats := time.NewTicker(10 * time.Second) | 848 | stats := time.NewTicker(10 * time.Second) | ||
| 278 | defer stats.Stop() | 849 | defer stats.Stop() | ||
| 279 | - | // seen is the latest content Seq per window, reported to wayland with | 850 | + | // seen is the latest content Seq per window. It is reported only while |
| 280 | - | // what the output scans out, after each frame decision. | 851 | + | // no frame is in flight: then the GPU has finished every frame that |
| 852 | + | // read older buffers (the kernel waited for their fence to flip). | |||
| 281 | seen := map[ports.WindowID]uint64{} | 853 | seen := map[ports.WindowID]uint64{} | ||
| 282 | reportDirty := false | 854 | reportDirty := false | ||
| 283 | - | flipped := false | 855 | + | var cursorWake <-chan struct{} |
| 856 | + | if o.cursor != nil { | |||
| 857 | + | cursorWake = o.cursor.wake | |||
| 858 | + | } | |||
| 284 | for { | 859 | for { | ||
| 285 | - | if flipped || reportDirty { | 860 | + | readWait := false |
| 286 | - | o.report(presented, flipped, seen) | 861 | + | if reportDirty && !o.pending { |
| 287 | - | flipped, reportDirty = false, false | 862 | + | if readWait = !o.readDone(); !readWait { |
| 288 | - | } else { | 863 | + | o.report(nil, seen) |
| 289 | - | o.flushReport(presented) | 864 | + | reportDirty = false |
| 865 | + | } | |||
| 290 | } | 866 | } | ||
| 291 | - | // An unsent report is retried soon, not only on the next event. | 867 | + | o.flushReport(presented) |
| 868 | + | // An unsent report is retried soon, not only on the next event; | |||
| 869 | + | // so is one that waits for an uncommitted frame's fence. | |||
| 292 | var retry <-chan time.Time | 870 | var retry <-chan time.Time | ||
| 293 | - | if o.unsent.Output != "" { | 871 | + | if len(o.unsent) > 0 || readWait { |
| 294 | retry = time.After(time.Millisecond) | 872 | retry = time.After(time.Millisecond) | ||
| 295 | } | 873 | } | ||
| 296 | // A composed screen that stopped changing still drops VRR on time. | 874 | // A composed screen that stopped changing still drops VRR on time. | ||
| 297 | var vrrOff <-chan time.Time | 875 | var vrrOff <-chan time.Time | ||
| 298 | - | if enabled && o.vrrOn && !o.composedSince.IsZero() { | 876 | + | if enabled && o.vrrOn && !o.pending && !o.composedSince.IsZero() { |
| 299 | vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince))) | 877 | vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince))) | ||
| 300 | } | 878 | } | ||
| 879 | + | // A commit whose event never comes must not stop the output. | |||
| 880 | + | var stuck <-chan time.Time | |||
| 881 | + | if enabled && o.pending { | |||
| 882 | + | stuck = time.After(max(0, time.Until(o.stuckAt))) | |||
| 883 | + | } | |||
| 884 | + | stateDirty := false | |||
| 301 | select { | 885 | select { | ||
| 302 | case <-retry: | 886 | case <-retry: | ||
| 303 | continue | 887 | continue | ||
| 304 | - | case <-vrrOff: | 888 | + | case <-stuck: |
| 305 | - | if o.setVRR(false); o.vrrOn { | 889 | + | if o.pending && o.stuckAt.After(time.Now()) { |
| 306 | - | o.composedSince = time.Now() // failed: retry later, not in a loop | 890 | + | continue // waits for a fence |
| 307 | } | 891 | } | ||
| 308 | - | continue | 892 | + | if o.expire() { |
| 893 | + | if err := o.modeset(); err != nil { | |||
| 894 | + | if !o.commitFailed(err, &enabled) { | |||
| 895 | + | return fmt.Errorf("recovery modeset: %w", err) | |||
| 896 | + | } | |||
| 897 | + | continue | |||
| 898 | + | } | |||
| 899 | + | } | |||
| 900 | + | dirty = haveScene | |||
| 901 | + | stateDirty = true | |||
| 902 | + | case <-vrrOff: | |||
| 903 | + | stateDirty = true | |||
| 904 | + | case <-cursorWake: | |||
| 905 | + | stateDirty = true | |||
| 309 | case <-ctx.Done(): | 906 | case <-ctx.Done(): | ||
| 310 | return nil | 907 | return nil | ||
| 311 | case on := <-active: | 908 | case on := <-active: | ||
| 312 | // Modeset on every enable: a fast disable+enable can coalesce to one true. | 909 | // Modeset on every enable: a fast disable+enable can coalesce to one true. | ||
| 313 | if on { | 910 | if on { | ||
| 314 | - | err := o.modeset() | 911 | + | var err error |
| 912 | + | if o.fbs[0] == 0 { | |||
| 913 | + | err = o.showImages(r, imagesDriver, nil) | |||
| 914 | + | } else { | |||
| 915 | + | err = o.modeset() | |||
| 916 | + | } | |||
| 315 | if lostMaster(err) { | 917 | if lostMaster(err) { | ||
| 316 | // The seat took DRM master back: wait for the next enable. | 918 | // The seat took DRM master back: wait for the next enable. | ||
| 317 | o.log.Warn().Err(err).Msg("resume") | 919 | o.log.Warn().Err(err).Msg("resume") | ||
| 318 | continue | 920 | continue | ||
| 319 | } | 921 | } | ||
| 320 | - | if refused(err) && !o.validated && o.kind < imagesDumb { | |||
| 321 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images") | |||
| 322 | - | o.freeImages() | |||
| 323 | - | err = o.showImages(r, o.kind+1, err) | |||
| 324 | - | } | |||
| 325 | if err != nil { | 922 | if err != nil { | ||
| 326 | o.log.Error().Err(err).Msg("resume") | 923 | o.log.Error().Err(err).Msg("resume") | ||
| 327 | return err | 924 | return err | ||
| 328 | } | 925 | } | ||
| 329 | - | o.validated = true | 926 | + | o.probeAsync(r) |
| 330 | dirty = haveScene | 927 | dirty = haveScene | ||
| 331 | - | if o.cursor != nil { | 928 | + | stateDirty = true |
| 332 | - | if err := o.cursor.Reapply(); err != nil { | |||
| 333 | - | o.log.Warn().Err(err).Msg("cursor") | |||
| 334 | - | } | |||
| 335 | - | } | |||
| 336 | } | 929 | } | ||
| 337 | enabled = on | 930 | enabled = on | ||
| 338 | o.log.Info().Bool("enabled", on).Msg("output") | 931 | o.log.Info().Bool("enabled", on).Msg("output") | ||
| 339 | - | case n := <-o.flipped: | 932 | + | case ev := <-o.flipped: |
| 340 | - | if !o.pending { | 933 | + | if !o.completed(ev, seen) { |
| 341 | continue // stale event from before a modeset | 934 | continue // stale event from before a modeset | ||
| 342 | } | 935 | } | ||
| 343 | - | o.pending = false | 936 | + | stateDirty = true |
| 344 | - | o.flips += n | 937 | + | if o.cursor != nil { |
| 345 | - | o.shown = o.queued | 938 | + | // An image that waited for a free slot loads now. |
| 346 | - | o.queued = 0 | 939 | + | if _, err := o.cursor.flushLater(r); err != nil { |
| 347 | - | flipped = true | 940 | + | o.log.Warn().Err(err).Msg("cursor") |
| 348 | - | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 941 | + | } |
| 349 | - | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | |||
| 350 | } | 942 | } | ||
| 351 | case s := <-scenes: | 943 | case s := <-scenes: | ||
| 352 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | 944 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | ||
| 353 | cursorScale = s.Scale | 945 | cursorScale = s.Scale | ||
| 354 | - | o.setCursor(loadCursor, want, s.Scale) | 946 | + | o.setCursor(r, loadCursor, want, s.Scale) |
| 947 | + | stateDirty = true | |||
| 355 | } | 948 | } | ||
| 356 | scene, haveScene, dirty = s, true, true | 949 | scene, haveScene, dirty = s, true, true | ||
| 357 | case c := <-cursor: | 950 | case c := <-cursor: | ||
| 358 | want = c | 951 | want = c | ||
| 359 | // Before the first scene the scale is unknown: loaded then. | 952 | // Before the first scene the scale is unknown: loaded then. | ||
| 360 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | 953 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | ||
| 361 | - | o.setCursor(loadCursor, want, cursorScale) | 954 | + | o.setCursor(r, loadCursor, want, cursorScale) |
| 955 | + | stateDirty = true | |||
| 362 | } | 956 | } | ||
| 363 | - | continue | |||
| 364 | case c := <-contents: | 957 | case c := <-contents: | ||
| 365 | if c.Empty() { | 958 | if c.Empty() { | ||
| 366 | delete(surfaces, c.ID) | 959 | delete(surfaces, c.ID) | ||
| @@ -375,53 +968,167 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 375 | dirty = dirty || scene.Shows(c.ID) | 968 | dirty = dirty || scene.Shows(c.ID) | ||
| 376 | case <-stats.C: | 969 | case <-stats.C: | ||
| 377 | o.dropClientFBs(time.Now(), false) | 970 | o.dropClientFBs(time.Now(), false) | ||
| 378 | - | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | 971 | + | ev := o.log.Info().Str("connector", o.conn.name).Int("frames", frame).Int("flips", o.flips).Bool("pending", o.pending) |
| 379 | if o.cursor != nil { | 972 | if o.cursor != nil { | ||
| 380 | cs := o.cursor.TakeStats() | 973 | cs := o.cursor.TakeStats() | ||
| 381 | - | ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_ioctls", cs.Ioctls).Dur("cursor_max_ioctl_ms", cs.MaxIoctl) | 974 | + | ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_commits", cs.Commits) |
| 382 | } | 975 | } | ||
| 383 | ev.Msg("stats") | 976 | ev.Msg("stats") | ||
| 384 | } | 977 | } | ||
| 385 | - | if !dirty || !haveScene || !enabled || o.pending { | 978 | + | if !enabled || o.pending { |
| 979 | + | continue | |||
| 980 | + | } | |||
| 981 | + | if !dirty || !haveScene { | |||
| 982 | + | if stateDirty { | |||
| 983 | + | err := o.commitState(o.wantVRR(o.shown != 0)) | |||
| 984 | + | if errors.Is(err, errOverlayDropped) { | |||
| 985 | + | dirty = haveScene | |||
| 986 | + | continue | |||
| 987 | + | } | |||
| 988 | + | if err != nil && !o.commitFailed(err, &enabled) { | |||
| 989 | + | return fmt.Errorf("commit: %w", err) | |||
| 990 | + | } | |||
| 991 | + | } | |||
| 386 | continue | 992 | continue | ||
| 387 | } | 993 | } | ||
| 388 | start := time.Now() | 994 | start := time.Now() | ||
| 389 | - | direct, err := o.tryScanout(scene, surfaces) | 995 | + | // shows is what the frame puts on screen: presentation feedback |
| 996 | + | // of windows it does not draw is discarded, not presented. | |||
| 997 | + | f := pendingFrame{shows: shownBy(scene, seen)} | |||
| 998 | + | fb, c := o.scanoutFrame(scene, surfaces) | |||
| 999 | + | direct := false | |||
| 1000 | + | if fb != 0 { | |||
| 1001 | + | direct, err = o.commitScanout(fb, c, pendingFrame{shows: map[ports.WindowID]uint64{c.ID: seen[c.ID]}}) | |||
| 1002 | + | } | |||
| 390 | if !direct { | 1003 | if !direct { | ||
| 1004 | + | ov, composed := o.overlayFrame(scene, surfaces) | |||
| 391 | r.UseTarget(o.back) | 1005 | r.UseTarget(o.back) | ||
| 392 | - | if err := r.Render(scene, surfaces); err != nil { | 1006 | + | done, rerr := r.Render(composed, surfaces) |
| 393 | - | return fmt.Errorf("render frame: %w", err) | 1007 | + | if rerr != nil { |
| 1008 | + | ov.close() | |||
| 1009 | + | return fmt.Errorf("render frame: %w", rerr) | |||
| 1010 | + | } | |||
| 1011 | + | if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) { | |||
| 1012 | + | // Refused: compose the window too (cached per buffer). | |||
| 1013 | + | if done != nil { | |||
| 1014 | + | done.Close() | |||
| 1015 | + | } | |||
| 1016 | + | ov.close() | |||
| 1017 | + | ov = overlayWin{} | |||
| 1018 | + | if done, rerr = r.Render(scene, surfaces); rerr != nil { | |||
| 1019 | + | return fmt.Errorf("render frame: %w", rerr) | |||
| 1020 | + | } | |||
| 1021 | + | } | |||
| 1022 | + | // The overlay buffer is on screen like a scanned-out one: it | |||
| 1023 | + | // is reported shown, so it is not released under the plane. | |||
| 1024 | + | f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true | |||
| 1025 | + | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | |||
| 1026 | + | if err != nil { | |||
| 1027 | + | // The GPU may still read client buffers for this frame. | |||
| 1028 | + | o.holdRead(done) | |||
| 1029 | + | } | |||
| 1030 | + | if done != nil { | |||
| 1031 | + | done.Close() | |||
| 1032 | + | } | |||
| 1033 | + | ov.close() | |||
| 1034 | + | if err != nil && o.overlayConflict(err, ov.buf) { | |||
| 1035 | + | err = errOverlayDropped | |||
| 1036 | + | } | |||
| 1037 | + | if err == nil { | |||
| 1038 | + | o.queued = ov.buf | |||
| 1039 | + | o.back = 1 - o.back | |||
| 394 | } | 1040 | } | ||
| 395 | - | err = o.present(r) | |||
| 396 | } | 1041 | } | ||
| 397 | - | copyStart := time.Now() | |||
| 398 | if err != nil { | 1042 | if err != nil { | ||
| 399 | - | if errors.Is(err, unix.EBUSY) { | 1043 | + | if errors.Is(err, errOverlayDropped) { |
| 400 | - | // A flip is still in flight; retry after its event. | 1044 | + | dirty = true |
| 401 | - | o.pending, o.flipStart = true, time.Now() | |||
| 402 | - | continue // dirty stays true | |||
| 403 | - | } | |||
| 404 | - | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { | |||
| 405 | - | // Lost DRM master mid-switch; the seat disable follows. | |||
| 406 | - | o.log.Warn().Err(err).Msg("flip refused") | |||
| 407 | - | enabled = false | |||
| 408 | continue | 1045 | continue | ||
| 409 | } | 1046 | } | ||
| 410 | - | o.log.Error().Err(err).Msg("page flip") | 1047 | + | if !o.commitFailed(err, &enabled) { |
| 411 | - | return fmt.Errorf("page flip: %w", err) | 1048 | + | return fmt.Errorf("page flip: %w", err) |
| 1049 | + | } | |||
| 1050 | + | continue // dirty stays true | |||
| 412 | } | 1051 | } | ||
| 413 | dirty = false | 1052 | dirty = false | ||
| 414 | - | reportDirty = true | |||
| 415 | frame++ | 1053 | 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") | 1054 | + | 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") |
| 417 | } | 1055 | } | ||
| 418 | } | 1056 | } | ||
| 419 | 1057 | ||||
| 420 | - | // setCursor loads a cursor for scale onto the cursor plane. | 1058 | + | // commitFailed handles a commit error that does not stop the output: a |
| 421 | - | func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64) { | 1059 | + | // commit still in flight (EBUSY, retried after its event) or DRM master |
| 1060 | + | // lost mid-switch (the seat disable follows). | |||
| 1061 | + | func (o *Output) commitFailed(err error, enabled *bool) bool { | |||
| 1062 | + | switch { | |||
| 1063 | + | case errors.Is(err, unix.EBUSY): | |||
| 1064 | + | // The kernel still works on an earlier commit (e.g. across a VT | |||
| 1065 | + | // switch, or one whose event already came): wait for any event | |||
| 1066 | + | // of this CRTC, or retry after busyRetry since none may come. | |||
| 1067 | + | o.endPending() | |||
| 1068 | + | now := time.Now() | |||
| 1069 | + | o.pending, o.flipStart, o.stuckAt = true, now, now.Add(busyRetry) | |||
| 1070 | + | o.pendingSerial = 0 | |||
| 1071 | + | // A run of EBUSY: each refusal follows the previous retry. | |||
| 1072 | + | if o.busySince.IsZero() || now.Sub(o.lastBusy) > 4*busyRetry { | |||
| 1073 | + | o.busySince = now | |||
| 1074 | + | o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy") | |||
| 1075 | + | } | |||
| 1076 | + | o.lastBusy = now | |||
| 1077 | + | return true | |||
| 1078 | + | case lostMaster(err): | |||
| 1079 | + | o.log.Warn().Err(err).Msg("commit refused") | |||
| 1080 | + | *enabled = false | |||
| 1081 | + | return true | |||
| 1082 | + | } | |||
| 1083 | + | o.log.Error().Err(err).Msg("commit") | |||
| 1084 | + | return false | |||
| 1085 | + | } | |||
| 1086 | + | ||||
| 1087 | + | // completed handles the event of the pending commit: a flipped frame is | |||
| 1088 | + | // reported with its kernel timestamp. It reports false for an event of | |||
| 1089 | + | // another commit, which is dropped. | |||
| 1090 | + | func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |||
| 1091 | + | if !o.pending || o.pendingSerial != 0 && ev.user>>userKindBits != o.pendingSerial { | |||
| 1092 | + | return false | |||
| 1093 | + | } | |||
| 1094 | + | f := o.pendingFrame | |||
| 1095 | + | o.endPending() | |||
| 1096 | + | o.busySince = time.Time{} | |||
| 1097 | + | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |||
| 1098 | + | o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip") | |||
| 1099 | + | } | |||
| 1100 | + | if o.cursor != nil { | |||
| 1101 | + | o.cursor.landed() | |||
| 1102 | + | } | |||
| 1103 | + | if !f.frame { | |||
| 1104 | + | return true | |||
| 1105 | + | } | |||
| 1106 | + | if f.composed { | |||
| 1107 | + | // Frames render in order: this flip's fence signalled after | |||
| 1108 | + | // those of frames rendered before it and not committed. | |||
| 1109 | + | o.dropRead() | |||
| 1110 | + | } | |||
| 1111 | + | o.flips++ | |||
| 1112 | + | o.shown = f.queued | |||
| 1113 | + | if o.queued == f.queued { | |||
| 1114 | + | o.queued = 0 | |||
| 1115 | + | } | |||
| 1116 | + | refresh := time.Duration(0) | |||
| 1117 | + | if !o.vrrOn { | |||
| 1118 | + | refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli()))) | |||
| 1119 | + | } | |||
| 1120 | + | 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) | |||
| 1121 | + | return true | |||
| 1122 | + | } | |||
| 1123 | + | ||||
| 1124 | + | // setCursor loads a cursor for scale into the cursor images. | |||
| 1125 | + | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) { | |||
| 1126 | + | if o.cursor.off { | |||
| 1127 | + | return | |||
| 1128 | + | } | |||
| 422 | img, err := load(c, scale, o.cursor.Limit()) | 1129 | img, err := load(c, scale, o.cursor.Limit()) | ||
| 423 | if err == nil { | 1130 | if err == nil { | ||
| 424 | - | err = o.cursor.SetImage(img.Pixels, img.W, img.H, img.HotX, img.HotY) | 1131 | + | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) |
| 425 | } | 1132 | } | ||
| 426 | if err != nil { | 1133 | if err != nil { | ||
| 427 | o.log.Warn().Err(err).Msg("cursor") | 1134 | o.log.Warn().Err(err).Msg("cursor") | ||
| @@ -430,26 +1137,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") | 1137 | 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 | } | 1138 | } | ||
| 432 | 1139 | ||||
| 433 | - | // report tells wayland what the output shows and has read, so replaced | 1140 | + | // 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 | 1141 | + | // folded into the next one. |
| 435 | - | // buffer is safe once a later content is seen and it is neither shown nor | 1142 | + | 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 | 1143 | ||||
| 444 | - | // flushReport sends the unsent report, if any, without blocking. | 1144 | + | // report queues what the output shows and has read, so replaced client |
| 445 | - | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | 1145 | + | // buffers can be released. A flip-less report merges into the newest |
| 446 | - | if o.unsent.Output == "" { | 1146 | + | // queued one; flips are never merged unless maxUnsent is reached. |
| 1147 | + | func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { | |||
| 1148 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |||
| 1149 | + | if n := len(o.unsent); n > 0 && (flip == nil || o.unsent[n-1].Flip == nil) { | |||
| 1150 | + | if flip == nil { | |||
| 1151 | + | r.Flip = o.unsent[n-1].Flip | |||
| 1152 | + | } | |||
| 1153 | + | o.unsent[n-1] = r | |||
| 447 | return | 1154 | return | ||
| 448 | } | 1155 | } | ||
| 449 | - | select { | 1156 | + | o.unsent = append(o.unsent, r) |
| 450 | - | case presented <- o.unsent: | 1157 | + | if len(o.unsent) > maxUnsent { |
| 451 | - | o.unsent = ports.OutputPresented{} | 1158 | + | old, next := o.unsent[0].Flip, o.unsent[1].Flip |
| 452 | - | default: | 1159 | + | if next != nil && old != nil { |
| 1160 | + | next.Merged += 1 + old.Merged | |||
| 1161 | + | for id, seq := range old.Shows { | |||
| 1162 | + | if _, ok := next.Shows[id]; !ok { | |||
| 1163 | + | next.Shows[id] = seq | |||
| 1164 | + | } | |||
| 1165 | + | } | |||
| 1166 | + | } | |||
| 1167 | + | o.unsent = slices.Delete(o.unsent, 0, 1) | |||
| 1168 | + | o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged") | |||
| 1169 | + | } | |||
| 1170 | + | } | |||
| 1171 | + | ||||
| 1172 | + | // flushReport sends queued reports without blocking. | |||
| 1173 | + | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |||
| 1174 | + | for len(o.unsent) > 0 { | |||
| 1175 | + | select { | |||
| 1176 | + | case presented <- o.unsent[0]: | |||
| 1177 | + | o.unsent = slices.Delete(o.unsent, 0, 1) | |||
| 1178 | + | default: | |||
| 1179 | + | return | |||
| 1180 | + | } | |||
| 453 | } | 1181 | } | ||
| 454 | } | 1182 | } | ||
| 455 | 1183 | ||||
| @@ -459,11 +1187,11 @@ type imageKind int | |||||
| 459 | const ( | 1187 | const ( | ||
| 460 | imagesDriver imageKind = iota // exported, modifier chosen by the driver | 1188 | imagesDriver imageKind = iota // exported, modifier chosen by the driver | ||
| 461 | imagesLinear // exported, linear | 1189 | imagesLinear // exported, linear | ||
| 462 | - | imagesDumb // dumb buffers filled by CPU copies | |||
| 463 | ) | 1190 | ) | ||
| 464 | 1191 | ||||
| 465 | // showImages sets up images from kind on and modesets them. KMS may refuse | 1192 | // showImages sets up images from kind on and modesets them. KMS may refuse | ||
| 466 | - | // an image only at modeset: then the next kind is tried. cause is why the | 1193 | + | // an image only in a modeset: a TEST_ONLY modeset checks it first, and the |
| 1194 | + | // next kind is tried on refusal. cause is why the | |||
| 467 | // previous kind failed, for the log. | 1195 | // previous kind failed, for the log. | ||
| 468 | func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error { | 1196 | func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error { | ||
| 469 | for { | 1197 | for { | ||
| @@ -472,18 +1200,15 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error | |||||
| 472 | return err | 1200 | return err | ||
| 473 | } | 1201 | } | ||
| 474 | o.kind = got | 1202 | o.kind = got | ||
| 475 | - | if err = o.modeset(); err == nil { | 1203 | + | if err = o.testModeset(); err == nil { |
| 476 | - | o.validated = true | 1204 | + | return o.modeset() |
| 477 | - | return nil | |||
| 478 | } | 1205 | } | ||
| 479 | - | if !refused(err) { | 1206 | + | if !refused(err) || got == imagesLinear { |
| 1207 | + | o.freeImages() | |||
| 480 | return err | 1208 | return err | ||
| 481 | } | 1209 | } | ||
| 482 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | 1210 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | ||
| 483 | o.freeImages() | 1211 | o.freeImages() | ||
| 484 | - | if got == imagesDumb { | |||
| 485 | - | return err | |||
| 486 | - | } | |||
| 487 | kind, cause = got+1, err | 1212 | kind, cause = got+1, err | ||
| 488 | } | 1213 | } | ||
| 489 | } | 1214 | } | ||
| @@ -493,17 +1218,17 @@ func refused(err error) bool { | |||||
| 493 | return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) | 1218 | return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) | ||
| 494 | } | 1219 | } | ||
| 495 | 1220 | ||||
| 496 | - | // lostMaster reports a modeset error meaning the seat is switched away. | 1221 | + | // lostMaster reports a commit error meaning the seat is switched away. |
| 497 | func lostMaster(err error) bool { | 1222 | func lostMaster(err error) bool { | ||
| 498 | return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) | 1223 | return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) | ||
| 499 | } | 1224 | } | ||
| 500 | 1225 | ||||
| 501 | - | // setupImages gives the output the two images it flips between: renderer | 1226 | + | // setupImages gives the output the two renderer images it flips between, |
| 502 | - | // targets exported as dmabufs, or dumb buffers as a logged fallback. A | 1227 | + | // exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that |
| 503 | - | // kind that fails falls through to the next one. | 1228 | + | // cannot export a scanout-capable image cannot drive the output. |
| 504 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | 1229 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | ||
| 505 | err := cause | 1230 | err := cause | ||
| 506 | - | for ; kind < imagesDumb; kind++ { | 1231 | + | for ; kind <= imagesLinear; kind++ { |
| 507 | mods := []uint64(nil) | 1232 | mods := []uint64(nil) | ||
| 508 | if kind == imagesLinear { | 1233 | if kind == imagesLinear { | ||
| 509 | mods = []uint64{0} | 1234 | mods = []uint64{0} | ||
| @@ -513,17 +1238,7 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima | |||||
| 513 | } | 1238 | } | ||
| 514 | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export") | 1239 | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export") | ||
| 515 | } | 1240 | } | ||
| 516 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)") | 1241 | + | 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 | } | 1242 | } | ||
| 528 | 1243 | ||||
| 529 | // exportImages makes the renderer's exported targets the output images. | 1244 | // exportImages makes the renderer's exported targets the output images. | ||
| @@ -534,7 +1249,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 534 | } | 1249 | } | ||
| 535 | for i := range bufs { | 1250 | for i := range bufs { | ||
| 536 | if err == nil { | 1251 | if err == nil { | ||
| 537 | - | o.fbs[i], err = o.card.addFB(&bufs[i]) | 1252 | + | o.fbs[i], err = o.k.addFB(&bufs[i], fourccXRGB) |
| 538 | } | 1253 | } | ||
| 539 | bufs[i].Planes[0].File.Close() | 1254 | bufs[i].Planes[0].File.Close() | ||
| 540 | } | 1255 | } | ||
| @@ -545,7 +1260,12 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 545 | break | 1260 | break | ||
| 546 | } | 1261 | } | ||
| 547 | r.UseTarget(i) | 1262 | r.UseTarget(i) | ||
| 548 | - | err = r.Render(ports.Scene{Background: "#000000"}, nil) | 1263 | + | var done *os.File |
| 1264 | + | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | |||
| 1265 | + | // The modeset is a blocking commit: wait for the clear. | |||
| 1266 | + | err = errors.Join(err, syncfile.Wait(context.Background(), done)) | |||
| 1267 | + | done.Close() | |||
| 1268 | + | } | |||
| 549 | } | 1269 | } | ||
| 550 | if err != nil { | 1270 | if err != nil { | ||
| 551 | o.freeImages() | 1271 | o.freeImages() | ||
| @@ -556,16 +1276,23 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 556 | return nil | 1276 | return nil | ||
| 557 | } | 1277 | } | ||
| 558 | 1278 | ||||
| 559 | - | // freeImages removes the output images; dumb buffers own their fb. | 1279 | + | // freeImages removes the output images' framebuffers. |
| 560 | func (o *Output) freeImages() { | 1280 | func (o *Output) freeImages() { | ||
| 561 | for i, fb := range o.fbs { | 1281 | for i, fb := range o.fbs { | ||
| 562 | - | if b := o.bufs[i]; b != nil { | 1282 | + | if fb != 0 { |
| 563 | - | b.destroy(o.fd) | 1283 | + | _ = 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 | } | 1284 | } | ||
| 569 | o.fbs[i] = 0 | 1285 | o.fbs[i] = 0 | ||
| 570 | } | 1286 | } | ||
| 571 | } | 1287 | } | ||
| 1288 | + | ||||
| 1289 | + | // shownBy is the content Seq of each window the scene draws. | |||
| 1290 | + | func shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 { | |||
| 1291 | + | out := map[ports.WindowID]uint64{} | |||
| 1292 | + | for id, seq := range seen { | |||
| 1293 | + | if s.Shows(id) { | |||
| 1294 | + | out[id] = seq | |||
| 1295 | + | } | |||
| 1296 | + | } | |||
| 1297 | + | return out | |||
| 1298 | + | } | |||
A internal/adapters/drm/output_test.go +449 -0
| @@ -0,0 +1,449 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "sync" | |||
| 7 | + | "sync/atomic" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 13 | + | "github.com/bnema/zerowrap" | |||
| 14 | + | "github.com/stretchr/testify/mock" | |||
| 15 | + | "golang.org/x/sys/unix" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // Object and property IDs of the test output. | |||
| 19 | + | const ( | |||
| 20 | + | tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52 | |||
| 21 | + | pFB, pCrtcID, pFence, pMode, pActive = 1, 2, 3, 4, 5 | |||
| 22 | + | pVRR, pConnCrtc = 6, 7 | |||
| 23 | + | ) | |||
| 24 | + | ||||
| 25 | + | 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} | |||
| 26 | + | ||||
| 27 | + | // commitRec is one recorded atomic commit. | |||
| 28 | + | type commitRec struct { | |||
| 29 | + | req atomicReq | |||
| 30 | + | flags uint32 | |||
| 31 | + | user uint64 | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // testOutput returns an output on a mocked kms that records commits; | |||
| 35 | + | // commit returns the next error of errs (nil when empty). | |||
| 36 | + | func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |||
| 37 | + | o, k, commits, _ := testOutputMu(t, errs...) | |||
| 38 | + | return o, k, commits | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // testOutputMu is testOutput with the mutex that guards the recorded | |||
| 42 | + | // commits (Run commits on its goroutine). | |||
| 43 | + | func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) { | |||
| 44 | + | k := newMockkms(t) | |||
| 45 | + | var commits []commitRec | |||
| 46 | + | commitMu := &sync.Mutex{} | |||
| 47 | + | k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error { | |||
| 48 | + | commitMu.Lock() | |||
| 49 | + | defer commitMu.Unlock() | |||
| 50 | + | c := commitRec{flags: flags, user: user} | |||
| 51 | + | c.req.objs = append([]uint32(nil), r.objs...) | |||
| 52 | + | for _, ps := range r.props { | |||
| 53 | + | c.req.props = append(c.req.props, append([]propValue(nil), ps...)) | |||
| 54 | + | } | |||
| 55 | + | commits = append(commits, c) | |||
| 56 | + | if len(errs) > 0 { | |||
| 57 | + | err := errs[0] | |||
| 58 | + | errs = errs[1:] | |||
| 59 | + | return err | |||
| 60 | + | } | |||
| 61 | + | return nil | |||
| 62 | + | }).Maybe() | |||
| 63 | + | primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps} | |||
| 64 | + | cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64) | |||
| 65 | + | cur.fbs = [2]uint32{90, 91} | |||
| 66 | + | o := &Output{k: k, serials: &atomic.Uint64{}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | |||
| 67 | + | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | |||
| 68 | + | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | |||
| 69 | + | return o, k, &commits, commitMu | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // eventOf is the flip event of commit c. | |||
| 73 | + | func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} } | |||
| 74 | + | ||||
| 75 | + | func objCount(c commitRec) int { return len(c.req.objs) } | |||
| 76 | + | ||||
| 77 | + | func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | |||
| 78 | + | o, _, commits := testOutput(t) | |||
| 79 | + | o.cursor.image = true | |||
| 80 | + | o.cursor.Move(12, 34) | |||
| 81 | + | fence, w, err := os.Pipe() | |||
| 82 | + | if err != nil { | |||
| 83 | + | t.Fatal(err) | |||
| 84 | + | } | |||
| 85 | + | defer fence.Close() | |||
| 86 | + | defer w.Close() | |||
| 87 | + | if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil { | |||
| 88 | + | t.Fatal(err) | |||
| 89 | + | } | |||
| 90 | + | c := (*commits)[0] | |||
| 91 | + | if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.pendingSerial { | |||
| 92 | + | t.Fatalf("flags %#x user %d", c.flags, c.user) | |||
| 93 | + | } | |||
| 94 | + | if v, _ := c.req.value(tPrimary, pFB); v != 70 { | |||
| 95 | + | t.Fatalf("primary fb %d", v) | |||
| 96 | + | } | |||
| 97 | + | if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) { | |||
| 98 | + | t.Fatalf("fence %d %v", v, ok) | |||
| 99 | + | } | |||
| 100 | + | if v, _ := c.req.value(tCursor, 14); v != 12 { | |||
| 101 | + | t.Fatalf("cursor x %d", v) | |||
| 102 | + | } | |||
| 103 | + | if v, _ := c.req.value(tCrtc, pVRR); v != 1 { | |||
| 104 | + | t.Fatalf("vrr %d", v) | |||
| 105 | + | } | |||
| 106 | + | if !o.pending || !o.vrrOn || o.cursor.applied.x != 12 { | |||
| 107 | + | t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.pending, o.vrrOn, o.cursor.applied) | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) { | |||
| 112 | + | o, _, commits := testOutput(t) | |||
| 113 | + | fence, w, _ := os.Pipe() | |||
| 114 | + | defer fence.Close() | |||
| 115 | + | defer w.Close() | |||
| 116 | + | if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil { | |||
| 117 | + | t.Fatal(err) | |||
| 118 | + | } | |||
| 119 | + | c := (*commits)[0] | |||
| 120 | + | if c.flags&flipAsyncFlag == 0 { | |||
| 121 | + | t.Fatal("not async") | |||
| 122 | + | } | |||
| 123 | + | if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 { | |||
| 124 | + | t.Fatalf("async commit %+v", c.req) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) { | |||
| 129 | + | o, _, commits := testOutput(t) | |||
| 130 | + | o.cursor.image = true | |||
| 131 | + | o.cursor.Move(5, 5) | |||
| 132 | + | if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil { | |||
| 133 | + | t.Fatal(err) | |||
| 134 | + | } | |||
| 135 | + | if (*commits)[0].flags&flipAsyncFlag != 0 { | |||
| 136 | + | t.Fatal("async commit moved the cursor") | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) { | |||
| 141 | + | o, _, commits := testOutput(t) | |||
| 142 | + | o.asyncFence = false | |||
| 143 | + | fence, w, _ := os.Pipe() | |||
| 144 | + | defer fence.Close() | |||
| 145 | + | defer w.Close() | |||
| 146 | + | if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil { | |||
| 147 | + | t.Fatal(err) | |||
| 148 | + | } | |||
| 149 | + | if (*commits)[0].flags&flipAsyncFlag != 0 { | |||
| 150 | + | t.Fatal("async commit with a fence the driver refuses") | |||
| 151 | + | } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | func TestScanoutAsyncRefusedRetriesSync(t *testing.T) { | |||
| 155 | + | o, _, commits := testOutput(t, unix.EINVAL) | |||
| 156 | + | o.shown, o.vrrOn = 1, true | |||
| 157 | + | buf := &ports.DMABuf{ID: 9} | |||
| 158 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |||
| 159 | + | ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{}) | |||
| 160 | + | if !ok || err != nil { | |||
| 161 | + | t.Fatalf("ok=%v err=%v", ok, err) | |||
| 162 | + | } | |||
| 163 | + | if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 { | |||
| 164 | + | t.Fatalf("commits %+v", *commits) | |||
| 165 | + | } | |||
| 166 | + | if !o.clientFBs[9].noAsync || o.queued != 9 { | |||
| 167 | + | t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued) | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func TestCursorCommitWaitsForEvent(t *testing.T) { | |||
| 172 | + | o, _, commits := testOutput(t) | |||
| 173 | + | o.cursor.image = true | |||
| 174 | + | o.cursor.Move(1, 1) | |||
| 175 | + | if err := o.commitState(false); err != nil { | |||
| 176 | + | t.Fatal(err) | |||
| 177 | + | } | |||
| 178 | + | c := (*commits)[0] | |||
| 179 | + | if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.pending { | |||
| 180 | + | t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending) | |||
| 181 | + | } | |||
| 182 | + | // Its event is not a frame: nothing is reported. | |||
| 183 | + | seen := map[ports.WindowID]uint64{} | |||
| 184 | + | o.completed(eventOf(c), seen) | |||
| 185 | + | if o.pending || len(o.unsent) != 0 || o.flips != 0 { | |||
| 186 | + | t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips) | |||
| 187 | + | } | |||
| 188 | + | // Nothing changed: no commit. | |||
| 189 | + | if err := o.commitState(false); err != nil || len(*commits) != 1 { | |||
| 190 | + | t.Fatalf("idle commit: %v, %d commits", err, len(*commits)) | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | func TestBusyCommitStaysPending(t *testing.T) { | |||
| 195 | + | o, _, _ := testOutput(t, unix.EBUSY) | |||
| 196 | + | err := o.commitFrame(70, nil, false, false, pendingFrame{}) | |||
| 197 | + | enabled := true | |||
| 198 | + | if !o.commitFailed(err, &enabled) || !o.pending || !enabled { | |||
| 199 | + | t.Fatalf("EBUSY: pending=%v enabled=%v", o.pending, enabled) | |||
| 200 | + | } | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | func TestFlipReportsTimestampAndShownFrame(t *testing.T) { | |||
| 204 | + | o, _, commits := testOutput(t) | |||
| 205 | + | o.vrrOn = false | |||
| 206 | + | o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0 | |||
| 207 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil { | |||
| 208 | + | t.Fatal(err) | |||
| 209 | + | } | |||
| 210 | + | o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4}) | |||
| 211 | + | if len(o.unsent) != 1 { | |||
| 212 | + | t.Fatalf("reports %d", len(o.unsent)) | |||
| 213 | + | } | |||
| 214 | + | f := o.unsent[0].Flip | |||
| 215 | + | if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock { | |||
| 216 | + | t.Fatalf("flip %+v", f) | |||
| 217 | + | } | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | func TestReportsKeepEveryFlip(t *testing.T) { | |||
| 221 | + | o, _, _ := testOutput(t) | |||
| 222 | + | seen := map[ports.WindowID]uint64{1: 1} | |||
| 223 | + | o.report(&ports.FlipInfo{Seq: 1}, seen) | |||
| 224 | + | o.report(nil, seen) | |||
| 225 | + | o.report(&ports.FlipInfo{Seq: 2}, seen) | |||
| 226 | + | o.report(nil, map[ports.WindowID]uint64{1: 3}) | |||
| 227 | + | if len(o.unsent) != 2 || o.unsent[0].Flip.Seq != 1 || o.unsent[1].Flip.Seq != 2 || o.unsent[1].Seen[1] != 3 { | |||
| 228 | + | t.Fatalf("unsent %+v", o.unsent) | |||
| 229 | + | } | |||
| 230 | + | for i := range maxUnsent + 3 { | |||
| 231 | + | o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen) | |||
| 232 | + | } | |||
| 233 | + | if len(o.unsent) != maxUnsent || o.unsent[0].Flip.Merged == 0 { | |||
| 234 | + | t.Fatalf("cap: %d reports, first merged %d", len(o.unsent), o.unsent[0].Flip.Merged) | |||
| 235 | + | } | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) { | |||
| 239 | + | o, k, commits := testOutput(t) | |||
| 240 | + | stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps} | |||
| 241 | + | o.stray = []*plane{stray} | |||
| 242 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once() | |||
| 243 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once() | |||
| 244 | + | if err := o.modeset(); err != nil { | |||
| 245 | + | t.Fatal(err) | |||
| 246 | + | } | |||
| 247 | + | c := (*commits)[0] | |||
| 248 | + | if c.flags != atomicAllowModes { | |||
| 249 | + | t.Fatalf("flags %#x", c.flags) | |||
| 250 | + | } | |||
| 251 | + | if v, _ := c.req.value(tCrtc, pMode); v != 99 { | |||
| 252 | + | t.Fatalf("mode blob %d", v) | |||
| 253 | + | } | |||
| 254 | + | for _, p := range []uint32{tCursor, tOverlay} { | |||
| 255 | + | if v, ok := c.req.value(p, pFB); !ok || v != 0 { | |||
| 256 | + | t.Fatalf("plane %d fb %d %v", p, v, ok) | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc { | |||
| 260 | + | t.Fatalf("connector crtc %d", v) | |||
| 261 | + | } | |||
| 262 | + | // The next modeset replaces the blob. | |||
| 263 | + | k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once() | |||
| 264 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once() | |||
| 265 | + | k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once() | |||
| 266 | + | if err := o.modeset(); err != nil { | |||
| 267 | + | t.Fatal(err) | |||
| 268 | + | } | |||
| 269 | + | if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok { | |||
| 270 | + | t.Fatal("plane on another CRTC turned off") | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | ||||
| 274 | + | func TestAsyncProbeAfterModeset(t *testing.T) { | |||
| 275 | + | o, _, commits := testOutput(t, unix.EINVAL) | |||
| 276 | + | o.asyncFence = true | |||
| 277 | + | r := portsmocks.NewMockRenderer(t) | |||
| 278 | + | fence, w, _ := os.Pipe() | |||
| 279 | + | defer w.Close() | |||
| 280 | + | r.EXPECT().UseTarget(mock.Anything).Return().Once() | |||
| 281 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once() | |||
| 282 | + | o.probeAsync(r) | |||
| 283 | + | c := (*commits)[0] | |||
| 284 | + | if c.flags != atomicTestOnly|flipAsyncFlag { | |||
| 285 | + | t.Fatalf("flags %#x", c.flags) | |||
| 286 | + | } | |||
| 287 | + | if _, ok := c.req.value(tPrimary, pFence); !ok { | |||
| 288 | + | t.Fatal("probe without a fence") | |||
| 289 | + | } | |||
| 290 | + | if o.asyncFence { | |||
| 291 | + | t.Fatal("refused probe kept asyncFence") | |||
| 292 | + | } | |||
| 293 | + | if _, err := fence.Stat(); err == nil { | |||
| 294 | + | t.Fatal("probe fence not closed") | |||
| 295 | + | } | |||
| 296 | + | o.probeAsync(r) // once only | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | func TestVRRToggleOnlyInsideFrames(t *testing.T) { | |||
| 300 | + | o, _, commits := testOutput(t) | |||
| 301 | + | o.vrrOn = true | |||
| 302 | + | o.composedSince = time.Now().Add(-time.Second) | |||
| 303 | + | if o.wantVRR(false) { | |||
| 304 | + | t.Fatal("VRR kept after vrrHold of composition") | |||
| 305 | + | } | |||
| 306 | + | o.composedSince = time.Now() | |||
| 307 | + | if !o.wantVRR(false) { | |||
| 308 | + | t.Fatal("VRR dropped before vrrHold") | |||
| 309 | + | } | |||
| 310 | + | if err := o.commitState(false); err != nil { | |||
| 311 | + | t.Fatal(err) | |||
| 312 | + | } | |||
| 313 | + | if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 { | |||
| 314 | + | t.Fatalf("vrr off commit %+v", (*commits)[0]) | |||
| 315 | + | } | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | func TestReadPlanes(t *testing.T) { | |||
| 319 | + | k := newMockkms(t) | |||
| 320 | + | k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil) | |||
| 321 | + | k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil) | |||
| 322 | + | k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil) | |||
| 323 | + | k.EXPECT().getBlob(uint32(5)).Return(nil, nil) | |||
| 324 | + | ps, err := readPlanes(k, 0) | |||
| 325 | + | if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor { | |||
| 326 | + | t.Fatalf("planes %+v %v", ps, err) | |||
| 327 | + | } | |||
| 328 | + | } | |||
| 329 | + | ||||
| 330 | + | func TestVRRRefusedIsDisabled(t *testing.T) { | |||
| 331 | + | o, _, commits := testOutput(t, unix.EINVAL) | |||
| 332 | + | if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil { | |||
| 333 | + | t.Fatal(err) | |||
| 334 | + | } | |||
| 335 | + | if len(*commits) != 2 || o.vrrProp != 0 || !o.pending { | |||
| 336 | + | t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp) | |||
| 337 | + | } | |||
| 338 | + | if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 { | |||
| 339 | + | t.Fatal("retry kept VRR on") | |||
| 340 | + | } | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) { | |||
| 344 | + | o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 345 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |||
| 346 | + | ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{}) | |||
| 347 | + | if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" { | |||
| 348 | + | t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed) | |||
| 349 | + | } | |||
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | // Run: while a frame is in flight its buffers may still be read by the | |||
| 353 | + | // GPU, so a newer content is reported only after the flip event; every | |||
| 354 | + | // frame fence is closed. | |||
| 355 | + | func TestRunReportsSeenAfterFlip(t *testing.T) { | |||
| 356 | + | o, k, commits, commitMu := testOutputMu(t) | |||
| 357 | + | o.cursor, o.tearing = nil, false | |||
| 358 | + | flips := make(chan flipEvent, 1) | |||
| 359 | + | o.flipped = flips | |||
| 360 | + | r := portsmocks.NewMockRenderer(t) | |||
| 361 | + | var mu sync.Mutex | |||
| 362 | + | var fences []*os.File | |||
| 363 | + | pipeBuf := func() ports.DMABuf { | |||
| 364 | + | f, w, _ := os.Pipe() | |||
| 365 | + | w.Close() | |||
| 366 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 367 | + | } | |||
| 368 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once() | |||
| 369 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 370 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 371 | + | f, w, _ := os.Pipe() | |||
| 372 | + | _, _ = w.Write([]byte{1}) // readable: signalled | |||
| 373 | + | w.Close() | |||
| 374 | + | mu.Lock() | |||
| 375 | + | fences = append(fences, f) | |||
| 376 | + | mu.Unlock() | |||
| 377 | + | return f, nil | |||
| 378 | + | }) | |||
| 379 | + | r.EXPECT().Close().Return().Once() | |||
| 380 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 381 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 382 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |||
| 383 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 384 | + | frameCommits := func() int { | |||
| 385 | + | commitMu.Lock() | |||
| 386 | + | defer commitMu.Unlock() | |||
| 387 | + | n := 0 | |||
| 388 | + | for _, c := range *commits { | |||
| 389 | + | if c.user&3 == userFrame { | |||
| 390 | + | n++ | |||
| 391 | + | } | |||
| 392 | + | } | |||
| 393 | + | return n | |||
| 394 | + | } | |||
| 395 | + | scenes := make(chan ports.Scene, 1) | |||
| 396 | + | contents := make(chan ports.SurfaceContent, 1) | |||
| 397 | + | presented := make(chan ports.OutputPresented, 8) | |||
| 398 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 399 | + | done := make(chan error, 1) | |||
| 400 | + | go func() { | |||
| 401 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented) | |||
| 402 | + | }() | |||
| 403 | + | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | |||
| 404 | + | waitFor(t, func() bool { return frameCommits() == 1 }) | |||
| 405 | + | // Frame 1 is in flight: content 3 replaces what it may be reading. | |||
| 406 | + | contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}} | |||
| 407 | + | select { | |||
| 408 | + | case p := <-presented: | |||
| 409 | + | t.Fatalf("report while a frame is in flight: %+v", p) | |||
| 410 | + | case <-time.After(30 * time.Millisecond): | |||
| 411 | + | } | |||
| 412 | + | commitMu.Lock() | |||
| 413 | + | last := (*commits)[len(*commits)-1] | |||
| 414 | + | commitMu.Unlock() | |||
| 415 | + | flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | |||
| 416 | + | select { | |||
| 417 | + | case p := <-presented: | |||
| 418 | + | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | |||
| 419 | + | t.Fatalf("report %+v", p) | |||
| 420 | + | } | |||
| 421 | + | case <-time.After(2 * time.Second): | |||
| 422 | + | t.Fatal("no report after the flip") | |||
| 423 | + | } | |||
| 424 | + | cancel() | |||
| 425 | + | if err := <-done; err != nil { | |||
| 426 | + | t.Fatal(err) | |||
| 427 | + | } | |||
| 428 | + | mu.Lock() | |||
| 429 | + | defer mu.Unlock() | |||
| 430 | + | if len(fences) < 3 { | |||
| 431 | + | t.Fatalf("%d renders", len(fences)) | |||
| 432 | + | } | |||
| 433 | + | for _, f := range fences { | |||
| 434 | + | if _, err := f.Stat(); err == nil { | |||
| 435 | + | t.Fatal("frame fence left open") | |||
| 436 | + | } | |||
| 437 | + | } | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | func waitFor(t *testing.T, cond func() bool) { | |||
| 441 | + | t.Helper() | |||
| 442 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 443 | + | for !cond() { | |||
| 444 | + | if time.Now().After(deadline) { | |||
| 445 | + | t.Fatal("timed out") | |||
| 446 | + | } | |||
| 447 | + | time.Sleep(2 * time.Millisecond) | |||
| 448 | + | } | |||
| 449 | + | } | |||
A internal/adapters/drm/overlay.go +223 -0
| @@ -0,0 +1,223 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "os" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Overlay plane: a window drawn from one opaque dmabuf, with nothing but | |||
| 13 | + | // the cursor above it, goes on an overlay plane at its place on screen; | |||
| 14 | + | // the rest of the scene is composed into the primary plane without it. | |||
| 15 | + | // The client buffer is shown with no GPU pass, like direct scanout, but | |||
| 16 | + | // the window need not cover the output. A refusal (TEST_ONLY) keeps the | |||
| 17 | + | // window composed and is cached per buffer. | |||
| 18 | + | ||||
| 19 | + | // pickOverlay takes an overlay plane above the primary one (zpos), when | |||
| 20 | + | // the output has one. | |||
| 21 | + | func (o *Output) pickOverlay() { | |||
| 22 | + | pz, ok := o.primary.zpos() | |||
| 23 | + | if !ok { | |||
| 24 | + | return | |||
| 25 | + | } | |||
| 26 | + | for _, p := range o.stray { | |||
| 27 | + | if p.typ != planeOverlay { | |||
| 28 | + | continue | |||
| 29 | + | } | |||
| 30 | + | if z, ok := p.zpos(); ok && z > pz { | |||
| 31 | + | o.overlay = p | |||
| 32 | + | return | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // zpos is the plane's zpos value when known. | |||
| 38 | + | func (p *plane) zpos() (uint64, bool) { | |||
| 39 | + | if p.props["zpos"] == 0 { | |||
| 40 | + | return 0, false | |||
| 41 | + | } | |||
| 42 | + | return p.zposValue, true | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // overlayWin is the window on the overlay plane: its buffer and where it | |||
| 46 | + | // shows, in physical pixels. | |||
| 47 | + | type overlayWin struct { | |||
| 48 | + | id ports.WindowID | |||
| 49 | + | fb uint32 | |||
| 50 | + | buf uint64 | |||
| 51 | + | rect ports.Rect // CRTC rect | |||
| 52 | + | w, h int // buffer size | |||
| 53 | + | // acquire is the client's explicit-sync fence, a duplicate owned by | |||
| 54 | + | // the frame loop (closed after the commit); nil without one. | |||
| 55 | + | acquire *os.File | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // overlayCandidate finds the one window the overlay can show: an opaque | |||
| 59 | + | // dmabuf at integer physical coordinates, drawn 1:1, with no other window | |||
| 60 | + | // or layer above it. reason is why none. | |||
| 61 | + | func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (ports.SceneWindow, ports.SurfaceContent, string) { | |||
| 62 | + | scale := s.Scale | |||
| 63 | + | if scale <= 0 { | |||
| 64 | + | scale = 1 | |||
| 65 | + | } | |||
| 66 | + | var pick *ports.SceneWindow | |||
| 67 | + | for i := range s.Windows { | |||
| 68 | + | w := &s.Windows[i] | |||
| 69 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |||
| 70 | + | continue | |||
| 71 | + | } | |||
| 72 | + | if w.Popup { | |||
| 73 | + | // Popups draw after every window, wherever they are listed. | |||
| 74 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above" | |||
| 75 | + | } | |||
| 76 | + | c := surfaces[w.ID] | |||
| 77 | + | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 { | |||
| 78 | + | pick = w | |||
| 79 | + | continue | |||
| 80 | + | } | |||
| 81 | + | if pick != nil { | |||
| 82 | + | // Something is drawn above the candidate. | |||
| 83 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above" | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | if pick == nil { | |||
| 87 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "no_candidate" | |||
| 88 | + | } | |||
| 89 | + | for _, l := range s.Layers { | |||
| 90 | + | if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 && !(pick.Fullscreen && l.Layer == ports.LayerTop) { | |||
| 91 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | if !pick.Fullscreen && !pick.Borderless && s.Border.Width > 0 { | |||
| 95 | + | // The overlay shows the buffer alone; its border would go with it. | |||
| 96 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "border" | |||
| 97 | + | } | |||
| 98 | + | c := surfaces[pick.ID] | |||
| 99 | + | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | |||
| 100 | + | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | |||
| 101 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "scaled" | |||
| 102 | + | } | |||
| 103 | + | x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale | |||
| 104 | + | if x != float64(int(x)) || y != float64(int(y)) { | |||
| 105 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "fractional_position" | |||
| 106 | + | } | |||
| 107 | + | return *pick, c, "" | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // overlayFrame decides the overlay of a frame: the window, and the scene | |||
| 111 | + | // the renderer composes (the window left out). A zero overlayWin means | |||
| 112 | + | // none; the reason is logged on change. | |||
| 113 | + | func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) { | |||
| 114 | + | reason := "no_plane" | |||
| 115 | + | var ov overlayWin | |||
| 116 | + | if o.overlay != nil && o.scanout { | |||
| 117 | + | var w ports.SceneWindow | |||
| 118 | + | var c ports.SurfaceContent | |||
| 119 | + | w, c, reason = overlayCandidate(s, surfaces) | |||
| 120 | + | if reason == "" { | |||
| 121 | + | var fb uint32 | |||
| 122 | + | if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" { | |||
| 123 | + | scale := s.Scale | |||
| 124 | + | if scale <= 0 { | |||
| 125 | + | scale = 1 | |||
| 126 | + | } | |||
| 127 | + | ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height, | |||
| 128 | + | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}, | |||
| 129 | + | acquire: dupFence(c.Acquire)} | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | if reason != o.overlayReason { | |||
| 134 | + | o.log.Info().Str("component", "render").Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay") | |||
| 135 | + | o.overlayReason = reason | |||
| 136 | + | } | |||
| 137 | + | if ov.fb == 0 { | |||
| 138 | + | return overlayWin{}, s | |||
| 139 | + | } | |||
| 140 | + | // The composed frame leaves the window out: the overlay shows it. | |||
| 141 | + | rest := s | |||
| 142 | + | rest.Windows = make([]ports.SceneWindow, 0, len(s.Windows)) | |||
| 143 | + | for _, w := range s.Windows { | |||
| 144 | + | if w.ID != ov.id { | |||
| 145 | + | rest.Windows = append(rest.Windows, w) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | return ov, rest | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // overlayProps puts ov on the overlay plane, or turns it off. | |||
| 152 | + | func (o *Output) overlayProps(req *atomicReq, ov overlayWin) { | |||
| 153 | + | p := o.overlay | |||
| 154 | + | if p == nil { | |||
| 155 | + | return | |||
| 156 | + | } | |||
| 157 | + | if ov.fb == 0 { | |||
| 158 | + | req.set(p.id, p.prop("FB_ID"), 0) | |||
| 159 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |||
| 160 | + | return | |||
| 161 | + | } | |||
| 162 | + | req.set(p.id, p.prop("FB_ID"), uint64(ov.fb)) | |||
| 163 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |||
| 164 | + | req.set(p.id, p.prop("SRC_X"), 0) | |||
| 165 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |||
| 166 | + | req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16) | |||
| 167 | + | req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16) | |||
| 168 | + | req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X))) | |||
| 169 | + | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | |||
| 170 | + | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | |||
| 171 | + | req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H)) | |||
| 172 | + | if ov.acquire != nil { | |||
| 173 | + | // KMS waits for the client's GPU work before scanning it out. | |||
| 174 | + | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd())) | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // close releases the frame's duplicate of the acquire fence. | |||
| 179 | + | func (ov overlayWin) close() { | |||
| 180 | + | if ov.acquire != nil { | |||
| 181 | + | ov.acquire.Close() | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // testOverlay asks KMS whether it takes the frame with ov on the overlay, | |||
| 186 | + | // the cursor at its current place included. A refusal is cached on the | |||
| 187 | + | // buffer. | |||
| 188 | + | func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |||
| 189 | + | req := &atomicReq{} | |||
| 190 | + | o.primaryProps(req, fb) | |||
| 191 | + | o.overlayProps(req, ov) | |||
| 192 | + | if o.cursor != nil { | |||
| 193 | + | o.cursor.props(req, o.crtc, o.cursor.desired()) | |||
| 194 | + | } | |||
| 195 | + | err := o.k.commit(req, atomicTestOnly, 0) | |||
| 196 | + | if err == nil { | |||
| 197 | + | return true | |||
| 198 | + | } | |||
| 199 | + | if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) { | |||
| 200 | + | cfb.overlayFailed = "overlay_refused" | |||
| 201 | + | } | |||
| 202 | + | o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused") | |||
| 203 | + | return false | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | // errOverlayDropped: a commit was refused with the overlay on; the frame is | |||
| 207 | + | // composed again without it. | |||
| 208 | + | var errOverlayDropped = errors.New("overlay dropped") | |||
| 209 | + | ||||
| 210 | + | // overlayConflict handles a commit refused with buffer buf on the overlay: | |||
| 211 | + | // the buffer leaves the overlay for good (e.g. the cursor moved where the | |||
| 212 | + | // plane forbids it) and the frame is composed. It reports whether it did. | |||
| 213 | + | func (o *Output) overlayConflict(err error, buf uint64) bool { | |||
| 214 | + | if buf == 0 || !errors.Is(err, unix.EINVAL) { | |||
| 215 | + | return false | |||
| 216 | + | } | |||
| 217 | + | if cfb := o.clientFBs[buf]; cfb != nil { | |||
| 218 | + | cfb.overlayFailed = "cursor_conflict" | |||
| 219 | + | } | |||
| 220 | + | o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay") | |||
| 221 | + | o.overlayReason = "cursor_conflict" | |||
| 222 | + | return true | |||
| 223 | + | } | |||
A internal/adapters/drm/overlay_test.go +182 -0
| @@ -0,0 +1,182 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 9 | + | "github.com/stretchr/testify/mock" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const pZpos = 30 | |||
| 14 | + | ||||
| 15 | + | // overlayOutput is a test output with an overlay plane above the primary. | |||
| 16 | + | func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |||
| 17 | + | o, k, commits := testOutput(t, errs...) | |||
| 18 | + | props := map[string]uint32{"zpos": pZpos} | |||
| 19 | + | for n, v := range planeProps { | |||
| 20 | + | props[n] = v | |||
| 21 | + | } | |||
| 22 | + | o.primary.props = props | |||
| 23 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}} | |||
| 24 | + | o.primary.formats = nil // the overlay's own formats decide | |||
| 25 | + | o.pickOverlay() | |||
| 26 | + | if o.overlay == nil { | |||
| 27 | + | t.Fatal("no overlay picked") | |||
| 28 | + | } | |||
| 29 | + | return o, k, commits | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // A 1:1 opaque dmabuf window with nothing above goes on the overlay. | |||
| 33 | + | func overlayScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) { | |||
| 34 | + | s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{ | |||
| 35 | + | {ID: 1, Rect: ports.Rect{W: 100, H: 100}}, | |||
| 36 | + | {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}}, | |||
| 37 | + | }} | |||
| 38 | + | return s, map[ports.WindowID]ports.SurfaceContent{ | |||
| 39 | + | 2: {ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 100, Height: 100, Format: fourccXRGB}}, | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func TestOverlayCandidate(t *testing.T) { | |||
| 44 | + | s, c := overlayScene() | |||
| 45 | + | if w, _, reason := overlayCandidate(s, c); reason != "" || w.ID != 2 { | |||
| 46 | + | t.Fatalf("candidate %v %q", w.ID, reason) | |||
| 47 | + | } | |||
| 48 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}}) | |||
| 49 | + | if _, _, reason := overlayCandidate(s, c); reason != "window_above" { | |||
| 50 | + | t.Fatalf("reason %q", reason) | |||
| 51 | + | } | |||
| 52 | + | s, _ = overlayScene() | |||
| 53 | + | s.Scale = 1.5 | |||
| 54 | + | if _, _, reason := overlayCandidate(s, c); reason != "scaled" { | |||
| 55 | + | t.Fatalf("reason %q", reason) | |||
| 56 | + | } | |||
| 57 | + | s, _ = overlayScene() | |||
| 58 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}} | |||
| 59 | + | if _, _, reason := overlayCandidate(s, c); reason != "layer_above" { | |||
| 60 | + | t.Fatalf("reason %q", reason) | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // The overlay is chosen and the composed scene leaves its window out; | |||
| 65 | + | // the frame commit carries the plane at the window's place. | |||
| 66 | + | func TestOverlayChosen(t *testing.T) { | |||
| 67 | + | o, k, commits := overlayOutput(t) | |||
| 68 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 69 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 70 | + | s, c := overlayScene() | |||
| 71 | + | ov, rest := o.overlayFrame(s, c) | |||
| 72 | + | if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 { | |||
| 73 | + | t.Fatalf("overlay %+v rest %+v", ov, rest.Windows) | |||
| 74 | + | } | |||
| 75 | + | if !o.testOverlay(70, ov) { | |||
| 76 | + | t.Fatal("test refused") | |||
| 77 | + | } | |||
| 78 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil { | |||
| 79 | + | t.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | last := (*commits)[len(*commits)-1] | |||
| 82 | + | if v, _ := last.req.value(tOverlay, pFB); v != 88 { | |||
| 83 | + | t.Fatalf("overlay fb %d", v) | |||
| 84 | + | } | |||
| 85 | + | if v, _ := last.req.value(tOverlay, 14); v != 100 { | |||
| 86 | + | t.Fatalf("overlay x %d", v) | |||
| 87 | + | } | |||
| 88 | + | if o.overlayOn != 9 { | |||
| 89 | + | t.Fatalf("overlayOn %d", o.overlayOn) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // A TEST_ONLY refusal composes the window and is cached on the buffer. | |||
| 94 | + | func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||
| 95 | + | o, k, _ := overlayOutput(t, unix.EINVAL) | |||
| 96 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 97 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 98 | + | s, c := overlayScene() | |||
| 99 | + | ov, _ := o.overlayFrame(s, c) | |||
| 100 | + | if o.testOverlay(70, ov) { | |||
| 101 | + | t.Fatal("refusal taken") | |||
| 102 | + | } | |||
| 103 | + | if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 { | |||
| 104 | + | t.Fatal("refused buffer tried again") | |||
| 105 | + | } | |||
| 106 | + | // The overlay's refusal does not bar direct scanout of the buffer. | |||
| 107 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 108 | + | if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" { | |||
| 109 | + | t.Fatalf("scanout after overlay refusal: %d %q", fb, reason) | |||
| 110 | + | } | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // Popups are drawn above every window: one listed first still blocks the | |||
| 114 | + | // overlay; a bordered window does too. | |||
| 115 | + | func TestOverlayPopupAndBorder(t *testing.T) { | |||
| 116 | + | s, c := overlayScene() | |||
| 117 | + | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...) | |||
| 118 | + | if _, _, reason := overlayCandidate(s, c); reason != "window_above" { | |||
| 119 | + | t.Fatalf("popup: %q", reason) | |||
| 120 | + | } | |||
| 121 | + | s, _ = overlayScene() | |||
| 122 | + | s.Border.Width = 2 | |||
| 123 | + | if _, _, reason := overlayCandidate(s, c); reason != "border" { | |||
| 124 | + | t.Fatalf("border: %q", reason) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // A cursor commit refused with the overlay on drops the overlay: the | |||
| 129 | + | // next frame is composed. | |||
| 130 | + | func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | |||
| 131 | + | o, k, _ := overlayOutput(t, nil, unix.EINVAL) | |||
| 132 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 133 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 134 | + | s, c := overlayScene() | |||
| 135 | + | ov, _ := o.overlayFrame(s, c) | |||
| 136 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil { | |||
| 137 | + | t.Fatal(err) | |||
| 138 | + | } | |||
| 139 | + | o.pending = false | |||
| 140 | + | o.cursor.image = true | |||
| 141 | + | o.cursor.Move(150, 50) | |||
| 142 | + | if err := o.commitState(false); err != errOverlayDropped { | |||
| 143 | + | t.Fatalf("err %v", err) | |||
| 144 | + | } | |||
| 145 | + | if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" { | |||
| 146 | + | t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // An explicit-sync window on the overlay: the plane carries a duplicate of | |||
| 151 | + | // the client's acquire fence, closed once the frame is committed. | |||
| 152 | + | func TestOverlayCarriesAcquireFence(t *testing.T) { | |||
| 153 | + | o, k, commits := overlayOutput(t) | |||
| 154 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 155 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 156 | + | s, c := overlayScene() | |||
| 157 | + | r, w, err := os.Pipe() | |||
| 158 | + | if err != nil { | |||
| 159 | + | t.Fatal(err) | |||
| 160 | + | } | |||
| 161 | + | defer r.Close() | |||
| 162 | + | defer w.Close() | |||
| 163 | + | win := c[2] | |||
| 164 | + | win.Acquire = r | |||
| 165 | + | c[2] = win | |||
| 166 | + | ov, _ := o.overlayFrame(s, c) | |||
| 167 | + | if ov.acquire == nil || ov.acquire.Fd() == r.Fd() { | |||
| 168 | + | t.Fatal("overlay has no duplicate of the acquire fence") | |||
| 169 | + | } | |||
| 170 | + | fd := ov.acquire.Fd() | |||
| 171 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil { | |||
| 172 | + | t.Fatal(err) | |||
| 173 | + | } | |||
| 174 | + | ov.close() | |||
| 175 | + | last := (*commits)[len(*commits)-1] | |||
| 176 | + | if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) { | |||
| 177 | + | t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd) | |||
| 178 | + | } | |||
| 179 | + | if _, err := ov.acquire.Stat(); err == nil { | |||
| 180 | + | t.Fatal("duplicate left open") | |||
| 181 | + | } | |||
| 182 | + | } | |||
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 +176 -47
| @@ -2,10 +2,13 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "os" | |||
| 6 | + | "slices" | |||
| 5 | "time" | 7 | "time" | ||
| 6 | "unsafe" | 8 | "unsafe" | ||
| 7 | 9 | ||||
| 8 | "github.com/bnema/nefertty/internal/ports" | 10 | "github.com/bnema/nefertty/internal/ports" | ||
| 11 | + | "golang.org/x/sys/unix" | |||
| 9 | ) | 12 | ) | ||
| 10 | 13 | ||||
| 11 | // Direct scanout (ADR 006): when one fullscreen window covers the output | 14 | // Direct scanout (ADR 006): when one fullscreen window covers the output | ||
| @@ -23,7 +26,6 @@ const ( | |||||
| 23 | 26 | ||||
| 24 | fbModifiers = 1 << 1 | 27 | fbModifiers = 1 << 1 | ||
| 25 | capAddFB2Modifiers = 0x10 | 28 | capAddFB2Modifiers = 0x10 | ||
| 26 | - | capAsyncPageFlip = 0x7 | |||
| 27 | modInvalid = 0x00ffffffffffffff | 29 | modInvalid = 0x00ffffffffffffff | ||
| 28 | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | 30 | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | ||
| 29 | ) | 31 | ) | ||
| @@ -43,6 +45,7 @@ type fbCmd2 struct { | |||||
| 43 | 45 | ||||
| 44 | // scanoutCandidate returns the window content the output can scan out | 46 | // scanoutCandidate returns the window content the output can scan out | ||
| 45 | // directly, or a reason why it cannot. w, h are the output's physical size. | 47 | // directly, or a reason why it cannot. w, h are the output's physical size. | ||
| 48 | + | // The plane's formats are checked by scanoutFB. | |||
| 46 | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | 49 | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | ||
| 47 | var full *ports.SceneWindow | 50 | var full *ports.SceneWindow | ||
| 48 | for i := range s.Windows { | 51 | for i := range s.Windows { | ||
| @@ -70,14 +73,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 70 | return c, "not_dmabuf" | 73 | return c, "not_dmabuf" | ||
| 71 | case len(c.Children) > 0: | 74 | case len(c.Children) > 0: | ||
| 72 | return c, "subsurfaces" | 75 | return c, "subsurfaces" | ||
| 73 | - | case len(c.DMABuf.Planes) != 1: | |||
| 74 | - | return c, "multi_plane" | |||
| 75 | case c.Width != w || c.Height != h: | 76 | case c.Width != w || c.Height != h: | ||
| 76 | return c, "size_mismatch" | 77 | return c, "size_mismatch" | ||
| 77 | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | 78 | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | ||
| 78 | return c, "logical_mismatch" | 79 | return c, "logical_mismatch" | ||
| 79 | - | case !scanoutable(c.DMABuf.Format): | |||
| 80 | - | return c, "format" | |||
| 81 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | 80 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | ||
| 82 | return c, "geometry_crop" | 81 | return c, "geometry_crop" | ||
| 83 | } | 82 | } | ||
| @@ -92,65 +91,132 @@ type clientFB struct { | |||||
| 92 | failed string | 91 | failed string | ||
| 93 | // noAsync: KMS refused an async flip to it; it flips at vblank. | 92 | // noAsync: KMS refused an async flip to it; it flips at vblank. | ||
| 94 | noAsync bool | 93 | noAsync bool | ||
| 94 | + | // overlayFailed is why the overlay plane refused it; kept apart from | |||
| 95 | + | // failed, so a refusal on one plane does not bar the other. | |||
| 96 | + | overlayFailed string | |||
| 97 | + | importErr bool | |||
| 95 | } | 98 | } | ||
| 96 | 99 | ||||
| 97 | - | // scanoutFB returns the framebuffer of a client buffer, importing it on | 100 | + | // scanoutFB returns the framebuffer of a client buffer for the primary |
| 98 | - | // first use. A buffer KMS refuses is remembered with its reason. | 101 | + | // plane, importing it on first use. A buffer KMS refuses is remembered |
| 102 | + | // with its reason. | |||
| 99 | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | 103 | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | ||
| 100 | - | if fb := o.clientFBs[b.ID]; fb != nil { | 104 | + | return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed }) |
| 101 | - | fb.last = now | 105 | + | } |
| 102 | - | return fb.fbID, fb.failed | 106 | + | |
| 107 | + | // overlayFB is scanoutFB for the overlay plane, with its own formats and | |||
| 108 | + | // refusal reason. | |||
| 109 | + | func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |||
| 110 | + | return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed }) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // planeFB imports b once as a framebuffer; reason is the plane's refusal | |||
| 114 | + | // field. Both planes share the framebuffer: they show the same format. | |||
| 115 | + | func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) { | |||
| 116 | + | fb := o.clientFBs[b.ID] | |||
| 117 | + | if fb == nil { | |||
| 118 | + | fb = &clientFB{} | |||
| 119 | + | o.clientFBs[b.ID] = fb | |||
| 103 | } | 120 | } | ||
| 104 | - | fb := &clientFB{last: now} | 121 | + | fb.last = now |
| 105 | - | o.clientFBs[b.ID] = fb | 122 | + | why := reason(fb) |
| 106 | - | id, err := o.card.addFB(b) | 123 | + | if *why != "" { |
| 107 | - | if err != nil { | 124 | + | return 0, *why |
| 108 | - | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | 125 | + | } |
| 109 | - | fb.failed = "import_failed" | 126 | + | format, ok := planeFormat(p, b) |
| 110 | - | return 0, fb.failed | 127 | + | if !ok { |
| 128 | + | *why = "format" | |||
| 129 | + | return 0, *why | |||
| 130 | + | } | |||
| 131 | + | if fb.fbID == 0 { | |||
| 132 | + | if fb.importErr { | |||
| 133 | + | *why = "import_failed" | |||
| 134 | + | return 0, *why | |||
| 135 | + | } | |||
| 136 | + | id, err := o.k.addFB(b, format) | |||
| 137 | + | if err != nil { | |||
| 138 | + | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |||
| 139 | + | fb.importErr, *why = true, "import_failed" | |||
| 140 | + | return 0, *why | |||
| 141 | + | } | |||
| 142 | + | fb.fbID = id | |||
| 111 | } | 143 | } | ||
| 112 | - | fb.fbID = id | 144 | + | return fb.fbID, "" |
| 113 | - | return id, "" | |||
| 114 | } | 145 | } | ||
| 115 | 146 | ||||
| 116 | - | // scanoutFormat is the framebuffer format for a client format. The primary | 147 | + | // scanoutFormat is the framebuffer format a client buffer is scanned out |
| 117 | - | // plane shows no alpha, and a legacy page flip keeps the format of the | 148 | + | // as on the primary plane (see planeFormat). |
| 118 | - | // composed image (XRGB8888): alpha formats are scanned out as their X | 149 | + | func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) } |
| 119 | - | // variant, others are refused. | 150 | + | |
| 120 | - | func scanoutFormat(f uint32) (uint32, bool) { | 151 | + | // planeFormat is the framebuffer format a client buffer shows as on p, |
| 121 | - | switch f { | 152 | + | // when p lists it with the buffer's modifier. Planes here show no alpha: |
| 122 | - | case fourccXRGB, fourccARGB: | 153 | + | // an alpha format goes as its X variant when the plane has that one. |
| 123 | - | return fourccXRGB, true | 154 | + | func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) { |
| 155 | + | has := func(f uint32) bool { | |||
| 156 | + | return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier}) | |||
| 157 | + | } | |||
| 158 | + | if x, ok := opaqueVariant[b.Format]; ok && has(x) { | |||
| 159 | + | return x, true | |||
| 124 | } | 160 | } | ||
| 125 | - | return 0, false | 161 | + | return b.Format, has(b.Format) |
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // opaqueVariant maps alpha formats to the same layout without alpha. | |||
| 165 | + | var opaqueVariant = map[uint32]uint32{ | |||
| 166 | + | fourccARGB: fourccXRGB, | |||
| 167 | + | fourccABGR: fourccXBGR, | |||
| 168 | + | fourccAR30: fourccXR30, | |||
| 169 | + | fourccAB30: fourccXB30, | |||
| 170 | + | fourccAR4H: fourccXR4H, | |||
| 171 | + | fourccAB4H: fourccXB4H, | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | // scanoutFormats are the formats a buffer can be scanned out with: in the | |||
| 175 | + | // primary plane's IN_FORMATS (directly or as its opaque variant) and | |||
| 176 | + | // composable by the renderer. | |||
| 177 | + | func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat { | |||
| 178 | + | var out []ports.DMABufFormat | |||
| 179 | + | for _, f := range sampled { | |||
| 180 | + | b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier} | |||
| 181 | + | if _, ok := o.scanoutFormat(&b); ok { | |||
| 182 | + | out = append(out, f) | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | return out | |||
| 126 | } | 186 | } | ||
| 127 | 187 | ||||
| 128 | const ( | 188 | const ( | ||
| 129 | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | 189 | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | ||
| 130 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | 190 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | ||
| 191 | + | fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24 | |||
| 192 | + | fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24 | |||
| 193 | + | fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24 | |||
| 194 | + | fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24 | |||
| 195 | + | fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24 | |||
| 196 | + | fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24 | |||
| 197 | + | fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24 | |||
| 198 | + | fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24 | |||
| 199 | + | fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24 | |||
| 200 | + | fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24 | |||
| 131 | ) | 201 | ) | ||
| 132 | 202 | ||||
| 133 | - | // addFB imports a dmabuf as a framebuffer: client buffers and the | 203 | + | // 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 | 204 | + | // 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 | 205 | // 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 | 206 | + | // close run under the lock and the handle is closed right away: the |
| 137 | // framebuffer keeps its own reference to the buffer. | 207 | // framebuffer keeps its own reference to the buffer. | ||
| 138 | - | func (c *Card) addFB(b *ports.DMABuf) (uint32, error) { | 208 | + | 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() | 209 | raw, err := b.Planes[0].File.SyscallConn() | ||
| 144 | if err != nil { | 210 | if err != nil { | ||
| 145 | return 0, err | 211 | return 0, err | ||
| 146 | } | 212 | } | ||
| 147 | - | c.gemMu.Lock() | 213 | + | k.gemMu.Lock() |
| 148 | - | defer c.gemMu.Unlock() | 214 | + | defer k.gemMu.Unlock() |
| 149 | var p primeHandle | 215 | var p primeHandle | ||
| 150 | var ioErr error | 216 | var ioErr error | ||
| 151 | if err := raw.Control(func(fd uintptr) { | 217 | if err := raw.Control(func(fd uintptr) { | ||
| 152 | p.fd = int32(fd) | 218 | p.fd = int32(fd) | ||
| 153 | - | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | 219 | + | ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) |
| 154 | }); err != nil { | 220 | }); err != nil { | ||
| 155 | return 0, err | 221 | return 0, err | ||
| 156 | } | 222 | } | ||
| @@ -159,19 +225,68 @@ func (c *Card) addFB(b *ports.DMABuf) (uint32, error) { | |||||
| 159 | } | 225 | } | ||
| 160 | defer func() { | 226 | defer func() { | ||
| 161 | g := gemClose{handle: p.handle} | 227 | g := gemClose{handle: p.handle} | ||
| 162 | - | _ = ioctl(c.fd, ioctlGemClose, unsafe.Pointer(&g)) | 228 | + | _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g)) |
| 163 | }() | 229 | }() | ||
| 164 | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} | 230 | 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 | 231 | + | useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers) |
| 166 | - | if b.Modifier != modInvalid && (b.Modifier != 0 || c.modifiers) { | 232 | + | if useMod { |
| 167 | - | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | 233 | + | cmd.flags = fbModifiers |
| 234 | + | } | |||
| 235 | + | // Planes in the same dmabuf share its GEM handle: each distinct | |||
| 236 | + | // handle is closed once. | |||
| 237 | + | closing := map[uint32]bool{p.handle: true} | |||
| 238 | + | for i, pl := range b.Planes { | |||
| 239 | + | if i >= len(cmd.handles) { | |||
| 240 | + | break | |||
| 241 | + | } | |||
| 242 | + | h := p.handle | |||
| 243 | + | if i > 0 { | |||
| 244 | + | var err error | |||
| 245 | + | if h, err = k.primeImport(pl.File); err != nil { | |||
| 246 | + | return 0, err | |||
| 247 | + | } | |||
| 248 | + | if !closing[h] { | |||
| 249 | + | closing[h] = true | |||
| 250 | + | defer k.gemClose(h) | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset | |||
| 254 | + | if useMod { | |||
| 255 | + | cmd.modifiers[i] = b.Modifier | |||
| 256 | + | } | |||
| 168 | } | 257 | } | ||
| 169 | - | if err := ioctl(c.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | 258 | + | if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { |
| 170 | return 0, errors.Join(errors.New("add fb2"), err) | 259 | return 0, errors.Join(errors.New("add fb2"), err) | ||
| 171 | } | 260 | } | ||
| 172 | return cmd.fbID, nil | 261 | return cmd.fbID, nil | ||
| 173 | } | 262 | } | ||
| 174 | 263 | ||||
| 264 | + | // primeImport returns the GEM handle of a dmabuf file. The caller holds | |||
| 265 | + | // gemMu and closes the handle. | |||
| 266 | + | func (k kmsDevice) primeImport(f *os.File) (uint32, error) { | |||
| 267 | + | raw, err := f.SyscallConn() | |||
| 268 | + | if err != nil { | |||
| 269 | + | return 0, err | |||
| 270 | + | } | |||
| 271 | + | var p primeHandle | |||
| 272 | + | var ioErr error | |||
| 273 | + | if err := raw.Control(func(fd uintptr) { | |||
| 274 | + | p.fd = int32(fd) | |||
| 275 | + | ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |||
| 276 | + | }); err != nil { | |||
| 277 | + | return 0, err | |||
| 278 | + | } | |||
| 279 | + | if ioErr != nil { | |||
| 280 | + | return 0, errors.Join(errors.New("prime import"), ioErr) | |||
| 281 | + | } | |||
| 282 | + | return p.handle, nil | |||
| 283 | + | } | |||
| 284 | + | ||||
| 285 | + | func (k kmsDevice) gemClose(h uint32) { | |||
| 286 | + | g := gemClose{handle: h} | |||
| 287 | + | _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g)) | |||
| 288 | + | } | |||
| 289 | + | ||||
| 175 | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all | 290 | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all | ||
| 176 | // of them, except those on screen or queued. | 291 | // of them, except those on screen or queued. | ||
| 177 | func (o *Output) dropClientFBs(now time.Time, all bool) { | 292 | func (o *Output) dropClientFBs(now time.Time, all bool) { | ||
| @@ -183,11 +298,25 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||||
| 183 | continue | 298 | continue | ||
| 184 | } | 299 | } | ||
| 185 | if fb.fbID != 0 { | 300 | if fb.fbID != 0 { | ||
| 186 | - | v := fb.fbID | 301 | + | _ = o.k.rmFB(fb.fbID) |
| 187 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |||
| 188 | } | 302 | } | ||
| 189 | delete(o.clientFBs, id) | 303 | delete(o.clientFBs, id) | ||
| 190 | } | 304 | } | ||
| 191 | } | 305 | } | ||
| 192 | 306 | ||||
| 193 | - | func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok } | 307 | + | // dupFence duplicates a sync file under its SyscallConn, so a concurrent |
| 308 | + | // Close cannot hand us a reused fd; nil when f is nil or closed. | |||
| 309 | + | func dupFence(f *os.File) *os.File { | |||
| 310 | + | if f == nil { | |||
| 311 | + | return nil | |||
| 312 | + | } | |||
| 313 | + | raw, err := f.SyscallConn() | |||
| 314 | + | if err != nil { | |||
| 315 | + | return nil | |||
| 316 | + | } | |||
| 317 | + | fd := -1 | |||
| 318 | + | if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 { | |||
| 319 | + | return nil | |||
| 320 | + | } | |||
| 321 | + | return os.NewFile(uintptr(fd), "acquire-fence") | |||
| 322 | + | } | |||
M internal/adapters/drm/scanout_test.go +95 -10
| @@ -2,8 +2,10 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | + | "time" | |||
| 5 | 6 | ||||
| 6 | "github.com/bnema/nefertty/internal/ports" | 7 | "github.com/bnema/nefertty/internal/ports" | ||
| 8 | + | "github.com/stretchr/testify/mock" | |||
| 7 | ) | 9 | ) | ||
| 8 | 10 | ||||
| 9 | func TestScanoutCandidate(t *testing.T) { | 11 | func TestScanoutCandidate(t *testing.T) { | ||
| @@ -38,11 +40,6 @@ func TestScanoutCandidate(t *testing.T) { | |||||
| 38 | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | 40 | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | ||
| 39 | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | 41 | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | ||
| 40 | {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"}, | 42 | {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"}, | ||
| 41 | - | {"unsupported format", nil, func(c *ports.SurfaceContent) { | |||
| 42 | - | d := *c.DMABuf | |||
| 43 | - | d.Format = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 | |||
| 44 | - | c.DMABuf = &d | |||
| 45 | - | }, "format"}, | |||
| 46 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | 43 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | ||
| 47 | } { | 44 | } { | ||
| 48 | s := scene | 45 | s := scene | ||
| @@ -60,11 +57,99 @@ func TestScanoutCandidate(t *testing.T) { | |||||
| 60 | } | 57 | } | ||
| 61 | } | 58 | } | ||
| 62 | 59 | ||||
| 63 | - | func TestScanoutFormat(t *testing.T) { | 60 | + | // A buffer scans out when the primary plane lists its format (or the |
| 64 | - | for f, want := range map[uint32]bool{fourccXRGB: true, fourccARGB: true, 'A' | 'B'<<8 | '2'<<16 | '4'<<24: false} { | 61 | + | // opaque variant) with its modifier; the tranche offers only formats the |
| 65 | - | got, ok := scanoutFormat(f) | 62 | + | // renderer also composes. |
| 66 | - | if ok != want || ok && got != fourccXRGB { | 63 | + | func TestScanoutFormatFollowsInFormats(t *testing.T) { |
| 67 | - | t.Errorf("%x: %x %v", f, got, ok) | 64 | + | const tiled = 0x0200000000000001 |
| 65 | + | o, k, _ := testOutput(t) | |||
| 66 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0}, {Format: fourccXR30, Modifier: tiled}} | |||
| 67 | + | nv12 := uint32('N' | 'V'<<8 | '1'<<16 | '2'<<24) | |||
| 68 | + | for _, tc := range []struct { | |||
| 69 | + | name string | |||
| 70 | + | format uint32 | |||
| 71 | + | mod uint64 | |||
| 72 | + | as uint32 | |||
| 73 | + | ok bool | |||
| 74 | + | }{ | |||
| 75 | + | {"argb as xrgb", fourccARGB, tiled, fourccXRGB, true}, | |||
| 76 | + | {"xrgb linear", fourccXRGB, 0, fourccXRGB, true}, | |||
| 77 | + | {"10 bit alpha as opaque", fourccAR30, tiled, fourccXR30, true}, | |||
| 78 | + | {"modifier not listed", fourccXRGB, 0x0200000000000002, 0, false}, | |||
| 79 | + | {"format not listed", nv12, tiled, 0, false}, | |||
| 80 | + | } { | |||
| 81 | + | got, ok := o.scanoutFormat(&ports.DMABuf{Format: tc.format, Modifier: tc.mod}) | |||
| 82 | + | if ok != tc.ok || ok && got != tc.as { | |||
| 83 | + | t.Errorf("%s: %#x %v", tc.name, got, ok) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | sampled := []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0x0200000000000002}, {Format: nv12, Modifier: tiled}} | |||
| 87 | + | if got := o.scanoutFormats(sampled); len(got) != 1 || got[0] != sampled[0] { | |||
| 88 | + | t.Fatalf("tranche %v", got) | |||
| 89 | + | } | |||
| 90 | + | // A buffer not in IN_FORMATS is refused before any import. | |||
| 91 | + | o.clientFBs = map[uint64]*clientFB{} | |||
| 92 | + | if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 4, Format: nv12, Modifier: tiled}, time.Now()); fb != 0 || reason != "format" { | |||
| 93 | + | t.Fatalf("fb %d reason %q", fb, reason) | |||
| 94 | + | } | |||
| 95 | + | // A listed one is imported with every plane (addFB builds ADDFB2). | |||
| 96 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 97 | + | if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 5, Format: fourccARGB, Modifier: tiled, Planes: []ports.DMABufPlane{{}, {}}}, time.Now()); fb != 88 || reason != "" { | |||
| 98 | + | t.Fatalf("fb %d reason %q", fb, reason) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // Each modeset reports the formats clients may allocate for scanout; | |||
| 103 | + | // with direct scanout off the list is empty. | |||
| 104 | + | func TestModesetSendsScanoutFormats(t *testing.T) { | |||
| 105 | + | const tiled = 0x0200000000000001 | |||
| 106 | + | for _, on := range []bool{true, false} { | |||
| 107 | + | o, k, _ := testOutput(t) | |||
| 108 | + | o.cursor = nil | |||
| 109 | + | o.scanout, o.device = on, 9 | |||
| 110 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}} | |||
| 111 | + | o.sampled = []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0}} | |||
| 112 | + | ch := make(chan ports.OutputFormats, 1) | |||
| 113 | + | o.formats = ch | |||
| 114 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once() | |||
| 115 | + | if err := o.modeset(); err != nil { | |||
| 116 | + | t.Fatal(err) | |||
| 68 | } | 117 | } | ||
| 118 | + | f := <-ch | |||
| 119 | + | if f.Output != "DP-1" || f.Device != 9 { | |||
| 120 | + | t.Fatalf("report %+v", f) | |||
| 121 | + | } | |||
| 122 | + | if on != (len(f.Formats) == 1 && f.Formats[0] == o.sampled[0]) || !on && len(f.Formats) != 0 { | |||
| 123 | + | t.Fatalf("scanout=%v formats %v", on, f.Formats) | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // A plane without IN_FORMATS (driver without modifiers) scans out its | |||
| 129 | + | // GETPLANE formats, linear. | |||
| 130 | + | func TestPlaneWithoutInFormatsIsLinear(t *testing.T) { | |||
| 131 | + | k := newMockkms(t) | |||
| 132 | + | k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1, formats: []uint32{fourccXRGB}}}, nil) | |||
| 133 | + | k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}}, nil) | |||
| 134 | + | ps, err := readPlanes(k, 0) | |||
| 135 | + | if err != nil || len(ps) != 1 { | |||
| 136 | + | t.Fatalf("%v %v", ps, err) | |||
| 137 | + | } | |||
| 138 | + | o := &Output{primary: ps[0]} | |||
| 139 | + | if f, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB}); !ok || f != fourccXRGB { | |||
| 140 | + | t.Fatalf("linear argb: %#x %v", f, ok) | |||
| 141 | + | } | |||
| 142 | + | if _, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB, Modifier: 0x0200000000000001}); ok { | |||
| 143 | + | t.Fatal("tiled accepted without IN_FORMATS") | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // A flip shows only the windows its scene draws: presentation feedback of | |||
| 148 | + | // a window on another workspace or hidden is discarded, not presented. | |||
| 149 | + | func TestShownByScene(t *testing.T) { | |||
| 150 | + | s := ports.Scene{Windows: []ports.SceneWindow{{ID: 1}, {ID: 2, Hidden: true}}, Layers: []ports.SceneLayer{{ID: 5}}} | |||
| 151 | + | got := shownBy(s, map[ports.WindowID]uint64{1: 3, 2: 4, 5: 6, 9: 7}) | |||
| 152 | + | if len(got) != 2 || got[1] != 3 || got[5] != 6 { | |||
| 153 | + | t.Fatalf("shows %v", got) | |||
| 69 | } | 154 | } | ||
| 70 | } | 155 | } | ||
A internal/adapters/drm/stuck_test.go +244 -0
| @@ -0,0 +1,244 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "sync" | |||
| 7 | + | "sync/atomic" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 13 | + | "github.com/stretchr/testify/mock" | |||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // runStuck runs an output whose commits return errs (after the start | |||
| 18 | + | // modeset test and modeset) and whose CRTC never sends an event. It | |||
| 19 | + | // returns the recorded commits once n were made, failing after 2s. | |||
| 20 | + | func runStuck(t *testing.T, n int, errs ...error) []commitRec { | |||
| 21 | + | t.Helper() | |||
| 22 | + | h := startStuck(t, nil, errs...) | |||
| 23 | + | h.scenes <- ports.Scene{Seq: 1} | |||
| 24 | + | waitFor(t, func() bool { return h.count() >= n }) | |||
| 25 | + | return h.stop() | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // stuckRun is an output run by startStuck. | |||
| 29 | + | type stuckRun struct { | |||
| 30 | + | t *testing.T | |||
| 31 | + | scenes chan ports.Scene | |||
| 32 | + | contents chan ports.SurfaceContent | |||
| 33 | + | active chan bool | |||
| 34 | + | presented chan ports.OutputPresented | |||
| 35 | + | count func() int | |||
| 36 | + | stop func() []commitRec | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // startStuck runs an output whose commits return errs (after the start | |||
| 40 | + | // modeset test and modeset) and whose CRTC never sends an event. render | |||
| 41 | + | // makes each frame's fence once the output started (nil: none). | |||
| 42 | + | func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |||
| 43 | + | t.Helper() | |||
| 44 | + | o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...) | |||
| 45 | + | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |||
| 46 | + | o.stuckAfter = 200 * time.Millisecond | |||
| 47 | + | o.flipped = make(chan flipEvent) // no event ever | |||
| 48 | + | r := portsmocks.NewMockRenderer(t) | |||
| 49 | + | buf := func() ports.DMABuf { | |||
| 50 | + | f, w, _ := os.Pipe() | |||
| 51 | + | w.Close() | |||
| 52 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 53 | + | } | |||
| 54 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 55 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 56 | + | var started atomic.Bool | |||
| 57 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 58 | + | if render == nil || !started.Load() { | |||
| 59 | + | return nil, nil | |||
| 60 | + | } | |||
| 61 | + | return render(), nil | |||
| 62 | + | }) | |||
| 63 | + | r.EXPECT().Close().Return().Once() | |||
| 64 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 65 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 66 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 67 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 68 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)} | |||
| 69 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 70 | + | done := make(chan error, 1) | |||
| 71 | + | go func() { | |||
| 72 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented) | |||
| 73 | + | }() | |||
| 74 | + | h.count = func() int { | |||
| 75 | + | commitMu.Lock() | |||
| 76 | + | defer commitMu.Unlock() | |||
| 77 | + | return len(*commits) | |||
| 78 | + | } | |||
| 79 | + | h.stop = func() []commitRec { | |||
| 80 | + | cancel() | |||
| 81 | + | if err := <-done; err != nil { | |||
| 82 | + | t.Fatal(err) | |||
| 83 | + | } | |||
| 84 | + | commitMu.Lock() | |||
| 85 | + | defer commitMu.Unlock() | |||
| 86 | + | return append([]commitRec(nil), (*commits)...) | |||
| 87 | + | } | |||
| 88 | + | waitFor(t, func() bool { return h.count() == 2 }) | |||
| 89 | + | started.Store(true) | |||
| 90 | + | return h | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // A commit whose event never comes must not stop the output: it is | |||
| 94 | + | // abandoned with a modeset, then the scene is committed again. | |||
| 95 | + | func TestMissingEventRecoversWithModeset(t *testing.T) { | |||
| 96 | + | c := runStuck(t, 5) | |||
| 97 | + | if c[2].user&3 != userFrame { | |||
| 98 | + | t.Fatalf("commit 2 is not a frame: %+v", c[2]) | |||
| 99 | + | } | |||
| 100 | + | if c[3].flags&atomicAllowModes == 0 { | |||
| 101 | + | t.Fatalf("no recovery modeset after the missing event: flags %#x", c[3].flags) | |||
| 102 | + | } | |||
| 103 | + | if c[4].user&3 != userFrame || c[4].user>>userKindBits <= c[2].user>>userKindBits { | |||
| 104 | + | t.Fatalf("scene not committed again after the modeset: %+v", c[4]) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // EBUSY with no commit of ours in flight gets no event: the commit is | |||
| 109 | + | // retried without one. | |||
| 110 | + | func TestBusyWithoutEventRetries(t *testing.T) { | |||
| 111 | + | c := runStuck(t, 4, unix.EBUSY) | |||
| 112 | + | if c[2].user&3 != userFrame || c[3].user&3 != userFrame || c[3].flags&atomicAllowModes != 0 { | |||
| 113 | + | t.Fatalf("busy frame not retried: %+v", c[2:]) | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // EBUSY that lasts past the limit ends in a modeset, and a modeset | |||
| 118 | + | // refused as busy is retried too. | |||
| 119 | + | func TestLastingBusyModesets(t *testing.T) { | |||
| 120 | + | errs := make([]error, 8) | |||
| 121 | + | for i := range errs { | |||
| 122 | + | errs[i] = unix.EBUSY | |||
| 123 | + | } | |||
| 124 | + | c := runStuck(t, 2+len(errs)+1, errs...) | |||
| 125 | + | found := false | |||
| 126 | + | for _, r := range c[2:] { | |||
| 127 | + | if r.flags&atomicAllowModes != 0 { | |||
| 128 | + | found = true | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | if !found { | |||
| 132 | + | t.Fatal("no modeset while EBUSY lasted") | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // fencePipe is a fence that signals when its write end is written. | |||
| 137 | + | func fencePipe(t *testing.T) (fence, signal *os.File) { | |||
| 138 | + | t.Helper() | |||
| 139 | + | r, w, err := os.Pipe() | |||
| 140 | + | if err != nil { | |||
| 141 | + | t.Fatal(err) | |||
| 142 | + | } | |||
| 143 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |||
| 144 | + | return r, w | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // A frame whose GPU fence has not signalled is slow, not lost: it keeps | |||
| 148 | + | // waiting past the limit; once the fence signalled and still no event | |||
| 149 | + | // came, the output modesets. | |||
| 150 | + | func TestUnsignalledFenceDelaysRecovery(t *testing.T) { | |||
| 151 | + | var mu sync.Mutex | |||
| 152 | + | var signals []*os.File | |||
| 153 | + | h := startStuck(t, func() *os.File { | |||
| 154 | + | f, w := fencePipe(t) | |||
| 155 | + | mu.Lock() | |||
| 156 | + | signals = append(signals, w) | |||
| 157 | + | mu.Unlock() | |||
| 158 | + | return f | |||
| 159 | + | }) | |||
| 160 | + | h.scenes <- ports.Scene{Seq: 1} | |||
| 161 | + | waitFor(t, func() bool { return h.count() == 3 }) | |||
| 162 | + | time.Sleep(3 * 200 * time.Millisecond) | |||
| 163 | + | if n := h.count(); n != 3 { | |||
| 164 | + | t.Fatalf("%d commits while the frame's fence is unsignalled", n) | |||
| 165 | + | } | |||
| 166 | + | mu.Lock() | |||
| 167 | + | _, _ = signals[0].Write([]byte{1}) | |||
| 168 | + | mu.Unlock() | |||
| 169 | + | waitFor(t, func() bool { return h.count() >= 4 }) | |||
| 170 | + | if c := h.stop(); c[3].flags&atomicAllowModes == 0 { | |||
| 171 | + | t.Fatalf("no modeset once the fence signalled: %#x", c[3].flags) | |||
| 172 | + | } | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | // A frame refused with EBUSY may still be read by the GPU: what was seen | |||
| 176 | + | // is reported only once its fence signalled. | |||
| 177 | + | func TestSeenWaitsForRefusedFrameFence(t *testing.T) { | |||
| 178 | + | var mu sync.Mutex | |||
| 179 | + | var signals []*os.File | |||
| 180 | + | h := startStuck(t, func() *os.File { | |||
| 181 | + | f, w := fencePipe(t) | |||
| 182 | + | mu.Lock() | |||
| 183 | + | signals = append(signals, w) | |||
| 184 | + | mu.Unlock() | |||
| 185 | + | return f | |||
| 186 | + | }, unix.EBUSY, unix.EACCES) | |||
| 187 | + | h.scenes <- ports.Scene{Seq: 1} | |||
| 188 | + | // EBUSY, then the retry loses DRM master: nothing is pending. | |||
| 189 | + | waitFor(t, func() bool { return h.count() == 4 }) | |||
| 190 | + | h.contents <- ports.SurfaceContent{ID: 1, Seq: 7} | |||
| 191 | + | select { | |||
| 192 | + | case p := <-h.presented: | |||
| 193 | + | t.Fatalf("seen reported while refused frames may be read: %+v", p) | |||
| 194 | + | case <-time.After(50 * time.Millisecond): | |||
| 195 | + | } | |||
| 196 | + | mu.Lock() | |||
| 197 | + | for _, w := range signals { | |||
| 198 | + | _, _ = w.Write([]byte{1}) | |||
| 199 | + | } | |||
| 200 | + | mu.Unlock() | |||
| 201 | + | select { | |||
| 202 | + | case p := <-h.presented: | |||
| 203 | + | if p.Seen[1] != 7 { | |||
| 204 | + | t.Fatalf("report %+v", p) | |||
| 205 | + | } | |||
| 206 | + | case <-time.After(2 * time.Second): | |||
| 207 | + | t.Fatal("seen not reported once the fences signalled") | |||
| 208 | + | } | |||
| 209 | + | h.stop() | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | // An EBUSY run ends when commits stop being refused: a later EBUSY is | |||
| 213 | + | // retried, not taken as the same run lasting past the limit. | |||
| 214 | + | func TestLaterBusyStartsNewRun(t *testing.T) { | |||
| 215 | + | o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY) | |||
| 216 | + | o.stuckAfter = 100 * time.Millisecond | |||
| 217 | + | enabled := true | |||
| 218 | + | o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) | |||
| 219 | + | time.Sleep(busyRetry) | |||
| 220 | + | if o.expire() { | |||
| 221 | + | t.Fatal("first EBUSY retry modesets") | |||
| 222 | + | } | |||
| 223 | + | time.Sleep(2 * o.stuckAfter) | |||
| 224 | + | o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) | |||
| 225 | + | time.Sleep(busyRetry) | |||
| 226 | + | if o.expire() { | |||
| 227 | + | t.Fatal("a new EBUSY taken as the old run: modeset") | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // A modeset that fails keeps what is on screen: buffers still shown are | |||
| 232 | + | // not released under the plane. | |||
| 233 | + | func TestFailedModesetKeepsShown(t *testing.T) { | |||
| 234 | + | o, k, _ := testOutput(t, unix.EACCES) | |||
| 235 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 236 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 237 | + | o.shown, o.queued = 5, 6 | |||
| 238 | + | if err := o.modeset(); err == nil { | |||
| 239 | + | t.Fatal("modeset succeeded") | |||
| 240 | + | } | |||
| 241 | + | if o.shown != 5 || o.queued != 6 { | |||
| 242 | + | t.Fatalf("shown %d queued %d after a failed modeset", o.shown, o.queued) | |||
| 243 | + | } | |||
| 244 | + | } | |||
M internal/adapters/headless/output.go +45 -5
| @@ -3,6 +3,7 @@ package headless | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "golang.org/x/sys/unix" | |||
| 6 | "image" | 7 | "image" | ||
| 7 | "image/png" | 8 | "image/png" | ||
| 8 | "maps" | 9 | "maps" | ||
| @@ -10,6 +11,7 @@ import ( | |||||
| 10 | "path/filepath" | 11 | "path/filepath" | ||
| 11 | "time" | 12 | "time" | ||
| 12 | 13 | ||||
| 14 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |||
| 13 | "github.com/bnema/nefertty/internal/ports" | 15 | "github.com/bnema/nefertty/internal/ports" | ||
| 14 | "github.com/bnema/zerowrap" | 16 | "github.com/bnema/zerowrap" | ||
| 15 | ) | 17 | ) | ||
| @@ -118,11 +120,24 @@ 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 = syncfile.Wait(ctx, done) | |||
| 131 | + | done.Close() | |||
| 132 | + | if err != nil { | |||
| 133 | + | if ctx.Err() != nil { | |||
| 134 | + | return nil | |||
| 135 | + | } | |||
| 136 | + | return fmt.Errorf("frame fence: %w", err) | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 124 | frame++ | 139 | frame++ | ||
| 125 | - | pending = opts.report(pending, seen) | 140 | + | pending = opts.flipped(pending, seen, scene, surfaces) |
| 126 | if opts.ScreenshotDir != "" { | 141 | if opts.ScreenshotDir != "" { | ||
| 127 | shot := r.Pixels() | 142 | shot := r.Pixels() | ||
| 128 | if opts.Cursor != nil { | 143 | if opts.Cursor != nil { | ||
| @@ -155,10 +170,8 @@ func writePNG(path string, img *image.RGBA) error { | |||||
| 155 | return os.Rename(f.Name(), path) | 170 | return os.Rename(f.Name(), path) | ||
| 156 | } | 171 | } | ||
| 157 | 172 | ||||
| 158 | - | // report sends what the output read; a full channel drops it. | |||
| 159 | // report sends what the output read, or returns it to retry when the | 173 | // report sends what the output read, or returns it to retry when the | ||
| 160 | - | // channel is full. Headless frames are not paced by a screen: Flip stays | 174 | + | // channel is full. Frames that were not drawn carry no flip. |
| 161 | - | // false and frame callbacks follow the refresh timer. | |||
| 162 | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | 175 | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | ||
| 163 | if opts.Presented == nil { | 176 | if opts.Presented == nil { | ||
| 164 | return nil | 177 | return nil | ||
| @@ -166,6 +179,33 @@ func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uin | |||||
| 166 | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | 179 | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | ||
| 167 | } | 180 | } | ||
| 168 | 181 | ||||
| 182 | + | // flipped reports a drawn frame as a flip at the current CLOCK_MONOTONIC | |||
| 183 | + | // time (software clock, refresh unknown), so presentation feedback and | |||
| 184 | + | // frame callbacks follow headless frames. A report still waiting is | |||
| 185 | + | // sent first. | |||
| 186 | + | func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.WindowID]uint64, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) *ports.OutputPresented { | |||
| 187 | + | if opts.Presented == nil { | |||
| 188 | + | return nil | |||
| 189 | + | } | |||
| 190 | + | var merged int | |||
| 191 | + | if pending != nil && opts.send(pending) != nil { | |||
| 192 | + | // The reader is behind: this frame's report replaces the waiting | |||
| 193 | + | // one, and its flip counts as merged (its feedback is discarded). | |||
| 194 | + | if pending.Flip != nil { | |||
| 195 | + | merged = 1 + pending.Flip.Merged | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | var ts unix.Timespec | |||
| 199 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |||
| 200 | + | shows := map[ports.WindowID]uint64{} | |||
| 201 | + | for id, c := range surfaces { | |||
| 202 | + | if scene.Shows(id) { | |||
| 203 | + | shows[id] = c.Seq | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows, Merged: merged}}) | |||
| 207 | + | } | |||
| 208 | + | ||||
| 169 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | 209 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | ||
| 170 | select { | 210 | select { | ||
| 171 | case opts.Presented <- *r: | 211 | case opts.Presented <- *r: | ||
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() | ||
M internal/adapters/libinput/forward.go +19 -1
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "time" | 6 | "time" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| 9 | + | "github.com/bnema/zerowrap" | |||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 11 | // forwarder decouples reading libinput from core. libinput keeps reading | 12 | // forwarder decouples reading libinput from core. libinput keeps reading | ||
| @@ -17,8 +18,15 @@ type forwarder struct { | |||||
| 17 | queue []ports.InputEvent | 18 | queue []ports.InputEvent | ||
| 18 | wake chan struct{} | 19 | wake chan struct{} | ||
| 19 | stats forwardStats | 20 | stats forwardStats | ||
| 21 | + | // The queue is unbounded (no event is ever dropped); long is set | |||
| 22 | + | // while it is past forwardWarn entries, warned once each way. | |||
| 23 | + | log zerowrap.Logger | |||
| 24 | + | long bool | |||
| 20 | } | 25 | } | ||
| 21 | 26 | ||||
| 27 | + | // forwardWarn is the queue length that means core has stalled. | |||
| 28 | + | const forwardWarn = 4096 | |||
| 29 | + | ||||
| 22 | // forwardStats is reset by take. | 30 | // forwardStats is reset by take. | ||
| 23 | type forwardStats struct { | 31 | type forwardStats struct { | ||
| 24 | Motions int // pointer motions read | 32 | Motions int // pointer motions read | ||
| @@ -27,7 +35,9 @@ type forwardStats struct { | |||||
| 27 | MaxBlock time.Duration // longest wait for core to accept one event | 35 | MaxBlock time.Duration // longest wait for core to accept one event | ||
| 28 | } | 36 | } | ||
| 29 | 37 | ||||
| 30 | - | func newForwarder() *forwarder { return &forwarder{wake: make(chan struct{}, 1)} } | 38 | + | func newForwarder(log zerowrap.Logger) *forwarder { |
| 39 | + | return &forwarder{wake: make(chan struct{}, 1), log: log} | |||
| 40 | + | } | |||
| 31 | 41 | ||||
| 32 | // push queues ev without blocking. | 42 | // push queues ev without blocking. | ||
| 33 | func (f *forwarder) push(ev ports.InputEvent) { | 43 | func (f *forwarder) push(ev ports.InputEvent) { | ||
| @@ -49,6 +59,10 @@ func (f *forwarder) push(ev ports.InputEvent) { | |||||
| 49 | } | 59 | } | ||
| 50 | } | 60 | } | ||
| 51 | f.queue = append(f.queue, ev) | 61 | f.queue = append(f.queue, ev) | ||
| 62 | + | if len(f.queue) > forwardWarn && !f.long { | |||
| 63 | + | f.long = true | |||
| 64 | + | f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("core is not reading input; events queue up") | |||
| 65 | + | } | |||
| 52 | f.mu.Unlock() | 66 | f.mu.Unlock() | ||
| 53 | select { | 67 | select { | ||
| 54 | case f.wake <- struct{}{}: | 68 | case f.wake <- struct{}{}: | ||
| @@ -65,6 +79,10 @@ func (f *forwarder) pop() (ports.InputEvent, bool) { | |||||
| 65 | ev := f.queue[0] | 79 | ev := f.queue[0] | ||
| 66 | f.queue[0] = nil | 80 | f.queue[0] = nil | ||
| 67 | f.queue = f.queue[1:] | 81 | f.queue = f.queue[1:] | ||
| 82 | + | if f.long && len(f.queue) < forwardWarn { | |||
| 83 | + | f.long = false | |||
| 84 | + | f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("input queue drained") | |||
| 85 | + | } | |||
| 68 | return ev, true | 86 | return ev, true | ||
| 69 | } | 87 | } | ||
| 70 | 88 | ||||
M internal/adapters/libinput/forward_test.go +63 -3
| @@ -1,17 +1,22 @@ | |||||
| 1 | package libinput | 1 | package libinput | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 4 | "context" | 5 | "context" | ||
| 6 | + | "strings" | |||
| 7 | + | "sync" | |||
| 5 | "testing" | 8 | "testing" | ||
| 6 | "time" | 9 | "time" | ||
| 7 | 10 | ||||
| 8 | "github.com/bnema/nefertty/internal/ports" | 11 | "github.com/bnema/nefertty/internal/ports" | ||
| 12 | + | "github.com/bnema/zerowrap" | |||
| 13 | + | "github.com/rs/zerolog" | |||
| 9 | ) | 14 | ) | ||
| 10 | 15 | ||||
| 11 | // A slow consumer must not block push, and stale motion must collapse into | 16 | // A slow consumer must not block push, and stale motion must collapse into | ||
| 12 | // the latest position while keys and buttons keep their order. | 17 | // the latest position while keys and buttons keep their order. | ||
| 13 | func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | 18 | func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | ||
| 14 | - | f := newForwarder() | 19 | + | f := newForwarder(zerowrap.Default()) |
| 15 | input := make(chan ports.InputEvent) // unbuffered: core is busy | 20 | input := make(chan ports.InputEvent) // unbuffered: core is busy | ||
| 16 | start := time.Now() | 21 | start := time.Now() | ||
| 17 | for i := range 1000 { | 22 | for i := range 1000 { | ||
| @@ -56,7 +61,7 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |||||
| 56 | } | 61 | } | ||
| 57 | 62 | ||||
| 58 | func TestForwarderStopsOnCancel(t *testing.T) { | 63 | func TestForwarderStopsOnCancel(t *testing.T) { | ||
| 59 | - | f := newForwarder() | 64 | + | f := newForwarder(zerowrap.Default()) |
| 60 | f.push(ports.PointerMotion{X: 1}) | 65 | f.push(ports.PointerMotion{X: 1}) | ||
| 61 | ctx, cancel := context.WithCancel(context.Background()) | 66 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 62 | done := make(chan struct{}) | 67 | done := make(chan struct{}) | ||
| @@ -71,7 +76,7 @@ func TestForwarderStopsOnCancel(t *testing.T) { | |||||
| 71 | 76 | ||||
| 72 | // Coalesced motions keep the sum of their relative deltas. | 77 | // Coalesced motions keep the sum of their relative deltas. | ||
| 73 | func TestForwarderSumsDeltas(t *testing.T) { | 78 | func TestForwarderSumsDeltas(t *testing.T) { | ||
| 74 | - | f := newForwarder() | 79 | + | f := newForwarder(zerowrap.Default()) |
| 75 | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | 80 | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | ||
| 76 | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | 81 | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | ||
| 77 | ev, _ := f.pop() | 82 | ev, _ := f.pop() | ||
| @@ -79,3 +84,58 @@ func TestForwarderSumsDeltas(t *testing.T) { | |||||
| 79 | t.Fatalf("got %+v, want %+v", ev, want) | 84 | t.Fatalf("got %+v, want %+v", ev, want) | ||
| 80 | } | 85 | } | ||
| 81 | } | 86 | } | ||
| 87 | + | ||||
| 88 | + | // 10 000 motions with core stalled are one event with exact delta sums. | |||
| 89 | + | func TestForwarderManyMotionsOneEvent(t *testing.T) { | |||
| 90 | + | f := newForwarder(zerowrap.Default()) | |||
| 91 | + | for i := range 10000 { | |||
| 92 | + | f.push(ports.PointerMotion{X: float64(i), DX: 0.5, UnaccelDY: 1}) | |||
| 93 | + | } | |||
| 94 | + | if len(f.queue) != 1 { | |||
| 95 | + | t.Fatalf("%d queued", len(f.queue)) | |||
| 96 | + | } | |||
| 97 | + | ev, _ := f.pop() | |||
| 98 | + | if m := ev.(ports.PointerMotion); m.X != 9999 || m.DX != 5000 || m.UnaccelDY != 10000 { | |||
| 99 | + | t.Fatalf("%+v", m) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | // Past forwardWarn queued events the forwarder warns once, and once more | |||
| 104 | + | // when the queue drains below it; nothing is dropped. | |||
| 105 | + | func TestForwarderWarnsOnceEachWay(t *testing.T) { | |||
| 106 | + | var buf lockedBuffer | |||
| 107 | + | f := newForwarder(zerowrap.Logger{Logger: zerolog.New(&buf)}) | |||
| 108 | + | for range forwardWarn + 100 { | |||
| 109 | + | f.push(ports.PointerButton{Button: 272}) | |||
| 110 | + | } | |||
| 111 | + | if n := strings.Count(buf.String(), "events queue up"); n != 1 { | |||
| 112 | + | t.Fatalf("%d warnings", n) | |||
| 113 | + | } | |||
| 114 | + | for range 200 { | |||
| 115 | + | f.pop() | |||
| 116 | + | } | |||
| 117 | + | if n := strings.Count(buf.String(), "input queue drained"); n != 1 { | |||
| 118 | + | t.Fatalf("%d drained logs", n) | |||
| 119 | + | } | |||
| 120 | + | if len(f.queue) != forwardWarn-100 { | |||
| 121 | + | t.Fatalf("dropped: %d left", len(f.queue)) | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // lockedBuffer is an io.Writer for a logger. | |||
| 126 | + | type lockedBuffer struct { | |||
| 127 | + | mu sync.Mutex | |||
| 128 | + | b bytes.Buffer | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func (l *lockedBuffer) Write(p []byte) (int, error) { | |||
| 132 | + | l.mu.Lock() | |||
| 133 | + | defer l.mu.Unlock() | |||
| 134 | + | return l.b.Write(p) | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func (l *lockedBuffer) String() string { | |||
| 138 | + | l.mu.Lock() | |||
| 139 | + | defer l.mu.Unlock() | |||
| 140 | + | return l.b.String() | |||
| 141 | + | } | |||
M internal/adapters/libinput/libinput.go +7 -2
| @@ -160,6 +160,9 @@ type Options struct { | |||||
| 160 | // the physical position on it. | 160 | // the physical position on it. | ||
| 161 | MoveCursor func(output string, x, y float64) | 161 | MoveCursor func(output string, x, y float64) | ||
| 162 | Log zerowrap.Logger | 162 | Log zerowrap.Logger | ||
| 163 | + | // LogMotion logs every pointer motion (--debug=input-motion); motions | |||
| 164 | + | // are otherwise only counted in the input stats. | |||
| 165 | + | LogMotion bool | |||
| 163 | } | 166 | } | ||
| 164 | 167 | ||||
| 165 | // statsEvery is how often Run logs input throughput. | 168 | // statsEvery is how often Run logs input throughput. | ||
| @@ -192,7 +195,7 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error | |||||
| 192 | p := newPointer(opts.Layout) | 195 | p := newPointer(opts.Layout) | ||
| 193 | p.moved(opts.MoveCursor) | 196 | p.moved(opts.MoveCursor) | ||
| 194 | fd := getFD(li) | 197 | fd := getFD(li) | ||
| 195 | - | fwd := newForwarder() | 198 | + | fwd := newForwarder(opts.Log) |
| 196 | fwdCtx, stopFwd := context.WithCancel(ctx) | 199 | fwdCtx, stopFwd := context.WithCancel(ctx) | ||
| 197 | var fwdDone sync.WaitGroup | 200 | var fwdDone sync.WaitGroup | ||
| 198 | fwdDone.Go(func() { fwd.run(fwdCtx, input) }) | 201 | fwdDone.Go(func() { fwd.run(fwdCtx, input) }) | ||
| @@ -278,7 +281,9 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) { | |||||
| 278 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | 281 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | ||
| 279 | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | 282 | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | ||
| 280 | p.moved(opts.MoveCursor) | 283 | p.moved(opts.MoveCursor) | ||
| 281 | - | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | 284 | + | if opts.LogMotion { |
| 285 | + | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | |||
| 286 | + | } | |||
| 282 | return m, nil | 287 | return m, nil | ||
| 283 | case evPointerAbs: | 288 | case evPointerAbs: | ||
| 284 | // Absolute devices (tablets, VMs) map to the whole layout. | 289 | // Absolute devices (tablets, VMs) map to the whole layout. | ||
A internal/adapters/syncfile/syncfile.go +38 -0
| @@ -0,0 +1,38 @@ | |||||
| 1 | + | // Package syncfile waits for Linux sync files (dma-fence fds), such as the | |||
| 2 | + | // GPU fence a frame render returns. | |||
| 3 | + | package syncfile | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "errors" | |||
| 8 | + | "fmt" | |||
| 9 | + | "os" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // pollSlice bounds one poll so a canceled ctx is seen. | |||
| 15 | + | const pollSlice = 100 // ms | |||
| 16 | + | ||||
| 17 | + | // Wait blocks until f signals or ctx ends. A sync file is signalled when | |||
| 18 | + | // it polls readable; an error or invalid fd is reported, not taken as | |||
| 19 | + | // signalled. | |||
| 20 | + | func Wait(ctx context.Context, f *os.File) error { | |||
| 21 | + | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |||
| 22 | + | for { | |||
| 23 | + | n, err := unix.Poll(fds, pollSlice) | |||
| 24 | + | switch { | |||
| 25 | + | case errors.Is(err, unix.EINTR): | |||
| 26 | + | continue | |||
| 27 | + | case err != nil: | |||
| 28 | + | return fmt.Errorf("poll sync file: %w", err) | |||
| 29 | + | case n > 0 && fds[0].Revents&unix.POLLIN != 0: | |||
| 30 | + | return nil | |||
| 31 | + | case n > 0: | |||
| 32 | + | return fmt.Errorf("sync file poll events %#x", fds[0].Revents) | |||
| 33 | + | } | |||
| 34 | + | if err := ctx.Err(); err != nil { | |||
| 35 | + | return err | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
A internal/adapters/syncfile/syncfile_test.go +53 -0
| @@ -0,0 +1,53 @@ | |||||
| 1 | + | package syncfile | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // A pipe stands in for a sync file: readable means signalled. | |||
| 12 | + | func TestWaitReturnsWhenReadable(t *testing.T) { | |||
| 13 | + | r, w, err := os.Pipe() | |||
| 14 | + | if err != nil { | |||
| 15 | + | t.Fatal(err) | |||
| 16 | + | } | |||
| 17 | + | defer r.Close() | |||
| 18 | + | go func() { | |||
| 19 | + | time.Sleep(10 * time.Millisecond) | |||
| 20 | + | _, _ = w.Write([]byte{1}) | |||
| 21 | + | w.Close() | |||
| 22 | + | }() | |||
| 23 | + | if err := Wait(context.Background(), r); err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func TestWaitStopsOnCancel(t *testing.T) { | |||
| 29 | + | r, w, err := os.Pipe() | |||
| 30 | + | if err != nil { | |||
| 31 | + | t.Fatal(err) | |||
| 32 | + | } | |||
| 33 | + | defer r.Close() | |||
| 34 | + | defer w.Close() | |||
| 35 | + | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) | |||
| 36 | + | defer cancel() | |||
| 37 | + | if err := Wait(ctx, r); !errors.Is(err, context.DeadlineExceeded) { | |||
| 38 | + | t.Fatalf("err %v", err) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // POLLERR alone (a write end whose reader is gone) is not a signal. | |||
| 43 | + | func TestWaitReportsErrorEvents(t *testing.T) { | |||
| 44 | + | r, w, err := os.Pipe() | |||
| 45 | + | if err != nil { | |||
| 46 | + | t.Fatal(err) | |||
| 47 | + | } | |||
| 48 | + | defer w.Close() | |||
| 49 | + | r.Close() | |||
| 50 | + | if err := Wait(context.Background(), w); err == nil { | |||
| 51 | + | t.Fatal("POLLERR taken as signalled") | |||
| 52 | + | } | |||
| 53 | + | } | |||
D internal/adapters/vulkan/blend.go +0 -190
| @@ -1,190 +0,0 @@ | |||||
| 1 | - | package vulkan | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | _ "embed" | |||
| 5 | - | "encoding/binary" | |||
| 6 | - | "unsafe" | |||
| 7 | - | ||||
| 8 | - | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | // Translucent client pixels are blended over what is below them; opaque | |||
| 12 | - | // ones keep the plain copy. The blend pipeline draws one quad per upload | |||
| 13 | - | // and its fragment shader reads the staged pixels straight from the | |||
| 14 | - | // staging buffer: no texture, no sampler. Wayland buffers are | |||
| 15 | - | // premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA. | |||
| 16 | - | // | |||
| 17 | - | // Limits: dmabuf buffers are still blitted, alpha ignored (blending them | |||
| 18 | - | // needs a sampled-image variant). The opaque region is not honoured, so | |||
| 19 | - | // an ARGB buffer blends even where it is opaque. | |||
| 20 | - | ||||
| 21 | - | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv | |||
| 22 | - | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv | |||
| 23 | - | ||||
| 24 | - | var ( | |||
| 25 | - | //go:embed shaders/blend.vert.spv | |||
| 26 | - | blendVert []byte | |||
| 27 | - | //go:embed shaders/blend.frag.spv | |||
| 28 | - | blendFrag []byte | |||
| 29 | - | ) | |||
| 30 | - | ||||
| 31 | - | // drawSize is one entry of the draw table (shaders: struct Draw). | |||
| 32 | - | const drawSize = 32 | |||
| 33 | - | ||||
| 34 | - | // blendPipeline is the graphics state for blended uploads. | |||
| 35 | - | type blendPipeline struct { | |||
| 36 | - | setLayout vk.DescriptorSetLayout | |||
| 37 | - | layout vk.PipelineLayout | |||
| 38 | - | pipeline vk.Pipeline | |||
| 39 | - | pool vk.DescriptorPool | |||
| 40 | - | set vk.DescriptorSet | |||
| 41 | - | } | |||
| 42 | - | ||||
| 43 | - | func (r *Renderer) createBlend() error { | |||
| 44 | - | d, b := r.dd, &r.blend | |||
| 45 | - | bindings := []vk.DescriptorSetLayoutBinding{ | |||
| 46 | - | {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |||
| 47 | - | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit}, | |||
| 48 | - | } | |||
| 49 | - | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]} | |||
| 50 | - | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil { | |||
| 51 | - | return err | |||
| 52 | - | } | |||
| 53 | - | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout} | |||
| 54 | - | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil { | |||
| 55 | - | return err | |||
| 56 | - | } | |||
| 57 | - | size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2} | |||
| 58 | - | dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size} | |||
| 59 | - | if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil { | |||
| 60 | - | return err | |||
| 61 | - | } | |||
| 62 | - | dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout} | |||
| 63 | - | if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil { | |||
| 64 | - | return err | |||
| 65 | - | } | |||
| 66 | - | vert, err := r.shaderModule(blendVert) | |||
| 67 | - | if err != nil { | |||
| 68 | - | return err | |||
| 69 | - | } | |||
| 70 | - | defer d.DestroyShaderModule(r.device, vert, nil) | |||
| 71 | - | frag, err := r.shaderModule(blendFrag) | |||
| 72 | - | if err != nil { | |||
| 73 | - | return err | |||
| 74 | - | } | |||
| 75 | - | defer d.DestroyShaderModule(r.device, frag, nil) | |||
| 76 | - | main := []byte("main\x00") | |||
| 77 | - | stages := []vk.PipelineShaderStageCreateInfo{ | |||
| 78 | - | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | |||
| 79 | - | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]}, | |||
| 80 | - | } | |||
| 81 | - | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | |||
| 82 | - | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | |||
| 83 | - | // The target size is fixed for the renderer's life: static viewport. | |||
| 84 | - | viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1} | |||
| 85 | - | scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}} | |||
| 86 | - | viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor} | |||
| 87 | - | raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1} | |||
| 88 | - | multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit} | |||
| 89 | - | attachment := vk.PipelineColorBlendAttachmentState{ | |||
| 90 | - | BlendEnable: 1, | |||
| 91 | - | SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd, | |||
| 92 | - | SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd, | |||
| 93 | - | ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit, | |||
| 94 | - | } | |||
| 95 | - | blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment} | |||
| 96 | - | format := vk.Format(vk.FormatB8g8r8a8Unorm) | |||
| 97 | - | rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format} | |||
| 98 | - | gpi := vk.GraphicsPipelineCreateInfo{ | |||
| 99 | - | SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering), | |||
| 100 | - | StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly, | |||
| 101 | - | ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample, | |||
| 102 | - | ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1, | |||
| 103 | - | } | |||
| 104 | - | return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline)) | |||
| 105 | - | } | |||
| 106 | - | ||||
| 107 | - | func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) { | |||
| 108 | - | // SPIR-V is read as words: copy into a uint32-aligned slice. | |||
| 109 | - | words := make([]uint32, len(code)/4) | |||
| 110 | - | for i := range words { | |||
| 111 | - | words[i] = binary.LittleEndian.Uint32(code[i*4:]) | |||
| 112 | - | } | |||
| 113 | - | info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]} | |||
| 114 | - | var m vk.ShaderModule | |||
| 115 | - | return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m)) | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | func (r *Renderer) destroyBlend() { | |||
| 119 | - | d, b := r.dd, &r.blend | |||
| 120 | - | if b.pipeline != 0 { | |||
| 121 | - | d.DestroyPipeline(r.device, b.pipeline, nil) | |||
| 122 | - | } | |||
| 123 | - | if b.pool != 0 { | |||
| 124 | - | d.DestroyDescriptorPool(r.device, b.pool, nil) | |||
| 125 | - | } | |||
| 126 | - | if b.layout != 0 { | |||
| 127 | - | d.DestroyPipelineLayout(r.device, b.layout, nil) | |||
| 128 | - | } | |||
| 129 | - | if b.setLayout != 0 { | |||
| 130 | - | d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil) | |||
| 131 | - | } | |||
| 132 | - | *b = blendPipeline{} | |||
| 133 | - | } | |||
| 134 | - | ||||
| 135 | - | // bindStaging points the set at the staging buffer: pixels blended | |||
| 136 | - | // pixel bytes from 0, and the draw table of n entries at tableOffset. | |||
| 137 | - | func (r *Renderer) bindStaging(pixels, tableOffset, n int) { | |||
| 138 | - | infos := []vk.DescriptorBufferInfo{ | |||
| 139 | - | {Buffer: r.staging, Range: vk.DeviceSize(max(pixels, 4))}, | |||
| 140 | - | {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: vk.DeviceSize(n * drawSize)}, | |||
| 141 | - | } | |||
| 142 | - | writes := []vk.WriteDescriptorSet{ | |||
| 143 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]}, | |||
| 144 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]}, | |||
| 145 | - | } | |||
| 146 | - | r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | |||
| 147 | - | } | |||
| 148 | - | ||||
| 149 | - | // putDraw writes draw entry i of table. | |||
| 150 | - | func (r *Renderer) putDraw(table []byte, i int, u *upload) { | |||
| 151 | - | e := table[i*drawSize : (i+1)*drawSize] | |||
| 152 | - | for j, v := range []uint32{ | |||
| 153 | - | uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y), | |||
| 154 | - | uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height), | |||
| 155 | - | } { | |||
| 156 | - | binary.LittleEndian.PutUint32(e[j*4:], v) | |||
| 157 | - | } | |||
| 158 | - | } | |||
| 159 | - | ||||
| 160 | - | // drawBlended draws uploads[first:last], all blended, into view: one | |||
| 161 | - | // instanced draw. The image is TRANSFER_DST before and after. | |||
| 162 | - | func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) { | |||
| 163 | - | d := r.dd | |||
| 164 | - | rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | |||
| 165 | - | b := vk.ImageMemoryBarrier{ | |||
| 166 | - | SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo, | |||
| 167 | - | SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit, | |||
| 168 | - | OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal, | |||
| 169 | - | } | |||
| 170 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 171 | - | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore} | |||
| 172 | - | info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment} | |||
| 173 | - | d.CmdBeginRendering(r.command, &info) | |||
| 174 | - | d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline) | |||
| 175 | - | d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil) | |||
| 176 | - | d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first)) | |||
| 177 | - | d.CmdEndRendering(r.command) | |||
| 178 | - | b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit | |||
| 179 | - | b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal | |||
| 180 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | - | // createView makes the color attachment view of a target image. | |||
| 184 | - | func (r *Renderer) createView(t *target) error { | |||
| 185 | - | info := vk.ImageViewCreateInfo{ | |||
| 186 | - | SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm, | |||
| 187 | - | SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}, | |||
| 188 | - | } | |||
| 189 | - | return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view)) | |||
| 190 | - | } | |||
A internal/adapters/vulkan/compose.go +312 -0
| @@ -0,0 +1,312 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | _ "embed" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "image" | |||
| 7 | + | "math" | |||
| 8 | + | "os" | |||
| 9 | + | "unsafe" | |||
| 10 | + | ||||
| 11 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // Composition is one graphics pipeline drawing every piece of a frame as | |||
| 15 | + | // a quad, in paint order, inside one dynamic rendering pass: solid fills, | |||
| 16 | + | // client dmabufs sampled as images, and client wl_shm pixels read from a | |||
| 17 | + | // GPU buffer (shm.go). Push constants carry each draw; the descriptor set | |||
| 18 | + | // holds its image and buffer. Client pixels are premultiplied, hence the | |||
| 19 | + | // blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1. | |||
| 20 | + | ||||
| 21 | + | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv | |||
| 22 | + | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv | |||
| 23 | + | ||||
| 24 | + | var ( | |||
| 25 | + | //go:embed shaders/compose.vert.spv | |||
| 26 | + | composeVert []byte | |||
| 27 | + | //go:embed shaders/compose.frag.spv | |||
| 28 | + | composeFrag []byte | |||
| 29 | + | ) | |||
| 30 | + | ||||
| 31 | + | // Draw modes and flags (shaders: modeSolid, modeImage, flag*). | |||
| 32 | + | const ( | |||
| 33 | + | modeSolid = 0 | |||
| 34 | + | modeImage = 1 | |||
| 35 | + | modeBuffer = 2 | |||
| 36 | + | ||||
| 37 | + | flagOpaque = 1 | |||
| 38 | + | flagExact = 2 | |||
| 39 | + | ) | |||
| 40 | + | ||||
| 41 | + | // pushConstants is struct Draw of the shaders (std430 push constant block). | |||
| 42 | + | type pushConstants struct { | |||
| 43 | + | rect [4]int32 | |||
| 44 | + | mapv [4]float32 | |||
| 45 | + | color [4]float32 | |||
| 46 | + | buf [4]uint32 | |||
| 47 | + | misc [4]uint32 | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // draw is one quad of a frame. | |||
| 51 | + | type draw struct { | |||
| 52 | + | pc pushConstants | |||
| 53 | + | set vk.DescriptorSet | |||
| 54 | + | im *imported // sampled client dmabuf, for ownership barriers | |||
| 55 | + | // acquire is the content's explicit-sync fence (nil: implicit). | |||
| 56 | + | acquire *os.File | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // composer is the pipeline state and the objects every draw can bind. | |||
| 60 | + | type composer struct { | |||
| 61 | + | setLayout vk.DescriptorSetLayout | |||
| 62 | + | layout vk.PipelineLayout | |||
| 63 | + | pipeline vk.Pipeline | |||
| 64 | + | sampler vk.Sampler | |||
| 65 | + | // Placeholders for the binding a draw does not use: every binding | |||
| 66 | + | // the shader declares must be valid. | |||
| 67 | + | dummyImage vk.Image | |||
| 68 | + | dummyMemory vk.DeviceMemory | |||
| 69 | + | dummyView vk.ImageView | |||
| 70 | + | dummyBuffer gpuBuffer | |||
| 71 | + | dummyPool vk.DescriptorPool | |||
| 72 | + | dummySet vk.DescriptorSet | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (r *Renderer) createComposer() error { | |||
| 76 | + | d, c := r.dd, &r.compose | |||
| 77 | + | bindings := []vk.DescriptorSetLayoutBinding{ | |||
| 78 | + | {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |||
| 79 | + | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |||
| 80 | + | } | |||
| 81 | + | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]} | |||
| 82 | + | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil { | |||
| 83 | + | return err | |||
| 84 | + | } | |||
| 85 | + | pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))} | |||
| 86 | + | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr} | |||
| 87 | + | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil { | |||
| 88 | + | return err | |||
| 89 | + | } | |||
| 90 | + | si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterLinear, MinFilter: vk.FilterLinear, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge} | |||
| 91 | + | if err := checked("vkCreateSampler", d.CreateSampler(r.device, &si, nil, &c.sampler)); err != nil { | |||
| 92 | + | return err | |||
| 93 | + | } | |||
| 94 | + | if err := r.createPipeline(); err != nil { | |||
| 95 | + | return err | |||
| 96 | + | } | |||
| 97 | + | return r.createDummies() | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func (r *Renderer) createPipeline() error { | |||
| 101 | + | d, c := r.dd, &r.compose | |||
| 102 | + | vert, err := r.shaderModule(composeVert) | |||
| 103 | + | if err != nil { | |||
| 104 | + | return err | |||
| 105 | + | } | |||
| 106 | + | defer d.DestroyShaderModule(r.device, vert, nil) | |||
| 107 | + | frag, err := r.shaderModule(composeFrag) | |||
| 108 | + | if err != nil { | |||
| 109 | + | return err | |||
| 110 | + | } | |||
| 111 | + | defer d.DestroyShaderModule(r.device, frag, nil) | |||
| 112 | + | main := []byte("main\x00") | |||
| 113 | + | stages := []vk.PipelineShaderStageCreateInfo{ | |||
| 114 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | |||
| 115 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]}, | |||
| 116 | + | } | |||
| 117 | + | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | |||
| 118 | + | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | |||
| 119 | + | // The target size is fixed for the renderer's life: static viewport. | |||
| 120 | + | viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1} | |||
| 121 | + | scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}} | |||
| 122 | + | viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor} | |||
| 123 | + | raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1} | |||
| 124 | + | multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit} | |||
| 125 | + | attachment := vk.PipelineColorBlendAttachmentState{ | |||
| 126 | + | BlendEnable: 1, | |||
| 127 | + | SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd, | |||
| 128 | + | SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd, | |||
| 129 | + | ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit, | |||
| 130 | + | } | |||
| 131 | + | blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment} | |||
| 132 | + | format := vk.Format(vk.FormatB8g8r8a8Unorm) | |||
| 133 | + | rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format} | |||
| 134 | + | gpi := vk.GraphicsPipelineCreateInfo{ | |||
| 135 | + | SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering), | |||
| 136 | + | StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly, | |||
| 137 | + | ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample, | |||
| 138 | + | ColorBlendState: &blendState, Layout: c.layout, BasePipelineIndex: -1, | |||
| 139 | + | } | |||
| 140 | + | return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &c.pipeline)) | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // createDummies makes the placeholder image (1×1, shader-read layout) and | |||
| 144 | + | // buffer, and the set fills bind. | |||
| 145 | + | func (r *Renderer) createDummies() error { | |||
| 146 | + | d, c := r.dd, &r.compose | |||
| 147 | + | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: 1, Height: 1, Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | |||
| 148 | + | if err := checked("vkCreateImage(dummy)", d.CreateImage(r.device, &ii, nil, &c.dummyImage)); err != nil { | |||
| 149 | + | return err | |||
| 150 | + | } | |||
| 151 | + | var req vk.MemoryRequirements | |||
| 152 | + | d.GetImageMemoryRequirements(r.device, c.dummyImage, &req) | |||
| 153 | + | kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit) | |||
| 154 | + | if err != nil { | |||
| 155 | + | return err | |||
| 156 | + | } | |||
| 157 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 158 | + | if err := checked("vkAllocateMemory(dummy)", d.AllocateMemory(r.device, &alloc, nil, &c.dummyMemory)); err != nil { | |||
| 159 | + | return err | |||
| 160 | + | } | |||
| 161 | + | if err := checked("vkBindImageMemory(dummy)", d.BindImageMemory(r.device, c.dummyImage, c.dummyMemory, 0)); err != nil { | |||
| 162 | + | return err | |||
| 163 | + | } | |||
| 164 | + | if c.dummyView, err = r.imageView(c.dummyImage, vk.FormatB8g8r8a8Unorm); err != nil { | |||
| 165 | + | return err | |||
| 166 | + | } | |||
| 167 | + | if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil { | |||
| 168 | + | return err | |||
| 169 | + | } | |||
| 170 | + | if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil { | |||
| 171 | + | return err | |||
| 172 | + | } | |||
| 173 | + | // Move the placeholder image to the layout every draw declares. | |||
| 174 | + | return r.oneShot(func(cmd vk.CommandBuffer) { | |||
| 175 | + | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange} | |||
| 176 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 177 | + | }) | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // newSet allocates a descriptor set from its own pool (the bindings lack | |||
| 181 | + | // vkFreeDescriptorSets: the pool is destroyed with the object). | |||
| 182 | + | func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer) (vk.DescriptorPool, vk.DescriptorSet, error) { | |||
| 183 | + | d := r.dd | |||
| 184 | + | sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}} | |||
| 185 | + | dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]} | |||
| 186 | + | var pool vk.DescriptorPool | |||
| 187 | + | if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil { | |||
| 188 | + | return 0, 0, err | |||
| 189 | + | } | |||
| 190 | + | var set vk.DescriptorSet | |||
| 191 | + | dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout} | |||
| 192 | + | if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil { | |||
| 193 | + | d.DestroyDescriptorPool(r.device, pool, nil) | |||
| 194 | + | return 0, 0, err | |||
| 195 | + | } | |||
| 196 | + | img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |||
| 197 | + | buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize} | |||
| 198 | + | writes := []vk.WriteDescriptorSet{ | |||
| 199 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}, | |||
| 200 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf}, | |||
| 201 | + | } | |||
| 202 | + | d.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | |||
| 203 | + | return pool, set, nil | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | func (r *Renderer) destroyComposer() { | |||
| 207 | + | d, c := r.dd, &r.compose | |||
| 208 | + | if c.dummyPool != 0 { | |||
| 209 | + | d.DestroyDescriptorPool(r.device, c.dummyPool, nil) | |||
| 210 | + | } | |||
| 211 | + | r.freeGPUBuffer(&c.dummyBuffer) | |||
| 212 | + | if c.dummyView != 0 { | |||
| 213 | + | d.DestroyImageView(r.device, c.dummyView, nil) | |||
| 214 | + | } | |||
| 215 | + | if c.dummyImage != 0 { | |||
| 216 | + | d.DestroyImage(r.device, c.dummyImage, nil) | |||
| 217 | + | } | |||
| 218 | + | if c.dummyMemory != 0 { | |||
| 219 | + | d.FreeMemory(r.device, c.dummyMemory, nil) | |||
| 220 | + | } | |||
| 221 | + | if c.sampler != 0 { | |||
| 222 | + | d.DestroySampler(r.device, c.sampler, nil) | |||
| 223 | + | } | |||
| 224 | + | if c.pipeline != 0 { | |||
| 225 | + | d.DestroyPipeline(r.device, c.pipeline, nil) | |||
| 226 | + | } | |||
| 227 | + | if c.layout != 0 { | |||
| 228 | + | d.DestroyPipelineLayout(r.device, c.layout, nil) | |||
| 229 | + | } | |||
| 230 | + | if c.setLayout != 0 { | |||
| 231 | + | d.DestroyDescriptorSetLayout(r.device, c.setLayout, nil) | |||
| 232 | + | } | |||
| 233 | + | *c = composer{} | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) { | |||
| 237 | + | // SPIR-V is read as words: copy into a uint32-aligned slice. | |||
| 238 | + | words := make([]uint32, len(code)/4) | |||
| 239 | + | for i := range words { | |||
| 240 | + | words[i] = binary.LittleEndian.Uint32(code[i*4:]) | |||
| 241 | + | } | |||
| 242 | + | info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]} | |||
| 243 | + | var m vk.ShaderModule | |||
| 244 | + | return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m)) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | var colorRange = vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | |||
| 248 | + | ||||
| 249 | + | // imageView makes a 2D color view of an image. | |||
| 250 | + | func (r *Renderer) imageView(img vk.Image, format vk.Format) (vk.ImageView, error) { | |||
| 251 | + | info := vk.ImageViewCreateInfo{SType: vk.StructureTypeImageViewCreateInfo, Image: img, ViewType: vk.ImageViewType2d, Format: format, SubresourceRange: colorRange} | |||
| 252 | + | var v vk.ImageView | |||
| 253 | + | return v, checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &v)) | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | // createView makes the color attachment view of a target image. | |||
| 257 | + | func (r *Renderer) createView(t *target) (err error) { | |||
| 258 | + | t.view, err = r.imageView(t.image, vk.FormatB8g8r8a8Unorm) | |||
| 259 | + | return err | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // fillDraw is a solid rect of c. | |||
| 263 | + | func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw { | |||
| 264 | + | dr := draw{set: r.compose.dummySet} | |||
| 265 | + | dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)} | |||
| 266 | + | dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1} | |||
| 267 | + | dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)} | |||
| 268 | + | return dr | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // contentDraw draws the visible rect of a w×h buffer mapped onto full | |||
| 272 | + | // (target pixels). exact: the buffer is drawn at its size. | |||
| 273 | + | func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32, opaque bool) draw { | |||
| 274 | + | var dr draw | |||
| 275 | + | sx, sy := float32(w)/float32(full.Dx()), float32(h)/float32(full.Dy()) | |||
| 276 | + | dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)} | |||
| 277 | + | dr.pc.mapv = [4]float32{-float32(full.Min.X) * sx, -float32(full.Min.Y) * sy, sx, sy} | |||
| 278 | + | flags := uint32(0) | |||
| 279 | + | if opaque { | |||
| 280 | + | flags |= flagOpaque | |||
| 281 | + | } | |||
| 282 | + | if full.Dx() == w && full.Dy() == h { | |||
| 283 | + | flags |= flagExact | |||
| 284 | + | } | |||
| 285 | + | dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)} | |||
| 286 | + | return dr | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | // recordDraws draws ds in one rendering pass that clears view to bg, or | |||
| 290 | + | // keeps its contents (keep). | |||
| 291 | + | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) { | |||
| 292 | + | d, c := r.dd, &r.compose | |||
| 293 | + | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | |||
| 294 | + | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear} | |||
| 295 | + | if keep { | |||
| 296 | + | attachment.LoadOp = vk.AttachmentLoadOpLoad | |||
| 297 | + | } | |||
| 298 | + | info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment} | |||
| 299 | + | d.CmdBeginRendering(cmd, &info) | |||
| 300 | + | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline) | |||
| 301 | + | var bound vk.DescriptorSet | |||
| 302 | + | for i := range ds { | |||
| 303 | + | dr := &ds[i] | |||
| 304 | + | if dr.set != bound { | |||
| 305 | + | d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil) | |||
| 306 | + | bound = dr.set | |||
| 307 | + | } | |||
| 308 | + | d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc)) | |||
| 309 | + | d.CmdDraw(cmd, 6, 1, 0, 0) | |||
| 310 | + | } | |||
| 311 | + | d.CmdEndRendering(cmd) | |||
| 312 | + | } | |||
A internal/adapters/vulkan/compose_test.go +215 -0
| @@ -0,0 +1,215 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "image/color" | |||
| 6 | + | "os" | |||
| 7 | + | "slices" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // near reports whether two colors are within tol per channel. | |||
| 16 | + | func near(a, b color.RGBA, tol int) bool { | |||
| 17 | + | d := func(x, y uint8) bool { v := int(x) - int(y); return v >= -tol && v <= tol } | |||
| 18 | + | return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // Scaled wl_shm pixels filter linearly: halfway between a black and a | |||
| 22 | + | // white texel is grey, not either one (nearest neighbour). | |||
| 23 | + | func TestRendererScaledSHMFiltersLinearly(t *testing.T) { | |||
| 24 | + | r, err := New(64, 16) | |||
| 25 | + | if err != nil { | |||
| 26 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 27 | + | } | |||
| 28 | + | defer r.Close() | |||
| 29 | + | // 2×1 buffer: black, white; drawn 64×16, texel centres at x=16, x=48. | |||
| 30 | + | px := []byte{0, 0, 0, 255, 255, 255, 255, 255} | |||
| 31 | + | c := shmContent(t, 2, 1, 8, px) | |||
| 32 | + | c.LogicalW, c.LogicalH = 64, 16 | |||
| 33 | + | scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}} | |||
| 34 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil { | |||
| 35 | + | t.Fatal(err) | |||
| 36 | + | } | |||
| 37 | + | got := r.Pixels().RGBAAt(32, 8) | |||
| 38 | + | if !near(got, color.RGBA{128, 128, 128, 255}, 8) { | |||
| 39 | + | t.Fatalf("midpoint %v, want grey", got) | |||
| 40 | + | } | |||
| 41 | + | if got := r.Pixels().RGBAAt(2, 8); !near(got, color.RGBA{0, 0, 0, 255}, 2) { | |||
| 42 | + | t.Fatalf("left edge %v, want black (clamped)", got) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Scaled dmabufs are sampled with the linear sampler. | |||
| 47 | + | func TestRendererScaledDMABufFiltersLinearly(t *testing.T) { | |||
| 48 | + | r, err := New(128, 4) | |||
| 49 | + | if err != nil { | |||
| 50 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 51 | + | } | |||
| 52 | + | defer r.Close() | |||
| 53 | + | linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0} | |||
| 54 | + | if !slices.Contains(r.DMABuf().Formats, linear) { | |||
| 55 | + | t.Skipf("linear ARGB8888 not importable") | |||
| 56 | + | } | |||
| 57 | + | // 64×1: left half black, right half white, drawn twice as wide. | |||
| 58 | + | f := udmabuf(t, 64, 1, func(x, _ int) [4]byte { | |||
| 59 | + | if x < 32 { | |||
| 60 | + | return [4]byte{0, 0, 0, 255} | |||
| 61 | + | } | |||
| 62 | + | return [4]byte{255, 255, 255, 255} | |||
| 63 | + | }) | |||
| 64 | + | buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}} | |||
| 65 | + | scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}} | |||
| 66 | + | c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf} | |||
| 67 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil { | |||
| 68 | + | t.Fatal(err) | |||
| 69 | + | } | |||
| 70 | + | // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31 | |||
| 71 | + | // (black, centre 31.5) and 32 (white, centre 32.5), 75% white. | |||
| 72 | + | if got := r.Pixels().RGBAAt(64, 2); !near(got, color.RGBA{191, 191, 191, 255}, 10) { | |||
| 73 | + | t.Fatalf("boundary %v, want a linear mix", got) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // Render returns a sync file that becomes readable when the GPU is done. | |||
| 78 | + | func TestRendererReturnsDoneFence(t *testing.T) { | |||
| 79 | + | r, err := New(32, 32) | |||
| 80 | + | if err != nil { | |||
| 81 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 82 | + | } | |||
| 83 | + | defer r.Close() | |||
| 84 | + | if !r.syncFD { | |||
| 85 | + | t.Skip("device cannot export sync files") | |||
| 86 | + | } | |||
| 87 | + | done, err := r.Render(ports.Scene{Background: "#102030"}, nil) | |||
| 88 | + | if err != nil { | |||
| 89 | + | t.Fatal(err) | |||
| 90 | + | } | |||
| 91 | + | if done == nil { | |||
| 92 | + | t.Fatal("no done fence") | |||
| 93 | + | } | |||
| 94 | + | defer done.Close() | |||
| 95 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |||
| 96 | + | defer cancel() | |||
| 97 | + | if err := syncfile.Wait(ctx, done); err != nil { | |||
| 98 | + | t.Fatal(err) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // An unchanged wl_shm window is not copied again; a new content is. | |||
| 103 | + | func TestRendererSHMCopiedOncePerContent(t *testing.T) { | |||
| 104 | + | r, err := New(32, 32) | |||
| 105 | + | if err != nil { | |||
| 106 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 107 | + | } | |||
| 108 | + | defer r.Close() | |||
| 109 | + | c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255}) | |||
| 110 | + | c.ID, c.Seq = 1, 1 | |||
| 111 | + | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}} | |||
| 112 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 113 | + | render := func() { | |||
| 114 | + | t.Helper() | |||
| 115 | + | done, err := r.Render(scene, contents) | |||
| 116 | + | if done != nil { | |||
| 117 | + | done.Close() | |||
| 118 | + | } | |||
| 119 | + | if err != nil { | |||
| 120 | + | t.Fatal(err) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | render() | |||
| 124 | + | if r.copied != 16*16*4 { | |||
| 125 | + | t.Fatalf("first frame copied %d", r.copied) | |||
| 126 | + | } | |||
| 127 | + | for range 3 { | |||
| 128 | + | render() | |||
| 129 | + | } | |||
| 130 | + | if r.copied != 16*16*4 { | |||
| 131 | + | t.Fatalf("unchanged content copied again: %d", r.copied) | |||
| 132 | + | } | |||
| 133 | + | c.Seq = 2 | |||
| 134 | + | contents[1] = c | |||
| 135 | + | render() | |||
| 136 | + | if r.copied != 2*16*16*4 { | |||
| 137 | + | t.Fatalf("new content copied %d", r.copied) | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // render draws a frame and closes its fence. | |||
| 142 | + | func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error { | |||
| 143 | + | done, err := r.Render(s, contents) | |||
| 144 | + | if done != nil { | |||
| 145 | + | done.Close() | |||
| 146 | + | } | |||
| 147 | + | return err | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // A wl_shm window's GPU buffer is rewritten only after every frame that | |||
| 151 | + | // read it completed: contents 1, 2, 3 back to back alternate two buffers, | |||
| 152 | + | // and the third copy waits for the frame that drew the first (the slot | |||
| 153 | + | // ring and shmCopyFor's waitFrame both guarantee it). | |||
| 154 | + | func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) { | |||
| 155 | + | r, err := New(32, 32) | |||
| 156 | + | if err != nil { | |||
| 157 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 158 | + | } | |||
| 159 | + | defer r.Close() | |||
| 160 | + | if !r.syncFD { | |||
| 161 | + | t.Skip("frames are finished on return without sync files") | |||
| 162 | + | } | |||
| 163 | + | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}} | |||
| 164 | + | var readers []uint64 | |||
| 165 | + | for seq := uint64(1); seq <= 3; seq++ { | |||
| 166 | + | c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255}) | |||
| 167 | + | c.ID, c.Seq = 1, seq | |||
| 168 | + | if seq == 3 { | |||
| 169 | + | // The buffer about to be reused was last read by frame 1. | |||
| 170 | + | if r.completed >= readers[0] { | |||
| 171 | + | t.Fatalf("frame %d already known done before the reuse", readers[0]) | |||
| 172 | + | } | |||
| 173 | + | } | |||
| 174 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil { | |||
| 175 | + | t.Fatal(err) | |||
| 176 | + | } | |||
| 177 | + | readers = append(readers, r.frame) | |||
| 178 | + | if seq == 3 && r.completed < readers[0] { | |||
| 179 | + | t.Fatalf("buffer of frame %d rewritten with completed=%d", readers[0], r.completed) | |||
| 180 | + | } | |||
| 181 | + | } | |||
| 182 | + | if got := r.Pixels().RGBAAt(4, 4); got != (color.RGBA{180, 0, 0, 255}) { | |||
| 183 | + | t.Fatalf("pixel %v", got) | |||
| 184 | + | } | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // A content's explicit acquire fence becomes a wait semaphore; a file | |||
| 188 | + | // that is not a sync file is refused (no semaphore) without failing the | |||
| 189 | + | // frame, and the caller's file stays open. | |||
| 190 | + | func TestRendererImportsAcquireFence(t *testing.T) { | |||
| 191 | + | r, err := New(8, 8) | |||
| 192 | + | if err != nil { | |||
| 193 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 194 | + | } | |||
| 195 | + | defer r.Close() | |||
| 196 | + | done, err := r.Render(ports.Scene{Background: "#000000"}, nil) | |||
| 197 | + | if err != nil || done == nil { | |||
| 198 | + | t.Skipf("no sync file: %v", err) | |||
| 199 | + | } | |||
| 200 | + | defer done.Close() | |||
| 201 | + | sem := r.importFence(done) | |||
| 202 | + | if sem == 0 { | |||
| 203 | + | t.Fatal("sync file not imported") | |||
| 204 | + | } | |||
| 205 | + | r.dd.DestroySemaphore(r.device, sem, nil) | |||
| 206 | + | if _, err := done.Stat(); err != nil { | |||
| 207 | + | t.Fatal("caller's fence closed") | |||
| 208 | + | } | |||
| 209 | + | f, w, _ := os.Pipe() | |||
| 210 | + | defer f.Close() | |||
| 211 | + | defer w.Close() | |||
| 212 | + | if sem := r.importFence(f); sem != 0 { | |||
| 213 | + | t.Fatal("pipe imported as a fence") | |||
| 214 | + | } | |||
| 215 | + | } | |||
A internal/adapters/vulkan/cursor.go +149 -0
| @@ -0,0 +1,149 @@ | |||||
| 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 || len(r.dmabuf.Formats) == 0 || 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 | + | // Exported images take an explicit DRM modifier: linear, as cursor | |||
| 59 | + | // planes scan out. The GPU never touches them (the CPU writes, KMS reads). | |||
| 60 | + | linear := uint64(0) | |||
| 61 | + | list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &linear} | |||
| 62 | + | external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |||
| 63 | + | 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.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | |||
| 64 | + | if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil { | |||
| 65 | + | return nil, ports.DMABuf{}, err | |||
| 66 | + | } | |||
| 67 | + | var req vk.MemoryRequirements | |||
| 68 | + | d.GetImageMemoryRequirements(r.device, c.image, &req) | |||
| 69 | + | kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |||
| 70 | + | if err != nil { | |||
| 71 | + | return nil, ports.DMABuf{}, err | |||
| 72 | + | } | |||
| 73 | + | dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: c.image} | |||
| 74 | + | export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |||
| 75 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 76 | + | if err := checked("vkAllocateMemory(cursor)", d.AllocateMemory(r.device, &alloc, nil, &c.memory)); err != nil { | |||
| 77 | + | return nil, ports.DMABuf{}, err | |||
| 78 | + | } | |||
| 79 | + | if err := checked("vkBindImageMemory(cursor)", d.BindImageMemory(r.device, c.image, c.memory, 0)); err != nil { | |||
| 80 | + | return nil, ports.DMABuf{}, err | |||
| 81 | + | } | |||
| 82 | + | var layout vk.SubresourceLayout | |||
| 83 | + | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT} | |||
| 84 | + | d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout) | |||
| 85 | + | c.pitch = int(layout.RowPitch) | |||
| 86 | + | // Cursor planes scan out packed rows from the start of the buffer. | |||
| 87 | + | if c.pitch != size*4 || layout.Offset != 0 { | |||
| 88 | + | return nil, ports.DMABuf{}, fmt.Errorf("cursor image layout pitch %d offset %d, want pitch %d offset 0", c.pitch, layout.Offset, size*4) | |||
| 89 | + | } | |||
| 90 | + | if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil { | |||
| 91 | + | return nil, ports.DMABuf{}, err | |||
| 92 | + | } | |||
| 93 | + | c.mapped = unsafe.Add(c.mapped, layout.Offset) | |||
| 94 | + | clear(unsafe.Slice((*byte)(c.mapped), c.pitch*size)) | |||
| 95 | + | var fd int32 | |||
| 96 | + | get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: c.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |||
| 97 | + | if err := checked("vkGetMemoryFdKHR(cursor)", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil { | |||
| 98 | + | return nil, ports.DMABuf{}, err | |||
| 99 | + | } | |||
| 100 | + | f := os.NewFile(uintptr(fd), "nefertty-cursor") | |||
| 101 | + | if f == nil { | |||
| 102 | + | return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd) | |||
| 103 | + | } | |||
| 104 | + | ok = true | |||
| 105 | + | 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 | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row) | |||
| 109 | + | // into cursor image i at its row pitch, cropped and cleared around. | |||
| 110 | + | func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | |||
| 111 | + | if i < 0 || i >= len(r.cursors) || r.cursors[i] == nil { | |||
| 112 | + | return errors.New("no cursor image") | |||
| 113 | + | } | |||
| 114 | + | if w < 0 || h < 0 || len(pixels) < w*h*4 { | |||
| 115 | + | return errors.New("short cursor image") | |||
| 116 | + | } | |||
| 117 | + | c := r.cursors[i] | |||
| 118 | + | mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) | |||
| 119 | + | clear(mem) | |||
| 120 | + | cw := min(w, c.size) * 4 | |||
| 121 | + | for y := range min(h, c.size) { | |||
| 122 | + | copy(mem[y*c.pitch:y*c.pitch+cw], pixels[y*w*4:y*w*4+cw]) | |||
| 123 | + | } | |||
| 124 | + | return nil | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (r *Renderer) freeCursor(c *cursorImage) { | |||
| 128 | + | if c.mapped != nil { | |||
| 129 | + | r.dd.UnmapMemory(r.device, c.memory) | |||
| 130 | + | c.mapped = nil | |||
| 131 | + | } | |||
| 132 | + | if c.image != 0 { | |||
| 133 | + | r.dd.DestroyImage(r.device, c.image, nil) | |||
| 134 | + | c.image = 0 | |||
| 135 | + | } | |||
| 136 | + | if c.memory != 0 { | |||
| 137 | + | r.dd.FreeMemory(r.device, c.memory, nil) | |||
| 138 | + | c.memory = 0 | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (r *Renderer) dropCursors() { | |||
| 143 | + | for i, c := range r.cursors { | |||
| 144 | + | if c != nil { | |||
| 145 | + | r.freeCursor(c) | |||
| 146 | + | r.cursors[i] = nil | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | } | |||
A internal/adapters/vulkan/cursor_test.go +48 -0
| @@ -0,0 +1,48 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "unsafe" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // Cursor images are packed (pitch = size*4), exported, and WriteCursor | |||
| 9 | + | // crops to the image and clears around the pixels. | |||
| 10 | + | func TestCursorBuffersWriteCursor(t *testing.T) { | |||
| 11 | + | r, err := New(64, 48) | |||
| 12 | + | if err != nil { | |||
| 13 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 14 | + | } | |||
| 15 | + | defer r.Close() | |||
| 16 | + | bufs, err := r.CursorBuffers(64) | |||
| 17 | + | if err != nil { | |||
| 18 | + | t.Skipf("no exportable cursor images: %v", err) | |||
| 19 | + | } | |||
| 20 | + | for _, b := range bufs { | |||
| 21 | + | if p := b.Planes[0]; p.Stride != 64*4 || p.Offset != 0 || b.Format != fourccARGB { | |||
| 22 | + | t.Fatalf("layout %+v format %#x", p, b.Format) | |||
| 23 | + | } | |||
| 24 | + | b.Planes[0].File.Close() | |||
| 25 | + | } | |||
| 26 | + | mem := func() []byte { c := r.cursors[1]; return unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) } | |||
| 27 | + | // A first image fills the corner; a second, smaller one must clear it. | |||
| 28 | + | big := make([]byte, 100*100*4) | |||
| 29 | + | for i := range big { | |||
| 30 | + | big[i] = 0xff | |||
| 31 | + | } | |||
| 32 | + | if err := r.WriteCursor(1, big, 100, 100); err != nil { // cropped to 64 | |||
| 33 | + | t.Fatal(err) | |||
| 34 | + | } | |||
| 35 | + | if m := mem(); m[63*256+63*4] != 0xff { | |||
| 36 | + | t.Fatal("cropped image not written") | |||
| 37 | + | } | |||
| 38 | + | if err := r.WriteCursor(1, []byte{1, 2, 3, 4}, 1, 1); err != nil { | |||
| 39 | + | t.Fatal(err) | |||
| 40 | + | } | |||
| 41 | + | m := mem() | |||
| 42 | + | if m[0] != 1 || m[3] != 4 || m[4] != 0 || m[256] != 0 || m[63*256+63*4] != 0 { | |||
| 43 | + | t.Fatal("image not cleared around the new pixels") | |||
| 44 | + | } | |||
| 45 | + | if err := r.WriteCursor(1, []byte{1}, 1, 1); err == nil { | |||
| 46 | + | t.Fatal("short image accepted") | |||
| 47 | + | } | |||
| 48 | + | } | |||
A internal/adapters/vulkan/damage.go +146 -0
| @@ -0,0 +1,146 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image" | |||
| 5 | + | "math" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Damage: an output target keeps what it holds (scene Seq, each window's | |||
| 11 | + | // content Seq). When the scene is the same, only the region changed since | |||
| 12 | + | // then is redrawn: every quad is clipped to it on the CPU and the pass | |||
| 13 | + | // keeps the rest of the image. A changed scene, a target never drawn or | |||
| 14 | + | // a history too short redraws everything. | |||
| 15 | + | ||||
| 16 | + | // damageRegion collects the region of a target to redraw this frame and | |||
| 17 | + | // what the target will hold after it: the windows drawn, their content | |||
| 18 | + | // Seq and rect. | |||
| 19 | + | type damageRegion struct { | |||
| 20 | + | held *target // nil: redraw everything | |||
| 21 | + | area image.Rectangle | |||
| 22 | + | seen map[ports.WindowID]bool | |||
| 23 | + | bound image.Rectangle | |||
| 24 | + | drawn map[ports.WindowID]heldWindow | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // heldWindow is a window a target holds: its content Seq and rect. | |||
| 28 | + | type heldWindow struct { | |||
| 29 | + | seq uint64 | |||
| 30 | + | rect image.Rectangle | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion { | |||
| 34 | + | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds, drawn: map[ports.WindowID]heldWindow{}} | |||
| 35 | + | // Seq 0 is a scene core did not number (setup, tests): redraw all. | |||
| 36 | + | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | |||
| 37 | + | d.held = tg | |||
| 38 | + | } | |||
| 39 | + | return d | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // all reports whether the whole target is redrawn. | |||
| 43 | + | func (d *damageRegion) all() bool { return d.held == nil } | |||
| 44 | + | ||||
| 45 | + | func (d *damageRegion) add(r image.Rectangle) { | |||
| 46 | + | if d.held != nil { | |||
| 47 | + | d.area = d.area.Union(r.Intersect(d.bound)) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // window records a window drawn in rect (physical): if its content changed | |||
| 52 | + | // and no finer damage comes (content), the whole rect is redrawn. | |||
| 53 | + | func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) { | |||
| 54 | + | if d.seen[id] { | |||
| 55 | + | return | |||
| 56 | + | } | |||
| 57 | + | d.seen[id] = true | |||
| 58 | + | d.drawn[id] = heldWindow{seq: c.Seq, rect: rect} | |||
| 59 | + | if d.held == nil { | |||
| 60 | + | return | |||
| 61 | + | } | |||
| 62 | + | hw, ok := d.held.windows[id] | |||
| 63 | + | held := hw.seq | |||
| 64 | + | if ok && held == c.Seq { | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | if !ok || c.Empty() || len(c.Children) > 0 { | |||
| 68 | + | d.add(rect) | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | rects, known := c.DamageSince(held) | |||
| 72 | + | if !known { | |||
| 73 | + | d.add(rect) | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | // Mapped by content, called next: nothing more here. | |||
| 77 | + | _ = rects | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // content maps a root surface's buffer damage onto full (where its w×h | |||
| 81 | + | // buffer is drawn), clipped to dst. Scaled buffers filter linearly: one | |||
| 82 | + | // more pixel around each rect. | |||
| 83 | + | func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, dst image.Rectangle) { | |||
| 84 | + | if d.held == nil { | |||
| 85 | + | return | |||
| 86 | + | } | |||
| 87 | + | hw, ok := d.held.windows[id] | |||
| 88 | + | held := hw.seq | |||
| 89 | + | if !ok || held == c.Seq || len(c.Children) > 0 { | |||
| 90 | + | return | |||
| 91 | + | } | |||
| 92 | + | rects, known := c.DamageSince(held) | |||
| 93 | + | if !known || c.Width <= 0 || c.Height <= 0 { | |||
| 94 | + | return | |||
| 95 | + | } | |||
| 96 | + | // Linear filtering spreads a texel over half a buffer pixel around | |||
| 97 | + | // it: scale/2 target pixels, plus one for rounding. | |||
| 98 | + | pad := 0 | |||
| 99 | + | if full.Dx() != c.Width || full.Dy() != c.Height { | |||
| 100 | + | s := max(float64(full.Dx())/float64(c.Width), float64(full.Dy())/float64(c.Height)) | |||
| 101 | + | pad = int(math.Ceil(s/2)) + 1 | |||
| 102 | + | } | |||
| 103 | + | for _, r := range rects { | |||
| 104 | + | x0 := full.Min.X + r.X*full.Dx()/c.Width - pad | |||
| 105 | + | y0 := full.Min.Y + r.Y*full.Dy()/c.Height - pad | |||
| 106 | + | x1 := full.Min.X + ((r.X+r.W)*full.Dx()+c.Width-1)/c.Width + pad | |||
| 107 | + | y1 := full.Min.Y + ((r.Y+r.H)*full.Dy()+c.Height-1)/c.Height + pad | |||
| 108 | + | d.add(image.Rect(x0, y0, x1, y1).Intersect(dst)) | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // finish adds the rects of windows the target holds that this frame does | |||
| 113 | + | // not draw (e.g. a window that moved to an overlay plane and back). | |||
| 114 | + | func (d *damageRegion) finish() { | |||
| 115 | + | if d.held == nil { | |||
| 116 | + | return | |||
| 117 | + | } | |||
| 118 | + | for id, hw := range d.held.windows { | |||
| 119 | + | if _, ok := d.drawn[id]; !ok { | |||
| 120 | + | d.add(hw.rect) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // clip limits the draws to the region; draws outside it are dropped. | |||
| 126 | + | func (d *damageRegion) clip(ds []draw) []draw { | |||
| 127 | + | if d.held == nil { | |||
| 128 | + | return ds | |||
| 129 | + | } | |||
| 130 | + | out := ds[:0] | |||
| 131 | + | for _, dr := range ds { | |||
| 132 | + | r := image.Rect(int(dr.pc.rect[0]), int(dr.pc.rect[1]), int(dr.pc.rect[2]), int(dr.pc.rect[3])).Intersect(d.area) | |||
| 133 | + | if r.Empty() { | |||
| 134 | + | continue | |||
| 135 | + | } | |||
| 136 | + | dr.pc.rect = [4]int32{int32(r.Min.X), int32(r.Min.Y), int32(r.Max.X), int32(r.Max.Y)} | |||
| 137 | + | out = append(out, dr) | |||
| 138 | + | } | |||
| 139 | + | return out | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // hold records what the target holds after this frame: only the windows | |||
| 143 | + | // it drew. | |||
| 144 | + | func (tg *target) hold(s ports.Scene, d *damageRegion) { | |||
| 145 | + | tg.valid, tg.sceneSeq, tg.windows = true, s.Seq, d.drawn | |||
| 146 | + | } | |||
A internal/adapters/vulkan/damage_test.go +136 -0
| @@ -0,0 +1,136 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "image/color" | |||
| 6 | + | "os" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // damagedContent writes px into the content's pool (the client drawing | |||
| 13 | + | // into the same buffer) and records the damage of the new Seq. | |||
| 14 | + | func damagedContent(t *testing.T, f *os.File, base ports.SurfaceContent, seq uint64, px []byte, hist []ports.SeqDamage) ports.SurfaceContent { | |||
| 15 | + | t.Helper() | |||
| 16 | + | if _, err := f.WriteAt(px, 0); err != nil { | |||
| 17 | + | t.Fatal(err) | |||
| 18 | + | } | |||
| 19 | + | c := base | |||
| 20 | + | c.Seq, c.DamageHistory = seq, hist | |||
| 21 | + | return c | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // Partial redraws are pixel-identical to full ones: with both targets | |||
| 25 | + | // ages (A then B, then one render) and with shm double buffers | |||
| 26 | + | // (render, skip, render). | |||
| 27 | + | func TestRendererDamageMatchesFullRedraw(t *testing.T) { | |||
| 28 | + | const w, h = 32, 32 | |||
| 29 | + | newR := func() *Renderer { | |||
| 30 | + | r, err := New(64, 48) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 33 | + | } | |||
| 34 | + | t.Cleanup(r.Close) | |||
| 35 | + | return r | |||
| 36 | + | } | |||
| 37 | + | damaged, full := newR(), newR() | |||
| 38 | + | bufsD, err := damaged.ExportTargets(2, nil) | |||
| 39 | + | if err != nil { | |||
| 40 | + | t.Skipf("no exportable targets: %v", err) | |||
| 41 | + | } | |||
| 42 | + | for _, b := range bufsD { | |||
| 43 | + | b.Planes[0].File.Close() | |||
| 44 | + | } | |||
| 45 | + | px := func(c color.RGBA, x0, y0, x1, y1 int, into []byte) []byte { | |||
| 46 | + | for y := y0; y < y1; y++ { | |||
| 47 | + | for x := x0; x < x1; x++ { | |||
| 48 | + | copy(into[(y*w+x)*4:], []byte{c.B, c.G, c.R, 255}) | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | return into | |||
| 52 | + | } | |||
| 53 | + | pixels := px(color.RGBA{10, 20, 30, 255}, 0, 0, w, h, make([]byte, w*h*4)) | |||
| 54 | + | base := shmContent(t, w, h, w*4, pixels) | |||
| 55 | + | base.ID = 1 | |||
| 56 | + | f := base.SHM.File | |||
| 57 | + | scene := ports.Scene{Seq: 5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: w, H: h}, Borderless: true}}} | |||
| 58 | + | hist := []ports.SeqDamage{{Seq: 1, Full: true}} | |||
| 59 | + | c := damagedContent(t, f, *base, 1, pixels, hist) | |||
| 60 | + | step := func(i int, c ports.SurfaceContent) { | |||
| 61 | + | t.Helper() | |||
| 62 | + | damaged.UseTarget(i % 2) | |||
| 63 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 64 | + | if err := render(damaged, scene, contents); err != nil { | |||
| 65 | + | t.Fatal(err) | |||
| 66 | + | } | |||
| 67 | + | if err := render(full, ports.Scene{Background: scene.Background, Windows: scene.Windows}, contents); err != nil { | |||
| 68 | + | t.Fatal(err) | |||
| 69 | + | } | |||
| 70 | + | if a, b := damaged.Pixels(), full.Pixels(); !bytes.Equal(a.Pix, b.Pix) { | |||
| 71 | + | t.Fatalf("frame %d differs from a full redraw", i) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | step(0, c) | |||
| 75 | + | step(1, c) // target 1 drawn whole once | |||
| 76 | + | before := damaged.redrawn | |||
| 77 | + | // A: a red square; B: a green one elsewhere. Target 0 holds Seq 1, | |||
| 78 | + | // so its next frame redraws the union of both. | |||
| 79 | + | for i, d := range []struct { | |||
| 80 | + | col color.RGBA | |||
| 81 | + | rect ports.Rect | |||
| 82 | + | }{{color.RGBA{255, 0, 0, 255}, ports.Rect{X: 2, Y: 2, W: 4, H: 4}}, {color.RGBA{0, 255, 0, 255}, ports.Rect{X: 20, Y: 10, W: 3, H: 5}}} { | |||
| 83 | + | seq := uint64(2 + i) | |||
| 84 | + | px(d.col, d.rect.X, d.rect.Y, d.rect.X+d.rect.W, d.rect.Y+d.rect.H, pixels) | |||
| 85 | + | hist = append(hist, ports.SeqDamage{Seq: seq, Rects: []ports.Rect{d.rect}}) | |||
| 86 | + | c = damagedContent(t, f, *base, seq, pixels, hist) | |||
| 87 | + | } | |||
| 88 | + | step(2, c) | |||
| 89 | + | if got := damaged.redrawn - before; got >= 64*48 { | |||
| 90 | + | t.Fatalf("partial frame redrew %d pixels", got) | |||
| 91 | + | } | |||
| 92 | + | // Render, skip (no frame), render: target 1 is two contents behind. | |||
| 93 | + | px(color.RGBA{0, 0, 255, 255}, 0, 30, 2, 32, pixels) | |||
| 94 | + | hist = append(hist, ports.SeqDamage{Seq: 4, Rects: []ports.Rect{{Y: 30, W: 2, H: 2}}}) | |||
| 95 | + | c = damagedContent(t, f, *base, 4, pixels, hist) | |||
| 96 | + | step(3, c) | |||
| 97 | + | step(4, c) | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // A window left out of a frame (on an overlay plane) and drawn again in | |||
| 101 | + | // the same scene is redrawn whole; one held but no longer drawn is | |||
| 102 | + | // cleared. | |||
| 103 | + | func TestRendererDamageWindowLeftOutAndBack(t *testing.T) { | |||
| 104 | + | newR := func() *Renderer { | |||
| 105 | + | r, err := New(64, 48) | |||
| 106 | + | if err != nil { | |||
| 107 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 108 | + | } | |||
| 109 | + | t.Cleanup(r.Close) | |||
| 110 | + | return r | |||
| 111 | + | } | |||
| 112 | + | damaged, full := newR(), newR() | |||
| 113 | + | c := solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}) | |||
| 114 | + | c.ID, c.Seq = 1, 1 | |||
| 115 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 116 | + | with := ports.Scene{Seq: 9, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 16}, Borderless: true}}} | |||
| 117 | + | without := with | |||
| 118 | + | without.Windows = nil | |||
| 119 | + | check := func(s ports.Scene, what string) { | |||
| 120 | + | t.Helper() | |||
| 121 | + | if err := render(damaged, s, contents); err != nil { | |||
| 122 | + | t.Fatal(err) | |||
| 123 | + | } | |||
| 124 | + | plain := s | |||
| 125 | + | plain.Seq = 0 | |||
| 126 | + | if err := render(full, plain, contents); err != nil { | |||
| 127 | + | t.Fatal(err) | |||
| 128 | + | } | |||
| 129 | + | if !bytes.Equal(damaged.Pixels().Pix, full.Pixels().Pix) { | |||
| 130 | + | t.Fatalf("%s differs from a full redraw", what) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | check(without, "overlay frame") | |||
| 134 | + | check(with, "overlay refused, window composed") | |||
| 135 | + | check(without, "window on the overlay again") | |||
| 136 | + | } | |||
M internal/adapters/vulkan/dmabuf.go +47 -14
| @@ -13,7 +13,7 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | // dmabuf import (linux-dmabuf clients): each client buffer becomes a | 14 | // dmabuf import (linux-dmabuf clients): each client buffer becomes a | ||
| 15 | // VkImage bound to its imported memory, cached by buffer ID while the | 15 | // VkImage bound to its imported memory, cached by buffer ID while the | ||
| 16 | - | // client keeps attaching it, and blitted into the output image. Sync is | 16 | + | // client keeps attaching it, and sampled by the compose shader. Sync is |
| 17 | // implicit: before sampling, the renderer waits on the fences the client's | 17 | // implicit: before sampling, the renderer waits on the fences the client's | ||
| 18 | // driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported | 18 | // driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported | ||
| 19 | // as a semaphore. | 19 | // as a semaphore. | ||
| @@ -86,7 +86,7 @@ func cstring(b []byte) string { | |||||
| 86 | } | 86 | } | ||
| 87 | 87 | ||||
| 88 | // probeDMABuf lists the fourcc/modifier pairs the device imports as | 88 | // probeDMABuf lists the fourcc/modifier pairs the device imports as | ||
| 89 | - | // single-plane blit sources, and the render node. | 89 | + | // single-plane sampled images with linear filtering, and the render node. |
| 90 | func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport { | 90 | func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport { | ||
| 91 | var sup ports.DMABufSupport | 91 | var sup ports.DMABufSupport | ||
| 92 | drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT} | 92 | drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT} | ||
| @@ -107,8 +107,9 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport { | |||||
| 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp) | 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp) | ||
| 108 | for _, m := range mods[:list.DrmFormatModifierCount] { | 108 | for _, m := range mods[:list.DrmFormatModifierCount] { | ||
| 109 | // Multi-plane modifiers (compression metadata) are left to a | 109 | // Multi-plane modifiers (compression metadata) are left to a | ||
| 110 | - | // later step; the blit needs a source. | 110 | + | // later step. |
| 111 | - | if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vk.FormatFeatureBlitSrcBit == 0 { | 111 | + | need := vk.FormatFeatureFlags(vk.FormatFeatureSampledImageBit | vk.FormatFeatureSampledImageFilterLinearBit) |
| 112 | + | if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&need != need { | |||
| 112 | continue | 113 | continue | ||
| 113 | } | 114 | } | ||
| 114 | if r.importable(physical, f.format, m.DrmFormatModifier) { | 115 | if r.importable(physical, f.format, m.DrmFormatModifier) { | ||
| @@ -124,7 +125,7 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport { | |||||
| 124 | func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool { | 125 | func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool { | ||
| 125 | mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive} | 126 | mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive} | ||
| 126 | ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | 127 | ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | ||
| 127 | - | info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit} | 128 | + | info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit} |
| 128 | extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties} | 129 | extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties} | ||
| 129 | out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)} | 130 | out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)} | ||
| 130 | if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success { | 131 | if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success { | ||
| @@ -136,10 +137,14 @@ func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modi | |||||
| 136 | // DMABuf reports the formats this renderer imports. | 137 | // DMABuf reports the formats this renderer imports. | ||
| 137 | func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf } | 138 | func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf } | ||
| 138 | 139 | ||||
| 139 | - | // imported is a client dmabuf bound to a VkImage. | 140 | + | // imported is a client dmabuf bound to a VkImage, with the view and set |
| 141 | + | // the compose shader samples it through. | |||
| 140 | type imported struct { | 142 | type imported struct { | ||
| 141 | image vk.Image | 143 | image vk.Image | ||
| 142 | memory vk.DeviceMemory | 144 | memory vk.DeviceMemory | ||
| 145 | + | view vk.ImageView | |||
| 146 | + | pool vk.DescriptorPool | |||
| 147 | + | set vk.DescriptorSet | |||
| 143 | fd int // our duplicate of plane 0, for implicit sync | 148 | fd int // our duplicate of plane 0, for implicit sync | ||
| 144 | // last is the frame that drew it, for eviction. | 149 | // last is the frame that drew it, for eviction. | ||
| 145 | last uint64 | 150 | last uint64 | ||
| @@ -177,7 +182,7 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) { | |||||
| 177 | layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)} | 182 | layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)} | ||
| 178 | explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout} | 183 | explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout} | ||
| 179 | external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | 184 | external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | ||
| 180 | - | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | 185 | + | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} |
| 181 | if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil { | 186 | if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil { | ||
| 182 | return nil, err | 187 | return nil, err | ||
| 183 | } | 188 | } | ||
| @@ -206,6 +211,12 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) { | |||||
| 206 | if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil { | 211 | if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil { | ||
| 207 | return nil, err | 212 | return nil, err | ||
| 208 | } | 213 | } | ||
| 214 | + | if im.view, err = r.imageView(im.image, format); err != nil { | |||
| 215 | + | return nil, err | |||
| 216 | + | } | |||
| 217 | + | if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil { | |||
| 218 | + | return nil, err | |||
| 219 | + | } | |||
| 209 | ok = true | 220 | ok = true | ||
| 210 | r.imports[b.ID] = im | 221 | r.imports[b.ID] = im | ||
| 211 | return im, nil | 222 | return im, nil | ||
| @@ -229,6 +240,12 @@ func dupFile(f *os.File) (int, error) { | |||||
| 229 | } | 240 | } | ||
| 230 | 241 | ||||
| 231 | func (r *Renderer) release(im *imported) { | 242 | func (r *Renderer) release(im *imported) { | ||
| 243 | + | if im.pool != 0 { | |||
| 244 | + | r.dd.DestroyDescriptorPool(r.device, im.pool, nil) | |||
| 245 | + | } | |||
| 246 | + | if im.view != 0 { | |||
| 247 | + | r.dd.DestroyImageView(r.device, im.view, nil) | |||
| 248 | + | } | |||
| 232 | if im.image != 0 { | 249 | if im.image != 0 { | ||
| 233 | r.dd.DestroyImage(r.device, im.image, nil) | 250 | r.dd.DestroyImage(r.device, im.image, nil) | ||
| 234 | } | 251 | } | ||
| @@ -240,13 +257,13 @@ func (r *Renderer) release(im *imported) { | |||||
| 240 | } | 257 | } | ||
| 241 | } | 258 | } | ||
| 242 | 259 | ||||
| 243 | - | // dropUnused frees imports not drawn for importTTL frames: the client moved | 260 | + | // dropUnused frees imports not drawn for importTTL frames (the client |
| 244 | - | // on to other buffers or went away. Called after a frame completes. | 261 | + | // moved on to other buffers or went away), once the last frame that drew |
| 262 | + | // them completed. | |||
| 245 | func (r *Renderer) dropUnused() { | 263 | func (r *Renderer) dropUnused() { | ||
| 246 | - | r.frame++ | |||
| 247 | for id, im := range r.imports { | 264 | for id, im := range r.imports { | ||
| 248 | if r.frame-im.last > importTTL { | 265 | if r.frame-im.last > importTTL { | ||
| 249 | - | r.release(im) | 266 | + | r.retire(im.last, func() { r.release(im) }) |
| 250 | delete(r.imports, id) | 267 | delete(r.imports, id) | ||
| 251 | } | 268 | } | ||
| 252 | } | 269 | } | ||
| @@ -270,15 +287,31 @@ func (r *Renderer) readFence(im *imported) vk.Semaphore { | |||||
| 270 | if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 { | 287 | if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 { | ||
| 271 | return 0 | 288 | return 0 | ||
| 272 | } | 289 | } | ||
| 290 | + | return r.importSyncFD(int(arg.fd)) | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | // importFence makes a wait semaphore of a sync file. The file stays the | |||
| 294 | + | // caller's: Vulkan takes a duplicate, made while the file is held open. | |||
| 295 | + | func (r *Renderer) importFence(f *os.File) vk.Semaphore { | |||
| 296 | + | fd, err := dupFile(f) | |||
| 297 | + | if err != nil { | |||
| 298 | + | return 0 | |||
| 299 | + | } | |||
| 300 | + | return r.importSyncFD(fd) | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | // importSyncFD imports a sync file fd as a temporary semaphore payload; | |||
| 304 | + | // the fd is Vulkan's on success and closed on failure. | |||
| 305 | + | func (r *Renderer) importSyncFD(fd int) vk.Semaphore { | |||
| 273 | var sem vk.Semaphore | 306 | var sem vk.Semaphore | ||
| 274 | si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo} | 307 | si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo} | ||
| 275 | if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success { | 308 | if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success { | ||
| 276 | - | unix.Close(int(arg.fd)) | 309 | + | unix.Close(fd) |
| 277 | return 0 | 310 | return 0 | ||
| 278 | } | 311 | } | ||
| 279 | - | imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: arg.fd} | 312 | + | imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: int32(fd)} |
| 280 | if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success { | 313 | if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success { | ||
| 281 | - | unix.Close(int(arg.fd)) | 314 | + | unix.Close(fd) |
| 282 | r.dd.DestroySemaphore(r.device, sem, nil) | 315 | r.dd.DestroySemaphore(r.device, sem, nil) | ||
| 283 | return 0 | 316 | return 0 | ||
| 284 | } | 317 | } | ||
M internal/adapters/vulkan/dmabuf_test.go +31 -2
| @@ -1,6 +1,7 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "image" | |||
| 4 | "image/color" | 5 | "image/color" | ||
| 5 | "os" | 6 | "os" | ||
| 6 | "slices" | 7 | "slices" | ||
| @@ -81,7 +82,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}}} | 82 | 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}}}} | 83 | 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}} | 84 | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}} | ||
| 84 | - | if err := r.Render(scene, contents); err != nil { | 85 | + | if err := render(r, scene, contents); err != nil { |
| 85 | t.Fatal(err) | 86 | t.Fatal(err) | ||
| 86 | } | 87 | } | ||
| 87 | px := r.Pixels() | 88 | px := r.Pixels() | ||
| @@ -98,7 +99,7 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||||
| 98 | } | 99 | } | ||
| 99 | // Frames without the buffer: its import is released after importTTL. | 100 | // Frames without the buffer: its import is released after importTTL. | ||
| 100 | for range importTTL + 1 { | 101 | for range importTTL + 1 { | ||
| 101 | - | if err := r.Render(scene, nil); err != nil { | 102 | + | if err := render(r, scene, nil); err != nil { |
| 102 | t.Fatal(err) | 103 | t.Fatal(err) | ||
| 103 | } | 104 | } | ||
| 104 | } | 105 | } | ||
| @@ -106,3 +107,31 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||||
| 106 | t.Fatalf("import kept: %d", len(r.imports)) | 107 | t.Fatalf("import kept: %d", len(r.imports)) | ||
| 107 | } | 108 | } | ||
| 108 | } | 109 | } | ||
| 110 | + | ||||
| 111 | + | // A subsurface dmabuf is read after its own acquire fence, not the root's. | |||
| 112 | + | func TestRendererSubsurfaceAcquire(t *testing.T) { | |||
| 113 | + | r, err := New(80, 32) | |||
| 114 | + | if err != nil { | |||
| 115 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 116 | + | } | |||
| 117 | + | defer r.Close() | |||
| 118 | + | linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0} | |||
| 119 | + | if !slices.Contains(r.DMABuf().Formats, linear) { | |||
| 120 | + | t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf()) | |||
| 121 | + | } | |||
| 122 | + | f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} }) | |||
| 123 | + | fence, w, _ := os.Pipe() | |||
| 124 | + | defer fence.Close() | |||
| 125 | + | defer w.Close() | |||
| 126 | + | child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence, | |||
| 127 | + | DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}} | |||
| 128 | + | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}} | |||
| 129 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}} | |||
| 130 | + | ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32))) | |||
| 131 | + | for _, d := range ds { | |||
| 132 | + | if d.im != nil && d.acquire == fence { | |||
| 133 | + | return | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | t.Fatal("subsurface draw has no acquire fence") | |||
| 137 | + | } | |||
A internal/adapters/vulkan/frame.go +365 -0
| @@ -0,0 +1,365 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "image" | |||
| 6 | + | "math" | |||
| 7 | + | "os" | |||
| 8 | + | "runtime" | |||
| 9 | + | "slices" | |||
| 10 | + | "unsafe" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 13 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // Frames are recorded into a ring of slots, one per frame in flight. The | |||
| 17 | + | // CPU never waits for a frame it just submitted: Render returns a sync | |||
| 18 | + | // file the display waits on (KMS IN_FENCE_FD). A slot's fence is waited | |||
| 19 | + | // only when the slot is reused, and objects a frame read (imports, GPU | |||
| 20 | + | // buffers, client semaphores) are freed only once that frame completed. | |||
| 21 | + | ||||
| 22 | + | // frameSlots is how many frames may be in flight: one per output target. | |||
| 23 | + | const frameSlots = 2 | |||
| 24 | + | ||||
| 25 | + | // frameSlot is the command buffer and sync objects of one frame in flight. | |||
| 26 | + | type frameSlot struct { | |||
| 27 | + | cmd vk.CommandBuffer | |||
| 28 | + | fence vk.Fence | |||
| 29 | + | // done is signalled with the frame and exported as its sync file. | |||
| 30 | + | done vk.Semaphore | |||
| 31 | + | frame uint64 // the frame recorded last, 0 before any | |||
| 32 | + | busy bool // submitted, fence not yet seen signalled | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // deferredFree runs once frame completed. | |||
| 36 | + | type deferredFree struct { | |||
| 37 | + | frame uint64 | |||
| 38 | + | free func() | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (r *Renderer) createSlots() error { | |||
| 42 | + | d := r.dd | |||
| 43 | + | cmds := make([]vk.CommandBuffer, frameSlots) | |||
| 44 | + | ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: frameSlots} | |||
| 45 | + | if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil { | |||
| 46 | + | return err | |||
| 47 | + | } | |||
| 48 | + | for i := range r.slots { | |||
| 49 | + | s := &r.slots[i] | |||
| 50 | + | s.cmd = cmds[i] | |||
| 51 | + | fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo} | |||
| 52 | + | if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil { | |||
| 53 | + | return err | |||
| 54 | + | } | |||
| 55 | + | if err := r.newDone(s); err != nil { | |||
| 56 | + | return err | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | return nil | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // newDone creates a slot's frame semaphore, exportable as a sync file when | |||
| 63 | + | // the device allows it; a device that refuses falls back to waiting for | |||
| 64 | + | // frames on the CPU (syncFD off). | |||
| 65 | + | func (r *Renderer) newDone(s *frameSlot) error { | |||
| 66 | + | d := r.dd | |||
| 67 | + | si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo} | |||
| 68 | + | export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit} | |||
| 69 | + | if r.syncFD { | |||
| 70 | + | si.Next = unsafe.Pointer(&export) | |||
| 71 | + | if d.CreateSemaphore(r.device, &si, nil, &s.done) == vk.Success { | |||
| 72 | + | return nil | |||
| 73 | + | } | |||
| 74 | + | r.syncFD = false | |||
| 75 | + | si.Next = nil | |||
| 76 | + | } | |||
| 77 | + | return checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done)) | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func (r *Renderer) destroySlots() { | |||
| 81 | + | d := r.dd | |||
| 82 | + | for i := range r.slots { | |||
| 83 | + | s := &r.slots[i] | |||
| 84 | + | if s.fence != 0 { | |||
| 85 | + | d.DestroyFence(r.device, s.fence, nil) | |||
| 86 | + | } | |||
| 87 | + | if s.done != 0 { | |||
| 88 | + | d.DestroySemaphore(r.device, s.done, nil) | |||
| 89 | + | } | |||
| 90 | + | if s.cmd != 0 { | |||
| 91 | + | d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd) | |||
| 92 | + | } | |||
| 93 | + | *s = frameSlot{} | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // waitFrame blocks until frame n completed. Frames not submitted yet and | |||
| 98 | + | // frames known done return at once. Every slot holding a frame up to n | |||
| 99 | + | // is waited and reset, so its fence is unsignalled before reuse. | |||
| 100 | + | func (r *Renderer) waitFrame(n uint64) error { | |||
| 101 | + | n = min(n, r.submitted) | |||
| 102 | + | if n <= r.completed { | |||
| 103 | + | return nil | |||
| 104 | + | } | |||
| 105 | + | for i := range r.slots { | |||
| 106 | + | s := &r.slots[i] | |||
| 107 | + | if !s.busy || s.frame > n { | |||
| 108 | + | continue | |||
| 109 | + | } | |||
| 110 | + | if err := checked("vkWaitForFences", r.dd.WaitForFences(r.device, 1, &s.fence, 1, math.MaxUint64)); err != nil { | |||
| 111 | + | return err | |||
| 112 | + | } | |||
| 113 | + | if err := checked("vkResetFences", r.dd.ResetFences(r.device, 1, &s.fence)); err != nil { | |||
| 114 | + | return err | |||
| 115 | + | } | |||
| 116 | + | s.busy = false | |||
| 117 | + | } | |||
| 118 | + | r.completed = n | |||
| 119 | + | r.runDeferred() | |||
| 120 | + | return nil | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // retire frees an object once the frame that last used it completed. | |||
| 124 | + | func (r *Renderer) retire(frame uint64, free func()) { | |||
| 125 | + | if frame <= r.completed { | |||
| 126 | + | free() | |||
| 127 | + | return | |||
| 128 | + | } | |||
| 129 | + | r.deferred = append(r.deferred, deferredFree{frame: frame, free: free}) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | func (r *Renderer) runDeferred() { | |||
| 133 | + | r.deferred = slices.DeleteFunc(r.deferred, func(f deferredFree) bool { | |||
| 134 | + | if f.frame <= r.completed { | |||
| 135 | + | f.free() | |||
| 136 | + | return true | |||
| 137 | + | } | |||
| 138 | + | return false | |||
| 139 | + | }) | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // idle records that the device finished every submitted frame. | |||
| 143 | + | func (r *Renderer) idle() { | |||
| 144 | + | for i := range r.slots { | |||
| 145 | + | s := &r.slots[i] | |||
| 146 | + | if s.busy { | |||
| 147 | + | _ = r.dd.ResetFences(r.device, 1, &s.fence) | |||
| 148 | + | s.busy = false | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | // Frames recorded but never submitted have no GPU work either. | |||
| 152 | + | r.completed = r.frame | |||
| 153 | + | r.runDeferred() | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | // Render draws a frame into the current target. done is signalled when | |||
| 157 | + | // the GPU finished it; nil when the device cannot export a sync file, | |||
| 158 | + | // then the frame is finished on return. | |||
| 159 | + | func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) { | |||
| 160 | + | // A frame that fails before its submit keeps its number: objects it | |||
| 161 | + | // touched are freed once a later frame completes (never early). | |||
| 162 | + | r.frame++ | |||
| 163 | + | slot := &r.slots[r.frame%frameSlots] | |||
| 164 | + | // The slot's previous frame must be done before its command buffer | |||
| 165 | + | // is recorded again. | |||
| 166 | + | if slot.busy { | |||
| 167 | + | if err := r.waitFrame(slot.frame); err != nil { | |||
| 168 | + | return nil, err | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | tg := r.target() | |||
| 172 | + | dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height)) | |||
| 173 | + | ds := r.draws(s, contents, dmg) | |||
| 174 | + | dmg.finish() | |||
| 175 | + | r.dropPools() | |||
| 176 | + | partial := !dmg.all() | |||
| 177 | + | if partial { | |||
| 178 | + | // The pass keeps the image: repaint the background under the | |||
| 179 | + | // region, then everything clipped to it. | |||
| 180 | + | ds = dmg.clip(append([]draw{r.fillDraw(dmg.area, parseColor(s.Background))}, ds...)) | |||
| 181 | + | r.redrawn += dmg.area.Dx() * dmg.area.Dy() | |||
| 182 | + | } else { | |||
| 183 | + | r.redrawn += r.width * r.height | |||
| 184 | + | } | |||
| 185 | + | d := r.dd | |||
| 186 | + | cmd := slot.cmd | |||
| 187 | + | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil { | |||
| 188 | + | return nil, err | |||
| 189 | + | } | |||
| 190 | + | begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit} | |||
| 191 | + | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil { | |||
| 192 | + | return nil, err | |||
| 193 | + | } | |||
| 194 | + | // A full redraw clears the target: its old contents never matter | |||
| 195 | + | // (UNDEFINED). A partial one keeps them. | |||
| 196 | + | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange} | |||
| 197 | + | if partial { | |||
| 198 | + | b.OldLayout = tg.layout | |||
| 199 | + | b.DstAccessMask |= vk.AccessColorAttachmentReadBit | |||
| 200 | + | } | |||
| 201 | + | if tg.exported { | |||
| 202 | + | // Acquire from the display (foreign queue family). | |||
| 203 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | |||
| 204 | + | } | |||
| 205 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 206 | + | // Client buffers come from the foreign queue family (their driver) | |||
| 207 | + | // and go back to it after the frame, so the next frame acquires them. | |||
| 208 | + | var dmas []*imported | |||
| 209 | + | acquires := map[*imported]*os.File{} | |||
| 210 | + | for _, dr := range ds { | |||
| 211 | + | if dr.im != nil && !slices.Contains(dmas, dr.im) { | |||
| 212 | + | dmas = append(dmas, dr.im) | |||
| 213 | + | dr.im.last = r.frame | |||
| 214 | + | } | |||
| 215 | + | if dr.acquire != nil { | |||
| 216 | + | acquires[dr.im] = dr.acquire | |||
| 217 | + | } | |||
| 218 | + | } | |||
| 219 | + | r.ownership(cmd, dmas, true) | |||
| 220 | + | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | |||
| 221 | + | r.ownership(cmd, dmas, false) | |||
| 222 | + | b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal | |||
| 223 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored | |||
| 224 | + | if tg.exported { | |||
| 225 | + | // Release to the display: KMS scans it out after the fence. | |||
| 226 | + | b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0 | |||
| 227 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | |||
| 228 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 229 | + | } else { | |||
| 230 | + | b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit | |||
| 231 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 232 | + | } | |||
| 233 | + | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil { | |||
| 234 | + | return nil, err | |||
| 235 | + | } | |||
| 236 | + | // Wait for the client's GPU writes to each buffer: its explicit | |||
| 237 | + | // acquire fence, else the buffer's implicit fences. | |||
| 238 | + | var waits []vk.Semaphore | |||
| 239 | + | for _, im := range dmas { | |||
| 240 | + | sem := vk.Semaphore(0) | |||
| 241 | + | if f := acquires[im]; f != nil { | |||
| 242 | + | sem = r.importFence(f) | |||
| 243 | + | } else { | |||
| 244 | + | sem = r.readFence(im) | |||
| 245 | + | } | |||
| 246 | + | if sem != 0 { | |||
| 247 | + | waits = append(waits, sem) | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | frame := r.frame | |||
| 251 | + | for _, sem := range waits { | |||
| 252 | + | r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) }) | |||
| 253 | + | } | |||
| 254 | + | submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd} | |||
| 255 | + | stages := make([]vk.PipelineStageFlags, len(waits)) | |||
| 256 | + | if len(waits) > 0 { | |||
| 257 | + | for i := range stages { | |||
| 258 | + | stages[i] = vk.PipelineStageFragmentShaderBit | |||
| 259 | + | } | |||
| 260 | + | submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0] | |||
| 261 | + | } | |||
| 262 | + | if r.syncFD { | |||
| 263 | + | submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done | |||
| 264 | + | } | |||
| 265 | + | if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil { | |||
| 266 | + | return nil, err | |||
| 267 | + | } | |||
| 268 | + | runtime.KeepAlive(stages) | |||
| 269 | + | runtime.KeepAlive(waits) | |||
| 270 | + | slot.frame, slot.busy, r.submitted = frame, true, frame | |||
| 271 | + | tg.layout = b.NewLayout | |||
| 272 | + | tg.hold(s, dmg) | |||
| 273 | + | r.last, r.readBack = tg, false | |||
| 274 | + | r.dropUnused() | |||
| 275 | + | r.dropShm() | |||
| 276 | + | if !r.syncFD { | |||
| 277 | + | return nil, r.waitFrame(frame) | |||
| 278 | + | } | |||
| 279 | + | var fd int32 | |||
| 280 | + | get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit} | |||
| 281 | + | if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success { | |||
| 282 | + | // The frame is drawn: finish it on the CPU. The semaphore stays | |||
| 283 | + | // signalled: replace it, so the slot's next submit signals a | |||
| 284 | + | // fresh one. | |||
| 285 | + | if err := r.waitFrame(frame); err != nil { | |||
| 286 | + | return nil, err | |||
| 287 | + | } | |||
| 288 | + | d.DestroySemaphore(r.device, slot.done, nil) | |||
| 289 | + | slot.done = 0 | |||
| 290 | + | return nil, r.newDone(slot) | |||
| 291 | + | } | |||
| 292 | + | if fd < 0 { | |||
| 293 | + | // Already signalled: the driver may return -1. | |||
| 294 | + | return nil, r.waitFrame(frame) | |||
| 295 | + | } | |||
| 296 | + | f := os.NewFile(uintptr(fd), "nefertty-frame") | |||
| 297 | + | if f == nil { | |||
| 298 | + | return nil, fmt.Errorf("invalid frame sync file %d", fd) | |||
| 299 | + | } | |||
| 300 | + | return f, nil | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | // ownership moves client images between their driver (foreign queue | |||
| 304 | + | // family, GENERAL: their contents survive) and the fragment shader. | |||
| 305 | + | func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) { | |||
| 306 | + | for _, im := range dmas { | |||
| 307 | + | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange} | |||
| 308 | + | if acquire { | |||
| 309 | + | b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal | |||
| 310 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | |||
| 311 | + | b.DstAccessMask = vk.AccessShaderReadBit | |||
| 312 | + | // The frame waits on the client's fence at the fragment | |||
| 313 | + | // stage: the transfer must come after that wait. | |||
| 314 | + | r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 315 | + | } else { | |||
| 316 | + | b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral | |||
| 317 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | |||
| 318 | + | b.SrcAccessMask = vk.AccessShaderReadBit | |||
| 319 | + | r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | // oneShot records and runs commands outside the frame ring and waits for | |||
| 325 | + | // them (setup and screenshots only, never per frame). | |||
| 326 | + | func (r *Renderer) oneShot(record func(vk.CommandBuffer)) error { | |||
| 327 | + | d := r.dd | |||
| 328 | + | var cmd vk.CommandBuffer | |||
| 329 | + | ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1} | |||
| 330 | + | if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmd)); err != nil { | |||
| 331 | + | return err | |||
| 332 | + | } | |||
| 333 | + | defer d.FreeCommandBuffers(r.device, r.pool, 1, &cmd) | |||
| 334 | + | var fence vk.Fence | |||
| 335 | + | fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo} | |||
| 336 | + | if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &fence)); err != nil { | |||
| 337 | + | return err | |||
| 338 | + | } | |||
| 339 | + | defer d.DestroyFence(r.device, fence, nil) | |||
| 340 | + | begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit} | |||
| 341 | + | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil { | |||
| 342 | + | return err | |||
| 343 | + | } | |||
| 344 | + | record(cmd) | |||
| 345 | + | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil { | |||
| 346 | + | return err | |||
| 347 | + | } | |||
| 348 | + | submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd} | |||
| 349 | + | if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, fence)); err != nil { | |||
| 350 | + | return err | |||
| 351 | + | } | |||
| 352 | + | return checked("vkWaitForFences", d.WaitForFences(r.device, 1, &fence, 1, math.MaxUint64)) | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | // Clear draws a background-only frame and waits for it. | |||
| 356 | + | func (r *Renderer) Clear(rgb [3]uint8) error { | |||
| 357 | + | done, err := r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil) | |||
| 358 | + | if done != nil { | |||
| 359 | + | done.Close() | |||
| 360 | + | } | |||
| 361 | + | if err != nil { | |||
| 362 | + | return err | |||
| 363 | + | } | |||
| 364 | + | return r.waitFrame(r.frame) | |||
| 365 | + | } | |||
M internal/adapters/vulkan/renderer.go +114 -469
| @@ -6,7 +6,6 @@ import ( | |||||
| 6 | "math" | 6 | "math" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "runtime" | 8 | "runtime" | ||
| 9 | - | "slices" | |||
| 10 | "strconv" | 9 | "strconv" | ||
| 11 | "unsafe" | 10 | "unsafe" | ||
| 12 | 11 | ||||
| @@ -30,42 +29,44 @@ type Renderer struct { | |||||
| 30 | current int | 29 | current int | ||
| 31 | renderMods []uint64 | 30 | renderMods []uint64 | ||
| 32 | // last is the target of the last frame; readback copies it into | 31 | // last is the target of the last frame; readback copies it into | ||
| 33 | - | // buffer only when Pixels or CopyBGRX asks (stale until then). | 32 | + | // buffer only when Pixels asks (stale until then). |
| 34 | - | last *target | 33 | + | last *target |
| 35 | - | readBack bool | 34 | + | readBack bool |
| 36 | - | buffer vk.Buffer | 35 | + | buffer vk.Buffer |
| 37 | - | bufferMemory vk.DeviceMemory | 36 | + | bufferMemory vk.DeviceMemory |
| 38 | - | staging vk.Buffer | 37 | + | mapped unsafe.Pointer |
| 39 | - | stagingMemory vk.DeviceMemory | 38 | + | pool vk.CommandPool |
| 40 | - | stagingMapped unsafe.Pointer | 39 | + | memory vk.PhysicalDeviceMemoryProperties |
| 41 | - | stagingSize int | 40 | + | family uint32 |
| 42 | - | mapped unsafe.Pointer | 41 | + | // Frames in flight (frame.go): frame is the last frame recorded, |
| 43 | - | pool vk.CommandPool | 42 | + | // submitted the last submitted, completed the last known finished. |
| 44 | - | command vk.CommandBuffer | 43 | + | slots [frameSlots]frameSlot |
| 45 | - | fence vk.Fence | 44 | + | frame, submitted, completed uint64 |
| 46 | - | memory vk.PhysicalDeviceMemoryProperties | 45 | + | deferred []deferredFree |
| 47 | - | family uint32 | 46 | + | // syncFD: frames export a sync file (else Render waits for them). |
| 47 | + | syncFD bool | |||
| 48 | // dmabuf is what clients may send; imports are their buffers by ID. | 48 | // dmabuf is what clients may send; imports are their buffers by ID. | ||
| 49 | dmabuf ports.DMABufSupport | 49 | dmabuf ports.DMABufSupport | ||
| 50 | imports map[uint64]*imported | 50 | imports map[uint64]*imported | ||
| 51 | // pools are client wl_shm pools mapped by pool ID. | 51 | // pools are client wl_shm pools mapped by pool ID. | ||
| 52 | pools map[uint64]*mapping | 52 | pools map[uint64]*mapping | ||
| 53 | - | // retired are pool mappings replaced this frame, unmapped after staging. | 53 | + | // retired are pool mappings replaced this frame, unmapped once the |
| 54 | + | // frame's copies are done. | |||
| 54 | retired [][]byte | 55 | retired [][]byte | ||
| 55 | - | frame uint64 | 56 | + | // shm are the GPU copies of wl_shm surfaces (shm.go); copied counts |
| 56 | - | // fillRegions is reused by Render for the bands of solid fills. | 57 | + | // the bytes copied into them, for tests. |
| 57 | - | fillRegions []vk.BufferImageCopy | 58 | + | shm map[shmKey]*shmSurface |
| 58 | - | // blend draws translucent client pixels (blend.go); align is the | 59 | + | copied int |
| 59 | - | // device's storage buffer offset alignment. | 60 | + | // redrawn counts the target pixels drawn, for tests. |
| 60 | - | blend blendPipeline | 61 | + | redrawn int |
| 61 | - | align int | 62 | + | // cursors are the exported cursor images (CursorBuffers). |
| 62 | - | maxRange int // bytes of blended pixels one frame may bind | 63 | + | cursors [2]*cursorImage |
| 64 | + | // compose draws every frame (compose.go); maxRange bounds one | |||
| 65 | + | // storage buffer binding. | |||
| 66 | + | compose composer | |||
| 67 | + | maxRange int | |||
| 63 | } | 68 | } | ||
| 64 | 69 | ||||
| 65 | - | // fillRows is the height of the strip staged for a solid fill: the CPU | |||
| 66 | - | // writes it once, the GPU copies it down the rect. | |||
| 67 | - | const fillRows = 64 | |||
| 68 | - | ||||
| 69 | func checked(name string, result vk.Result) error { | 70 | func checked(name string, result vk.Result) error { | ||
| 70 | if err := vk.Check(result); err != nil { | 71 | if err := vk.Check(result); err != nil { | ||
| 71 | return fmt.Errorf("%s: %w", name, err) | 72 | return fmt.Errorf("%s: %w", name, err) | ||
| @@ -77,7 +78,7 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 77 | if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) { | 78 | if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) { | ||
| 78 | return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height) | 79 | return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height) | ||
| 79 | } | 80 | } | ||
| 80 | - | r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}} | 81 | + | r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}, shm: map[shmKey]*shmSurface{}} |
| 81 | defer func() { | 82 | defer func() { | ||
| 82 | if err != nil { | 83 | if err != nil { | ||
| 83 | r.Close() | 84 | r.Close() | ||
| @@ -153,7 +154,6 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 153 | r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory) | 154 | r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory) | ||
| 154 | var properties vk.PhysicalDeviceProperties | 155 | var properties vk.PhysicalDeviceProperties | ||
| 155 | r.id.GetPhysicalDeviceProperties(physical, &properties) | 156 | r.id.GetPhysicalDeviceProperties(physical, &properties) | ||
| 156 | - | r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4)) | |||
| 157 | r.maxRange = int(properties.Limits.MaxStorageBufferRange) | 157 | r.maxRange = int(properties.Limits.MaxStorageBufferRange) | ||
| 158 | r.family = family | 158 | r.family = family | ||
| 159 | priority := float32(1) | 159 | priority := float32(1) | ||
| @@ -187,6 +187,9 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 187 | r.dmabuf = sup | 187 | r.dmabuf = sup | ||
| 188 | r.probeRenderModifiers(physical) | 188 | r.probeRenderModifiers(physical) | ||
| 189 | } | 189 | } | ||
| 190 | + | // VK_KHR_external_semaphore_fd (enabled above): frames export | |||
| 191 | + | // their fence as a sync file. | |||
| 192 | + | r.syncFD = r.dd.GetSemaphoreFdKHR != nil | |||
| 190 | } | 193 | } | ||
| 191 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | 194 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | ||
| 192 | extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1} | 195 | extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1} | ||
| @@ -214,7 +217,7 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 214 | return | 217 | return | ||
| 215 | } | 218 | } | ||
| 216 | r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req) | 219 | r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req) | ||
| 217 | - | // Readback (screenshots, fallback) reads this buffer: uncached memory is ~100x slower. | 220 | + | // Readback (screenshots) reads this buffer: uncached memory is ~100x slower. |
| 218 | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit) | 221 | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit) | ||
| 219 | if err != nil { | 222 | if err != nil { | ||
| 220 | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | 223 | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | ||
| @@ -232,44 +235,17 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 232 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | 235 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | ||
| 233 | return | 236 | return | ||
| 234 | } | 237 | } | ||
| 235 | - | // Start with two frame-sized regions; later frames grow staging on demand. | |||
| 236 | - | stagingSize := size * 2 | |||
| 237 | - | r.stagingSize = int(stagingSize) | |||
| 238 | - | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive} | |||
| 239 | - | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | |||
| 240 | - | return | |||
| 241 | - | } | |||
| 242 | - | r.dd.GetBufferMemoryRequirements(r.device, r.staging, &req) | |||
| 243 | - | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |||
| 244 | - | if err != nil { | |||
| 245 | - | return | |||
| 246 | - | } | |||
| 247 | - | alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 248 | - | if err = checked("vkAllocateMemory(staging)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.stagingMemory)); err != nil { | |||
| 249 | - | return | |||
| 250 | - | } | |||
| 251 | - | if err = checked("vkBindBufferMemory(staging)", r.dd.BindBufferMemory(r.device, r.staging, r.stagingMemory, 0)); err != nil { | |||
| 252 | - | return | |||
| 253 | - | } | |||
| 254 | - | if err = checked("vkMapMemory(staging)", r.dd.MapMemory(r.device, r.stagingMemory, 0, stagingSize, 0, &r.stagingMapped)); err != nil { | |||
| 255 | - | return | |||
| 256 | - | } | |||
| 257 | pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family} | 238 | pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family} | ||
| 258 | if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil { | 239 | if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil { | ||
| 259 | return | 240 | return | ||
| 260 | } | 241 | } | ||
| 261 | - | ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1} | 242 | + | if err = r.createSlots(); err != nil { |
| 262 | - | if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil { | |||
| 263 | return | 243 | return | ||
| 264 | } | 244 | } | ||
| 265 | if err = r.createView(&r.own); err != nil { | 245 | if err = r.createView(&r.own); err != nil { | ||
| 266 | return | 246 | return | ||
| 267 | } | 247 | } | ||
| 268 | - | if err = r.createBlend(); err != nil { | 248 | + | if err = r.createComposer(); err != nil { |
| 269 | - | return | |||
| 270 | - | } | |||
| 271 | - | fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo} | |||
| 272 | - | if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil { | |||
| 273 | return | 249 | return | ||
| 274 | } | 250 | } | ||
| 275 | runtime.KeepAlive(priority) | 251 | runtime.KeepAlive(priority) | ||
| @@ -323,111 +299,9 @@ func windowColor(id ports.WindowID) [3]uint8 { | |||||
| 323 | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | 299 | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | ||
| 324 | } | 300 | } | ||
| 325 | 301 | ||||
| 326 | - | // ensureStaging grows the mapped transfer buffer before recording any commands. | 302 | + | // draws walks the scene into quads in paint order, copying new wl_shm |
| 327 | - | func (r *Renderer) ensureStaging(size int) error { | 303 | + | // content into GPU buffers on the way. |
| 328 | - | if size <= r.stagingSize { | 304 | + | func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw { |
| 329 | - | return nil | |||
| 330 | - | } | |||
| 331 | - | d := r.dd | |||
| 332 | - | capacity := size | |||
| 333 | - | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive} | |||
| 334 | - | var buffer vk.Buffer | |||
| 335 | - | if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil { | |||
| 336 | - | return err | |||
| 337 | - | } | |||
| 338 | - | var memory vk.DeviceMemory | |||
| 339 | - | defer func() { | |||
| 340 | - | if buffer != 0 { | |||
| 341 | - | d.DestroyBuffer(r.device, buffer, nil) | |||
| 342 | - | } | |||
| 343 | - | if memory != 0 { | |||
| 344 | - | d.FreeMemory(r.device, memory, nil) | |||
| 345 | - | } | |||
| 346 | - | }() | |||
| 347 | - | var req vk.MemoryRequirements | |||
| 348 | - | d.GetBufferMemoryRequirements(r.device, buffer, &req) | |||
| 349 | - | kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |||
| 350 | - | if err != nil { | |||
| 351 | - | return err | |||
| 352 | - | } | |||
| 353 | - | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 354 | - | if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil { | |||
| 355 | - | return err | |||
| 356 | - | } | |||
| 357 | - | if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil { | |||
| 358 | - | return err | |||
| 359 | - | } | |||
| 360 | - | var mapped unsafe.Pointer | |||
| 361 | - | if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil { | |||
| 362 | - | return err | |||
| 363 | - | } | |||
| 364 | - | d.UnmapMemory(r.device, r.stagingMemory) | |||
| 365 | - | d.DestroyBuffer(r.device, r.staging, nil) | |||
| 366 | - | d.FreeMemory(r.device, r.stagingMemory, nil) | |||
| 367 | - | r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity | |||
| 368 | - | buffer, memory = 0, 0 | |||
| 369 | - | return nil | |||
| 370 | - | } | |||
| 371 | - | ||||
| 372 | - | 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) | |||
| 374 | - | } | |||
| 375 | - | ||||
| 376 | - | // upload is one piece of a frame: a solid fill, client pixels staged from | |||
| 377 | - | // a wl_shm pool, or a dmabuf blitted in place. | |||
| 378 | - | type upload struct { | |||
| 379 | - | rect image.Rectangle | |||
| 380 | - | offset int | |||
| 381 | - | pixels []byte | |||
| 382 | - | stride int | |||
| 383 | - | // dst covers src; they differ in size when the buffer is not drawn at | |||
| 384 | - | // the physical size (integer-scale clients, rounding). | |||
| 385 | - | dst, src image.Rectangle | |||
| 386 | - | color [3]uint8 | |||
| 387 | - | opaque bool | |||
| 388 | - | // blend draws the staged pixels over the target instead of copying. | |||
| 389 | - | blend bool | |||
| 390 | - | // dma is a client GPU buffer, blitted instead of copied from staging. | |||
| 391 | - | dma *imported | |||
| 392 | - | // fill is a solid color: staging holds one strip of rows. | |||
| 393 | - | fill bool | |||
| 394 | - | } | |||
| 395 | - | ||||
| 396 | - | // size is the staging bytes of an upload: a strip for a fill. | |||
| 397 | - | func (u *upload) size() int { | |||
| 398 | - | if u.fill { | |||
| 399 | - | return u.rect.Dx() * min(u.rect.Dy(), fillRows) * 4 | |||
| 400 | - | } | |||
| 401 | - | return u.rect.Dx() * u.rect.Dy() * 4 | |||
| 402 | - | } | |||
| 403 | - | ||||
| 404 | - | // stage copies the visible pixels of a client buffer into dst, resampling | |||
| 405 | - | // when the buffer is not drawn at its size. | |||
| 406 | - | func (u *upload) stage(dst []byte) { | |||
| 407 | - | for row := 0; row < u.rect.Dy(); row++ { | |||
| 408 | - | line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4] | |||
| 409 | - | y := u.rect.Min.Y + row | |||
| 410 | - | if u.dst.Size() == u.src.Size() { | |||
| 411 | - | start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4 | |||
| 412 | - | copy(line, u.pixels[start:start+len(line)]) | |||
| 413 | - | } else { | |||
| 414 | - | // Nearest-neighbour resample, sampling pixel centres. | |||
| 415 | - | sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2) | |||
| 416 | - | src := u.pixels[sy*u.stride:] | |||
| 417 | - | for i := 0; i < u.rect.Dx(); i++ { | |||
| 418 | - | sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2) | |||
| 419 | - | copy(line[i*4:i*4+4], src[sx*4:sx*4+4]) | |||
| 420 | - | } | |||
| 421 | - | } | |||
| 422 | - | if u.opaque { | |||
| 423 | - | for i := 3; i < len(line); i += 4 { | |||
| 424 | - | line[i] = 255 | |||
| 425 | - | } | |||
| 426 | - | } | |||
| 427 | - | } | |||
| 428 | - | } | |||
| 429 | - | ||||
| 430 | - | 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. | 305 | // Scene rects are logical; everything below works in physical pixels. | ||
| 432 | scale := s.Scale | 306 | scale := s.Scale | ||
| 433 | if scale <= 0 { | 307 | if scale <= 0 { | ||
| @@ -435,21 +309,20 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 435 | } | 309 | } | ||
| 436 | phys := func(v int) int { return int(math.Round(float64(v) * scale)) } | 310 | phys := func(v int) int { return int(math.Round(float64(v) * scale)) } | ||
| 437 | physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) } | 311 | physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) } | ||
| 438 | - | var uploads []upload | 312 | + | var draws []draw |
| 439 | bounds := image.Rect(0, 0, r.width, r.height) | 313 | bounds := image.Rect(0, 0, r.width, r.height) | ||
| 440 | add := func(rect image.Rectangle, c [3]uint8) { | 314 | add := func(rect image.Rectangle, c [3]uint8) { | ||
| 441 | rect = rect.Intersect(bounds) | 315 | rect = rect.Intersect(bounds) | ||
| 442 | if rect.Empty() { | 316 | if rect.Empty() { | ||
| 443 | return | 317 | return | ||
| 444 | } | 318 | } | ||
| 445 | - | // A fill stages a strip of up to fillRows rows; the GPU copies the | 319 | + | draws = append(draws, r.fillDraw(rect, c)) |
| 446 | - | // strip down the rect. | |||
| 447 | - | uploads = append(uploads, upload{rect: rect, color: c, fill: true}) | |||
| 448 | } | 320 | } | ||
| 449 | - | // addContent maps src (buffer pixels) onto dst (physical pixels). | 321 | + | // addContent draws the part of a buffer mapped onto full (physical |
| 450 | - | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | 322 | + | // pixels) that dst shows. key names the surface, seq its content. |
| 323 | + | addContent := func(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) { | |||
| 451 | rect := dst.Intersect(bounds) | 324 | rect := dst.Intersect(bounds) | ||
| 452 | - | if rect.Empty() || src.Empty() { | 325 | + | if rect.Empty() { |
| 453 | return | 326 | return | ||
| 454 | } | 327 | } | ||
| 455 | if content.DMABuf != nil { | 328 | if content.DMABuf != nil { | ||
| @@ -458,7 +331,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 458 | // The window shows its background until a buffer imports. | 331 | // The window shows its background until a buffer imports. | ||
| 459 | return | 332 | return | ||
| 460 | } | 333 | } | ||
| 461 | - | uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im}) | 334 | + | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) |
| 335 | + | dr.set, dr.im = im.set, im | |||
| 336 | + | dr.acquire = content.Acquire | |||
| 337 | + | draws = append(draws, dr) | |||
| 462 | return | 338 | return | ||
| 463 | } | 339 | } | ||
| 464 | b := content.SHM | 340 | b := content.SHM | ||
| @@ -466,11 +342,24 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 466 | if err != nil { | 342 | if err != nil { | ||
| 467 | return | 343 | return | ||
| 468 | } | 344 | } | ||
| 469 | - | uploads = append(uploads, upload{rect: rect, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src}) | 345 | + | st := shmState{w: content.Width, h: content.Height, seq: seq} |
| 346 | + | // Damage history is the root surface's: children copy in full. | |||
| 347 | + | var damage func(uint64) ([]ports.Rect, bool) | |||
| 348 | + | if key.index == 0 { | |||
| 349 | + | damage = content.DamageSince | |||
| 350 | + | } | |||
| 351 | + | c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage) | |||
| 352 | + | if err != nil { | |||
| 353 | + | return | |||
| 354 | + | } | |||
| 355 | + | dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque) | |||
| 356 | + | dr.set = c.set | |||
| 357 | + | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | |||
| 358 | + | draws = append(draws, dr) | |||
| 470 | } | 359 | } | ||
| 471 | // drawSurface draws one surface buffer with its origin at (x, y) | 360 | // drawSurface draws one surface buffer with its origin at (x, y) | ||
| 472 | // logical, clipped to clip (logical). | 361 | // logical, clipped to clip (logical). | ||
| 473 | - | drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle) { | 362 | + | drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64) { |
| 474 | lw, lh := content.LogicalW, content.LogicalH | 363 | lw, lh := content.LogicalW, content.LogicalH | ||
| 475 | if lw <= 0 || lh <= 0 { | 364 | if lw <= 0 || lh <= 0 { | ||
| 476 | lw, lh = content.Width, content.Height | 365 | lw, lh = content.Width, content.Height | ||
| @@ -492,26 +381,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 492 | if dst.Empty() { | 381 | if dst.Empty() { | ||
| 493 | return | 382 | return | ||
| 494 | } | 383 | } | ||
| 495 | - | // The visible part of dst in buffer pixels. | 384 | + | if key.index == 0 { |
| 496 | - | bx := func(v int) int { return (v - full.Min.X) * content.Width / full.Dx() } | 385 | + | dmg.content(key.win, content, full, dst) |
| 497 | - | by := func(v int) int { return (v - full.Min.Y) * content.Height / full.Dy() } | 386 | + | } |
| 498 | - | src := image.Rect(bx(dst.Min.X), by(dst.Min.Y), bx(dst.Max.X), by(dst.Max.Y)) | 387 | + | addContent(dst, full, content, key, seq) |
| 499 | - | addContent(dst, src, content) | |||
| 500 | } | 388 | } | ||
| 501 | // place draws a surface tree with its window geometry at (x, y) | 389 | // place draws a surface tree with its window geometry at (x, y) | ||
| 502 | // logical, clipped to w×h logical: client shadows fall outside. | 390 | // logical, clipped to w×h logical: client shadows fall outside. | ||
| 503 | - | place := func(content *ports.SurfaceContent, x, y, w, h int) { | 391 | + | // Every surface of the tree is keyed by its window and place in it; |
| 392 | + | // the tree's Seq changes with any of them. | |||
| 393 | + | place := func(id ports.WindowID, content *ports.SurfaceContent, x, y, w, h int) { | |||
| 504 | clip := image.Rect(x, y, x+w, y+h) | 394 | clip := image.Rect(x, y, x+w, y+h) | ||
| 505 | ox, oy := x-content.Geometry.X, y-content.Geometry.Y | 395 | ox, oy := x-content.Geometry.X, y-content.Geometry.Y | ||
| 506 | for i := range content.Children { | 396 | for i := range content.Children { | ||
| 507 | if ch := &content.Children[i]; ch.Below { | 397 | if ch := &content.Children[i]; ch.Below { | ||
| 508 | - | drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip) | 398 | + | drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq) |
| 509 | } | 399 | } | ||
| 510 | } | 400 | } | ||
| 511 | - | drawSurface(content, ox, oy, clip) | 401 | + | drawSurface(content, ox, oy, clip, shmKey{id, 0}, content.Seq) |
| 512 | for i := range content.Children { | 402 | for i := range content.Children { | ||
| 513 | if ch := &content.Children[i]; !ch.Below { | 403 | if ch := &content.Children[i]; !ch.Below { | ||
| 514 | - | drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip) | 404 | + | drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq) |
| 515 | } | 405 | } | ||
| 516 | } | 406 | } | ||
| 517 | } | 407 | } | ||
| @@ -527,7 +417,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 527 | continue | 417 | continue | ||
| 528 | } | 418 | } | ||
| 529 | content := contents[layer.ID] | 419 | content := contents[layer.ID] | ||
| 530 | - | place(&content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H) | 420 | + | dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)) |
| 421 | + | place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H) | |||
| 531 | } | 422 | } | ||
| 532 | } | 423 | } | ||
| 533 | addLayers(false) | 424 | addLayers(false) | ||
| @@ -547,11 +438,12 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 547 | cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b | 438 | cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b | ||
| 548 | body := physRect(cx, cy, cw, ch) | 439 | body := physRect(cx, cy, cw, ch) | ||
| 549 | content := contents[w.ID] | 440 | content := contents[w.ID] | ||
| 441 | + | dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H)) | |||
| 550 | if content.Empty() { | 442 | if content.Empty() { | ||
| 551 | add(body, windowColor(w.ID)) | 443 | add(body, windowColor(w.ID)) | ||
| 552 | } else { | 444 | } else { | ||
| 553 | add(body, parseColor(s.Background)) | 445 | add(body, parseColor(s.Background)) | ||
| 554 | - | place(&content, cx, cy, cw, ch) | 446 | + | place(w.ID, &content, cx, cy, cw, ch) |
| 555 | } | 447 | } | ||
| 556 | borderColor := s.Border.Inactive | 448 | borderColor := s.Border.Inactive | ||
| 557 | if w.Focused { | 449 | if w.Focused { | ||
| @@ -576,225 +468,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 576 | for _, w := range s.Windows { | 468 | for _, w := range s.Windows { | ||
| 577 | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | 469 | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | ||
| 578 | content := contents[w.ID] | 470 | content := contents[w.ID] | ||
| 579 | - | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | 471 | + | dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)) |
| 472 | + | place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | |||
| 580 | } | 473 | } | ||
| 581 | } | 474 | } | ||
| 582 | } | 475 | } | ||
| 583 | addPopups(false) | 476 | addPopups(false) | ||
| 584 | addLayers(true) | 477 | addLayers(true) | ||
| 585 | addPopups(true) | 478 | addPopups(true) | ||
| 586 | - | // Blended pixels are staged first, so the shaders bind only them, | 479 | + | return draws |
| 587 | - | // within the device's storage buffer range; past it they are copied. | |||
| 588 | - | // The draw table follows them, then the other uploads. | |||
| 589 | - | blends, used := 0, 0 | |||
| 590 | - | for k := range uploads { | |||
| 591 | - | u := &uploads[k] | |||
| 592 | - | if u.blend && used+u.size() > r.maxRange { | |||
| 593 | - | u.blend = false | |||
| 594 | - | } | |||
| 595 | - | if u.blend { | |||
| 596 | - | u.offset = used | |||
| 597 | - | used += u.size() | |||
| 598 | - | blends++ | |||
| 599 | - | } | |||
| 600 | - | } | |||
| 601 | - | pixelBytes := used | |||
| 602 | - | tableOffset := (used + r.align - 1) / r.align * r.align | |||
| 603 | - | used = tableOffset + blends*drawSize | |||
| 604 | - | for k := range uploads { | |||
| 605 | - | if u := &uploads[k]; !u.blend && u.dma == nil { | |||
| 606 | - | u.offset = used | |||
| 607 | - | used += u.size() | |||
| 608 | - | } | |||
| 609 | - | } | |||
| 610 | - | if err := r.ensureStaging(used); err != nil { | |||
| 611 | - | return err | |||
| 612 | - | } | |||
| 613 | - | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | |||
| 614 | - | if blends > 0 { | |||
| 615 | - | r.bindStaging(pixelBytes, tableOffset, blends) | |||
| 616 | - | i := 0 | |||
| 617 | - | for k := range uploads { | |||
| 618 | - | if uploads[k].blend { | |||
| 619 | - | r.putDraw(data[tableOffset:], i, &uploads[k]) | |||
| 620 | - | i++ | |||
| 621 | - | } | |||
| 622 | - | } | |||
| 623 | - | } | |||
| 624 | - | for _, u := range uploads { | |||
| 625 | - | if u.dma != nil { | |||
| 626 | - | continue | |||
| 627 | - | } | |||
| 628 | - | if u.fill { | |||
| 629 | - | line := data[u.offset : u.offset+u.rect.Dx()*min(u.rect.Dy(), fillRows)*4] | |||
| 630 | - | for i := 0; i < len(line); i += 4 { | |||
| 631 | - | line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255 | |||
| 632 | - | } | |||
| 633 | - | continue | |||
| 634 | - | } | |||
| 635 | - | dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4] | |||
| 636 | - | // The client may shrink its pool under the mapping: its window | |||
| 637 | - | // then shows garbage for this frame, never a crash. | |||
| 638 | - | copyGuarded(func() { u.stage(dst) }) | |||
| 639 | - | } | |||
| 640 | - | rgb := parseColor(s.Background) | |||
| 641 | - | d := r.dd | |||
| 642 | - | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | |||
| 643 | - | return err | |||
| 644 | - | } | |||
| 645 | - | begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo} | |||
| 646 | - | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil { | |||
| 647 | - | return err | |||
| 648 | - | } | |||
| 649 | - | rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | |||
| 650 | - | tg := r.target() | |||
| 651 | - | srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit) | |||
| 652 | - | srcAccess := vk.AccessFlags(0) | |||
| 653 | - | if tg.layout == vk.ImageLayoutTransferSrcOptimal { | |||
| 654 | - | srcStage = vk.PipelineStageTransferBit | |||
| 655 | - | srcAccess = vk.AccessTransferReadBit | |||
| 656 | - | } | |||
| 657 | - | // The whole target is cleared: its old contents never matter. | |||
| 658 | - | barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: tg.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: tg.image, SubresourceRange: rangeInfo} | |||
| 659 | - | if tg.exported { | |||
| 660 | - | // Acquire from the display (foreign queue family). | |||
| 661 | - | barrier.OldLayout = vk.ImageLayoutUndefined | |||
| 662 | - | barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | |||
| 663 | - | srcStage, barrier.SrcAccessMask = vk.PipelineStageTopOfPipeBit, 0 | |||
| 664 | - | } | |||
| 665 | - | d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 666 | - | barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0 | |||
| 667 | - | // VkClearColorValue is a union; UNORM formats read it as float32. | |||
| 668 | - | unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) } | |||
| 669 | - | color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)} | |||
| 670 | - | d.CmdClearColorImage(r.command, tg.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo) | |||
| 671 | - | barrier.SrcAccessMask = vk.AccessTransferWriteBit | |||
| 672 | - | barrier.DstAccessMask = vk.AccessTransferWriteBit | |||
| 673 | - | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |||
| 674 | - | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | |||
| 675 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 676 | - | // Client buffers come from the foreign queue family (their driver) and | |||
| 677 | - | // go back to it after the frame, so the next frame acquires them again. | |||
| 678 | - | var dmas []*imported | |||
| 679 | - | for _, u := range uploads { | |||
| 680 | - | if u.dma != nil && !slices.Contains(dmas, u.dma) { | |||
| 681 | - | dmas = append(dmas, u.dma) | |||
| 682 | - | } | |||
| 683 | - | } | |||
| 684 | - | ownership := func(acquire bool) { | |||
| 685 | - | for _, im := range dmas { | |||
| 686 | - | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: rangeInfo} | |||
| 687 | - | if acquire { | |||
| 688 | - | // GENERAL, never UNDEFINED: the client's contents must survive. | |||
| 689 | - | b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutTransferSrcOptimal | |||
| 690 | - | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | |||
| 691 | - | b.DstAccessMask = vk.AccessTransferReadBit | |||
| 692 | - | } else { | |||
| 693 | - | b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral | |||
| 694 | - | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | |||
| 695 | - | b.SrcAccessMask = vk.AccessTransferReadBit | |||
| 696 | - | } | |||
| 697 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 698 | - | } | |||
| 699 | - | } | |||
| 700 | - | ownership(true) | |||
| 701 | - | drawn := 0 // blended uploads drawn so far: their index in the draw table | |||
| 702 | - | for i := 0; i < len(uploads); i++ { | |||
| 703 | - | f := uploads[i] | |||
| 704 | - | if i > 0 && !f.blend { | |||
| 705 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 706 | - | } | |||
| 707 | - | if f.blend { | |||
| 708 | - | // Consecutive blended uploads share one draw: blending follows | |||
| 709 | - | // primitive order within it. | |||
| 710 | - | n := 1 | |||
| 711 | - | for i+n < len(uploads) && uploads[i+n].blend { | |||
| 712 | - | n++ | |||
| 713 | - | } | |||
| 714 | - | r.drawBlended(tg.image, tg.view, drawn, drawn+n) | |||
| 715 | - | drawn += n | |||
| 716 | - | i += n - 1 | |||
| 717 | - | continue | |||
| 718 | - | } | |||
| 719 | - | rect := f.rect | |||
| 720 | - | if f.dma != nil { | |||
| 721 | - | // Map the visible part of dst back onto the buffer. | |||
| 722 | - | sx := func(x int) int32 { return int32(f.src.Min.X + (x-f.dst.Min.X)*f.src.Dx()/f.dst.Dx()) } | |||
| 723 | - | sy := func(y int) int32 { return int32(f.src.Min.Y + (y-f.dst.Min.Y)*f.src.Dy()/f.dst.Dy()) } | |||
| 724 | - | layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1} | |||
| 725 | - | blit := vk.ImageBlit{ | |||
| 726 | - | SrcSubresource: layers, | |||
| 727 | - | SrcOffsets: [2]vk.Offset3D{{X: sx(rect.Min.X), Y: sy(rect.Min.Y)}, {X: sx(rect.Max.X), Y: sy(rect.Max.Y), Z: 1}}, | |||
| 728 | - | DstSubresource: layers, | |||
| 729 | - | DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}}, | |||
| 730 | - | } | |||
| 731 | - | d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest) | |||
| 732 | - | continue | |||
| 733 | - | } | |||
| 734 | - | layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1} | |||
| 735 | - | if f.fill { | |||
| 736 | - | // Every band reads the same staged strip. | |||
| 737 | - | r.fillRegions = r.fillRegions[:0] | |||
| 738 | - | for y := rect.Min.Y; y < rect.Max.Y; y += fillRows { | |||
| 739 | - | h := min(fillRows, rect.Max.Y-y) | |||
| 740 | - | r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}}) | |||
| 741 | - | } | |||
| 742 | - | d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0]) | |||
| 743 | - | continue | |||
| 744 | - | } | |||
| 745 | - | region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}} | |||
| 746 | - | d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |||
| 747 | - | } | |||
| 748 | - | ownership(false) | |||
| 749 | - | barrier.SrcAccessMask = vk.AccessTransferWriteBit | |||
| 750 | - | barrier.DstAccessMask = vk.AccessTransferReadBit | |||
| 751 | - | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |||
| 752 | - | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | |||
| 753 | - | if tg.exported { | |||
| 754 | - | // Release to the display: KMS scans it out after the fence. | |||
| 755 | - | barrier.NewLayout = vk.ImageLayoutGeneral | |||
| 756 | - | barrier.DstAccessMask = 0 | |||
| 757 | - | barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | |||
| 758 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 759 | - | } else { | |||
| 760 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 761 | - | } | |||
| 762 | - | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil { | |||
| 763 | - | return err | |||
| 764 | - | } | |||
| 765 | - | // Implicit sync: wait for the client's GPU writes to each buffer. | |||
| 766 | - | var waits []vk.Semaphore | |||
| 767 | - | for _, im := range dmas { | |||
| 768 | - | if sem := r.readFence(im); sem != 0 { | |||
| 769 | - | waits = append(waits, sem) | |||
| 770 | - | } | |||
| 771 | - | } | |||
| 772 | - | defer func() { | |||
| 773 | - | for _, sem := range waits { | |||
| 774 | - | d.DestroySemaphore(r.device, sem, nil) | |||
| 775 | - | } | |||
| 776 | - | }() | |||
| 777 | - | submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command} | |||
| 778 | - | stages := make([]vk.PipelineStageFlags, len(waits)) | |||
| 779 | - | if len(waits) > 0 { | |||
| 780 | - | for i := range stages { | |||
| 781 | - | stages[i] = vk.PipelineStageTransferBit | |||
| 782 | - | } | |||
| 783 | - | submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0] | |||
| 784 | - | } | |||
| 785 | - | if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil { | |||
| 786 | - | return err | |||
| 787 | - | } | |||
| 788 | - | runtime.KeepAlive(stages) | |||
| 789 | - | tg.layout = barrier.NewLayout | |||
| 790 | - | r.last, r.readBack = tg, false | |||
| 791 | - | // The fence wait is the GPU completion ADR 014 requires before a flip. | |||
| 792 | - | if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil { | |||
| 793 | - | return err | |||
| 794 | - | } | |||
| 795 | - | r.dropUnused() | |||
| 796 | - | r.dropPools() | |||
| 797 | - | return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence)) | |||
| 798 | } | 480 | } | ||
| 799 | 481 | ||||
| 800 | func (r *Renderer) Pixels() *image.RGBA { | 482 | func (r *Renderer) Pixels() *image.RGBA { | ||
| @@ -810,71 +492,50 @@ func (r *Renderer) Pixels() *image.RGBA { | |||||
| 810 | return out | 492 | return out | ||
| 811 | } | 493 | } | ||
| 812 | 494 | ||||
| 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. | 495 | // Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE. | ||
| 827 | const ( | 496 | const ( | ||
| 828 | queueFamilyIgnored = ^uint32(0) | 497 | queueFamilyIgnored = ^uint32(0) | ||
| 829 | wholeSize = ^vk.DeviceSize(0) | 498 | wholeSize = ^vk.DeviceSize(0) | ||
| 830 | ) | 499 | ) | ||
| 831 | 500 | ||||
| 832 | - | // readback copies the last frame into the host buffer, once per frame. | 501 | + | // readback copies the last frame into the host buffer, once per frame |
| 502 | + | // (screenshots only). | |||
| 833 | func (r *Renderer) readback() error { | 503 | func (r *Renderer) readback() error { | ||
| 834 | if r.readBack || r.last == nil || r.last.image == 0 { | 504 | if r.readBack || r.last == nil || r.last.image == 0 { | ||
| 835 | return nil | 505 | return nil | ||
| 836 | } | 506 | } | ||
| 837 | - | d := r.dd | 507 | + | // The copy reads the frame: wait for it, and keep the ring ordered. |
| 838 | - | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | 508 | + | if err := r.waitFrame(r.submitted); err != nil { |
| 839 | - | return err | |||
| 840 | - | } | |||
| 841 | - | begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo} | |||
| 842 | - | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil { | |||
| 843 | return err | 509 | return err | ||
| 844 | } | 510 | } | ||
| 845 | t := r.last | 511 | t := r.last | ||
| 846 | - | rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | 512 | + | err := r.oneShot(func(cmd vk.CommandBuffer) { |
| 847 | - | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo} | 513 | + | d := r.dd |
| 848 | - | if t.exported { | 514 | + | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: t.image, SubresourceRange: colorRange} |
| 849 | - | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | 515 | + | if t.exported { |
| 850 | - | b.SrcAccessMask = 0 | 516 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family |
| 851 | - | } | 517 | + | } |
| 852 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) | 518 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) |
| 853 | - | region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}} | 519 | + | region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}} |
| 854 | - | d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, ®ion) | 520 | + | d.CmdCopyImageToBuffer(cmd, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, ®ion) |
| 855 | - | // Make the copy visible to host reads of the mapped buffer. | 521 | + | // Make the copy visible to host reads of the mapped buffer. |
| 856 | - | hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize} | 522 | + | hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize} |
| 857 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil) | 523 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil) |
| 858 | - | if t.exported { | 524 | + | if t.exported { |
| 859 | - | b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral | 525 | + | b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral |
| 860 | - | b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0 | 526 | + | b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0 |
| 861 | - | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | 527 | + | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT |
| 862 | - | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b) | 528 | + | d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b) |
| 863 | - | } else { | 529 | + | } |
| 864 | - | t.layout = vk.ImageLayoutTransferSrcOptimal | 530 | + | }) |
| 865 | - | } | 531 | + | if err != nil { |
| 866 | - | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil { | |||
| 867 | - | return err | |||
| 868 | - | } | |||
| 869 | - | submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command} | |||
| 870 | - | if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil { | |||
| 871 | return err | 532 | return err | ||
| 872 | } | 533 | } | ||
| 873 | - | if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil { | 534 | + | if !t.exported { |
| 874 | - | return err | 535 | + | t.layout = vk.ImageLayoutTransferSrcOptimal |
| 875 | } | 536 | } | ||
| 876 | r.readBack = true | 537 | r.readBack = true | ||
| 877 | - | return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence)) | 538 | + | return nil |
| 878 | } | 539 | } | ||
| 879 | 540 | ||||
| 880 | func (r *Renderer) Close() { | 541 | func (r *Renderer) Close() { | ||
| @@ -885,38 +546,22 @@ func (r *Renderer) Close() { | |||||
| 885 | d := r.dd | 546 | d := r.dd | ||
| 886 | if r.device != 0 { | 547 | if r.device != 0 { | ||
| 887 | _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device)) | 548 | _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device)) | ||
| 549 | + | r.idle() | |||
| 888 | } | 550 | } | ||
| 889 | r.dropTargets() | 551 | r.dropTargets() | ||
| 552 | + | r.dropCursors() | |||
| 890 | for id, im := range r.imports { | 553 | for id, im := range r.imports { | ||
| 891 | r.release(im) | 554 | r.release(im) | ||
| 892 | delete(r.imports, id) | 555 | delete(r.imports, id) | ||
| 893 | } | 556 | } | ||
| 894 | r.frame += importTTL + 1 | 557 | r.frame += importTTL + 1 | ||
| 895 | r.dropPools() | 558 | r.dropPools() | ||
| 896 | - | if r.fence != 0 { | 559 | + | r.dropShm() |
| 897 | - | d.DestroyFence(r.device, r.fence, nil) | 560 | + | r.destroySlots() |
| 898 | - | r.fence = 0 | |||
| 899 | - | } | |||
| 900 | - | if r.command != 0 { | |||
| 901 | - | d.FreeCommandBuffers(r.device, r.pool, 1, &r.command) | |||
| 902 | - | r.command = 0 | |||
| 903 | - | } | |||
| 904 | if r.pool != 0 { | 561 | if r.pool != 0 { | ||
| 905 | d.DestroyCommandPool(r.device, r.pool, nil) | 562 | d.DestroyCommandPool(r.device, r.pool, nil) | ||
| 906 | r.pool = 0 | 563 | r.pool = 0 | ||
| 907 | } | 564 | } | ||
| 908 | - | if r.stagingMapped != nil { | |||
| 909 | - | d.UnmapMemory(r.device, r.stagingMemory) | |||
| 910 | - | r.stagingMapped = nil | |||
| 911 | - | } | |||
| 912 | - | if r.staging != 0 { | |||
| 913 | - | d.DestroyBuffer(r.device, r.staging, nil) | |||
| 914 | - | r.staging = 0 | |||
| 915 | - | } | |||
| 916 | - | if r.stagingMemory != 0 { | |||
| 917 | - | d.FreeMemory(r.device, r.stagingMemory, nil) | |||
| 918 | - | r.stagingMemory = 0 | |||
| 919 | - | } | |||
| 920 | if r.mapped != nil { | 565 | if r.mapped != nil { | ||
| 921 | d.UnmapMemory(r.device, r.bufferMemory) | 566 | d.UnmapMemory(r.device, r.bufferMemory) | ||
| 922 | r.mapped = nil | 567 | r.mapped = nil | ||
| @@ -929,7 +574,7 @@ func (r *Renderer) Close() { | |||||
| 929 | d.FreeMemory(r.device, r.bufferMemory, nil) | 574 | d.FreeMemory(r.device, r.bufferMemory, nil) | ||
| 930 | r.bufferMemory = 0 | 575 | r.bufferMemory = 0 | ||
| 931 | } | 576 | } | ||
| 932 | - | r.destroyBlend() | 577 | + | r.destroyComposer() |
| 933 | r.freeTarget(&r.own) | 578 | r.freeTarget(&r.own) | ||
| 934 | d.DestroyDevice(r.device, nil) | 579 | d.DestroyDevice(r.device, nil) | ||
| 935 | r.device = 0 | 580 | r.device = 0 | ||
M internal/adapters/vulkan/renderer_test.go +14 -15
| @@ -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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 := render(r, 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 { | ||
| @@ -335,17 +335,16 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) { | |||||
| 335 | } | 335 | } | ||
| 336 | } | 336 | } | ||
| 337 | 337 | ||||
| 338 | - | // A fill taller than one staged strip is copied down in bands: every row | 338 | + | // A tall fill covers every row through the last one. |
| 339 | - | // through the last one gets the color. | |||
| 340 | func TestRendererTallFill(t *testing.T) { | 339 | func TestRendererTallFill(t *testing.T) { | ||
| 341 | - | h := fillRows*2 + 7 | 340 | + | h := 135 |
| 342 | r, err := New(8, h) | 341 | r, err := New(8, h) | ||
| 343 | if err != nil { | 342 | if err != nil { | ||
| 344 | t.Skipf("Vulkan unavailable: %v", err) | 343 | t.Skipf("Vulkan unavailable: %v", err) | ||
| 345 | } | 344 | } | ||
| 346 | defer r.Close() | 345 | 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}}}} | 346 | 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 { | 347 | + | if err := render(r, scene, nil); err != nil { |
| 349 | t.Fatal(err) | 348 | t.Fatal(err) | ||
| 350 | } | 349 | } | ||
| 351 | c := windowColor(1) | 350 | c := windowColor(1) | ||
| @@ -403,7 +402,7 @@ func TestRendererAlphaBlend(t *testing.T) { | |||||
| 403 | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, | 402 | {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}}, | 403 | {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}}, | ||
| 405 | }} | 404 | }} | ||
| 406 | - | if err := r.Render(scene, contents); err != nil { | 405 | + | if err := render(r, scene, contents); err != nil { |
| 407 | t.Fatal(err) | 406 | t.Fatal(err) | ||
| 408 | } | 407 | } | ||
| 409 | px := r.Pixels() | 408 | px := r.Pixels() | ||
D internal/adapters/vulkan/shaders/blend.frag +0 -22
| @@ -1,22 +0,0 @@ | |||||
| 1 | - | #version 450 | |||
| 2 | - | // Reads the staged B8G8R8A8 pixel under the fragment: premultiplied, as | |||
| 3 | - | // Wayland ARGB buffers are, so the pipeline blends ONE, ONE_MINUS_SRC_ALPHA. | |||
| 4 | - | ||||
| 5 | - | struct Draw { | |||
| 6 | - | ivec4 rect; | |||
| 7 | - | uvec4 pixels; | |||
| 8 | - | }; | |||
| 9 | - | ||||
| 10 | - | layout(std430, set = 0, binding = 0) readonly buffer Pixels { uint pixels[]; }; | |||
| 11 | - | layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; }; | |||
| 12 | - | ||||
| 13 | - | layout(location = 0) flat in uint draw; | |||
| 14 | - | layout(location = 0) out vec4 color; | |||
| 15 | - | ||||
| 16 | - | void main() { | |||
| 17 | - | Draw d = draws[draw]; | |||
| 18 | - | ivec2 p = ivec2(gl_FragCoord.xy) - d.rect.xy; | |||
| 19 | - | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | |||
| 20 | - | vec4 bgra = unpackUnorm4x8(pixels[d.pixels.x + uint(p.y) * d.pixels.y + uint(p.x)]); | |||
| 21 | - | color = bgra.zyxw; | |||
| 22 | - | } | |||
D internal/adapters/vulkan/shaders/blend.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
D internal/adapters/vulkan/shaders/blend.vert +0 -21
| @@ -1,21 +0,0 @@ | |||||
| 1 | - | #version 450 | |||
| 2 | - | // One quad per instance: the instance indexes the draw table. | |||
| 3 | - | ||||
| 4 | - | struct Draw { | |||
| 5 | - | ivec4 rect; // x0, y0, x1, y1 in target pixels | |||
| 6 | - | uvec4 pixels; // first pixel word in staging, row width, target w, target h | |||
| 7 | - | }; | |||
| 8 | - | ||||
| 9 | - | layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; }; | |||
| 10 | - | ||||
| 11 | - | layout(location = 0) flat out uint draw; | |||
| 12 | - | ||||
| 13 | - | void main() { | |||
| 14 | - | Draw d = draws[gl_InstanceIndex]; | |||
| 15 | - | // Two triangles: 0,1,2 and 2,1,3 of the corners. | |||
| 16 | - | const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1)); | |||
| 17 | - | ivec2 c = corners[gl_VertexIndex]; | |||
| 18 | - | vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w); | |||
| 19 | - | gl_Position = vec4(p / vec2(d.pixels.zw) * 2.0 - 1.0, 0.0, 1.0); | |||
| 20 | - | draw = uint(gl_InstanceIndex); | |||
| 21 | - | } | |||
D internal/adapters/vulkan/shaders/blend.vert.spv +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/vulkan/shaders/compose.frag +55 -0
| @@ -0,0 +1,55 @@ | |||||
| 1 | + | #version 450 | |||
| 2 | + | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | |||
| 3 | + | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | |||
| 4 | + | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | |||
| 5 | + | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | |||
| 6 | + | // texel under the fragment, pixel exact. | |||
| 7 | + | ||||
| 8 | + | layout(push_constant) uniform Draw { | |||
| 9 | + | ivec4 rect; | |||
| 10 | + | vec4 map; | |||
| 11 | + | vec4 color; | |||
| 12 | + | uvec4 buf; | |||
| 13 | + | uvec4 misc; | |||
| 14 | + | } d; | |||
| 15 | + | ||||
| 16 | + | layout(set = 0, binding = 0) uniform sampler2D tex; | |||
| 17 | + | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | |||
| 18 | + | ||||
| 19 | + | layout(location = 0) out vec4 color; | |||
| 20 | + | ||||
| 21 | + | const uint modeSolid = 0u; | |||
| 22 | + | const uint modeImage = 1u; | |||
| 23 | + | const uint flagOpaque = 1u; | |||
| 24 | + | const uint flagExact = 2u; | |||
| 25 | + | ||||
| 26 | + | // texel reads buffer pixel p, clamped to the buffer (clamp to edge). | |||
| 27 | + | vec4 texel(ivec2 p) { | |||
| 28 | + | p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1); | |||
| 29 | + | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | |||
| 30 | + | return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw; | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | void main() { | |||
| 34 | + | vec2 src = d.map.xy + gl_FragCoord.xy * d.map.zw; | |||
| 35 | + | bool exact = (d.misc.y & flagExact) != 0u; | |||
| 36 | + | vec4 c; | |||
| 37 | + | if (d.misc.x == modeSolid) { | |||
| 38 | + | c = d.color; | |||
| 39 | + | } else if (d.misc.x == modeImage) { | |||
| 40 | + | ivec2 size = textureSize(tex, 0); | |||
| 41 | + | c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size)); | |||
| 42 | + | } else if (exact) { | |||
| 43 | + | c = texel(ivec2(floor(src))); | |||
| 44 | + | } else { | |||
| 45 | + | // Bilinear: four texel reads around the sample point. | |||
| 46 | + | vec2 p = src - 0.5; | |||
| 47 | + | ivec2 i = ivec2(floor(p)); | |||
| 48 | + | vec2 f = p - vec2(i); | |||
| 49 | + | c = mix(mix(texel(i), texel(i + ivec2(1, 0)), f.x), mix(texel(i + ivec2(0, 1)), texel(i + ivec2(1, 1)), f.x), f.y); | |||
| 50 | + | } | |||
| 51 | + | if ((d.misc.y & flagOpaque) != 0u) { | |||
| 52 | + | c.a = 1.0; | |||
| 53 | + | } | |||
| 54 | + | color = c; | |||
| 55 | + | } | |||
A internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/vulkan/shaders/compose.vert +17 -0
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | #version 450 | |||
| 2 | + | // One quad per draw: two triangles over the visible target rect. | |||
| 3 | + | ||||
| 4 | + | layout(push_constant) uniform Draw { | |||
| 5 | + | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | |||
| 6 | + | vec4 map; // source texel = map.xy + fragment centre * map.zw | |||
| 7 | + | vec4 color; // solid fills: premultiplied RGBA | |||
| 8 | + | uvec4 buf; // buffer draws: first word, row width, height, unused | |||
| 9 | + | uvec4 misc; // mode, flags, target width, target height | |||
| 10 | + | } d; | |||
| 11 | + | ||||
| 12 | + | void main() { | |||
| 13 | + | const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1)); | |||
| 14 | + | ivec2 c = corners[gl_VertexIndex]; | |||
| 15 | + | vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w); | |||
| 16 | + | gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0); | |||
| 17 | + | } | |||
A internal/adapters/vulkan/shaders/compose.vert.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shm.go +198 -7
| @@ -3,15 +3,20 @@ package vulkan | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "runtime/debug" | 5 | "runtime/debug" | ||
| 6 | + | "unsafe" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| 9 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 8 | "golang.org/x/sys/unix" | 10 | "golang.org/x/sys/unix" | ||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| 11 | - | // wl_shm buffers are read in place: each client pool is mapped once, read | 13 | + | // wl_shm buffers: each client pool is mapped once, read only. A surface's |
| 12 | - | // only, and the visible pixels are copied from it into staging when a frame | 14 | + | // pixels are copied once per new content into a persistently mapped, |
| 13 | - | // is drawn. Mappings follow the dmabuf import rules: kept while drawn, | 15 | + | // host-visible GPU buffer (device-local when the GPU exposes it, ReBAR) |
| 14 | - | // dropped after importTTL frames without use. | 16 | + | // that the fragment shader reads directly: one memcpy per row, no staging |
| 17 | + | // copy, no resample, no alpha fix-up on the CPU. Each surface has two GPU | |||
| 18 | + | // buffers used in turn, so the CPU never writes one a frame in flight | |||
| 19 | + | // reads. Mappings and buffers are dropped after importTTL frames unused. | |||
| 15 | 20 | ||||
| 16 | // mapping is a client pool mapped read-only. | 21 | // mapping is a client pool mapped read-only. | ||
| 17 | type mapping struct { | 22 | type mapping struct { | ||
| @@ -27,8 +32,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) { | |||||
| 27 | m.last = r.frame | 32 | m.last = r.frame | ||
| 28 | return m.data, nil | 33 | return m.data, nil | ||
| 29 | } | 34 | } | ||
| 30 | - | // Uploads collected earlier this frame may still slice the old | 35 | + | // Draws collected earlier this frame may still read the old |
| 31 | - | // mapping: it is unmapped once staging is done. | 36 | + | // mapping: it is unmapped once the frame is recorded. |
| 32 | r.retired = append(r.retired, m.data) | 37 | r.retired = append(r.retired, m.data) | ||
| 33 | delete(r.pools, b.Pool) | 38 | delete(r.pools, b.Pool) | ||
| 34 | } | 39 | } | ||
| @@ -53,7 +58,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) { | |||||
| 53 | } | 58 | } | ||
| 54 | 59 | ||||
| 55 | // dropPools unmaps replaced mappings and pools not drawn for importTTL | 60 | // dropPools unmaps replaced mappings and pools not drawn for importTTL | ||
| 56 | - | // frames. | 61 | + | // frames. The GPU never reads a mapping: this runs once the CPU copies of |
| 62 | + | // a frame are done. | |||
| 57 | func (r *Renderer) dropPools() { | 63 | func (r *Renderer) dropPools() { | ||
| 58 | for _, data := range r.retired { | 64 | for _, data := range r.retired { | ||
| 59 | _ = unix.Munmap(data) | 65 | _ = unix.Munmap(data) | ||
| @@ -80,3 +86,188 @@ func copyGuarded(copy func()) (ok bool) { | |||||
| 80 | copy() | 86 | copy() | ||
| 81 | return true | 87 | return true | ||
| 82 | } | 88 | } | ||
| 89 | + | ||||
| 90 | + | // gpuBuffer is a host-visible storage buffer, mapped for its whole life. | |||
| 91 | + | type gpuBuffer struct { | |||
| 92 | + | buffer vk.Buffer | |||
| 93 | + | memory vk.DeviceMemory | |||
| 94 | + | mapped unsafe.Pointer | |||
| 95 | + | size int | |||
| 96 | + | // last is the frame that last read it: it is written or freed only | |||
| 97 | + | // once that frame completed. | |||
| 98 | + | last uint64 | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | func (r *Renderer) newGPUBuffer(b *gpuBuffer, size int) error { | |||
| 102 | + | d := r.dd | |||
| 103 | + | ok := false | |||
| 104 | + | defer func() { | |||
| 105 | + | if !ok { | |||
| 106 | + | r.freeGPUBuffer(b) | |||
| 107 | + | } | |||
| 108 | + | }() | |||
| 109 | + | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(size), Usage: vk.BufferUsageStorageBufferBit, SharingMode: vk.SharingModeExclusive} | |||
| 110 | + | if err := checked("vkCreateBuffer(shm)", d.CreateBuffer(r.device, &info, nil, &b.buffer)); err != nil { | |||
| 111 | + | return err | |||
| 112 | + | } | |||
| 113 | + | var req vk.MemoryRequirements | |||
| 114 | + | d.GetBufferMemoryRequirements(r.device, b.buffer, &req) | |||
| 115 | + | host := vk.MemoryPropertyFlags(vk.MemoryPropertyHostVisibleBit | vk.MemoryPropertyHostCoherentBit) | |||
| 116 | + | kind, err := r.findMemoryType(req.MemoryTypeBits, host|vk.MemoryPropertyDeviceLocalBit) | |||
| 117 | + | if err != nil { | |||
| 118 | + | kind, err = r.findMemoryType(req.MemoryTypeBits, host) | |||
| 119 | + | } | |||
| 120 | + | if err != nil { | |||
| 121 | + | return err | |||
| 122 | + | } | |||
| 123 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 124 | + | if err := checked("vkAllocateMemory(shm)", d.AllocateMemory(r.device, &alloc, nil, &b.memory)); err != nil { | |||
| 125 | + | return err | |||
| 126 | + | } | |||
| 127 | + | if err := checked("vkBindBufferMemory(shm)", d.BindBufferMemory(r.device, b.buffer, b.memory, 0)); err != nil { | |||
| 128 | + | return err | |||
| 129 | + | } | |||
| 130 | + | if err := checked("vkMapMemory(shm)", d.MapMemory(r.device, b.memory, 0, vk.DeviceSize(size), 0, &b.mapped)); err != nil { | |||
| 131 | + | return err | |||
| 132 | + | } | |||
| 133 | + | b.size, ok = size, true | |||
| 134 | + | return nil | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func (r *Renderer) freeGPUBuffer(b *gpuBuffer) { | |||
| 138 | + | d := r.dd | |||
| 139 | + | if b.mapped != nil { | |||
| 140 | + | d.UnmapMemory(r.device, b.memory) | |||
| 141 | + | } | |||
| 142 | + | if b.buffer != 0 { | |||
| 143 | + | d.DestroyBuffer(r.device, b.buffer, nil) | |||
| 144 | + | } | |||
| 145 | + | if b.memory != 0 { | |||
| 146 | + | d.FreeMemory(r.device, b.memory, nil) | |||
| 147 | + | } | |||
| 148 | + | *b = gpuBuffer{} | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // shmKey is a surface of a window: index 0 is the root, i+1 its child i. | |||
| 152 | + | type shmKey struct { | |||
| 153 | + | win ports.WindowID | |||
| 154 | + | index int | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // shmState is what a GPU buffer holds: a w×h client buffer at a content | |||
| 158 | + | // Seq. | |||
| 159 | + | type shmState struct { | |||
| 160 | + | w, h int | |||
| 161 | + | seq uint64 | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // shmCopy is one of a surface's two GPU buffers and the set binding it. | |||
| 165 | + | type shmCopy struct { | |||
| 166 | + | gpu gpuBuffer | |||
| 167 | + | pool vk.DescriptorPool | |||
| 168 | + | set vk.DescriptorSet | |||
| 169 | + | holds shmState | |||
| 170 | + | valid bool | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | // shmSurface is the GPU copy of one surface's wl_shm content. | |||
| 174 | + | type shmSurface struct { | |||
| 175 | + | bufs [2]*shmCopy | |||
| 176 | + | last uint64 // frame that drew it | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // shmCopyFor returns the GPU buffer holding a surface's content st, copying | |||
| 180 | + | // the client pixels (stride bytes per row from offset) into one when no | |||
| 181 | + | // buffer holds them yet. A buffer holding an older content of the same | |||
| 182 | + | // size gets only what changed since (damage, in buffer pixels; false: | |||
| 183 | + | // everything). | |||
| 184 | + | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |||
| 185 | + | s := r.shm[key] | |||
| 186 | + | if s == nil { | |||
| 187 | + | s = &shmSurface{} | |||
| 188 | + | r.shm[key] = s | |||
| 189 | + | } | |||
| 190 | + | s.last = r.frame | |||
| 191 | + | for _, c := range s.bufs { | |||
| 192 | + | if c != nil && c.valid && c.holds == st { | |||
| 193 | + | c.gpu.last = r.frame | |||
| 194 | + | return c, nil | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | // The older buffer takes the new content. | |||
| 198 | + | i := 0 | |||
| 199 | + | if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) { | |||
| 200 | + | i = 1 | |||
| 201 | + | } | |||
| 202 | + | size := st.w * st.h * 4 | |||
| 203 | + | if size > r.maxRange { | |||
| 204 | + | return nil, fmt.Errorf("shm buffer of %d bytes past the storage buffer range", size) | |||
| 205 | + | } | |||
| 206 | + | c := s.bufs[i] | |||
| 207 | + | if c != nil && c.gpu.size != size { | |||
| 208 | + | r.retire(c.gpu.last, func() { r.freeShmCopy(c) }) | |||
| 209 | + | s.bufs[i], c = nil, nil | |||
| 210 | + | } | |||
| 211 | + | if c == nil { | |||
| 212 | + | c = &shmCopy{} | |||
| 213 | + | if err := r.newGPUBuffer(&c.gpu, size); err != nil { | |||
| 214 | + | return nil, err | |||
| 215 | + | } | |||
| 216 | + | var err error | |||
| 217 | + | if c.pool, c.set, err = r.newSet(r.compose.dummyView, c.gpu.buffer); err != nil { | |||
| 218 | + | r.freeGPUBuffer(&c.gpu) | |||
| 219 | + | return nil, err | |||
| 220 | + | } | |||
| 221 | + | s.bufs[i] = c | |||
| 222 | + | } | |||
| 223 | + | if err := r.waitFrame(c.gpu.last); err != nil { | |||
| 224 | + | return nil, err | |||
| 225 | + | } | |||
| 226 | + | dst := unsafe.Slice((*byte)(c.gpu.mapped), size) | |||
| 227 | + | row := st.w * 4 | |||
| 228 | + | rects := []ports.Rect{{W: st.w, H: st.h}} | |||
| 229 | + | if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil { | |||
| 230 | + | if d, ok := damage(c.holds.seq); ok { | |||
| 231 | + | rects = d | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | // The client may shrink its pool under the mapping: its window then | |||
| 235 | + | // shows garbage for this frame, never a crash, and is copied again. | |||
| 236 | + | c.valid = copyGuarded(func() { | |||
| 237 | + | for _, rc := range rects { | |||
| 238 | + | x0, y0 := max(rc.X, 0), max(rc.Y, 0) | |||
| 239 | + | x1, y1 := min(rc.X+rc.W, st.w), min(rc.Y+rc.H, st.h) | |||
| 240 | + | for y := y0; y < y1; y++ { | |||
| 241 | + | d := y*row + x0*4 | |||
| 242 | + | s := offset + y*stride + x0*4 | |||
| 243 | + | n := (x1 - x0) * 4 | |||
| 244 | + | copy(dst[d:d+n], pixels[s:s+n]) | |||
| 245 | + | r.copied += n | |||
| 246 | + | } | |||
| 247 | + | } | |||
| 248 | + | }) | |||
| 249 | + | c.holds, c.gpu.last = st, r.frame | |||
| 250 | + | return c, nil | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | func (r *Renderer) freeShmCopy(c *shmCopy) { | |||
| 254 | + | if c.pool != 0 { | |||
| 255 | + | r.dd.DestroyDescriptorPool(r.device, c.pool, nil) | |||
| 256 | + | c.pool = 0 | |||
| 257 | + | } | |||
| 258 | + | r.freeGPUBuffer(&c.gpu) | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | // dropShm frees the GPU buffers of surfaces not drawn for importTTL frames. | |||
| 262 | + | func (r *Renderer) dropShm() { | |||
| 263 | + | for key, s := range r.shm { | |||
| 264 | + | if r.frame-s.last > importTTL { | |||
| 265 | + | for _, c := range s.bufs { | |||
| 266 | + | if c != nil { | |||
| 267 | + | r.retire(c.gpu.last, func() { r.freeShmCopy(c) }) | |||
| 268 | + | } | |||
| 269 | + | } | |||
| 270 | + | delete(r.shm, key) | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | } | |||
M internal/adapters/vulkan/targets.go +14 -15
| @@ -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 { | ||
| @@ -28,6 +28,11 @@ type target struct { | |||||
| 28 | // exported images are shared with the display: ownership goes to the | 28 | // exported images are shared with the display: ownership goes to the | ||
| 29 | // foreign queue family after each frame, and KMS reads them. | 29 | // foreign queue family after each frame, and KMS reads them. | ||
| 30 | exported bool | 30 | exported bool | ||
| 31 | + | // What the image holds (damage.go): the scene Seq and each window's | |||
| 32 | + | // content Seq; valid once a frame was drawn whole. | |||
| 33 | + | valid bool | |||
| 34 | + | sceneSeq uint64 | |||
| 35 | + | windows map[ports.WindowID]heldWindow | |||
| 31 | } | 36 | } | ||
| 32 | 37 | ||||
| 33 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | 38 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | ||
| @@ -105,9 +110,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||||
| 105 | mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount) | 110 | mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount) | ||
| 106 | list.DrmFormatModifierProperties = &mods[0] | 111 | list.DrmFormatModifierProperties = &mods[0] | ||
| 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | 112 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | ||
| 108 | - | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | 113 | + | need := vk.FormatFeatureFlags(formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) |
| 109 | - | // Targets are drawn on too (blend.go): a transfer-only modifier is | 114 | + | // Targets are drawn on (compose.go) and read back for screenshots. |
| 110 | - | // not a target, and without any the display falls back to readback. | |||
| 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | 115 | for _, m := range mods[:list.DrmFormatModifierCount] { | ||
| 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | 116 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | ||
| 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | 117 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | ||
| @@ -127,18 +131,12 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool | |||||
| 127 | return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0 | 131 | return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0 | ||
| 128 | } | 132 | } | ||
| 129 | 133 | ||||
| 130 | - | // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from | 134 | + | // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the |
| 131 | - | // the generated bindings. | 135 | + | // generated bindings. |
| 132 | - | const ( | 136 | + | const formatFeatureTransferSrc = 1 << 14 |
| 133 | - | formatFeatureTransferSrc = 1 << 14 | |||
| 134 | - | formatFeatureTransferDst = 1 << 15 | |||
| 135 | - | ) | |||
| 136 | - | ||||
| 137 | - | // Targets are copied into and drawn on (blend.go). | |||
| 138 | - | const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit | |||
| 139 | 137 | ||||
| 140 | - | // Staging feeds copies and, as a storage buffer, the blend shaders. | 138 | + | // Targets are drawn on (compose.go) and copied from for screenshots. |
| 141 | - | const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit | 139 | + | const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit |
| 142 | 140 | ||||
| 143 | // exportTarget creates one exported image with a modifier from mods. | 141 | // exportTarget creates one exported image with a modifier from mods. | ||
| 144 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | 142 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | ||
| @@ -213,6 +211,7 @@ func (r *Renderer) freeTarget(t *target) { | |||||
| 213 | func (r *Renderer) dropTargets() { | 211 | func (r *Renderer) dropTargets() { | ||
| 214 | if len(r.targets) > 0 && r.device != 0 { | 212 | if len(r.targets) > 0 && r.device != 0 { | ||
| 215 | _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device)) | 213 | _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device)) | ||
| 214 | + | r.idle() | |||
| 216 | } | 215 | } | ||
| 217 | for _, t := range r.targets { | 216 | for _, t := range r.targets { | ||
| 218 | if r.last == t { | 217 | if r.last == t { | ||
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 := render(r, 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 := render(viewer, 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}) { | ||
A internal/adapters/wayland/damage_test.go +109 -0
| @@ -0,0 +1,109 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Damage reaches the renderer in buffer pixels, per content, with the | |||
| 14 | + | // window's recent history; a new buffer without damage is a full change. | |||
| 15 | + | func TestDamageHistory(t *testing.T) { | |||
| 16 | + | s, events, _, contents, dir := contentServer(t) | |||
| 17 | + | c := protocolClient(t, s, dir) | |||
| 18 | + | comp := bindVersion(t, c, "wl_compositor", 4) | |||
| 19 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 20 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 21 | + | registerProtocol(t, c, shm) | |||
| 22 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |||
| 23 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 24 | + | registerProtocol(t, c, surf) | |||
| 25 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 26 | + | serials := make(chan uint32, 8) | |||
| 27 | + | xp := &configureProxy{serial: serials} | |||
| 28 | + | xp.SetID(xdg) | |||
| 29 | + | c.Context().Register(xp) | |||
| 30 | + | registerProtocol(t, c, top) | |||
| 31 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 32 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 33 | + | if err := c.Roundtrip(); err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |||
| 37 | + | fd, err := unix.MemfdCreate("damage", 0) | |||
| 38 | + | if err != nil { | |||
| 39 | + | t.Fatal(err) | |||
| 40 | + | } | |||
| 41 | + | defer unix.Close(fd) | |||
| 42 | + | if err := unix.Ftruncate(fd, 2*16*16*4); err != nil { | |||
| 43 | + | t.Fatal(err) | |||
| 44 | + | } | |||
| 45 | + | pool := c.AllocateID() | |||
| 46 | + | registerProtocol(t, c, pool) | |||
| 47 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(2*16*16*4)); err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 50 | + | bufs := [2]uint32{c.AllocateID(), c.AllocateID()} | |||
| 51 | + | for i, b := range bufs { | |||
| 52 | + | registerProtocol(t, c, b) | |||
| 53 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, b, int32(i*16*16*4), int32(16), int32(16), int32(64), uint32(0)) | |||
| 54 | + | } | |||
| 55 | + | next := func() ports.SurfaceContent { | |||
| 56 | + | t.Helper() | |||
| 57 | + | if err := c.Roundtrip(); err != nil { | |||
| 58 | + | t.Fatal(err) | |||
| 59 | + | } | |||
| 60 | + | deadline := time.After(2 * time.Second) | |||
| 61 | + | for { | |||
| 62 | + | select { | |||
| 63 | + | case got := <-contents: | |||
| 64 | + | if got.Width == 16 { | |||
| 65 | + | return got | |||
| 66 | + | } | |||
| 67 | + | case <-deadline: | |||
| 68 | + | t.Fatal("no content") | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | // A new size: full. | |||
| 73 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 74 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(4), int32(4)) | |||
| 75 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 76 | + | mapped(t, events, 2*time.Second) | |||
| 77 | + | first := next() | |||
| 78 | + | if h := first.DamageHistory; len(h) == 0 || h[len(h)-1].Seq != first.Seq || !h[len(h)-1].Full { | |||
| 79 | + | t.Fatalf("resize: %+v", h) | |||
| 80 | + | } | |||
| 81 | + | // Buffer and surface damage, clipped to the buffer. | |||
| 82 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 83 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(2), int32(3), int32(4), int32(5)) | |||
| 84 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamage, int32(14), int32(14), int32(1<<30), int32(1<<30)) | |||
| 85 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 86 | + | second := next() | |||
| 87 | + | got, ok := second.DamageSince(first.Seq) | |||
| 88 | + | want := []ports.Rect{{X: 2, Y: 3, W: 4, H: 5}, {X: 14, Y: 14, W: 2, H: 2}} | |||
| 89 | + | if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] { | |||
| 90 | + | t.Fatalf("damage %v %v", got, ok) | |||
| 91 | + | } | |||
| 92 | + | // Same buffer size, new logical size (buffer scale): full. | |||
| 93 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 94 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetBufferScale, int32(2)) | |||
| 95 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(1), int32(1)) | |||
| 96 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 97 | + | scaled := next() | |||
| 98 | + | if _, ok := scaled.DamageSince(second.Seq); ok { | |||
| 99 | + | t.Fatal("logical resize kept partial damage") | |||
| 100 | + | } | |||
| 101 | + | second = scaled | |||
| 102 | + | // No damage with a new buffer: full. | |||
| 103 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 104 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 105 | + | third := next() | |||
| 106 | + | if _, ok := third.DamageSince(second.Seq); ok { | |||
| 107 | + | t.Fatal("undamaged new buffer taken as unchanged") | |||
| 108 | + | } | |||
| 109 | + | } | |||
M internal/adapters/wayland/dmabuf.go +99 -13
| @@ -15,7 +15,11 @@ import ( | |||||
| 15 | 15 | ||||
| 16 | // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies. | 16 | // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies. | ||
| 17 | // The formats are those the renderer imports (ports.DMABufSupport); without | 17 | // The formats are those the renderer imports (ports.DMABufSupport); without | ||
| 18 | - | // any, the global is not advertised and clients use wl_shm. | 18 | + | // any, the global is not advertised and clients use wl_shm. Surface |
| 19 | + | // feedback also offers, first, a scanout tranche while the surface's | |||
| 20 | + | // window is fullscreen on an output that reported scanout formats | |||
| 21 | + | // (ports.OutputFormats): a buffer allocated from it can be flipped to the | |||
| 22 | + | // display with no composition. | |||
| 19 | 23 | ||||
| 20 | const dmabufVersion = 4 | 24 | const dmabufVersion = 4 | ||
| 21 | 25 | ||||
| @@ -30,7 +34,7 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error | |||||
| 30 | if err != nil { | 34 | if err != nil { | ||
| 31 | return err | 35 | return err | ||
| 32 | } | 36 | } | ||
| 33 | - | s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table} | 37 | + | s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}} |
| 34 | return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) { | 38 | return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) { | ||
| 35 | r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf) | 39 | r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf) | ||
| 36 | if err != nil || v >= 4 { | 40 | if err != nil || v >= 4 { | ||
| @@ -77,6 +81,11 @@ type dmabufGlobal struct { | |||||
| 77 | support ports.DMABufSupport | 81 | support ports.DMABufSupport | ||
| 78 | table *os.File | 82 | table *os.File | ||
| 79 | nextID uint64 | 83 | nextID uint64 | ||
| 84 | + | // scanout are the outputs' direct scanout formats by output name. | |||
| 85 | + | scanout map[string]ports.OutputFormats | |||
| 86 | + | // feedbacks are the live feedback objects and their surface (nil: | |||
| 87 | + | // default feedback). | |||
| 88 | + | feedbacks map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface | |||
| 80 | } | 89 | } | ||
| 81 | 90 | ||||
| 82 | func (g *dmabufGlobal) close() { | 91 | func (g *dmabufGlobal) close() { | ||
| @@ -98,35 +107,112 @@ func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) | |||||
| 98 | } | 107 | } | ||
| 99 | 108 | ||||
| 100 | func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) { | 109 | func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) { | ||
| 101 | - | g.feedback(r, id) | 110 | + | g.feedback(r, id, nil) |
| 102 | } | 111 | } | ||
| 103 | 112 | ||||
| 104 | - | // GetSurfaceFeedback sends the default feedback: one renderer, one tranche. | 113 | + | // GetSurfaceFeedback follows the surface: a scanout tranche comes first |
| 105 | - | // Scanout tranches come with direct scanout. | 114 | + | // while it can be scanned out (see scanoutFor). |
| 106 | - | func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, _ *wayland.Surface) { | 115 | + | func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *wayland.Surface) { |
| 107 | - | g.feedback(r, id) | 116 | + | g.feedback(r, id, g.server.surfaceOf(surf)) |
| 108 | } | 117 | } | ||
| 109 | 118 | ||||
| 110 | - | func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) { | 119 | + | func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *surface) { |
| 111 | fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{}) | 120 | fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{}) | ||
| 112 | if err != nil { | 121 | if err != nil { | ||
| 113 | return | 122 | return | ||
| 114 | } | 123 | } | ||
| 115 | - | dev := make([]byte, 8) | 124 | + | g.feedbacks[fb] = surf |
| 116 | - | binary.NativeEndian.PutUint64(dev, g.support.Device) | 125 | + | fb.OnDestroy = func() { delete(g.feedbacks, fb) } |
| 126 | + | g.send(fb, surf) | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // devBytes is a dev_t as the protocol's array. | |||
| 130 | + | func devBytes(dev uint64) []byte { | |||
| 131 | + | b := make([]byte, 8) | |||
| 132 | + | binary.NativeEndian.PutUint64(b, dev) | |||
| 133 | + | return b | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // send sends the whole feedback: table, main device, the scanout tranche | |||
| 137 | + | // when the surface has one, then the renderer tranche. | |||
| 138 | + | func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, surf *surface) { | |||
| 139 | + | fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats))) | |||
| 140 | + | main := devBytes(g.support.Device) | |||
| 141 | + | fb.SendMainDevice(main) | |||
| 142 | + | if out, ok := g.scanoutFor(surf); ok { | |||
| 143 | + | var indices []byte | |||
| 144 | + | for _, f := range out.Formats { | |||
| 145 | + | if i := slices.Index(g.support.Formats, f); i >= 0 { | |||
| 146 | + | indices = binary.NativeEndian.AppendUint16(indices, uint16(i)) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | if len(indices) > 0 { | |||
| 150 | + | fb.SendTrancheTargetDevice(devBytes(out.Device)) | |||
| 151 | + | fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)) | |||
| 152 | + | fb.SendTrancheFormats(indices) | |||
| 153 | + | fb.SendTrancheDone() | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 117 | indices := make([]byte, 2*len(g.support.Formats)) | 156 | indices := make([]byte, 2*len(g.support.Formats)) | ||
| 118 | for i := range g.support.Formats { | 157 | for i := range g.support.Formats { | ||
| 119 | binary.NativeEndian.PutUint16(indices[i*2:], uint16(i)) | 158 | binary.NativeEndian.PutUint16(indices[i*2:], uint16(i)) | ||
| 120 | } | 159 | } | ||
| 121 | - | fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats))) | 160 | + | fb.SendTrancheTargetDevice(main) |
| 122 | - | fb.SendMainDevice(dev) | |||
| 123 | - | fb.SendTrancheTargetDevice(dev) | |||
| 124 | fb.SendTrancheFlags(0) | 161 | fb.SendTrancheFlags(0) | ||
| 125 | fb.SendTrancheFormats(indices) | 162 | fb.SendTrancheFormats(indices) | ||
| 126 | fb.SendTrancheDone() | 163 | fb.SendTrancheDone() | ||
| 127 | fb.SendDone() | 164 | fb.SendDone() | ||
| 128 | } | 165 | } | ||
| 129 | 166 | ||||
| 167 | + | // scanoutFor is the scanout offer for a surface: its window is fullscreen | |||
| 168 | + | // on an output that reported scanout formats. | |||
| 169 | + | func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) { | |||
| 170 | + | if surf == nil || surf.destroyed { | |||
| 171 | + | return ports.OutputFormats{}, false | |||
| 172 | + | } | |||
| 173 | + | root := surf.root() | |||
| 174 | + | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen { | |||
| 175 | + | return ports.OutputFormats{}, false | |||
| 176 | + | } | |||
| 177 | + | out, ok := g.scanout[root.xdg.window.last.Output] | |||
| 178 | + | return out, ok && len(out.Formats) > 0 | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // setOutputFormats records an output's scanout formats and sends the | |||
| 182 | + | // feedback of the surfaces fullscreen on it again. Outputs offer only | |||
| 183 | + | // formats the renderer samples (a refused buffer is composed), so they | |||
| 184 | + | // are all in the table. | |||
| 185 | + | func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) { | |||
| 186 | + | if old, ok := g.scanout[f.Output]; ok && old.Device == f.Device && slices.Equal(old.Formats, f.Formats) { | |||
| 187 | + | return | |||
| 188 | + | } | |||
| 189 | + | if len(f.Formats) == 0 { | |||
| 190 | + | delete(g.scanout, f.Output) | |||
| 191 | + | } else { | |||
| 192 | + | g.scanout[f.Output] = f | |||
| 193 | + | } | |||
| 194 | + | for fb, surf := range g.feedbacks { | |||
| 195 | + | if surf != nil && !surf.destroyed { | |||
| 196 | + | if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Fullscreen && root.xdg.window.last.Output == f.Output { | |||
| 197 | + | g.send(fb, surf) | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // resendSurface sends the feedback of a window's surfaces again, when its | |||
| 204 | + | // fullscreen state or output changed. | |||
| 205 | + | func (g *dmabufGlobal) resendSurface(root *surface) { | |||
| 206 | + | if g == nil { | |||
| 207 | + | return | |||
| 208 | + | } | |||
| 209 | + | for fb, surf := range g.feedbacks { | |||
| 210 | + | if surf != nil && surf.root() == root { | |||
| 211 | + | g.send(fb, surf) | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | } | |||
| 215 | + | ||||
| 130 | type feedback struct{} | 216 | type feedback struct{} | ||
| 131 | 217 | ||||
| 132 | func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {} | 218 | func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {} | ||
M internal/adapters/wayland/dmabuf_test.go +115 -0
| @@ -172,3 +172,118 @@ func TestWestonDMABuf(t *testing.T) { | |||||
| 172 | } | 172 | } | ||
| 173 | } | 173 | } | ||
| 174 | } | 174 | } | ||
| 175 | + | ||||
| 176 | + | // feedbackProxy records the tranches of each feedback round (done). | |||
| 177 | + | type feedbackProxy struct { | |||
| 178 | + | wlturbo.BaseProxy | |||
| 179 | + | rounds chan []tranche | |||
| 180 | + | cur []tranche | |||
| 181 | + | open tranche | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | type tranche struct { | |||
| 185 | + | flags uint32 | |||
| 186 | + | indices []uint16 | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func (p *feedbackProxy) Dispatch(e *wlturbo.Event) { | |||
| 190 | + | switch uint32(e.Opcode) { | |||
| 191 | + | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable: | |||
| 192 | + | if fd := e.Fd(); fd != 0 { | |||
| 193 | + | unix.Close(int(fd)) | |||
| 194 | + | } | |||
| 195 | + | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags: | |||
| 196 | + | p.open.flags = e.Uint32() | |||
| 197 | + | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats: | |||
| 198 | + | a := e.Array() | |||
| 199 | + | for i := 0; i+1 < len(a); i += 2 { | |||
| 200 | + | p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8) | |||
| 201 | + | } | |||
| 202 | + | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone: | |||
| 203 | + | p.cur, p.open = append(p.cur, p.open), tranche{} | |||
| 204 | + | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone: | |||
| 205 | + | p.rounds <- p.cur | |||
| 206 | + | p.cur = nil | |||
| 207 | + | } | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // A fullscreen surface on an output with scanout formats gets a scanout | |||
| 211 | + | // tranche first; leaving fullscreen sends the renderer tranche alone. | |||
| 212 | + | func TestDMABufScanoutTranche(t *testing.T) { | |||
| 213 | + | tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001} | |||
| 214 | + | sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}} | |||
| 215 | + | dir := t.TempDir() | |||
| 216 | + | events := make(chan ports.ClientEvent, 16) | |||
| 217 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 218 | + | formats := make(chan ports.OutputFormats, 1) | |||
| 219 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland")) | |||
| 220 | + | if err != nil { | |||
| 221 | + | t.Fatal(err) | |||
| 222 | + | } | |||
| 223 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 224 | + | done := make(chan error, 1) | |||
| 225 | + | go func() { done <- s.Run(ctx) }() | |||
| 226 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 227 | + | c := protocolClient(t, s, dir) | |||
| 228 | + | w, surf, xdg := surfaceMapper(t, c, events)() | |||
| 229 | + | // Our configures must not block the mapper's one-slot proxy. | |||
| 230 | + | registerProtocol(t, c, xdg) | |||
| 231 | + | dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion) | |||
| 232 | + | fb := c.AllocateID() | |||
| 233 | + | proxy := &feedbackProxy{rounds: make(chan []tranche, 8)} | |||
| 234 | + | proxy.SetID(fb) | |||
| 235 | + | c.Context().Register(proxy) | |||
| 236 | + | requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf) | |||
| 237 | + | round := func(what string) []tranche { | |||
| 238 | + | t.Helper() | |||
| 239 | + | deadline := time.After(2 * time.Second) | |||
| 240 | + | for { | |||
| 241 | + | if err := c.Roundtrip(); err != nil { | |||
| 242 | + | t.Fatal(err) | |||
| 243 | + | } | |||
| 244 | + | select { | |||
| 245 | + | case r := <-proxy.rounds: | |||
| 246 | + | return r | |||
| 247 | + | case <-deadline: | |||
| 248 | + | t.Fatalf("no feedback: %s", what) | |||
| 249 | + | default: | |||
| 250 | + | time.Sleep(5 * time.Millisecond) | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | } | |||
| 254 | + | scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout) | |||
| 255 | + | if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 { | |||
| 256 | + | t.Fatalf("tiled window: %+v", r) | |||
| 257 | + | } | |||
| 258 | + | formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}} | |||
| 259 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"} | |||
| 260 | + | r := round("fullscreen") | |||
| 261 | + | for len(r) == 1 { // the formats may land after the configure | |||
| 262 | + | r = round("fullscreen with formats") | |||
| 263 | + | } | |||
| 264 | + | if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 { | |||
| 265 | + | t.Fatalf("fullscreen: %+v", r) | |||
| 266 | + | } | |||
| 267 | + | // Fullscreen on another output: its formats (none) apply at once. | |||
| 268 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1"} | |||
| 269 | + | if r := round("other output"); len(r) != 1 { | |||
| 270 | + | t.Fatalf("moved output: %+v", r) | |||
| 271 | + | } | |||
| 272 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"} | |||
| 273 | + | if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 { | |||
| 274 | + | t.Fatalf("left fullscreen: %+v", r) | |||
| 275 | + | } | |||
| 276 | + | // A destroyed feedback gets nothing more (a protocol error would | |||
| 277 | + | // kill the connection) and leaves the server's list. | |||
| 278 | + | requestProtocol(t, c, fb, linuxdmabuf.ZwpLinuxDmabufFeedbackV1RequestDestroy) | |||
| 279 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"} | |||
| 280 | + | formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7} | |||
| 281 | + | if err := c.Roundtrip(); err != nil { | |||
| 282 | + | t.Fatal(err) | |||
| 283 | + | } | |||
| 284 | + | var n int | |||
| 285 | + | s.display.Do(func() { n = len(s.dmabuf.feedbacks) }) | |||
| 286 | + | if n != 0 { | |||
| 287 | + | t.Fatalf("%d feedbacks kept", n) | |||
| 288 | + | } | |||
| 289 | + | } | |||
M internal/adapters/wayland/fifo.go +42 -5
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "github.com/bnema/purego-libwayland/protocol/committiming" | 7 | "github.com/bnema/purego-libwayland/protocol/committiming" | ||
| 8 | "github.com/bnema/purego-libwayland/protocol/contenttype" | 8 | "github.com/bnema/purego-libwayland/protocol/contenttype" | ||
| 9 | "github.com/bnema/purego-libwayland/protocol/fifo" | 9 | "github.com/bnema/purego-libwayland/protocol/fifo" | ||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 10 | "github.com/bnema/purego-libwayland/protocol/wayland" | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 11 | "github.com/bnema/purego-libwayland/server" | 12 | "github.com/bnema/purego-libwayland/server" | ||
| 12 | "golang.org/x/sys/unix" | 13 | "golang.org/x/sys/unix" | ||
| @@ -24,7 +25,7 @@ import ( | |||||
| 24 | // queued; the pacer (frames.go) applies queued updates in order when they | 25 | // queued; the pacer (frames.go) applies queued updates in order when they | ||
| 25 | // are ready. wp_content_type_v1 is recorded and logged only. | 26 | // are ready. wp_content_type_v1 is recorded and logged only. | ||
| 26 | 27 | ||||
| 27 | - | func registerPresentation(d *server.Display, s *Server) error { | 28 | + | func registerPresentationConstraints(d *server.Display, s *Server) error { |
| 28 | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | 29 | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | ||
| 29 | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | 30 | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | ||
| 30 | }); err != nil { | 31 | }); err != nil { | ||
| @@ -216,6 +217,10 @@ type update struct { | |||||
| 216 | barrier bool | 217 | barrier bool | ||
| 217 | wait bool | 218 | wait bool | ||
| 218 | at time.Time | 219 | at time.Time | ||
| 220 | + | feedback []*presentationtime.WpPresentationFeedback | |||
| 221 | + | sync *commitSync | |||
| 222 | + | damage []ports.Rect | |||
| 223 | + | bufDamage []ports.Rect | |||
| 219 | } | 224 | } | ||
| 220 | 225 | ||||
| 221 | type childMove struct { | 226 | type childMove struct { | ||
| @@ -226,8 +231,10 @@ type childMove struct { | |||||
| 226 | // takePending moves the pending state into an update: one-shot state is | 231 | // takePending moves the pending state into an update: one-shot state is | ||
| 227 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | 232 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | ||
| 228 | func (s *surface) takePending() update { | 233 | func (s *surface) takePending() update { | ||
| 229 | - | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime} | 234 | + | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync} |
| 230 | s.attached, s.pending, s.callbacks = false, nil, nil | 235 | s.attached, s.pending, s.callbacks = false, nil, nil | ||
| 236 | + | s.pendingFeedback, s.pendingSync = nil, nil | |||
| 237 | + | s.pendingDamage, s.pendingBufDamage = nil, nil | |||
| 231 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | 238 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | ||
| 232 | if v := s.viewport; v != nil { | 239 | if v := s.viewport; v != nil { | ||
| 233 | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | 240 | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | ||
| @@ -256,6 +263,8 @@ func (s *surface) putPending(u update) { | |||||
| 256 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | 263 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | ||
| 257 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | 264 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | ||
| 258 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | 265 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | ||
| 266 | + | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | |||
| 267 | + | s.pendingFeedback, s.pendingSync = u.feedback, u.sync | |||
| 259 | if u.vp != nil && s.viewport == u.vp { | 268 | if u.vp != nil && s.viewport == u.vp { | ||
| 260 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | 269 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | ||
| 261 | } | 270 | } | ||
| @@ -277,13 +286,31 @@ func (s *surface) putPending(u update) { | |||||
| 277 | 286 | ||||
| 278 | // mustWait reports whether the pending commit cannot apply now. | 287 | // mustWait reports whether the pending commit cannot apply now. | ||
| 279 | func (s *surface) mustWait(now time.Time) bool { | 288 | func (s *surface) mustWait(now time.Time) bool { | ||
| 280 | - | return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) | 289 | + | return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) || !s.syncReady(s.pendingSync) |
| 281 | } | 290 | } | ||
| 282 | 291 | ||||
| 283 | // tooEarly reports whether content applied now would show before at: | 292 | // tooEarly reports whether content applied now would show before at: | ||
| 284 | // it reaches the screen one refresh later. | 293 | // it reaches the screen one refresh later. | ||
| 285 | func (s *surface) tooEarly(at, now time.Time) bool { | 294 | func (s *surface) tooEarly(at, now time.Time) bool { | ||
| 286 | - | return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s)))) | 295 | + | return !at.IsZero() && at.After(s.server.nextRefresh(s.server.fifoOutput(s), now)) |
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | // nextRefresh is when the output's next frame reaches the screen: the | |||
| 299 | + | // vblank after now counted from its last flip (kernel time, refresh | |||
| 300 | + | // period), or one period from now without flips or while VRR is on. | |||
| 301 | + | func (s *Server) nextRefresh(name string, now time.Time) time.Time { | |||
| 302 | + | p := s.outputPeriod(name) | |||
| 303 | + | f, ok := s.flips[name] | |||
| 304 | + | if !ok || f.When <= 0 || f.Refresh <= 0 { | |||
| 305 | + | return now.Add(p) | |||
| 306 | + | } | |||
| 307 | + | mono := monotonic(now) | |||
| 308 | + | since := mono - f.When | |||
| 309 | + | if since < 0 || since > time.Second { | |||
| 310 | + | return now.Add(p) | |||
| 311 | + | } | |||
| 312 | + | n := since/f.Refresh + 1 | |||
| 313 | + | return now.Add(f.When + n*f.Refresh - mono) | |||
| 287 | } | 314 | } | ||
| 288 | 315 | ||||
| 289 | // queueUpdate queues the pending commit until it can apply. | 316 | // queueUpdate queues the pending commit until it can apply. | ||
| @@ -318,6 +345,13 @@ func (s *surface) dropQueue() { | |||||
| 318 | } else if len(u.callbacks) > 0 { | 345 | } else if len(u.callbacks) > 0 { | ||
| 319 | s.server.queueFrames(s.server.frameOutput(s), u.callbacks) | 346 | s.server.queueFrames(s.server.frameOutput(s), u.callbacks) | ||
| 320 | } | 347 | } | ||
| 348 | + | for _, fb := range u.feedback { | |||
| 349 | + | discard(fb) | |||
| 350 | + | } | |||
| 351 | + | s.dropSync(u.sync) | |||
| 352 | + | if u.sync != nil { | |||
| 353 | + | continue // explicit sync: the release point replaced release | |||
| 354 | + | } | |||
| 321 | if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() { | 355 | if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() { | ||
| 322 | released[u.buffer.Resource] = true | 356 | released[u.buffer.Resource] = true | ||
| 323 | u.buffer.SendRelease() | 357 | u.buffer.SendRelease() | ||
| @@ -384,8 +418,11 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration | |||||
| 384 | if u.wait && surf.barrier { | 418 | if u.wait && surf.barrier { | ||
| 385 | break | 419 | break | ||
| 386 | } | 420 | } | ||
| 421 | + | if !surf.syncReady(u.sync) { | |||
| 422 | + | break | |||
| 423 | + | } | |||
| 387 | if surf.tooEarly(u.at, now) { | 424 | if surf.tooEarly(u.at, now) { | ||
| 388 | - | wait = min(wait, u.at.Add(-p).Sub(now)) | 425 | + | wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond) |
| 389 | break | 426 | break | ||
| 390 | } | 427 | } | ||
| 391 | surf.queue = surf.queue[1:] | 428 | surf.queue = surf.queue[1:] | ||
M internal/adapters/wayland/frames.go +44 -5
| @@ -2,6 +2,7 @@ package wayland | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "golang.org/x/sys/unix" | |||
| 5 | "slices" | 6 | "slices" | ||
| 6 | "time" | 7 | "time" | ||
| 7 | 8 | ||||
| @@ -34,7 +35,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 34 | case <-s.display.Stopped(): | 35 | case <-s.display.Stopped(): | ||
| 35 | return | 36 | return | ||
| 36 | case p := <-presented: | 37 | case p := <-presented: | ||
| 37 | - | flipped[p.Output] = flipped[p.Output] || p.Flip | 38 | + | flipped[p.Output] = flipped[p.Output] || paces(p) |
| 38 | reports = append(reports, p) | 39 | reports = append(reports, p) | ||
| 39 | case <-s.frameReady: | 40 | case <-s.frameReady: | ||
| 40 | case <-timer.C: | 41 | case <-timer.C: | ||
| @@ -45,7 +46,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 45 | for { | 46 | for { | ||
| 46 | select { | 47 | select { | ||
| 47 | case p := <-presented: | 48 | case p := <-presented: | ||
| 48 | - | flipped[p.Output] = flipped[p.Output] || p.Flip | 49 | + | flipped[p.Output] = flipped[p.Output] || paces(p) |
| 49 | reports = append(reports, p) | 50 | reports = append(reports, p) | ||
| 50 | default: | 51 | default: | ||
| 51 | break drain | 52 | break drain | ||
| @@ -54,10 +55,28 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 54 | wait, idle := defaultFramePeriod, false | 55 | wait, idle := defaultFramePeriod, false | ||
| 55 | if !s.display.Do(func() { | 56 | if !s.display.Do(func() { | ||
| 56 | if ctx.Err() == nil { | 57 | if ctx.Err() == nil { | ||
| 58 | + | // Each flip answers its own presentation feedbacks, in | |||
| 59 | + | // order: flips are never merged here. | |||
| 60 | + | for _, p := range reports { | |||
| 61 | + | if p.Flip != nil { | |||
| 62 | + | s.flips[p.Output] = *p.Flip | |||
| 63 | + | s.presentFlip(p.Output, p.Flip) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | s.dropFeedbacks(time.Now()) | |||
| 67 | + | if len(s.feedbacks) > 0 { | |||
| 68 | + | wait = feedbackTimeout | |||
| 69 | + | } | |||
| 57 | var due []string | 70 | var due []string | ||
| 58 | - | due, wait, idle = s.dueFrames(time.Now(), flipped) | 71 | + | var dueWait time.Duration |
| 72 | + | due, dueWait, idle = s.dueFrames(time.Now(), flipped) | |||
| 73 | + | if len(s.feedbacks) > 0 { | |||
| 74 | + | // Unanswered feedbacks time out: keep ticking. | |||
| 75 | + | dueWait, idle = min(dueWait, feedbackTimeout), false | |||
| 76 | + | } | |||
| 77 | + | wait = dueWait | |||
| 59 | for _, name := range due { | 78 | for _, name := range due { | ||
| 60 | - | s.sendFrames(name) | 79 | + | s.sendFrames(name, flipped[name]) |
| 61 | } | 80 | } | ||
| 62 | if s.releaseHeld(time.Now(), reports) && idle { | 81 | if s.releaseHeld(time.Now(), reports) && idle { | ||
| 63 | // Held buffers wait for a flip or heldTimeout. | 82 | // Held buffers wait for a flip or heldTimeout. | ||
| @@ -167,13 +186,24 @@ func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []strin | |||||
| 167 | return fire, max(wait, time.Millisecond), !waiting | 186 | return fire, max(wait, time.Millisecond), !waiting | ||
| 168 | } | 187 | } | ||
| 169 | 188 | ||||
| 170 | - | func (s *Server) sendFrames(name string) { | 189 | + | // paces reports whether a report paces frame callbacks: a flip of a real |
| 190 | + | // display. Software flips (headless) answer presentation feedback, but | |||
| 191 | + | // callbacks keep the output's refresh period. | |||
| 192 | + | func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock } | |||
| 193 | + | ||||
| 194 | + | // sendFrames fires an output's callbacks; flipped: a flip of it fired them. | |||
| 195 | + | func (s *Server) sendFrames(name string, flipped bool) { | |||
| 171 | callbacks := s.awaiting[name] | 196 | callbacks := s.awaiting[name] | ||
| 172 | delete(s.awaiting, name) | 197 | delete(s.awaiting, name) | ||
| 173 | if len(callbacks) == 0 { | 198 | if len(callbacks) == 0 { | ||
| 174 | return | 199 | return | ||
| 175 | } | 200 | } | ||
| 201 | + | // done carries the flip's time when the output flips (the frame the | |||
| 202 | + | // client drew for is on screen), else now. | |||
| 176 | ms := uint32(time.Since(s.started).Milliseconds()) | 203 | ms := uint32(time.Since(s.started).Milliseconds()) | ||
| 204 | + | if f, ok := s.flips[name]; ok && flipped && f.When > 0 { | |||
| 205 | + | ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds()) | |||
| 206 | + | } | |||
| 177 | for _, cb := range callbacks { | 207 | for _, cb := range callbacks { | ||
| 178 | if !cb.Resource.Alive() { | 208 | if !cb.Resource.Alive() { | ||
| 179 | continue | 209 | continue | ||
| @@ -191,3 +221,12 @@ func framePeriod(refreshMilli int) time.Duration { | |||||
| 191 | } | 221 | } | ||
| 192 | return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli)) | 222 | return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli)) | ||
| 193 | } | 223 | } | ||
| 224 | + | ||||
| 225 | + | // monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps. | |||
| 226 | + | func monotonic(t time.Time) time.Duration { | |||
| 227 | + | var ts unix.Timespec | |||
| 228 | + | if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil { | |||
| 229 | + | return 0 | |||
| 230 | + | } | |||
| 231 | + | return time.Duration(ts.Nano()) - time.Since(t) | |||
| 232 | + | } | |||
M internal/adapters/wayland/globals.go +3 -0
| @@ -18,7 +18,10 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | ||
| 19 | func() error { return registerPointerConstraints(d, s) }, | 19 | func() error { return registerPointerConstraints(d, s) }, | ||
| 20 | func() error { return registerTearing(d, s) }, | 20 | func() error { return registerTearing(d, s) }, | ||
| 21 | + | func() error { return registerPresentationConstraints(d, s) }, | |||
| 21 | func() error { return registerPresentation(d, s) }, | 22 | func() error { return registerPresentation(d, s) }, | ||
| 23 | + | func() error { return registerSyncobj(d, s) }, | |||
| 24 | + | func() error { return registerInhibit(d, s) }, | |||
| 22 | func() error { return registerActivation(d, s) }, | 25 | func() error { return registerActivation(d, s) }, | ||
| 23 | func() error { return registerVirtualKeyboard(d, s) }, | 26 | func() error { return registerVirtualKeyboard(d, s) }, | ||
| 24 | func() error { | 27 | func() error { | ||
A internal/adapters/wayland/inhibit.go +163 -0
| @@ -0,0 +1,163 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | + | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Inhibitors. keyboard-shortcuts-inhibit: a window asks for every key, | |||
| 12 | + | // compositor binds included, while it has keyboard focus; core decides | |||
| 13 | + | // when it is in effect (ports.ShortcutsInhibitState) and the client hears | |||
| 14 | + | // active/inactive. idle-inhibit: a window keeps the session awake while | |||
| 15 | + | // it is mapped. Both are reported to core per window. | |||
| 16 | + | ||||
| 17 | + | // inhibitor is one inhibitor object on a surface. | |||
| 18 | + | type inhibitor struct { | |||
| 19 | + | surf *surface | |||
| 20 | + | shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle | |||
| 21 | + | window ports.WindowID // reported to core (0: not yet) | |||
| 22 | + | active bool // shortcuts: last state sent | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func registerInhibit(d *server.Display, s *Server) error { | |||
| 26 | + | if err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 27 | + | _, _ = keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1(c, int32(v), id, shortcutsManager{s}) | |||
| 28 | + | }); err != nil { | |||
| 29 | + | return err | |||
| 30 | + | } | |||
| 31 | + | return idleinhibit.NewZwpIdleInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 32 | + | _, _ = idleinhibit.NewZwpIdleInhibitManagerV1(c, int32(v), id, idleManager{s}) | |||
| 33 | + | }) | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | type shortcutsManager struct{ server *Server } | |||
| 37 | + | ||||
| 38 | + | func (shortcutsManager) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1) {} | |||
| 39 | + | ||||
| 40 | + | // InhibitShortcuts adds a surface's inhibitor; one per surface and seat | |||
| 41 | + | // (there is one seat). | |||
| 42 | + | func (m shortcutsManager) InhibitShortcuts(r *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) { | |||
| 43 | + | state := m.server.surfaceOf(surf) | |||
| 44 | + | if state == nil { | |||
| 45 | + | return | |||
| 46 | + | } | |||
| 47 | + | for _, in := range m.server.inhibitors { | |||
| 48 | + | if in.surf == state && in.shortcut != nil { | |||
| 49 | + | r.PostError(uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited), "surface already inhibits shortcuts") | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | res, err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitorV1(r.Client(), r.Version(), id, shortcutsInhibitor{}) | |||
| 54 | + | if err != nil { | |||
| 55 | + | return | |||
| 56 | + | } | |||
| 57 | + | in := &inhibitor{surf: state, shortcut: res} | |||
| 58 | + | m.server.addInhibitor(in) | |||
| 59 | + | res.OnDestroy = func() { m.server.removeInhibitor(in) } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | type shortcutsInhibitor struct{} | |||
| 63 | + | ||||
| 64 | + | func (shortcutsInhibitor) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1) {} | |||
| 65 | + | ||||
| 66 | + | type idleManager struct{ server *Server } | |||
| 67 | + | ||||
| 68 | + | func (idleManager) Destroy(*idleinhibit.ZwpIdleInhibitManagerV1) {} | |||
| 69 | + | ||||
| 70 | + | // CreateInhibitor adds an idle inhibitor; a surface may have several. | |||
| 71 | + | func (m idleManager) CreateInhibitor(r *idleinhibit.ZwpIdleInhibitManagerV1, id uint32, surf *wayland.Surface) { | |||
| 72 | + | state := m.server.surfaceOf(surf) | |||
| 73 | + | if state == nil { | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | res, err := idleinhibit.NewZwpIdleInhibitorV1(r.Client(), r.Version(), id, idleInhibitor{}) | |||
| 77 | + | if err != nil { | |||
| 78 | + | return | |||
| 79 | + | } | |||
| 80 | + | in := &inhibitor{surf: state} | |||
| 81 | + | m.server.addInhibitor(in) | |||
| 82 | + | res.OnDestroy = func() { m.server.removeInhibitor(in) } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | type idleInhibitor struct{} | |||
| 86 | + | ||||
| 87 | + | func (idleInhibitor) Destroy(*idleinhibit.ZwpIdleInhibitorV1) {} | |||
| 88 | + | ||||
| 89 | + | func (s *Server) addInhibitor(in *inhibitor) { | |||
| 90 | + | s.inhibitors = append(s.inhibitors, in) | |||
| 91 | + | s.syncInhibitors() | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func (s *Server) removeInhibitor(in *inhibitor) { | |||
| 95 | + | for i, x := range s.inhibitors { | |||
| 96 | + | if x == in { | |||
| 97 | + | s.inhibitors = append(s.inhibitors[:i], s.inhibitors[i+1:]...) | |||
| 98 | + | break | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | in.surf = nil | |||
| 102 | + | s.syncInhibitors() | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // syncInhibitors tells core, per window, whether it has inhibitors of | |||
| 106 | + | // each kind, after any change: an inhibitor added or removed, a window | |||
| 107 | + | // mapped or unmapped. | |||
| 108 | + | func (s *Server) syncInhibitors() { | |||
| 109 | + | shortcuts, idle := map[ports.WindowID]bool{}, map[ports.WindowID]bool{} | |||
| 110 | + | for _, in := range s.inhibitors { | |||
| 111 | + | in.window = 0 | |||
| 112 | + | if in.surf != nil && !in.surf.destroyed { | |||
| 113 | + | in.window = in.surf.root().windowID() | |||
| 114 | + | } | |||
| 115 | + | if in.window == 0 { | |||
| 116 | + | continue | |||
| 117 | + | } | |||
| 118 | + | if in.shortcut != nil { | |||
| 119 | + | shortcuts[in.window] = true | |||
| 120 | + | } else { | |||
| 121 | + | idle[in.window] = true | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | |||
| 125 | + | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | |||
| 126 | + | } | |||
| 127 | + | for id, on := range diffSets(s.idleWindows, idle) { | |||
| 128 | + | s.emit(ports.IdleInhibit{Window: id, Active: on}) | |||
| 129 | + | } | |||
| 130 | + | s.shortcutWindows, s.idleWindows = shortcuts, idle | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // diffSets is the membership changes from old to cur. | |||
| 134 | + | func diffSets(old, cur map[ports.WindowID]bool) map[ports.WindowID]bool { | |||
| 135 | + | out := map[ports.WindowID]bool{} | |||
| 136 | + | for id := range cur { | |||
| 137 | + | if !old[id] { | |||
| 138 | + | out[id] = true | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | for id := range old { | |||
| 142 | + | if !cur[id] { | |||
| 143 | + | out[id] = false | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | return out | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // setShortcutsInhibit applies core's decision to a window's shortcuts | |||
| 150 | + | // inhibitor. | |||
| 151 | + | func (s *Server) setShortcutsInhibit(c ports.ShortcutsInhibitState) { | |||
| 152 | + | for _, in := range s.inhibitors { | |||
| 153 | + | if in.shortcut == nil || in.window != c.Window || in.active == c.Active || !in.shortcut.Resource.Alive() { | |||
| 154 | + | continue | |||
| 155 | + | } | |||
| 156 | + | in.active = c.Active | |||
| 157 | + | if c.Active { | |||
| 158 | + | in.shortcut.SendActive() | |||
| 159 | + | } else { | |||
| 160 | + | in.shortcut.SendInactive() | |||
| 161 | + | } | |||
| 162 | + | } | |||
| 163 | + | } | |||
A internal/adapters/wayland/inhibit_test.go +118 -0
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit" | |||
| 10 | + | "github.com/bnema/wlturbo" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | type inhibitorProxy struct { | |||
| 14 | + | wlturbo.BaseProxy | |||
| 15 | + | states chan bool | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func (p *inhibitorProxy) Dispatch(e *wlturbo.Event) { | |||
| 19 | + | p.states <- uint32(e.Opcode) == keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1EventActive | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // clientEvent waits for the next client event of type T. | |||
| 23 | + | func clientEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 24 | + | t.Helper() | |||
| 25 | + | deadline := time.After(2 * time.Second) | |||
| 26 | + | for { | |||
| 27 | + | select { | |||
| 28 | + | case ev := <-events: | |||
| 29 | + | if v, ok := ev.(T); ok { | |||
| 30 | + | return v | |||
| 31 | + | } | |||
| 32 | + | case <-deadline: | |||
| 33 | + | var zero T | |||
| 34 | + | t.Fatalf("no %T", zero) | |||
| 35 | + | return zero | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func TestShortcutsInhibitor(t *testing.T) { | |||
| 41 | + | s, events, commands, _, dir := contentServer(t) | |||
| 42 | + | c := protocolClient(t, s, dir) | |||
| 43 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 44 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 45 | + | registerProtocol(t, c, seat) | |||
| 46 | + | mgr := bindProtocol(t, c, "zwp_keyboard_shortcuts_inhibit_manager_v1") | |||
| 47 | + | in := c.AllocateID() | |||
| 48 | + | p := &inhibitorProxy{states: make(chan bool, 4)} | |||
| 49 | + | p.SetID(in) | |||
| 50 | + | c.Context().Register(p) | |||
| 51 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, in, surf, seat) | |||
| 52 | + | if v := clientEvent[ports.ShortcutsInhibit](t, events); v != (ports.ShortcutsInhibit{Window: w.ID, Active: true}) { | |||
| 53 | + | t.Fatal(v) | |||
| 54 | + | } | |||
| 55 | + | // Core decides: active, then inactive. | |||
| 56 | + | state := func(active bool) { | |||
| 57 | + | t.Helper() | |||
| 58 | + | commands <- ports.ShortcutsInhibitState{Window: w.ID, Active: active} | |||
| 59 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 60 | + | for time.Now().Before(deadline) { | |||
| 61 | + | if err := c.Roundtrip(); err != nil { | |||
| 62 | + | t.Fatal(err) | |||
| 63 | + | } | |||
| 64 | + | select { | |||
| 65 | + | case got := <-p.states: | |||
| 66 | + | if got != active { | |||
| 67 | + | t.Fatalf("state %v", got) | |||
| 68 | + | } | |||
| 69 | + | return | |||
| 70 | + | case <-time.After(5 * time.Millisecond): | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | t.Fatal("no active/inactive event") | |||
| 74 | + | } | |||
| 75 | + | state(true) | |||
| 76 | + | state(false) | |||
| 77 | + | // Destroying it tells core. | |||
| 78 | + | requestProtocol(t, c, in, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1RequestDestroy) | |||
| 79 | + | if v := clientEvent[ports.ShortcutsInhibit](t, events); v.Active { | |||
| 80 | + | t.Fatal(v) | |||
| 81 | + | } | |||
| 82 | + | // A second inhibitor on the same surface is a protocol error. | |||
| 83 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat) | |||
| 84 | + | requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat) | |||
| 85 | + | expectProtocolError(t, c, mgr, uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited)) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // Idle inhibitors: several per surface, the window is reported once and | |||
| 89 | + | // released when the last one goes. | |||
| 90 | + | func TestIdleInhibitors(t *testing.T) { | |||
| 91 | + | s, events, _, _, dir := contentServer(t) | |||
| 92 | + | c := protocolClient(t, s, dir) | |||
| 93 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 94 | + | mgr := bindProtocol(t, c, "zwp_idle_inhibit_manager_v1") | |||
| 95 | + | a, b := c.AllocateID(), c.AllocateID() | |||
| 96 | + | registerProtocol(t, c, a) | |||
| 97 | + | registerProtocol(t, c, b) | |||
| 98 | + | requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, a, surf) | |||
| 99 | + | requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, b, surf) | |||
| 100 | + | if v := clientEvent[ports.IdleInhibit](t, events); v != (ports.IdleInhibit{Window: w.ID, Active: true}) { | |||
| 101 | + | t.Fatal(v) | |||
| 102 | + | } | |||
| 103 | + | requestProtocol(t, c, a, idleinhibit.ZwpIdleInhibitorV1RequestDestroy) | |||
| 104 | + | requestProtocol(t, c, b, idleinhibit.ZwpIdleInhibitorV1RequestDestroy) | |||
| 105 | + | if v := clientEvent[ports.IdleInhibit](t, events); v.Active { | |||
| 106 | + | t.Fatal(v) | |||
| 107 | + | } | |||
| 108 | + | if err := c.Roundtrip(); err != nil { | |||
| 109 | + | t.Fatal(err) | |||
| 110 | + | } | |||
| 111 | + | select { | |||
| 112 | + | case ev := <-events: | |||
| 113 | + | if _, ok := ev.(ports.IdleInhibit); ok { | |||
| 114 | + | t.Fatalf("extra %+v", ev) | |||
| 115 | + | } | |||
| 116 | + | default: | |||
| 117 | + | } | |||
| 118 | + | } | |||
M internal/adapters/wayland/layer.go +1 -1
| @@ -167,7 +167,7 @@ func (l *layerSurface) unmap() { | |||||
| 167 | l.acked = false | 167 | l.acked = false | ||
| 168 | l.serials = nil | 168 | l.serials = nil | ||
| 169 | l.shell.server.layerChanged() | 169 | l.shell.server.layerChanged() | ||
| 170 | - | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}) | 170 | + | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}, damage{full: true}) |
| 171 | } | 171 | } | ||
| 172 | 172 | ||||
| 173 | // close tells the client its output is gone; the surface stays unmapped | 173 | // close tells the client its output is gone; the surface stays unmapped | ||
A internal/adapters/wayland/presentation.go +193 -0
| @@ -0,0 +1,193 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 9 | + | "github.com/bnema/purego-libwayland/server" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // wp_presentation: a client learns when each commit reached the screen. | |||
| 14 | + | // A feedback belongs to the commit it was requested before; on commit it | |||
| 15 | + | // waits for the flip that shows its window's content (FlipInfo.Shows) | |||
| 16 | + | // and gets that flip's kernel timestamp, vblank counter and refresh. | |||
| 17 | + | // A content replaced before any flip showed it, a destroyed surface or a | |||
| 18 | + | // surface on no output is discarded. | |||
| 19 | + | ||||
| 20 | + | const presentationVersion = 2 | |||
| 21 | + | ||||
| 22 | + | // feedbackWait is a committed feedback: presented once a flip on the | |||
| 23 | + | // surface's output shows content seq of window win. | |||
| 24 | + | type feedbackWait struct { | |||
| 25 | + | fb *presentationtime.WpPresentationFeedback | |||
| 26 | + | surf *surface | |||
| 27 | + | win ports.WindowID | |||
| 28 | + | seq uint64 | |||
| 29 | + | at time.Time | |||
| 30 | + | // replaced is the window content Seq of the surface's next commit (0: | |||
| 31 | + | // none yet): a flip showing it or later shows the replacement. | |||
| 32 | + | replaced uint64 | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func registerPresentation(d *server.Display, s *Server) error { | |||
| 36 | + | return presentationtime.NewWpPresentationGlobal(d, presentationVersion, func(c server.Client, v, id uint32) { | |||
| 37 | + | r, err := presentationtime.NewWpPresentation(c, int32(v), id, presentation{s}) | |||
| 38 | + | if err == nil { | |||
| 39 | + | r.SendClockId(unix.CLOCK_MONOTONIC) | |||
| 40 | + | } | |||
| 41 | + | }) | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | type presentation struct{ server *Server } | |||
| 45 | + | ||||
| 46 | + | func (presentation) Destroy(*presentationtime.WpPresentation) {} | |||
| 47 | + | ||||
| 48 | + | // Feedback attaches a feedback to the surface's next commit. | |||
| 49 | + | func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland.Surface, id uint32) { | |||
| 50 | + | fb, err := presentationtime.NewWpPresentationFeedback(r.Client(), r.Version(), id, feedbackHandler{}) | |||
| 51 | + | if err != nil { | |||
| 52 | + | return | |||
| 53 | + | } | |||
| 54 | + | state := p.server.surfaceOf(surf) | |||
| 55 | + | if state == nil || state.destroyed { | |||
| 56 | + | fb.SendDiscarded() | |||
| 57 | + | fb.Destroy() | |||
| 58 | + | return | |||
| 59 | + | } | |||
| 60 | + | state.pendingFeedback = append(state.pendingFeedback, fb) | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | type feedbackHandler struct{} | |||
| 64 | + | ||||
| 65 | + | // Destroy is not a request of wp_presentation_feedback; the handler | |||
| 66 | + | // satisfies the generated interface. | |||
| 67 | + | func (feedbackHandler) Destroy(*presentationtime.WpPresentationFeedback) {} | |||
| 68 | + | ||||
| 69 | + | // commitFeedback moves a surface's pending feedbacks to its window's | |||
| 70 | + | // content just emitted, or discards them when the surface shows nowhere. | |||
| 71 | + | // A commit with a new buffer (fresh) replaces the surface's earlier | |||
| 72 | + | // waiting feedbacks. | |||
| 73 | + | func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedback, fresh bool) { | |||
| 74 | + | srv := s.server | |||
| 75 | + | root := s.root() | |||
| 76 | + | win := root.windowID() | |||
| 77 | + | if win != 0 && fresh { | |||
| 78 | + | srv.contentMu.Lock() | |||
| 79 | + | seq := srv.contentSeq[win] | |||
| 80 | + | srv.contentMu.Unlock() | |||
| 81 | + | for i := range srv.feedbacks { | |||
| 82 | + | if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq { | |||
| 83 | + | w.replaced = seq | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | if len(pending) == 0 { | |||
| 88 | + | return | |||
| 89 | + | } | |||
| 90 | + | output := srv.frameOutput(s) | |||
| 91 | + | if win == 0 || output == "" { | |||
| 92 | + | for _, fb := range pending { | |||
| 93 | + | discard(fb) | |||
| 94 | + | } | |||
| 95 | + | return | |||
| 96 | + | } | |||
| 97 | + | srv.contentMu.Lock() | |||
| 98 | + | seq := srv.contentSeq[win] | |||
| 99 | + | srv.contentMu.Unlock() | |||
| 100 | + | for _, fb := range pending { | |||
| 101 | + | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func discard(fb *presentationtime.WpPresentationFeedback) { | |||
| 106 | + | if fb.Resource.Alive() { | |||
| 107 | + | fb.SendDiscarded() | |||
| 108 | + | fb.Destroy() | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // presentFlip answers the feedbacks a flip of output answers: a window | |||
| 113 | + | // content at or after a feedback's is shown; the feedback is presented, | |||
| 114 | + | // or discarded when its surface committed again by then. | |||
| 115 | + | func (s *Server) presentFlip(output string, f *ports.FlipInfo) { | |||
| 116 | + | if f == nil || len(s.feedbacks) == 0 { | |||
| 117 | + | return | |||
| 118 | + | } | |||
| 119 | + | o := s.outputByNameExact(output) | |||
| 120 | + | kept := s.feedbacks[:0] | |||
| 121 | + | for _, w := range s.feedbacks { | |||
| 122 | + | shown, ok := f.Shows[w.win] | |||
| 123 | + | switch { | |||
| 124 | + | case !w.fb.Resource.Alive(): | |||
| 125 | + | case w.surf.destroyed: | |||
| 126 | + | discard(w.fb) | |||
| 127 | + | case s.frameOutput(w.surf) != output || !ok || shown < w.seq: | |||
| 128 | + | kept = append(kept, w) | |||
| 129 | + | case f.Merged > 0: | |||
| 130 | + | // The reader fell behind: which flip showed it is unknown. | |||
| 131 | + | discard(w.fb) | |||
| 132 | + | default: | |||
| 133 | + | // Shown, unless the surface's next commit is shown too. | |||
| 134 | + | s.sendPresented(w, o, f, w.replaced == 0 || shown < w.replaced) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | clear(s.feedbacks[len(kept):]) | |||
| 138 | + | s.feedbacks = kept | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // sendPresented sends presented for the flip; exact is false when a | |||
| 142 | + | // later commit of the surface replaced it before the flip (then it is | |||
| 143 | + | // discarded). | |||
| 144 | + | func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exact bool) { | |||
| 145 | + | if !exact { | |||
| 146 | + | discard(w.fb) | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | if o != nil { | |||
| 150 | + | for _, r := range o.resources { | |||
| 151 | + | if r.Resource.Alive() && r.Client() == w.fb.Client() { | |||
| 152 | + | w.fb.SendSyncOutput(r) | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 156 | + | flags := uint32(0) | |||
| 157 | + | // A software clock (headless) claims nothing about the display. | |||
| 158 | + | if !f.Async && f.HardwareClock { | |||
| 159 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync) | |||
| 160 | + | } | |||
| 161 | + | if f.HardwareClock { | |||
| 162 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion) | |||
| 163 | + | } | |||
| 164 | + | if f.ZeroCopy != 0 && f.ZeroCopy == w.win { | |||
| 165 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) | |||
| 166 | + | } | |||
| 167 | + | sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second) | |||
| 168 | + | w.fb.SendPresented(uint32(sec>>32), uint32(sec), nsec, uint32(f.Refresh), uint32(f.Seq>>32), uint32(f.Seq), flags) | |||
| 169 | + | w.fb.Destroy() | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // feedbackTimeout discards a feedback no flip answered: its window is | |||
| 173 | + | // hidden, on an output that does not flip (headless, switched away), or | |||
| 174 | + | // was never drawn. | |||
| 175 | + | const feedbackTimeout = time.Second | |||
| 176 | + | ||||
| 177 | + | // dropFeedbacks discards the feedbacks of destroyed surfaces, of outputs | |||
| 178 | + | // that are gone, and those older than feedbackTimeout. | |||
| 179 | + | func (s *Server) dropFeedbacks(now time.Time) { | |||
| 180 | + | kept := s.feedbacks[:0] | |||
| 181 | + | for _, w := range s.feedbacks { | |||
| 182 | + | if !w.fb.Resource.Alive() { | |||
| 183 | + | continue | |||
| 184 | + | } | |||
| 185 | + | if w.surf.destroyed || s.frameOutput(w.surf) == "" || now.Sub(w.at) > feedbackTimeout { | |||
| 186 | + | discard(w.fb) | |||
| 187 | + | continue | |||
| 188 | + | } | |||
| 189 | + | kept = append(kept, w) | |||
| 190 | + | } | |||
| 191 | + | clear(s.feedbacks[len(kept):]) | |||
| 192 | + | s.feedbacks = kept | |||
| 193 | + | } | |||
A internal/adapters/wayland/presentation_test.go +284 -0
| @@ -0,0 +1,284 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/nefertty/internal/logging" | |||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 12 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // presentedEvent is one feedback answer: presented (with its fields) or | |||
| 18 | + | // discarded. | |||
| 19 | + | type presentedEvent struct { | |||
| 20 | + | discarded bool | |||
| 21 | + | when time.Duration | |||
| 22 | + | refresh uint32 | |||
| 23 | + | seq uint64 | |||
| 24 | + | flags uint32 | |||
| 25 | + | syncOutputs int | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | type feedbackEvents struct { | |||
| 29 | + | wlturbo.BaseProxy | |||
| 30 | + | out chan presentedEvent | |||
| 31 | + | sync int | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (p *feedbackEvents) Dispatch(e *wlturbo.Event) { | |||
| 35 | + | switch uint32(e.Opcode) { | |||
| 36 | + | case presentationtime.WpPresentationFeedbackEventSyncOutput: | |||
| 37 | + | p.sync++ | |||
| 38 | + | case presentationtime.WpPresentationFeedbackEventPresented: | |||
| 39 | + | d := e.Data() | |||
| 40 | + | u := func(i int) uint32 { return binary.NativeEndian.Uint32(d[i*4:]) } | |||
| 41 | + | sec := uint64(u(0))<<32 | uint64(u(1)) | |||
| 42 | + | p.out <- presentedEvent{when: time.Duration(sec)*time.Second + time.Duration(u(2)), refresh: u(3), seq: uint64(u(4))<<32 | uint64(u(5)), flags: u(6), syncOutputs: p.sync} | |||
| 43 | + | case presentationtime.WpPresentationFeedbackEventDiscarded: | |||
| 44 | + | p.out <- presentedEvent{discarded: true} | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // presentServer is a server whose flips the test sends. | |||
| 49 | + | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) { | |||
| 50 | + | t.Helper() | |||
| 51 | + | dir := t.TempDir() | |||
| 52 | + | events := make(chan ports.ClientEvent, 16) | |||
| 53 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 54 | + | contents := make(chan ports.SurfaceContent, 64) | |||
| 55 | + | presented := make(chan ports.OutputPresented, 16) | |||
| 56 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland")) | |||
| 57 | + | if err != nil { | |||
| 58 | + | t.Fatal(err) | |||
| 59 | + | } | |||
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 61 | + | done := make(chan error, 1) | |||
| 62 | + | go func() { done <- s.Run(ctx) }() | |||
| 63 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 64 | + | return s, events, commands, contents, presented, dir | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func TestPresentationFeedback(t *testing.T) { | |||
| 68 | + | s, events, commands, contents, presented, dir := presentServer(t) | |||
| 69 | + | c := protocolClient(t, s, dir) | |||
| 70 | + | w, surf, xdg := surfaceMapper(t, c, events)() | |||
| 71 | + | registerProtocol(t, c, xdg) | |||
| 72 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"} | |||
| 73 | + | for deadline := time.Now().Add(2 * time.Second); ; { | |||
| 74 | + | var out string | |||
| 75 | + | s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) }) | |||
| 76 | + | if out != "" { | |||
| 77 | + | break | |||
| 78 | + | } | |||
| 79 | + | if time.Now().After(deadline) { | |||
| 80 | + | t.Fatal("configure not applied") | |||
| 81 | + | } | |||
| 82 | + | time.Sleep(2 * time.Millisecond) | |||
| 83 | + | } | |||
| 84 | + | registerProtocol(t, c, bindProtocol(t, c, "wl_output")) | |||
| 85 | + | pres := bindProtocol(t, c, "wp_presentation") | |||
| 86 | + | registerProtocol(t, c, pres) | |||
| 87 | + | // Drain contents so seqs are known. | |||
| 88 | + | lastSeq := func() uint64 { | |||
| 89 | + | t.Helper() | |||
| 90 | + | var seq uint64 | |||
| 91 | + | deadline := time.After(2 * time.Second) | |||
| 92 | + | for { | |||
| 93 | + | select { | |||
| 94 | + | case got := <-contents: | |||
| 95 | + | if got.ID == w.ID { | |||
| 96 | + | seq = got.Seq | |||
| 97 | + | } | |||
| 98 | + | case <-time.After(50 * time.Millisecond): | |||
| 99 | + | if seq != 0 { | |||
| 100 | + | return seq | |||
| 101 | + | } | |||
| 102 | + | case <-deadline: | |||
| 103 | + | t.Fatal("no content") | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | feedback := func() *feedbackEvents { | |||
| 108 | + | t.Helper() | |||
| 109 | + | fb := c.AllocateID() | |||
| 110 | + | p := &feedbackEvents{out: make(chan presentedEvent, 4)} | |||
| 111 | + | p.SetID(fb) | |||
| 112 | + | c.Context().Register(p) | |||
| 113 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb) | |||
| 114 | + | return p | |||
| 115 | + | } | |||
| 116 | + | answer := func(p *feedbackEvents, what string) presentedEvent { | |||
| 117 | + | t.Helper() | |||
| 118 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 119 | + | for time.Now().Before(deadline) { | |||
| 120 | + | if err := c.Roundtrip(); err != nil { | |||
| 121 | + | t.Fatal(err) | |||
| 122 | + | } | |||
| 123 | + | select { | |||
| 124 | + | case ev := <-p.out: | |||
| 125 | + | return ev | |||
| 126 | + | case <-time.After(5 * time.Millisecond): | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | t.Fatalf("no answer: %s", what) | |||
| 130 | + | return presentedEvent{} | |||
| 131 | + | } | |||
| 132 | + | // A fresh 1×1 buffer per commit: every commit is a new content. | |||
| 133 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 134 | + | registerProtocol(t, c, shm) | |||
| 135 | + | fd, err := unix.MemfdCreate("present", 0) | |||
| 136 | + | if err != nil { | |||
| 137 | + | t.Fatal(err) | |||
| 138 | + | } | |||
| 139 | + | defer unix.Close(fd) | |||
| 140 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 141 | + | t.Fatal(err) | |||
| 142 | + | } | |||
| 143 | + | pool := c.AllocateID() | |||
| 144 | + | registerProtocol(t, c, pool) | |||
| 145 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |||
| 146 | + | t.Fatal(err) | |||
| 147 | + | } | |||
| 148 | + | commit := func() { | |||
| 149 | + | buf := c.AllocateID() | |||
| 150 | + | registerProtocol(t, c, buf) | |||
| 151 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 152 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 153 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 154 | + | if err := c.Roundtrip(); err != nil { | |||
| 155 | + | t.Fatal(err) | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) { | |||
| 159 | + | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | |||
| 160 | + | } | |||
| 161 | + | // Presented with the flip's time, counter and flags. | |||
| 162 | + | a := feedback() | |||
| 163 | + | commit() | |||
| 164 | + | seqA := lastSeq() | |||
| 165 | + | flip(5*time.Second+7, 42, seqA, w.ID) | |||
| 166 | + | ev := answer(a, "presented") | |||
| 167 | + | if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 { | |||
| 168 | + | t.Fatalf("presented %+v", ev) | |||
| 169 | + | } | |||
| 170 | + | want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy) | |||
| 171 | + | if ev.flags != want { | |||
| 172 | + | t.Fatalf("flags %#x want %#x", ev.flags, want) | |||
| 173 | + | } | |||
| 174 | + | // Two flips in one drain: two presented events, each with its time. | |||
| 175 | + | b := feedback() | |||
| 176 | + | commit() | |||
| 177 | + | seqB := lastSeq() | |||
| 178 | + | d := feedback() | |||
| 179 | + | commit() | |||
| 180 | + | seqD := lastSeq() | |||
| 181 | + | flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay | |||
| 182 | + | flip(7*time.Second, 44, seqD, 0) | |||
| 183 | + | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 { | |||
| 184 | + | t.Fatalf("first %+v", ev) | |||
| 185 | + | } | |||
| 186 | + | if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 { | |||
| 187 | + | t.Fatalf("second %+v", ev) | |||
| 188 | + | } | |||
| 189 | + | // Replaced before shown: discarded when the flip shows the newer one. | |||
| 190 | + | old := feedback() | |||
| 191 | + | commit() | |||
| 192 | + | lastSeq() | |||
| 193 | + | commit() | |||
| 194 | + | seqNew := lastSeq() | |||
| 195 | + | flip(8*time.Second, 45, seqNew, 0) | |||
| 196 | + | if ev := answer(old, "replaced"); !ev.discarded { | |||
| 197 | + | t.Fatalf("replaced: %+v", ev) | |||
| 198 | + | } | |||
| 199 | + | // A subsurface's feedback follows its root: presented with the root's | |||
| 200 | + | // content that the flip shows. | |||
| 201 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 202 | + | sub := bindProtocol(t, c, "wl_subcompositor") | |||
| 203 | + | child, subsurf := c.AllocateID(), c.AllocateID() | |||
| 204 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |||
| 205 | + | registerProtocol(t, c, child) | |||
| 206 | + | requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf) | |||
| 207 | + | registerProtocol(t, c, subsurf) | |||
| 208 | + | cfb := c.AllocateID() | |||
| 209 | + | cp := &feedbackEvents{out: make(chan presentedEvent, 1)} | |||
| 210 | + | cp.SetID(cfb) | |||
| 211 | + | c.Context().Register(cp) | |||
| 212 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb) | |||
| 213 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 214 | + | commit() // the parent applies the subsurface | |||
| 215 | + | seqC := lastSeq() | |||
| 216 | + | flip(8500*time.Millisecond, 50, seqC, 0) | |||
| 217 | + | if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 { | |||
| 218 | + | t.Fatalf("subsurface %+v", ev) | |||
| 219 | + | } | |||
| 220 | + | // Surface destroyed: discarded. | |||
| 221 | + | gone := feedback() | |||
| 222 | + | commit() | |||
| 223 | + | lastSeq() | |||
| 224 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | |||
| 225 | + | flip(9*time.Second, 46, 0, 0) | |||
| 226 | + | if ev := answer(gone, "destroyed"); !ev.discarded { | |||
| 227 | + | t.Fatalf("destroyed: %+v", ev) | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // A hidden window (no output) gets discarded at once. | |||
| 232 | + | func TestPresentationHiddenDiscarded(t *testing.T) { | |||
| 233 | + | s, events, _, _, _, dir := presentServer(t) | |||
| 234 | + | c := protocolClient(t, s, dir) | |||
| 235 | + | _, surf, _ := surfaceMapper(t, c, events)() | |||
| 236 | + | pres := bindProtocol(t, c, "wp_presentation") | |||
| 237 | + | registerProtocol(t, c, pres) | |||
| 238 | + | fb := c.AllocateID() | |||
| 239 | + | p := &feedbackEvents{out: make(chan presentedEvent, 1)} | |||
| 240 | + | p.SetID(fb) | |||
| 241 | + | c.Context().Register(p) | |||
| 242 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb) | |||
| 243 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 244 | + | if err := c.Roundtrip(); err != nil { | |||
| 245 | + | t.Fatal(err) | |||
| 246 | + | } | |||
| 247 | + | if err := c.Roundtrip(); err != nil { | |||
| 248 | + | t.Fatal(err) | |||
| 249 | + | } | |||
| 250 | + | select { | |||
| 251 | + | case ev := <-p.out: | |||
| 252 | + | if !ev.discarded { | |||
| 253 | + | t.Fatalf("%+v", ev) | |||
| 254 | + | } | |||
| 255 | + | default: | |||
| 256 | + | t.Fatal("not discarded") | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | // Commit timing: a commit targeted two refreshes ahead of the last flip | |||
| 261 | + | // is not applied on the next one. | |||
| 262 | + | func TestNextRefreshFollowsFlips(t *testing.T) { | |||
| 263 | + | s := &Server{flips: map[string]ports.FlipInfo{}} | |||
| 264 | + | s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}} | |||
| 265 | + | now := time.Now() | |||
| 266 | + | mono := monotonic(now) | |||
| 267 | + | period := time.Second / 60 | |||
| 268 | + | s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period} | |||
| 269 | + | next := s.nextRefresh("A", now) | |||
| 270 | + | if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond { | |||
| 271 | + | t.Fatalf("next refresh in %v, want %v", d, period*3/4) | |||
| 272 | + | } | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | // Software flips (headless) answer feedback without flags and do not pace | |||
| 276 | + | // frame callbacks. | |||
| 277 | + | func TestSoftwareFlipsDoNotPace(t *testing.T) { | |||
| 278 | + | if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) { | |||
| 279 | + | t.Fatal("software flip paces callbacks") | |||
| 280 | + | } | |||
| 281 | + | if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) { | |||
| 282 | + | t.Fatal("display flip does not pace") | |||
| 283 | + | } | |||
| 284 | + | } | |||
M internal/adapters/wayland/release.go +39 -11
| @@ -21,7 +21,10 @@ import ( | |||||
| 21 | const heldTimeout = 100 * time.Millisecond | 21 | const heldTimeout = 100 * time.Millisecond | ||
| 22 | 22 | ||||
| 23 | type heldBuffer struct { | 23 | type heldBuffer struct { | ||
| 24 | - | res *wayland.Buffer | 24 | + | res *wayland.Buffer |
| 25 | + | // sync is the buffer's explicit-sync state: its release point is | |||
| 26 | + | // signalled instead of wl_buffer.release. | |||
| 27 | + | sync syncHold | |||
| 25 | window ports.WindowID | 28 | window ports.WindowID | ||
| 26 | id uint64 // DMABuf ID, 0 for wl_shm | 29 | id uint64 // DMABuf ID, 0 for wl_shm | ||
| 27 | after uint64 // the window's content Seq when the buffer was replaced | 30 | after uint64 // the window's content Seq when the buffer was replaced | ||
| @@ -30,17 +33,14 @@ type heldBuffer struct { | |||||
| 30 | 33 | ||||
| 31 | // releaseBuffer releases a replaced buffer, or holds it until the output | 34 | // releaseBuffer releases a replaced buffer, or holds it until the output | ||
| 32 | // showing its window no longer reads it. | 35 | // showing its window no longer reads it. | ||
| 33 | - | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | 36 | + | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) { |
| 34 | - | if !b.Resource.Alive() { | |||
| 35 | - | return | |||
| 36 | - | } | |||
| 37 | window, name := surf.root().windowID(), s.frameOutput(surf) | 37 | window, name := surf.root().windowID(), s.frameOutput(surf) | ||
| 38 | - | if window == 0 || name == "" { | 38 | + | if !b.Resource.Alive() || window == 0 || name == "" { |
| 39 | // Not drawn (unmapped, hidden, a cursor): nothing reads it. | 39 | // Not drawn (unmapped, hidden, a cursor): nothing reads it. | ||
| 40 | - | b.SendRelease() | 40 | + | s.release(b, sync) |
| 41 | return | 41 | return | ||
| 42 | } | 42 | } | ||
| 43 | - | h := heldBuffer{res: b, window: window, at: time.Now()} | 43 | + | h := heldBuffer{res: b, sync: sync, window: window, at: time.Now()} |
| 44 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | 44 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | ||
| 45 | h.id = d.buf.ID | 45 | h.id = d.buf.ID | ||
| 46 | } | 46 | } | ||
| @@ -55,6 +55,36 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||||
| 55 | } | 55 | } | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | + | // release gives a buffer back: its release point is signalled when it | |||
| 59 | + | // has one (explicit sync), else wl_buffer.release. | |||
| 60 | + | func (s *Server) release(b *wayland.Buffer, sync syncHold) { | |||
| 61 | + | if sync.release.set() || sync.acquire != nil { | |||
| 62 | + | s.releaseSync(sync) | |||
| 63 | + | return | |||
| 64 | + | } | |||
| 65 | + | if b != nil && b.Resource.Alive() { | |||
| 66 | + | b.SendRelease() | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // releaseBufferSync holds only a buffer's explicit-sync points: the same | |||
| 71 | + | // buffer committed again with new points (clients reuse one buffer). | |||
| 72 | + | func (s *Server) releaseBufferSync(surf *surface, sync syncHold) { | |||
| 73 | + | if !sync.release.set() && sync.acquire == nil { | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | window := surf.root().windowID() | |||
| 77 | + | if window == 0 || s.frameOutput(surf) == "" { | |||
| 78 | + | s.releaseSync(sync) | |||
| 79 | + | return | |||
| 80 | + | } | |||
| 81 | + | s.contentMu.Lock() | |||
| 82 | + | after := s.contentSeq[window] | |||
| 83 | + | s.contentMu.Unlock() | |||
| 84 | + | s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()}) | |||
| 85 | + | s.wakePacer() | |||
| 86 | + | } | |||
| 87 | + | ||||
| 58 | // releaseHeld records the output reports and releases the buffers no | 88 | // releaseHeld records the output reports and releases the buffers no | ||
| 59 | // output reads. Every output reads every window's content, so a buffer | 89 | // output reads. Every output reads every window's content, so a buffer | ||
| 60 | // waits until no live output shows or queues it and all of them have seen | 90 | // waits until no live output shows or queues it and all of them have seen | ||
| @@ -79,9 +109,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo | |||||
| 79 | kept = append(kept, h) | 109 | kept = append(kept, h) | ||
| 80 | continue | 110 | continue | ||
| 81 | } | 111 | } | ||
| 82 | - | if h.res.Resource.Alive() { | 112 | + | s.release(h.res, h.sync) |
| 83 | - | h.res.SendRelease() | |||
| 84 | - | } | |||
| 85 | } | 113 | } | ||
| 86 | clear(s.held[len(kept):]) | 114 | clear(s.held[len(kept):]) | ||
| 87 | s.held = kept | 115 | s.held = kept | ||
M internal/adapters/wayland/server.go +117 -15
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | ||
| 8 | "os" | 8 | "os" | ||
| 9 | + | "slices" | |||
| 9 | "sync" | 10 | "sync" | ||
| 10 | "time" | 11 | "time" | ||
| 11 | 12 | ||||
| @@ -23,9 +24,15 @@ type Options struct { | |||||
| 23 | RuntimeDir string | 24 | RuntimeDir string | ||
| 24 | Outputs ports.Layout | 25 | Outputs ports.Layout | ||
| 25 | // DMABuf is what the renderer imports; empty disables linux-dmabuf. | 26 | // DMABuf is what the renderer imports; empty disables linux-dmabuf. | ||
| 26 | - | DMABuf ports.DMABufSupport | 27 | + | DMABuf ports.DMABufSupport |
| 28 | + | // SyncobjNode is the render node (/dev/dri/renderD*) timeline | |||
| 29 | + | // syncobjs are imported on; empty or without timeline support, no | |||
| 30 | + | // explicit sync is offered. | |||
| 31 | + | SyncobjNode string | |||
| 27 | Keymap string | 32 | Keymap string | ||
| 28 | RepeatRate, RepeatDelay int | 33 | RepeatRate, RepeatDelay int | ||
| 34 | + | // syncDev replaces the render node's syncobj interface (tests). | |||
| 35 | + | syncDev syncobjDevice | |||
| 29 | } | 36 | } | ||
| 30 | 37 | ||||
| 31 | // Channels carries client notifications and commands. Events may be unbuffered. | 38 | // Channels carries client notifications and commands. Events may be unbuffered. | ||
| @@ -38,6 +45,9 @@ type Channels struct { | |||||
| 38 | // Presented paces frame callbacks on the outputs' page flips; outputs | 45 | // Presented paces frame callbacks on the outputs' page flips; outputs | ||
| 39 | // that do not flip (idle, headless) are paced at their refresh rate. | 46 | // that do not flip (idle, headless) are paced at their refresh rate. | ||
| 40 | Presented <-chan ports.OutputPresented | 47 | Presented <-chan ports.OutputPresented | ||
| 48 | + | // OutputFormats are the outputs' direct scanout formats, offered in | |||
| 49 | + | // dmabuf feedback to fullscreen surfaces. | |||
| 50 | + | OutputFormats <-chan ports.OutputFormats | |||
| 41 | } | 51 | } | ||
| 42 | type Server struct { | 52 | type Server struct { | ||
| 43 | display *server.Display | 53 | display *server.Display | ||
| @@ -98,17 +108,32 @@ type Server struct { | |||||
| 98 | // Events, which happens only at shutdown (core owns the receiving end). A | 108 | // Events, which happens only at shutdown (core owns the receiving end). A | ||
| 99 | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | 109 | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | ||
| 100 | // the display goroutine. | 110 | // the display goroutine. | ||
| 101 | - | eventMu sync.Mutex | 111 | + | eventMu sync.Mutex |
| 102 | - | events []ports.ClientEvent | 112 | + | events []ports.ClientEvent |
| 103 | - | eventReady chan struct{} | 113 | + | eventReady chan struct{} |
| 104 | - | contentMu sync.Mutex | 114 | + | contentMu sync.Mutex |
| 105 | - | contents map[ports.WindowID]ports.SurfaceContent | 115 | + | contents map[ports.WindowID]ports.SurfaceContent |
| 106 | - | contentSeq map[ports.WindowID]uint64 | 116 | + | contentSeq map[ports.WindowID]uint64 |
| 107 | - | contentNotify chan struct{} | 117 | + | // damage is each window's recent damage (contentMu). |
| 108 | - | contentReady chan struct{} | 118 | + | damage map[ports.WindowID][]ports.SeqDamage |
| 109 | - | outputs []*output | 119 | + | // feedbacks wait for the flip that shows their content (presentation.go). |
| 110 | - | focusedOutput string | 120 | + | feedbacks []feedbackWait |
| 111 | - | fractions map[*surface]*fractionalscale.WpFractionalScaleV1 | 121 | + | // Explicit sync (syncobj.go): the render node, imported timelines by |
| 122 | + | // resource, and the acquire point waiter. | |||
| 123 | + | syncDev syncobjDevice | |||
| 124 | + | timelines map[*server.Resource]*timeline | |||
| 125 | + | syncWait *syncWaiter | |||
| 126 | + | // flips is each output's latest flip (pacer, under Do). | |||
| 127 | + | flips map[string]ports.FlipInfo | |||
| 128 | + | // inhibitors are the live inhibitor objects (inhibit.go); | |||
| 129 | + | // shortcutWindows and idleWindows what core was told. | |||
| 130 | + | inhibitors []*inhibitor | |||
| 131 | + | shortcutWindows, idleWindows map[ports.WindowID]bool | |||
| 132 | + | contentNotify chan struct{} | |||
| 133 | + | contentReady chan struct{} | |||
| 134 | + | outputs []*output | |||
| 135 | + | focusedOutput string | |||
| 136 | + | fractions map[*surface]*fractionalscale.WpFractionalScaleV1 | |||
| 112 | // cursorSurface is the wl_pointer.set_cursor surface in use. | 137 | // cursorSurface is the wl_pointer.set_cursor surface in use. | ||
| 113 | cursorSurface *surface | 138 | cursorSurface *surface | ||
| 114 | // cursorMu guards only the latest cursor change for forwardCursors. | 139 | // cursorMu guards only the latest cursor change for forwardCursors. | ||
| @@ -159,9 +184,30 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 159 | d.Close() | 184 | d.Close() | ||
| 160 | return nil, err | 185 | return nil, err | ||
| 161 | } | 186 | } | ||
| 162 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 187 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} |
| 163 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 188 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 164 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | 189 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | ||
| 190 | + | s.flips = map[string]ports.FlipInfo{} | |||
| 191 | + | s.timelines = map[*server.Resource]*timeline{} | |||
| 192 | + | var node *renderNode | |||
| 193 | + | if opts.syncDev != nil { | |||
| 194 | + | s.syncDev = opts.syncDev | |||
| 195 | + | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |||
| 196 | + | cleanup() | |||
| 197 | + | d.Close() | |||
| 198 | + | return nil, err | |||
| 199 | + | } | |||
| 200 | + | } else if opts.SyncobjNode != "" { | |||
| 201 | + | if node, err = openSyncobj(opts.SyncobjNode); err != nil { | |||
| 202 | + | log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off") | |||
| 203 | + | node = nil | |||
| 204 | + | } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |||
| 205 | + | node.close() | |||
| 206 | + | node = nil | |||
| 207 | + | } else { | |||
| 208 | + | s.syncDev = node | |||
| 209 | + | } | |||
| 210 | + | } | |||
| 165 | s.tokens = map[string]activationToken{} | 211 | s.tokens = map[string]activationToken{} | ||
| 166 | if opts.Keymap != "" { | 212 | if opts.Keymap != "" { | ||
| 167 | fd, size, e := keymapFile(opts.Keymap) | 213 | fd, size, e := keymapFile(opts.Keymap) | ||
| @@ -177,6 +223,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 177 | unix.Close(s.keymapFD) | 223 | unix.Close(s.keymapFD) | ||
| 178 | } | 224 | } | ||
| 179 | s.dmabuf.close() | 225 | s.dmabuf.close() | ||
| 226 | + | if s.syncWait != nil { | |||
| 227 | + | s.syncWait.close() | |||
| 228 | + | } | |||
| 229 | + | if node != nil { | |||
| 230 | + | node.close() | |||
| 231 | + | } | |||
| 180 | cleanup() | 232 | cleanup() | ||
| 181 | } | 233 | } | ||
| 182 | if err = registerGlobals(d, opts, s); err != nil { | 234 | if err = registerGlobals(d, opts, s); err != nil { | ||
| @@ -196,7 +248,7 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 196 | s.started = time.Now() | 248 | s.started = time.Now() | ||
| 197 | s.ctx = ctx | 249 | s.ctx = ctx | ||
| 198 | var wg sync.WaitGroup | 250 | var wg sync.WaitGroup | ||
| 199 | - | wg.Add(5) | 251 | + | wg.Add(6) |
| 200 | go func() { defer wg.Done(); s.forward(ctx) }() | 252 | go func() { defer wg.Done(); s.forward(ctx) }() | ||
| 201 | go func() { defer wg.Done(); s.forwardCursors(ctx) }() | 253 | go func() { defer wg.Done(); s.forwardCursors(ctx) }() | ||
| 202 | go func() { defer wg.Done(); s.forwardContents(ctx) }() | 254 | go func() { defer wg.Done(); s.forwardContents(ctx) }() | ||
| @@ -233,11 +285,44 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 233 | } | 285 | } | ||
| 234 | }() | 286 | }() | ||
| 235 | go func() { defer wg.Done(); s.pace(ctx) }() | 287 | go func() { defer wg.Done(); s.pace(ctx) }() | ||
| 288 | + | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | |||
| 289 | + | if s.syncWait != nil { | |||
| 290 | + | wg.Add(1) | |||
| 291 | + | go func() { defer wg.Done(); s.syncWait.run(ctx) }() | |||
| 292 | + | } | |||
| 236 | err := s.display.Run(ctx) | 293 | err := s.display.Run(ctx) | ||
| 237 | wg.Wait() | 294 | wg.Wait() | ||
| 238 | return err | 295 | return err | ||
| 239 | } | 296 | } | ||
| 240 | 297 | ||||
| 298 | + | // damageHistory is how many contents a window's damage history covers: | |||
| 299 | + | // enough for a renderer target a few frames old. | |||
| 300 | + | const damageHistory = 4 | |||
| 301 | + | ||||
| 302 | + | // forwardOutputFormats applies the outputs' scanout formats on the | |||
| 303 | + | // display goroutine. | |||
| 304 | + | func (s *Server) forwardOutputFormats(ctx context.Context) { | |||
| 305 | + | for { | |||
| 306 | + | select { | |||
| 307 | + | case <-ctx.Done(): | |||
| 308 | + | return | |||
| 309 | + | case <-s.display.Stopped(): | |||
| 310 | + | return | |||
| 311 | + | case f, ok := <-s.channels.OutputFormats: | |||
| 312 | + | if !ok { | |||
| 313 | + | return | |||
| 314 | + | } | |||
| 315 | + | if !s.display.Do(func() { | |||
| 316 | + | if s.dmabuf != nil { | |||
| 317 | + | s.dmabuf.setOutputFormats(f) | |||
| 318 | + | } | |||
| 319 | + | }) { | |||
| 320 | + | return | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | } | |||
| 324 | + | } | |||
| 325 | + | ||||
| 241 | func (s *Server) emit(ev ports.ClientEvent) { | 326 | func (s *Server) emit(ev ports.ClientEvent) { | ||
| 242 | if s.channels.Events == nil { | 327 | if s.channels.Events == nil { | ||
| 243 | return | 328 | return | ||
| @@ -275,13 +360,24 @@ func (s *Server) forward(ctx context.Context) { | |||||
| 275 | } | 360 | } | ||
| 276 | } | 361 | } | ||
| 277 | 362 | ||||
| 278 | - | func (s *Server) emitContent(c ports.SurfaceContent) { | 363 | + | func (s *Server) emitContent(c ports.SurfaceContent, d damage) { |
| 279 | if s.channels.Contents == nil { | 364 | if s.channels.Contents == nil { | ||
| 280 | return | 365 | return | ||
| 281 | } | 366 | } | ||
| 282 | s.contentMu.Lock() | 367 | s.contentMu.Lock() | ||
| 283 | s.contentSeq[c.ID]++ | 368 | s.contentSeq[c.ID]++ | ||
| 284 | c.Seq = s.contentSeq[c.ID] | 369 | c.Seq = s.contentSeq[c.ID] | ||
| 370 | + | // The window's damage history: its last damageHistory contents. | |||
| 371 | + | h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}) | |||
| 372 | + | if len(h) > damageHistory { | |||
| 373 | + | h = h[len(h)-damageHistory:] | |||
| 374 | + | } | |||
| 375 | + | if c.Empty() { | |||
| 376 | + | delete(s.damage, c.ID) | |||
| 377 | + | } else { | |||
| 378 | + | s.damage[c.ID] = h | |||
| 379 | + | } | |||
| 380 | + | c.DamageHistory = slices.Clone(h) | |||
| 285 | s.contents[c.ID] = c | 381 | s.contents[c.ID] = c | ||
| 286 | close(s.contentNotify) | 382 | close(s.contentNotify) | ||
| 287 | s.contentNotify = make(chan struct{}) | 383 | s.contentNotify = make(chan struct{}) | ||
| @@ -473,8 +569,12 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 473 | return | 569 | return | ||
| 474 | } | 570 | } | ||
| 475 | s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure") | 571 | s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure") | ||
| 572 | + | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output | |||
| 476 | w.last, w.hasLast = c, true | 573 | w.last, w.hasLast = c, true | ||
| 477 | w.sendConfigure() | 574 | w.sendConfigure() | ||
| 575 | + | if scanoutChanged && w.xdg.surface != nil { | |||
| 576 | + | s.dmabuf.resendSurface(w.xdg.surface) | |||
| 577 | + | } | |||
| 478 | if surf := w.xdg.surface; surf != nil && c.Output != "" { | 578 | if surf := w.xdg.surface; surf != nil && c.Output != "" { | ||
| 479 | surf.sendTreeScale() | 579 | surf.sendTreeScale() | ||
| 480 | } | 580 | } | ||
| @@ -482,6 +582,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 482 | s.setOutputs(c) | 582 | s.setOutputs(c) | ||
| 483 | case ports.SlotsPending: | 583 | case ports.SlotsPending: | ||
| 484 | s.slotsPending = c.Pending | 584 | s.slotsPending = c.Pending | ||
| 585 | + | case ports.ShortcutsInhibitState: | |||
| 586 | + | s.setShortcutsInhibit(c) | |||
| 485 | case ports.CloseWindow: | 587 | case ports.CloseWindow: | ||
| 486 | w := s.windows[c.ID] | 588 | w := s.windows[c.ID] | ||
| 487 | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | 589 | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | ||
M internal/adapters/wayland/surface.go +126 -11
| @@ -1,6 +1,8 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 5 | + | "image" | |||
| 4 | "time" | 6 | "time" | ||
| 5 | 7 | ||||
| 6 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| @@ -60,8 +62,36 @@ type surface struct { | |||||
| 60 | // contentType is the wp_content_type_v1; contentKind the committed type. | 62 | // contentType is the wp_content_type_v1; contentKind the committed type. | ||
| 61 | contentType *contentTypeHandler | 63 | contentType *contentTypeHandler | ||
| 62 | contentKind, pendingKind uint32 | 64 | contentKind, pendingKind uint32 | ||
| 65 | + | // Damage requested since the last commit: in surface (logical) and | |||
| 66 | + | // buffer pixels; committed is what the last commit changed, in buffer | |||
| 67 | + | // pixels (full: everything), read by the window's damage history. | |||
| 68 | + | pendingDamage, pendingBufDamage []ports.Rect | |||
| 69 | + | committed damage | |||
| 70 | + | // pendingFeedback are wp_presentation feedbacks for the next commit. | |||
| 71 | + | pendingFeedback []*presentationtime.WpPresentationFeedback | |||
| 72 | + | // Explicit sync (syncobj.go): the surface's syncobj object; the | |||
| 73 | + | // committed points of the pending commit (pendingSync, with the wait | |||
| 74 | + | // for its acquire point); and the current buffer's hold. | |||
| 75 | + | sync *syncState | |||
| 76 | + | pendingSync *commitSync | |||
| 77 | + | hold syncHold | |||
| 63 | } | 78 | } | ||
| 64 | 79 | ||||
| 80 | + | // commitSync is a commit's explicit-sync points and acquire wait. | |||
| 81 | + | type commitSync struct { | |||
| 82 | + | acquire, release syncPoint | |||
| 83 | + | wait *syncWait | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // damage is what one commit changed in a surface's buffer. | |||
| 87 | + | type damage struct { | |||
| 88 | + | full bool | |||
| 89 | + | rects []ports.Rect | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // maxDamageRects bounds one commit's rects: past it the commit is full. | |||
| 93 | + | const maxDamageRects = 32 | |||
| 94 | + | ||||
| 65 | // subState is the subsurface tree: parent is set on subsurfaces, children | 95 | // subState is the subsurface tree: parent is set on subsurfaces, children | ||
| 66 | // are ordered bottom to top, below marks children under their parent. | 96 | // are ordered bottom to top, below marks children under their parent. | ||
| 67 | type subState struct { | 97 | type subState struct { | ||
| @@ -127,11 +157,16 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||||
| 127 | } | 157 | } | ||
| 128 | } | 158 | } | ||
| 129 | 159 | ||||
| 130 | - | // redraw sends the tree of a mapped root to the outputs. | 160 | + | // redraw sends the tree of a mapped root to the outputs. Only a commit |
| 161 | + | // of the root's own buffer with no other change keeps partial damage. | |||
| 131 | func (s *surface) redraw() { | 162 | func (s *surface) redraw() { | ||
| 132 | r := s.root() | 163 | r := s.root() | ||
| 133 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | 164 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | ||
| 134 | - | s.server.emitContent(r.tree(id)) | 165 | + | d := damage{full: true} |
| 166 | + | if s == r && len(r.sub.children) == 0 { | |||
| 167 | + | d = r.committed | |||
| 168 | + | } | |||
| 169 | + | s.server.emitContent(r.tree(id), d) | |||
| 135 | } | 170 | } | ||
| 136 | } | 171 | } | ||
| 137 | 172 | ||||
| @@ -154,6 +189,10 @@ func (s *surface) detach() { | |||||
| 154 | 189 | ||||
| 155 | func (s *surface) Destroy(*wayland.Surface) { | 190 | func (s *surface) Destroy(*wayland.Surface) { | ||
| 156 | s.destroyed = true | 191 | s.destroyed = true | ||
| 192 | + | for _, fb := range s.pendingFeedback { | |||
| 193 | + | discard(fb) | |||
| 194 | + | } | |||
| 195 | + | s.pendingFeedback = nil | |||
| 157 | s.tearing = nil // the control becomes inert | 196 | s.tearing = nil // the control becomes inert | ||
| 158 | s.dropQueue() | 197 | s.dropQueue() | ||
| 159 | if s.server.cursorSurface == s { | 198 | if s.server.cursorSurface == s { | ||
| @@ -171,8 +210,11 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 171 | } | 210 | } | ||
| 172 | s.callbacks = nil | 211 | s.callbacks = nil | ||
| 173 | // The client may reuse the buffer on another surface. | 212 | // The client may reuse the buffer on another surface. | ||
| 213 | + | s.dropSync(s.pendingSync) | |||
| 214 | + | s.pendingSync = nil | |||
| 174 | if s.current != nil { | 215 | if s.current != nil { | ||
| 175 | - | s.server.releaseBuffer(s, s.current) | 216 | + | s.server.releaseBuffer(s, s.current, s.hold) |
| 217 | + | s.hold = syncHold{} | |||
| 176 | } | 218 | } | ||
| 177 | s.current, s.pending = nil, nil | 219 | s.current, s.pending = nil, nil | ||
| 178 | if s.role != nil { | 220 | if s.role != nil { | ||
| @@ -201,7 +243,12 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 201 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | 243 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | ||
| 202 | return | 244 | return | ||
| 203 | } | 245 | } | ||
| 204 | - | // A commit behind a fifo barrier or a future timestamp waits (fifo.go). | 246 | + | if !s.checkSyncCommit() { |
| 247 | + | return | |||
| 248 | + | } | |||
| 249 | + | s.takeSyncPoints() | |||
| 250 | + | // A commit behind a fifo barrier, a future timestamp or an acquire | |||
| 251 | + | // point without a fence yet waits (fifo.go). | |||
| 205 | if s.mustWait(time.Now()) { | 252 | if s.mustWait(time.Now()) { | ||
| 206 | s.queueUpdate() | 253 | s.queueUpdate() | ||
| 207 | return | 254 | return | ||
| @@ -229,14 +276,22 @@ func (s *surface) applyCommit() { | |||||
| 229 | if s.viewport != nil { | 276 | if s.viewport != nil { | ||
| 230 | s.viewport.commit() | 277 | s.viewport.commit() | ||
| 231 | } | 278 | } | ||
| 279 | + | cs := s.pendingSync | |||
| 280 | + | s.pendingSync = nil | |||
| 232 | if s.attached { | 281 | if s.attached { | ||
| 233 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | 282 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | ||
| 234 | - | s.server.releaseBuffer(s, s.current) | 283 | + | s.server.releaseBuffer(s, s.current, s.hold) |
| 284 | + | } else if cs != nil || s.pending == nil { | |||
| 285 | + | // Same buffer again, or detached: the old points are done | |||
| 286 | + | // with once the new ones take over. | |||
| 287 | + | s.server.releaseBufferSync(s, s.hold) | |||
| 235 | } | 288 | } | ||
| 289 | + | s.hold = syncHold{} | |||
| 236 | s.current = s.pending | 290 | s.current = s.pending | ||
| 237 | s.pending = nil | 291 | s.pending = nil | ||
| 238 | s.attached = false | 292 | s.attached = false | ||
| 239 | } | 293 | } | ||
| 294 | + | s.applySync(cs) | |||
| 240 | if len(s.callbacks) > 0 { | 295 | if len(s.callbacks) > 0 { | ||
| 241 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | 296 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | ||
| 242 | } | 297 | } | ||
| @@ -248,10 +303,13 @@ func (s *surface) applyCommit() { | |||||
| 248 | if state, ok := s.server.buffers[s.current.Resource]; ok { | 303 | if state, ok := s.server.buffers[s.current.Resource]; ok { | ||
| 249 | if c, ok := state.content(0); ok { | 304 | if c, ok := state.content(0); ok { | ||
| 250 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | 305 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | ||
| 306 | + | resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH | |||
| 307 | + | s.commitDamage(true, resized, c.Width, c.Height) | |||
| 251 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | 308 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | ||
| 252 | s.lastW, s.lastH = c.Width, c.Height | 309 | s.lastW, s.lastH = c.Width, c.Height | ||
| 253 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | 310 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | ||
| 254 | } | 311 | } | ||
| 312 | + | c.Acquire = s.hold.acquire | |||
| 255 | s.content, s.has = c, true | 313 | s.content, s.has = c, true | ||
| 256 | } else if shm, ok := state.(*buffer); ok { | 314 | } else if shm, ok := state.(*buffer); ok { | ||
| 257 | shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | 315 | shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | ||
| @@ -260,6 +318,9 @@ func (s *surface) applyCommit() { | |||||
| 260 | // replaces them. | 318 | // replaces them. | ||
| 261 | } | 319 | } | ||
| 262 | } | 320 | } | ||
| 321 | + | if !fresh { | |||
| 322 | + | s.commitDamage(false, false, 0, 0) | |||
| 323 | + | } | |||
| 263 | if s.current == nil { | 324 | if s.current == nil { | ||
| 264 | s.content, s.has = ports.SurfaceContent{}, false | 325 | s.content, s.has = ports.SurfaceContent{}, false | ||
| 265 | } | 326 | } | ||
| @@ -279,15 +340,69 @@ func (s *surface) applyCommit() { | |||||
| 279 | if s.xdg != nil && s.xdg.window != nil { | 340 | if s.xdg != nil && s.xdg.window != nil { | ||
| 280 | s.xdg.window.afterCommit() | 341 | s.xdg.window.afterCommit() | ||
| 281 | } | 342 | } | ||
| 282 | - | if fresh || moved || geometry || hinted || s.sub.parent != nil { | 343 | + | drawn := fresh || moved || geometry || hinted || s.sub.parent != nil |
| 344 | + | if drawn { | |||
| 345 | + | if moved || geometry { | |||
| 346 | + | s.committed = damage{full: true} | |||
| 347 | + | } | |||
| 283 | s.redraw() | 348 | s.redraw() | ||
| 284 | } | 349 | } | ||
| 350 | + | fb := s.pendingFeedback | |||
| 351 | + | s.pendingFeedback = nil | |||
| 352 | + | s.commitFeedback(fb, fresh) | |||
| 353 | + | } | |||
| 354 | + | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | |||
| 355 | + | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | |||
| 356 | + | s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) { | |||
| 360 | + | if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects { | |||
| 361 | + | s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |||
| 362 | + | } | |||
| 363 | + | } | |||
| 364 | + | ||||
| 365 | + | // commitDamage turns the requested damage into buffer pixels of a bw×bh | |||
| 366 | + | // buffer. Surface damage scales by the buffer/logical ratio, rounded out. | |||
| 367 | + | // A new size, too many rects or none at all with a new buffer (clients | |||
| 368 | + | // that do not report damage) is a full change. | |||
| 369 | + | func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |||
| 370 | + | surf, buf := s.pendingDamage, s.pendingBufDamage | |||
| 371 | + | s.pendingDamage, s.pendingBufDamage = nil, nil | |||
| 372 | + | if !fresh { | |||
| 373 | + | s.committed = damage{} | |||
| 374 | + | return | |||
| 375 | + | } | |||
| 376 | + | if resized || len(surf)+len(buf) == 0 || len(surf)+len(buf) > maxDamageRects { | |||
| 377 | + | s.committed = damage{full: true} | |||
| 378 | + | return | |||
| 379 | + | } | |||
| 380 | + | lw, lh := s.logicalSize(bw, bh) | |||
| 381 | + | d := damage{rects: make([]ports.Rect, 0, len(surf)+len(buf))} | |||
| 382 | + | bounds := image.Rect(0, 0, bw, bh) | |||
| 383 | + | add := func(r image.Rectangle) { | |||
| 384 | + | if r = r.Intersect(bounds); !r.Empty() { | |||
| 385 | + | d.rects = append(d.rects, ports.Rect{X: r.Min.X, Y: r.Min.Y, W: r.Dx(), H: r.Dy()}) | |||
| 386 | + | } | |||
| 387 | + | } | |||
| 388 | + | for _, r := range buf { | |||
| 389 | + | add(image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H)) | |||
| 390 | + | } | |||
| 391 | + | for _, r := range surf { | |||
| 392 | + | if lw <= 0 || lh <= 0 { | |||
| 393 | + | s.committed = damage{full: true} | |||
| 394 | + | return | |||
| 395 | + | } | |||
| 396 | + | // Clients may damage INT32_MAX-sized rects: clamp before scaling. | |||
| 397 | + | x0, y0 := max(r.X, 0), max(r.Y, 0) | |||
| 398 | + | x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh) | |||
| 399 | + | add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh)) | |||
| 400 | + | } | |||
| 401 | + | s.committed = d | |||
| 285 | } | 402 | } | ||
| 286 | - | func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {} | 403 | + | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} |
| 287 | - | func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {} | 404 | + | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} |
| 288 | - | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | 405 | + | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} |
| 289 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |||
| 290 | - | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | |||
| 291 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | 406 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | ||
| 292 | if v < 1 { | 407 | if v < 1 { | ||
| 293 | r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive") | 408 | r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive") | ||
A internal/adapters/wayland/syncdev.go +128 -0
| @@ -0,0 +1,128 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "os" | |||
| 6 | + | "runtime" | |||
| 7 | + | "unsafe" | |||
| 8 | + | ||||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // syncobjDevice is the DRM syncobj interface of a render node: explicit | |||
| 13 | + | // sync timelines clients import (wp_linux_drm_syncobj_v1). | |||
| 14 | + | type syncobjDevice interface { | |||
| 15 | + | // fdToHandle imports a timeline syncobj fd. | |||
| 16 | + | fdToHandle(fd int) (uint32, error) | |||
| 17 | + | // exportSyncFile returns a sync file of a timeline point's fence. | |||
| 18 | + | exportSyncFile(h uint32, point uint64) (*os.File, error) | |||
| 19 | + | // signal signals a timeline point (a release point). | |||
| 20 | + | signal(h uint32, point uint64) error | |||
| 21 | + | // eventfd writes efd once the point has a fence (WAIT_AVAILABLE). | |||
| 22 | + | eventfd(h uint32, point uint64, efd int) error | |||
| 23 | + | destroy(h uint32) error | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | const ( | |||
| 27 | + | ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy) | |||
| 28 | + | ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle) | |||
| 29 | + | ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle) | |||
| 30 | + | ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array) | |||
| 31 | + | ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd) | |||
| 32 | + | ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap) | |||
| 33 | + | ||||
| 34 | + | syncobjExportSyncFile = 1 << 0 | |||
| 35 | + | syncobjTimeline = 1 << 1 | |||
| 36 | + | syncobjWaitAvailable = 1 << 2 | |||
| 37 | + | capSyncobjTimeline = 0x14 | |||
| 38 | + | ) | |||
| 39 | + | ||||
| 40 | + | type syncobjHandle struct { | |||
| 41 | + | handle, flags uint32 | |||
| 42 | + | fd int32 | |||
| 43 | + | pad uint32 | |||
| 44 | + | point uint64 | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | type syncobjDestroy struct{ handle, pad uint32 } | |||
| 48 | + | ||||
| 49 | + | type syncobjTimelineArray struct { | |||
| 50 | + | handles, points uint64 | |||
| 51 | + | count, flags uint32 | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | type syncobjEventfd struct { | |||
| 55 | + | handle, flags uint32 | |||
| 56 | + | point uint64 | |||
| 57 | + | fd int32 | |||
| 58 | + | pad uint32 | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | type drmCap struct{ capability, value uint64 } | |||
| 62 | + | ||||
| 63 | + | // renderNode is a DRM render node opened for syncobj ioctls. | |||
| 64 | + | type renderNode struct{ fd int } | |||
| 65 | + | ||||
| 66 | + | // openSyncobj opens a render node that supports timeline syncobjs. | |||
| 67 | + | func openSyncobj(path string) (*renderNode, error) { | |||
| 68 | + | fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0) | |||
| 69 | + | if err != nil { | |||
| 70 | + | return nil, err | |||
| 71 | + | } | |||
| 72 | + | c := drmCap{capability: capSyncobjTimeline} | |||
| 73 | + | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&c)) != nil || c.value == 0 { | |||
| 74 | + | unix.Close(fd) | |||
| 75 | + | return nil, fmt.Errorf("%s: no timeline syncobjs", path) | |||
| 76 | + | } | |||
| 77 | + | return &renderNode{fd: fd}, nil | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func (n *renderNode) close() { unix.Close(n.fd) } | |||
| 81 | + | ||||
| 82 | + | func ioctl(fd int, req uintptr, arg unsafe.Pointer) error { | |||
| 83 | + | for { | |||
| 84 | + | _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)) | |||
| 85 | + | if errno == unix.EINTR || errno == unix.EAGAIN { | |||
| 86 | + | continue | |||
| 87 | + | } | |||
| 88 | + | if errno != 0 { | |||
| 89 | + | return errno | |||
| 90 | + | } | |||
| 91 | + | return nil | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | func (n *renderNode) fdToHandle(fd int) (uint32, error) { | |||
| 96 | + | a := syncobjHandle{fd: int32(fd)} | |||
| 97 | + | if err := ioctl(n.fd, ioctlSyncobjFDToHandle, unsafe.Pointer(&a)); err != nil { | |||
| 98 | + | return 0, err | |||
| 99 | + | } | |||
| 100 | + | return a.handle, nil | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func (n *renderNode) exportSyncFile(h uint32, point uint64) (*os.File, error) { | |||
| 104 | + | a := syncobjHandle{handle: h, flags: syncobjExportSyncFile | syncobjTimeline, fd: -1, point: point} | |||
| 105 | + | if err := ioctl(n.fd, ioctlSyncobjHandleToFD, unsafe.Pointer(&a)); err != nil { | |||
| 106 | + | return nil, err | |||
| 107 | + | } | |||
| 108 | + | return os.NewFile(uintptr(a.fd), "acquire-fence"), nil | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func (n *renderNode) signal(h uint32, point uint64) error { | |||
| 112 | + | handles, points := [1]uint32{h}, [1]uint64{point} | |||
| 113 | + | a := syncobjTimelineArray{handles: uint64(uintptr(unsafe.Pointer(&handles))), points: uint64(uintptr(unsafe.Pointer(&points))), count: 1} | |||
| 114 | + | err := ioctl(n.fd, ioctlSyncobjTimelineSignal, unsafe.Pointer(&a)) | |||
| 115 | + | runtime.KeepAlive(&handles) | |||
| 116 | + | runtime.KeepAlive(&points) | |||
| 117 | + | return err | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func (n *renderNode) eventfd(h uint32, point uint64, efd int) error { | |||
| 121 | + | a := syncobjEventfd{handle: h, flags: syncobjWaitAvailable, point: point, fd: int32(efd)} | |||
| 122 | + | return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a)) | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | func (n *renderNode) destroy(h uint32) error { | |||
| 126 | + | a := syncobjDestroy{handle: h} | |||
| 127 | + | return ioctl(n.fd, ioctlSyncobjDestroy, unsafe.Pointer(&a)) | |||
| 128 | + | } | |||
A internal/adapters/wayland/syncobj.go +422 -0
| @@ -0,0 +1,422 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "sync/atomic" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |||
| 11 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 12 | + | "github.com/bnema/purego-libwayland/server" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // wp_linux_drm_syncobj_v1: explicit sync. A client names, per commit, the | |||
| 17 | + | // timeline point its GPU signals when the buffer is drawn (acquire) and | |||
| 18 | + | // the point we signal once nothing reads the buffer any more (release). | |||
| 19 | + | // A commit waits (like a fifo barrier) until its acquire point has a | |||
| 20 | + | // fence; the fence then travels with the content as a sync file the | |||
| 21 | + | // renderer and the display wait on, so no CPU waits for the GPU. The | |||
| 22 | + | // release point replaces wl_buffer.release. | |||
| 23 | + | ||||
| 24 | + | const syncobjVersion = 1 | |||
| 25 | + | ||||
| 26 | + | // timeline is an imported client timeline. | |||
| 27 | + | type timeline struct { | |||
| 28 | + | dev syncobjDevice | |||
| 29 | + | handle uint32 | |||
| 30 | + | alive bool | |||
| 31 | + | // uses counts committed points still to signal or wait on. | |||
| 32 | + | uses int | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // syncPoint is a point on a timeline. | |||
| 36 | + | type syncPoint struct { | |||
| 37 | + | tl *timeline | |||
| 38 | + | point uint64 | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (p syncPoint) set() bool { return p.tl != nil } | |||
| 42 | + | ||||
| 43 | + | // syncState is a surface's wp_linux_drm_syncobj_surface_v1 state. | |||
| 44 | + | type syncState struct { | |||
| 45 | + | resource *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1 | |||
| 46 | + | acquire, release syncPoint // pending, for the next commit | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func registerSyncobj(d *server.Display, s *Server) error { | |||
| 50 | + | if s.syncDev == nil { | |||
| 51 | + | return nil | |||
| 52 | + | } | |||
| 53 | + | return linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1Global(d, syncobjVersion, func(c server.Client, v, id uint32) { | |||
| 54 | + | _, _ = linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1(c, int32(v), id, syncManager{s}) | |||
| 55 | + | }) | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | type syncManager struct{ server *Server } | |||
| 59 | + | ||||
| 60 | + | func (syncManager) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1) {} | |||
| 61 | + | ||||
| 62 | + | func (m syncManager) GetSurface(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, surf *wayland.Surface) { | |||
| 63 | + | state := m.server.surfaceOf(surf) | |||
| 64 | + | if state == nil { | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | if state.sync != nil { | |||
| 68 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists), "surface already has a syncobj surface") | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | st := &syncState{} | |||
| 72 | + | res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjSurfaceV1(r.Client(), r.Version(), id, syncSurface{state, st}) | |||
| 73 | + | if err != nil { | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | st.resource = res | |||
| 77 | + | state.sync = st | |||
| 78 | + | res.OnDestroy = func() { | |||
| 79 | + | if state.sync == st { | |||
| 80 | + | state.sync = nil | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | func (m syncManager) ImportTimeline(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, fd int) { | |||
| 86 | + | defer unix.Close(fd) | |||
| 87 | + | h, err := m.server.syncDev.fdToHandle(fd) | |||
| 88 | + | if err != nil { | |||
| 89 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline), "invalid timeline fd") | |||
| 90 | + | return | |||
| 91 | + | } | |||
| 92 | + | tl := &timeline{dev: m.server.syncDev, handle: h, alive: true} | |||
| 93 | + | res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjTimelineV1(r.Client(), r.Version(), id, timelineHandler{}) | |||
| 94 | + | if err != nil { | |||
| 95 | + | _ = tl.dev.destroy(h) | |||
| 96 | + | return | |||
| 97 | + | } | |||
| 98 | + | m.server.timelines[res.Resource] = tl | |||
| 99 | + | res.OnDestroy = func() { | |||
| 100 | + | delete(m.server.timelines, res.Resource) | |||
| 101 | + | // Points already committed keep the syncobj: destroyed once no | |||
| 102 | + | // held buffer or waiting commit uses it (timeline.drop). | |||
| 103 | + | tl.alive = false | |||
| 104 | + | m.server.dropTimeline(tl) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | type timelineHandler struct{} | |||
| 109 | + | ||||
| 110 | + | func (timelineHandler) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1) {} | |||
| 111 | + | ||||
| 112 | + | type syncSurface struct { | |||
| 113 | + | surf *surface | |||
| 114 | + | st *syncState | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (syncSurface) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1) {} | |||
| 118 | + | ||||
| 119 | + | func (h syncSurface) point(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) (syncPoint, bool) { | |||
| 120 | + | if h.surf.destroyed { | |||
| 121 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface), "surface destroyed") | |||
| 122 | + | return syncPoint{}, false | |||
| 123 | + | } | |||
| 124 | + | t := h.surf.server.timelines[tl.Resource] | |||
| 125 | + | if t == nil { | |||
| 126 | + | return syncPoint{}, false | |||
| 127 | + | } | |||
| 128 | + | return syncPoint{tl: t, point: uint64(hi)<<32 | uint64(lo)}, true | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func (h syncSurface) SetAcquirePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) { | |||
| 132 | + | if p, ok := h.point(r, tl, hi, lo); ok { | |||
| 133 | + | h.st.acquire = p | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func (h syncSurface) SetReleasePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) { | |||
| 138 | + | if p, ok := h.point(r, tl, hi, lo); ok { | |||
| 139 | + | h.st.release = p | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // checkSyncCommit applies the protocol rules to a commit with sync | |||
| 144 | + | // points; false means a protocol error was posted. | |||
| 145 | + | func (s *surface) checkSyncCommit() bool { | |||
| 146 | + | st := s.sync | |||
| 147 | + | if st == nil { | |||
| 148 | + | return true | |||
| 149 | + | } | |||
| 150 | + | a, r := st.acquire, st.release | |||
| 151 | + | // Points belong only to a commit that attaches a non-null buffer. | |||
| 152 | + | hasBuffer := s.attached && s.pending != nil | |||
| 153 | + | res := st.resource | |||
| 154 | + | switch { | |||
| 155 | + | case !a.set() && !r.set(): | |||
| 156 | + | if hasBuffer { | |||
| 157 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point") | |||
| 158 | + | return false | |||
| 159 | + | } | |||
| 160 | + | return true | |||
| 161 | + | case !hasBuffer: | |||
| 162 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer), "points without a buffer") | |||
| 163 | + | case !a.set(): | |||
| 164 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "no acquire point") | |||
| 165 | + | case !r.set(): | |||
| 166 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint), "no release point") | |||
| 167 | + | case a.tl == r.tl && a.point >= r.point: | |||
| 168 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints), "release point not after acquire point") | |||
| 169 | + | case s.pendingIsSHM(): | |||
| 170 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer), "explicit sync on a wl_shm buffer") | |||
| 171 | + | default: | |||
| 172 | + | return true | |||
| 173 | + | } | |||
| 174 | + | return false | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // pendingIsSHM reports whether the buffer of the next commit is wl_shm. | |||
| 178 | + | func (s *surface) pendingIsSHM() bool { | |||
| 179 | + | b := s.pending | |||
| 180 | + | if !s.attached { | |||
| 181 | + | b = s.current | |||
| 182 | + | } | |||
| 183 | + | if b == nil { | |||
| 184 | + | return false | |||
| 185 | + | } | |||
| 186 | + | _, dma := s.server.buffers[b.Resource].(*dmabufBuffer) | |||
| 187 | + | return !dma | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // syncWaiter watches acquire points: each gets an eventfd the kernel | |||
| 191 | + | // writes once the point has a fence, and the pacer is woken to apply the | |||
| 192 | + | // commits that waited for it. | |||
| 193 | + | type syncWaiter struct { | |||
| 194 | + | mu sync.Mutex | |||
| 195 | + | ready map[*syncWait]bool | |||
| 196 | + | wake func() | |||
| 197 | + | pipeR *os.File | |||
| 198 | + | pipeW *os.File | |||
| 199 | + | added []*syncWait // new waits for run (under mu) | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | // syncWait is one acquire point a queued commit waits for. | |||
| 203 | + | type syncWait struct { | |||
| 204 | + | efd int | |||
| 205 | + | done bool // the eventfd fired or the wait was cancelled (pacer, under Do) | |||
| 206 | + | cancelled atomic.Bool // run stops polling it and closes efd | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||
| 210 | + | r, w, err := os.Pipe() | |||
| 211 | + | if err != nil { | |||
| 212 | + | return nil, err | |||
| 213 | + | } | |||
| 214 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // watch registers an eventfd for p; the wait is ready once it fires. | |||
| 218 | + | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||
| 219 | + | efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK) | |||
| 220 | + | if err != nil { | |||
| 221 | + | return nil, err | |||
| 222 | + | } | |||
| 223 | + | if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil { | |||
| 224 | + | unix.Close(efd) | |||
| 225 | + | return nil, err | |||
| 226 | + | } | |||
| 227 | + | w := &syncWait{efd: efd} | |||
| 228 | + | sw.mu.Lock() | |||
| 229 | + | sw.added = append(sw.added, w) | |||
| 230 | + | sw.mu.Unlock() | |||
| 231 | + | _, _ = sw.pipeW.Write([]byte{0}) | |||
| 232 | + | return w, nil | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | // fired reports whether w's point has a fence, and forgets w once it has. | |||
| 236 | + | func (sw *syncWaiter) fired(w *syncWait) bool { | |||
| 237 | + | sw.mu.Lock() | |||
| 238 | + | defer sw.mu.Unlock() | |||
| 239 | + | if sw.ready[w] { | |||
| 240 | + | delete(sw.ready, w) | |||
| 241 | + | w.done = true | |||
| 242 | + | } | |||
| 243 | + | return w.done | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // cancel stops watching w (the commit was dropped). | |||
| 247 | + | func (sw *syncWaiter) cancel(w *syncWait) { | |||
| 248 | + | w.cancelled.Store(true) | |||
| 249 | + | sw.mu.Lock() | |||
| 250 | + | delete(sw.ready, w) | |||
| 251 | + | sw.mu.Unlock() | |||
| 252 | + | w.done = true | |||
| 253 | + | _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | // run polls every watched eventfd; it owns and closes them. | |||
| 257 | + | func (sw *syncWaiter) run(ctx context.Context) { | |||
| 258 | + | defer sw.pipeR.Close() | |||
| 259 | + | var waits []*syncWait | |||
| 260 | + | defer func() { | |||
| 261 | + | sw.mu.Lock() | |||
| 262 | + | waits = append(waits, sw.added...) | |||
| 263 | + | sw.added = nil | |||
| 264 | + | sw.mu.Unlock() | |||
| 265 | + | for _, w := range waits { | |||
| 266 | + | unix.Close(w.efd) | |||
| 267 | + | } | |||
| 268 | + | }() | |||
| 269 | + | for ctx.Err() == nil { | |||
| 270 | + | live := waits[:0] | |||
| 271 | + | for _, w := range waits { | |||
| 272 | + | if w.cancelled.Load() { | |||
| 273 | + | unix.Close(w.efd) | |||
| 274 | + | continue | |||
| 275 | + | } | |||
| 276 | + | live = append(live, w) | |||
| 277 | + | } | |||
| 278 | + | waits = live | |||
| 279 | + | fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}} | |||
| 280 | + | for _, w := range waits { | |||
| 281 | + | fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN}) | |||
| 282 | + | } | |||
| 283 | + | n, err := unix.Poll(fds, 100) | |||
| 284 | + | if err != nil && !errors.Is(err, unix.EINTR) { | |||
| 285 | + | return | |||
| 286 | + | } | |||
| 287 | + | if n <= 0 { | |||
| 288 | + | continue | |||
| 289 | + | } | |||
| 290 | + | fired := false | |||
| 291 | + | kept := waits[:0] | |||
| 292 | + | for i, w := range waits { | |||
| 293 | + | if w.cancelled.Load() { | |||
| 294 | + | unix.Close(w.efd) | |||
| 295 | + | continue | |||
| 296 | + | } | |||
| 297 | + | if fds[i+1].Revents == 0 { | |||
| 298 | + | kept = append(kept, w) | |||
| 299 | + | continue | |||
| 300 | + | } | |||
| 301 | + | unix.Close(w.efd) | |||
| 302 | + | sw.mu.Lock() | |||
| 303 | + | sw.ready[w] = true | |||
| 304 | + | sw.mu.Unlock() | |||
| 305 | + | fired = true | |||
| 306 | + | } | |||
| 307 | + | waits = kept | |||
| 308 | + | // New waits join the next poll. | |||
| 309 | + | if fds[0].Revents != 0 { | |||
| 310 | + | var b [64]byte | |||
| 311 | + | _, _ = sw.pipeR.Read(b[:]) | |||
| 312 | + | sw.mu.Lock() | |||
| 313 | + | waits = append(waits, sw.added...) | |||
| 314 | + | sw.added = nil | |||
| 315 | + | sw.mu.Unlock() | |||
| 316 | + | } | |||
| 317 | + | if fired { | |||
| 318 | + | sw.wake() | |||
| 319 | + | } | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // close stops the waiter's input side. | |||
| 324 | + | func (sw *syncWaiter) close() { sw.pipeW.Close() } | |||
| 325 | + | ||||
| 326 | + | // syncHold is a buffer's explicit-sync state while it is on screen: the | |||
| 327 | + | // acquire fence handed to readers and the release point to signal. | |||
| 328 | + | type syncHold struct { | |||
| 329 | + | acquire *os.File | |||
| 330 | + | release syncPoint | |||
| 331 | + | } | |||
| 332 | + | ||||
| 333 | + | // releaseSync signals the release point once nothing reads the buffer, | |||
| 334 | + | // and closes the acquire fence. | |||
| 335 | + | func (s *Server) releaseSync(h syncHold) { | |||
| 336 | + | if h.acquire != nil { | |||
| 337 | + | h.acquire.Close() | |||
| 338 | + | } | |||
| 339 | + | if h.release.set() { | |||
| 340 | + | if err := h.release.tl.dev.signal(h.release.tl.handle, h.release.point); err != nil { | |||
| 341 | + | s.log.Warn().Str("component", "wayland").Err(err).Msg("release point") | |||
| 342 | + | } | |||
| 343 | + | h.release.tl.uses-- | |||
| 344 | + | s.dropTimeline(h.release.tl) | |||
| 345 | + | } | |||
| 346 | + | } | |||
| 347 | + | ||||
| 348 | + | // dropTimeline destroys a timeline the client destroyed once no held | |||
| 349 | + | // buffer still needs to signal it. | |||
| 350 | + | func (s *Server) dropTimeline(tl *timeline) { | |||
| 351 | + | if !tl.alive && tl.uses <= 0 && tl.handle != 0 { | |||
| 352 | + | _ = tl.dev.destroy(tl.handle) | |||
| 353 | + | tl.handle = 0 | |||
| 354 | + | } | |||
| 355 | + | } | |||
| 356 | + | ||||
| 357 | + | // takeSyncPoints moves the surface's requested points to the commit and | |||
| 358 | + | // starts waiting for the acquire point's fence. | |||
| 359 | + | func (s *surface) takeSyncPoints() { | |||
| 360 | + | st := s.sync | |||
| 361 | + | if st == nil || !st.acquire.set() { | |||
| 362 | + | return | |||
| 363 | + | } | |||
| 364 | + | cs := &commitSync{acquire: st.acquire, release: st.release} | |||
| 365 | + | st.acquire, st.release = syncPoint{}, syncPoint{} | |||
| 366 | + | cs.acquire.tl.uses++ | |||
| 367 | + | cs.release.tl.uses++ | |||
| 368 | + | w, err := s.server.syncWait.watch(cs.acquire) | |||
| 369 | + | if err != nil { | |||
| 370 | + | // The point cannot be watched: apply now, readers wait on the | |||
| 371 | + | // fence (or its absence) when they read the buffer. | |||
| 372 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire point") | |||
| 373 | + | } else { | |||
| 374 | + | cs.wait = w | |||
| 375 | + | } | |||
| 376 | + | s.pendingSync = cs | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // syncReady reports whether a commit's acquire point has its fence. | |||
| 380 | + | func (s *surface) syncReady(cs *commitSync) bool { | |||
| 381 | + | return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait) | |||
| 382 | + | } | |||
| 383 | + | ||||
| 384 | + | // dropSync forgets a commit that will never apply. Its release point is | |||
| 385 | + | // signalled only once the acquire point has a fence: signalling earlier | |||
| 386 | + | // could move a shared timeline past a point the client has not submitted. | |||
| 387 | + | func (s *surface) dropSync(cs *commitSync) { | |||
| 388 | + | if cs == nil { | |||
| 389 | + | return | |||
| 390 | + | } | |||
| 391 | + | ready := cs.wait != nil && s.server.syncWait.fired(cs.wait) | |||
| 392 | + | if cs.wait != nil { | |||
| 393 | + | s.server.syncWait.cancel(cs.wait) | |||
| 394 | + | } | |||
| 395 | + | cs.acquire.tl.uses-- | |||
| 396 | + | s.server.dropTimeline(cs.acquire.tl) | |||
| 397 | + | if ready { | |||
| 398 | + | s.server.releaseSync(syncHold{release: cs.release}) | |||
| 399 | + | return | |||
| 400 | + | } | |||
| 401 | + | cs.release.tl.uses-- | |||
| 402 | + | s.server.dropTimeline(cs.release.tl) | |||
| 403 | + | } | |||
| 404 | + | ||||
| 405 | + | // applySync makes a commit's points the current buffer's: its acquire | |||
| 406 | + | // fence goes with the content, its release point waits for release. | |||
| 407 | + | func (s *surface) applySync(cs *commitSync) { | |||
| 408 | + | if cs == nil { | |||
| 409 | + | return | |||
| 410 | + | } | |||
| 411 | + | if cs.wait != nil { | |||
| 412 | + | s.server.syncWait.cancel(cs.wait) | |||
| 413 | + | } | |||
| 414 | + | f, err := cs.acquire.tl.dev.exportSyncFile(cs.acquire.tl.handle, cs.acquire.point) | |||
| 415 | + | if err != nil { | |||
| 416 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire fence") | |||
| 417 | + | f = nil | |||
| 418 | + | } | |||
| 419 | + | cs.acquire.tl.uses-- | |||
| 420 | + | s.server.dropTimeline(cs.acquire.tl) | |||
| 421 | + | s.hold = syncHold{acquire: f, release: cs.release} | |||
| 422 | + | } | |||
A internal/adapters/wayland/syncobj_device_mock_test.go +346 -0
| @@ -0,0 +1,346 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package wayland | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "os" | |||
| 9 | + | ||||
| 10 | + | mock "github.com/stretchr/testify/mock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // newMocksyncobjDevice creates a new instance of mocksyncobjDevice. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 14 | + | // The first argument is typically a *testing.T value. | |||
| 15 | + | func newMocksyncobjDevice(t interface { | |||
| 16 | + | mock.TestingT | |||
| 17 | + | Cleanup(func()) | |||
| 18 | + | }) *mocksyncobjDevice { | |||
| 19 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 20 | + | helper.Helper() | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | mock := &mocksyncobjDevice{} | |||
| 24 | + | mock.Mock.Test(t) | |||
| 25 | + | ||||
| 26 | + | t.Cleanup(func() { | |||
| 27 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 28 | + | helper.Helper() | |||
| 29 | + | } | |||
| 30 | + | mock.AssertExpectations(t) | |||
| 31 | + | }) | |||
| 32 | + | ||||
| 33 | + | return mock | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // mocksyncobjDevice is an autogenerated mock type for the syncobjDevice type | |||
| 37 | + | type mocksyncobjDevice struct { | |||
| 38 | + | mock.Mock | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | type mocksyncobjDevice_Expecter struct { | |||
| 42 | + | mock *mock.Mock | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (_m *mocksyncobjDevice) EXPECT() *mocksyncobjDevice_Expecter { | |||
| 46 | + | return &mocksyncobjDevice_Expecter{mock: &_m.Mock} | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // destroy provides a mock function for the type mocksyncobjDevice | |||
| 50 | + | func (_mock *mocksyncobjDevice) destroy(h uint32) error { | |||
| 51 | + | ret := _mock.Called(h) | |||
| 52 | + | ||||
| 53 | + | if len(ret) == 0 { | |||
| 54 | + | panic("no return value specified for destroy") | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | var r0 error | |||
| 58 | + | if returnFunc, ok := ret.Get(0).(func(uint32) error); ok { | |||
| 59 | + | r0 = returnFunc(h) | |||
| 60 | + | } else { | |||
| 61 | + | r0 = ret.Error(0) | |||
| 62 | + | } | |||
| 63 | + | return r0 | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // mocksyncobjDevice_destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroy' | |||
| 67 | + | type mocksyncobjDevice_destroy_Call struct { | |||
| 68 | + | *mock.Call | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // destroy is a helper method to define mock.On call | |||
| 72 | + | // - h uint32 | |||
| 73 | + | func (_e *mocksyncobjDevice_Expecter) destroy(h any) *mocksyncobjDevice_destroy_Call { | |||
| 74 | + | return &mocksyncobjDevice_destroy_Call{Call: _e.mock.On("destroy", h)} | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func (_c *mocksyncobjDevice_destroy_Call) Run(run func(h uint32)) *mocksyncobjDevice_destroy_Call { | |||
| 78 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 79 | + | var arg0 uint32 | |||
| 80 | + | if args[0] != nil { | |||
| 81 | + | arg0 = args[0].(uint32) | |||
| 82 | + | } | |||
| 83 | + | run( | |||
| 84 | + | arg0, | |||
| 85 | + | ) | |||
| 86 | + | }) | |||
| 87 | + | return _c | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | func (_c *mocksyncobjDevice_destroy_Call) Return(err error) *mocksyncobjDevice_destroy_Call { | |||
| 91 | + | _c.Call.Return(err) | |||
| 92 | + | return _c | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | func (_c *mocksyncobjDevice_destroy_Call) RunAndReturn(run func(h uint32) error) *mocksyncobjDevice_destroy_Call { | |||
| 96 | + | _c.Call.Return(run) | |||
| 97 | + | return _c | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // eventfd provides a mock function for the type mocksyncobjDevice | |||
| 101 | + | func (_mock *mocksyncobjDevice) eventfd(h uint32, point uint64, efd int) error { | |||
| 102 | + | ret := _mock.Called(h, point, efd) | |||
| 103 | + | ||||
| 104 | + | if len(ret) == 0 { | |||
| 105 | + | panic("no return value specified for eventfd") | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | var r0 error | |||
| 109 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64, int) error); ok { | |||
| 110 | + | r0 = returnFunc(h, point, efd) | |||
| 111 | + | } else { | |||
| 112 | + | r0 = ret.Error(0) | |||
| 113 | + | } | |||
| 114 | + | return r0 | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // mocksyncobjDevice_eventfd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'eventfd' | |||
| 118 | + | type mocksyncobjDevice_eventfd_Call struct { | |||
| 119 | + | *mock.Call | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | // eventfd is a helper method to define mock.On call | |||
| 123 | + | // - h uint32 | |||
| 124 | + | // - point uint64 | |||
| 125 | + | // - efd int | |||
| 126 | + | func (_e *mocksyncobjDevice_Expecter) eventfd(h any, point any, efd any) *mocksyncobjDevice_eventfd_Call { | |||
| 127 | + | return &mocksyncobjDevice_eventfd_Call{Call: _e.mock.On("eventfd", h, point, efd)} | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func (_c *mocksyncobjDevice_eventfd_Call) Run(run func(h uint32, point uint64, efd int)) *mocksyncobjDevice_eventfd_Call { | |||
| 131 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 132 | + | var arg0 uint32 | |||
| 133 | + | if args[0] != nil { | |||
| 134 | + | arg0 = args[0].(uint32) | |||
| 135 | + | } | |||
| 136 | + | var arg1 uint64 | |||
| 137 | + | if args[1] != nil { | |||
| 138 | + | arg1 = args[1].(uint64) | |||
| 139 | + | } | |||
| 140 | + | var arg2 int | |||
| 141 | + | if args[2] != nil { | |||
| 142 | + | arg2 = args[2].(int) | |||
| 143 | + | } | |||
| 144 | + | run( | |||
| 145 | + | arg0, | |||
| 146 | + | arg1, | |||
| 147 | + | arg2, | |||
| 148 | + | ) | |||
| 149 | + | }) | |||
| 150 | + | return _c | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | func (_c *mocksyncobjDevice_eventfd_Call) Return(err error) *mocksyncobjDevice_eventfd_Call { | |||
| 154 | + | _c.Call.Return(err) | |||
| 155 | + | return _c | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | func (_c *mocksyncobjDevice_eventfd_Call) RunAndReturn(run func(h uint32, point uint64, efd int) error) *mocksyncobjDevice_eventfd_Call { | |||
| 159 | + | _c.Call.Return(run) | |||
| 160 | + | return _c | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | // exportSyncFile provides a mock function for the type mocksyncobjDevice | |||
| 164 | + | func (_mock *mocksyncobjDevice) exportSyncFile(h uint32, point uint64) (*os.File, error) { | |||
| 165 | + | ret := _mock.Called(h, point) | |||
| 166 | + | ||||
| 167 | + | if len(ret) == 0 { | |||
| 168 | + | panic("no return value specified for exportSyncFile") | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | var r0 *os.File | |||
| 172 | + | var r1 error | |||
| 173 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) (*os.File, error)); ok { | |||
| 174 | + | return returnFunc(h, point) | |||
| 175 | + | } | |||
| 176 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) *os.File); ok { | |||
| 177 | + | r0 = returnFunc(h, point) | |||
| 178 | + | } else { | |||
| 179 | + | if ret.Get(0) != nil { | |||
| 180 | + | r0 = ret.Get(0).(*os.File) | |||
| 181 | + | } | |||
| 182 | + | } | |||
| 183 | + | if returnFunc, ok := ret.Get(1).(func(uint32, uint64) error); ok { | |||
| 184 | + | r1 = returnFunc(h, point) | |||
| 185 | + | } else { | |||
| 186 | + | r1 = ret.Error(1) | |||
| 187 | + | } | |||
| 188 | + | return r0, r1 | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | // mocksyncobjDevice_exportSyncFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'exportSyncFile' | |||
| 192 | + | type mocksyncobjDevice_exportSyncFile_Call struct { | |||
| 193 | + | *mock.Call | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | // exportSyncFile is a helper method to define mock.On call | |||
| 197 | + | // - h uint32 | |||
| 198 | + | // - point uint64 | |||
| 199 | + | func (_e *mocksyncobjDevice_Expecter) exportSyncFile(h any, point any) *mocksyncobjDevice_exportSyncFile_Call { | |||
| 200 | + | return &mocksyncobjDevice_exportSyncFile_Call{Call: _e.mock.On("exportSyncFile", h, point)} | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | func (_c *mocksyncobjDevice_exportSyncFile_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_exportSyncFile_Call { | |||
| 204 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 205 | + | var arg0 uint32 | |||
| 206 | + | if args[0] != nil { | |||
| 207 | + | arg0 = args[0].(uint32) | |||
| 208 | + | } | |||
| 209 | + | var arg1 uint64 | |||
| 210 | + | if args[1] != nil { | |||
| 211 | + | arg1 = args[1].(uint64) | |||
| 212 | + | } | |||
| 213 | + | run( | |||
| 214 | + | arg0, | |||
| 215 | + | arg1, | |||
| 216 | + | ) | |||
| 217 | + | }) | |||
| 218 | + | return _c | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | func (_c *mocksyncobjDevice_exportSyncFile_Call) Return(file *os.File, err error) *mocksyncobjDevice_exportSyncFile_Call { | |||
| 222 | + | _c.Call.Return(file, err) | |||
| 223 | + | return _c | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | func (_c *mocksyncobjDevice_exportSyncFile_Call) RunAndReturn(run func(h uint32, point uint64) (*os.File, error)) *mocksyncobjDevice_exportSyncFile_Call { | |||
| 227 | + | _c.Call.Return(run) | |||
| 228 | + | return _c | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // fdToHandle provides a mock function for the type mocksyncobjDevice | |||
| 232 | + | func (_mock *mocksyncobjDevice) fdToHandle(fd int) (uint32, error) { | |||
| 233 | + | ret := _mock.Called(fd) | |||
| 234 | + | ||||
| 235 | + | if len(ret) == 0 { | |||
| 236 | + | panic("no return value specified for fdToHandle") | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | var r0 uint32 | |||
| 240 | + | var r1 error | |||
| 241 | + | if returnFunc, ok := ret.Get(0).(func(int) (uint32, error)); ok { | |||
| 242 | + | return returnFunc(fd) | |||
| 243 | + | } | |||
| 244 | + | if returnFunc, ok := ret.Get(0).(func(int) uint32); ok { | |||
| 245 | + | r0 = returnFunc(fd) | |||
| 246 | + | } else { | |||
| 247 | + | r0 = ret.Get(0).(uint32) | |||
| 248 | + | } | |||
| 249 | + | if returnFunc, ok := ret.Get(1).(func(int) error); ok { | |||
| 250 | + | r1 = returnFunc(fd) | |||
| 251 | + | } else { | |||
| 252 | + | r1 = ret.Error(1) | |||
| 253 | + | } | |||
| 254 | + | return r0, r1 | |||
| 255 | + | } | |||
| 256 | + | ||||
| 257 | + | // mocksyncobjDevice_fdToHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fdToHandle' | |||
| 258 | + | type mocksyncobjDevice_fdToHandle_Call struct { | |||
| 259 | + | *mock.Call | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // fdToHandle is a helper method to define mock.On call | |||
| 263 | + | // - fd int | |||
| 264 | + | func (_e *mocksyncobjDevice_Expecter) fdToHandle(fd any) *mocksyncobjDevice_fdToHandle_Call { | |||
| 265 | + | return &mocksyncobjDevice_fdToHandle_Call{Call: _e.mock.On("fdToHandle", fd)} | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | func (_c *mocksyncobjDevice_fdToHandle_Call) Run(run func(fd int)) *mocksyncobjDevice_fdToHandle_Call { | |||
| 269 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 270 | + | var arg0 int | |||
| 271 | + | if args[0] != nil { | |||
| 272 | + | arg0 = args[0].(int) | |||
| 273 | + | } | |||
| 274 | + | run( | |||
| 275 | + | arg0, | |||
| 276 | + | ) | |||
| 277 | + | }) | |||
| 278 | + | return _c | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | func (_c *mocksyncobjDevice_fdToHandle_Call) Return(v uint32, err error) *mocksyncobjDevice_fdToHandle_Call { | |||
| 282 | + | _c.Call.Return(v, err) | |||
| 283 | + | return _c | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | func (_c *mocksyncobjDevice_fdToHandle_Call) RunAndReturn(run func(fd int) (uint32, error)) *mocksyncobjDevice_fdToHandle_Call { | |||
| 287 | + | _c.Call.Return(run) | |||
| 288 | + | return _c | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // signal provides a mock function for the type mocksyncobjDevice | |||
| 292 | + | func (_mock *mocksyncobjDevice) signal(h uint32, point uint64) error { | |||
| 293 | + | ret := _mock.Called(h, point) | |||
| 294 | + | ||||
| 295 | + | if len(ret) == 0 { | |||
| 296 | + | panic("no return value specified for signal") | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | var r0 error | |||
| 300 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) error); ok { | |||
| 301 | + | r0 = returnFunc(h, point) | |||
| 302 | + | } else { | |||
| 303 | + | r0 = ret.Error(0) | |||
| 304 | + | } | |||
| 305 | + | return r0 | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | // mocksyncobjDevice_signal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'signal' | |||
| 309 | + | type mocksyncobjDevice_signal_Call struct { | |||
| 310 | + | *mock.Call | |||
| 311 | + | } | |||
| 312 | + | ||||
| 313 | + | // signal is a helper method to define mock.On call | |||
| 314 | + | // - h uint32 | |||
| 315 | + | // - point uint64 | |||
| 316 | + | func (_e *mocksyncobjDevice_Expecter) signal(h any, point any) *mocksyncobjDevice_signal_Call { | |||
| 317 | + | return &mocksyncobjDevice_signal_Call{Call: _e.mock.On("signal", h, point)} | |||
| 318 | + | } | |||
| 319 | + | ||||
| 320 | + | func (_c *mocksyncobjDevice_signal_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_signal_Call { | |||
| 321 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 322 | + | var arg0 uint32 | |||
| 323 | + | if args[0] != nil { | |||
| 324 | + | arg0 = args[0].(uint32) | |||
| 325 | + | } | |||
| 326 | + | var arg1 uint64 | |||
| 327 | + | if args[1] != nil { | |||
| 328 | + | arg1 = args[1].(uint64) | |||
| 329 | + | } | |||
| 330 | + | run( | |||
| 331 | + | arg0, | |||
| 332 | + | arg1, | |||
| 333 | + | ) | |||
| 334 | + | }) | |||
| 335 | + | return _c | |||
| 336 | + | } | |||
| 337 | + | ||||
| 338 | + | func (_c *mocksyncobjDevice_signal_Call) Return(err error) *mocksyncobjDevice_signal_Call { | |||
| 339 | + | _c.Call.Return(err) | |||
| 340 | + | return _c | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | func (_c *mocksyncobjDevice_signal_Call) RunAndReturn(run func(h uint32, point uint64) error) *mocksyncobjDevice_signal_Call { | |||
| 344 | + | _c.Call.Return(run) | |||
| 345 | + | return _c | |||
| 346 | + | } | |||
A internal/adapters/wayland/syncobj_test.go +461 -0
| @@ -0,0 +1,461 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "strings" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | "unsafe" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/nefertty/internal/logging" | |||
| 13 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 14 | + | "github.com/bnema/purego-libwayland/protocol/linuxdmabuf" | |||
| 15 | + | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |||
| 16 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 17 | + | "github.com/bnema/wlturbo" | |||
| 18 | + | "github.com/stretchr/testify/mock" | |||
| 19 | + | "golang.org/x/sys/unix" | |||
| 20 | + | ) | |||
| 21 | + | ||||
| 22 | + | func TestSyncobjStructSizes(t *testing.T) { | |||
| 23 | + | for _, tc := range []struct { | |||
| 24 | + | name string | |||
| 25 | + | got uintptr | |||
| 26 | + | req uintptr | |||
| 27 | + | }{ | |||
| 28 | + | {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy}, | |||
| 29 | + | {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD}, | |||
| 30 | + | {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle}, | |||
| 31 | + | {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal}, | |||
| 32 | + | {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd}, | |||
| 33 | + | {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap}, | |||
| 34 | + | } { | |||
| 35 | + | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |||
| 36 | + | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // syncHarness is a server with a mocked syncobj device and a client with | |||
| 42 | + | // a mapped window, a dmabuf buffer and the syncobj objects. | |||
| 43 | + | type syncHarness struct { | |||
| 44 | + | t *testing.T | |||
| 45 | + | s *Server | |||
| 46 | + | c *wlturbo.Display | |||
| 47 | + | dev *mocksyncobjDevice | |||
| 48 | + | contents chan ports.SurfaceContent | |||
| 49 | + | presented chan ports.OutputPresented | |||
| 50 | + | surf uint32 | |||
| 51 | + | syncSurf uint32 | |||
| 52 | + | timeline uint32 | |||
| 53 | + | win ports.WindowID | |||
| 54 | + | mu sync.Mutex | |||
| 55 | + | efds map[uint64]int // eventfd by acquire point | |||
| 56 | + | signals []uint64 | |||
| 57 | + | fences []*os.File | |||
| 58 | + | releases map[uint32]*syncReleaseProxy | |||
| 59 | + | mgr uint32 | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func newSyncHarness(t *testing.T) *syncHarness { | |||
| 63 | + | h := &syncHarness{t: t, efds: map[uint64]int{}} | |||
| 64 | + | h.dev = newMocksyncobjDevice(t) | |||
| 65 | + | h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe() | |||
| 66 | + | h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe() | |||
| 67 | + | h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error { | |||
| 68 | + | // Keep a duplicate: the waiter closes its own when it fires. | |||
| 69 | + | dup, err := unix.Dup(efd) | |||
| 70 | + | if err != nil { | |||
| 71 | + | return err | |||
| 72 | + | } | |||
| 73 | + | h.mu.Lock() | |||
| 74 | + | h.efds[point] = dup | |||
| 75 | + | h.mu.Unlock() | |||
| 76 | + | return nil | |||
| 77 | + | }).Maybe() | |||
| 78 | + | h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) { | |||
| 79 | + | r, w, err := os.Pipe() | |||
| 80 | + | if err != nil { | |||
| 81 | + | return nil, err | |||
| 82 | + | } | |||
| 83 | + | w.Close() | |||
| 84 | + | h.mu.Lock() | |||
| 85 | + | h.fences = append(h.fences, r) | |||
| 86 | + | h.mu.Unlock() | |||
| 87 | + | return r, nil | |||
| 88 | + | }).Maybe() | |||
| 89 | + | h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error { | |||
| 90 | + | h.mu.Lock() | |||
| 91 | + | h.signals = append(h.signals, point) | |||
| 92 | + | h.mu.Unlock() | |||
| 93 | + | return nil | |||
| 94 | + | }).Maybe() | |||
| 95 | + | dir := t.TempDir() | |||
| 96 | + | events := make(chan ports.ClientEvent, 16) | |||
| 97 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 98 | + | h.contents = make(chan ports.SurfaceContent, 64) | |||
| 99 | + | h.presented = make(chan ports.OutputPresented, 16) | |||
| 100 | + | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | |||
| 101 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland")) | |||
| 102 | + | if err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 106 | + | done := make(chan error, 1) | |||
| 107 | + | go func() { done <- s.Run(ctx) }() | |||
| 108 | + | t.Cleanup(func() { | |||
| 109 | + | cancel() | |||
| 110 | + | <-done | |||
| 111 | + | h.mu.Lock() | |||
| 112 | + | for _, fd := range h.efds { | |||
| 113 | + | unix.Close(fd) | |||
| 114 | + | } | |||
| 115 | + | h.mu.Unlock() | |||
| 116 | + | }) | |||
| 117 | + | h.s = s | |||
| 118 | + | h.c = protocolClient(t, s, dir) | |||
| 119 | + | w, surf, xdg := surfaceMapper(t, h.c, events)() | |||
| 120 | + | registerProtocol(t, h.c, xdg) | |||
| 121 | + | h.win, h.surf = w.ID, surf | |||
| 122 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"} | |||
| 123 | + | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |||
| 124 | + | h.syncSurf = h.c.AllocateID() | |||
| 125 | + | requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf) | |||
| 126 | + | registerProtocol(t, h.c, h.syncSurf) | |||
| 127 | + | fd, err := unix.MemfdCreate("timeline", 0) | |||
| 128 | + | if err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | defer unix.Close(fd) | |||
| 132 | + | h.timeline = h.c.AllocateID() | |||
| 133 | + | registerProtocol(t, h.c, h.timeline) | |||
| 134 | + | if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil { | |||
| 135 | + | t.Fatal(err) | |||
| 136 | + | } | |||
| 137 | + | for deadline := time.Now().Add(2 * time.Second); ; { | |||
| 138 | + | var out string | |||
| 139 | + | s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) }) | |||
| 140 | + | if out != "" { | |||
| 141 | + | break | |||
| 142 | + | } | |||
| 143 | + | if time.Now().After(deadline) { | |||
| 144 | + | t.Fatal("configure not applied") | |||
| 145 | + | } | |||
| 146 | + | time.Sleep(2 * time.Millisecond) | |||
| 147 | + | } | |||
| 148 | + | return h | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it). | |||
| 152 | + | func (h *syncHarness) dmabuf() uint32 { | |||
| 153 | + | t, c := h.t, h.c | |||
| 154 | + | dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion) | |||
| 155 | + | fd, err := unix.MemfdCreate("buf", 0) | |||
| 156 | + | if err != nil { | |||
| 157 | + | t.Fatal(err) | |||
| 158 | + | } | |||
| 159 | + | defer unix.Close(fd) | |||
| 160 | + | if err := unix.Ftruncate(fd, 256*16); err != nil { | |||
| 161 | + | t.Fatal(err) | |||
| 162 | + | } | |||
| 163 | + | p := c.AllocateID() | |||
| 164 | + | requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p) | |||
| 165 | + | registerProtocol(t, c, p) | |||
| 166 | + | if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil { | |||
| 167 | + | t.Fatal(err) | |||
| 168 | + | } | |||
| 169 | + | b := c.AllocateID() | |||
| 170 | + | rel := &syncReleaseProxy{released: make(chan struct{}, 4)} | |||
| 171 | + | rel.SetID(b) | |||
| 172 | + | c.Context().Register(rel) | |||
| 173 | + | requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0)) | |||
| 174 | + | h.releases[b] = rel | |||
| 175 | + | return b | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | type syncReleaseProxy struct { | |||
| 179 | + | wlturbo.BaseProxy | |||
| 180 | + | released chan struct{} | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) { | |||
| 184 | + | if uint32(e.Opcode) == wayland.BufferEventRelease { | |||
| 185 | + | p.released <- struct{}{} | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func (h *syncHarness) setPoints(acquire, release uint64) { | |||
| 190 | + | requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | |||
| 191 | + | requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | func (h *syncHarness) commit(buf uint32, acquire, release uint64) { | |||
| 195 | + | t, c := h.t, h.c | |||
| 196 | + | requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 197 | + | h.setPoints(acquire, release) | |||
| 198 | + | requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit) | |||
| 199 | + | if err := c.Roundtrip(); err != nil { | |||
| 200 | + | t.Fatal(err) | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // content waits for a DMABuf content of the window. | |||
| 205 | + | func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) { | |||
| 206 | + | deadline := time.After(wait) | |||
| 207 | + | for { | |||
| 208 | + | select { | |||
| 209 | + | case c := <-h.contents: | |||
| 210 | + | if c.ID == h.win && c.DMABuf != nil { | |||
| 211 | + | return c, true | |||
| 212 | + | } | |||
| 213 | + | case <-deadline: | |||
| 214 | + | return ports.SurfaceContent{}, false | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | // fire makes the kernel's eventfd for an acquire point readable. | |||
| 220 | + | func (h *syncHarness) fire(point uint64) { | |||
| 221 | + | h.t.Helper() | |||
| 222 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 223 | + | for { | |||
| 224 | + | h.mu.Lock() | |||
| 225 | + | fd, ok := h.efds[point] | |||
| 226 | + | h.mu.Unlock() | |||
| 227 | + | if ok { | |||
| 228 | + | var one [8]byte | |||
| 229 | + | one[0] = 1 | |||
| 230 | + | if _, err := unix.Write(fd, one[:]); err != nil { | |||
| 231 | + | h.t.Fatal(err) | |||
| 232 | + | } | |||
| 233 | + | return | |||
| 234 | + | } | |||
| 235 | + | if time.Now().After(deadline) { | |||
| 236 | + | h.t.Fatalf("no eventfd for point %d", point) | |||
| 237 | + | } | |||
| 238 | + | time.Sleep(2 * time.Millisecond) | |||
| 239 | + | } | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | func (h *syncHarness) signalled() []uint64 { | |||
| 243 | + | h.mu.Lock() | |||
| 244 | + | defer h.mu.Unlock() | |||
| 245 | + | return append([]uint64(nil), h.signals...) | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | // A commit waits for its acquire point; the content carries the fence; | |||
| 249 | + | // replacing it signals the release point once outputs moved on, and | |||
| 250 | + | // wl_buffer.release is never sent. The acquire fence closes only then. | |||
| 251 | + | func TestSyncobjAcquireAndRelease(t *testing.T) { | |||
| 252 | + | h := newSyncHarness(t) | |||
| 253 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 254 | + | a, b := h.dmabuf(), h.dmabuf() | |||
| 255 | + | h.commit(a, 1, 2) | |||
| 256 | + | if _, ok := h.content(50 * time.Millisecond); ok { | |||
| 257 | + | t.Fatal("content before the acquire point had a fence") | |||
| 258 | + | } | |||
| 259 | + | h.fire(1) | |||
| 260 | + | first, ok := h.content(2 * time.Second) | |||
| 261 | + | if !ok || first.Acquire == nil { | |||
| 262 | + | t.Fatalf("content %+v", first) | |||
| 263 | + | } | |||
| 264 | + | h.commit(b, 3, 4) | |||
| 265 | + | h.fire(3) | |||
| 266 | + | second, ok := h.content(2 * time.Second) | |||
| 267 | + | if !ok { | |||
| 268 | + | t.Fatal("second content") | |||
| 269 | + | } | |||
| 270 | + | if _, err := first.Acquire.Stat(); err != nil { | |||
| 271 | + | t.Fatal("acquire fence closed before release") | |||
| 272 | + | } | |||
| 273 | + | // The output read the second content: the first buffer is released. | |||
| 274 | + | h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}} | |||
| 275 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 276 | + | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |||
| 277 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 278 | + | t.Fatal(err) | |||
| 279 | + | } | |||
| 280 | + | time.Sleep(2 * time.Millisecond) | |||
| 281 | + | } | |||
| 282 | + | if got := h.signalled(); len(got) != 1 || got[0] != 2 { | |||
| 283 | + | t.Fatalf("signalled %v", got) | |||
| 284 | + | } | |||
| 285 | + | if _, err := first.Acquire.Stat(); err == nil { | |||
| 286 | + | t.Fatal("acquire fence left open after release") | |||
| 287 | + | } | |||
| 288 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 289 | + | t.Fatal(err) | |||
| 290 | + | } | |||
| 291 | + | select { | |||
| 292 | + | case <-h.releases[a].released: | |||
| 293 | + | t.Fatal("wl_buffer.release sent under explicit sync") | |||
| 294 | + | default: | |||
| 295 | + | } | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | func TestSyncobjProtocolErrors(t *testing.T) { | |||
| 299 | + | for _, tc := range []struct { | |||
| 300 | + | name string | |||
| 301 | + | run func(h *syncHarness) | |||
| 302 | + | obj func(h *syncHarness) uint32 | |||
| 303 | + | code uint32 | |||
| 304 | + | }{ | |||
| 305 | + | {"conflicting points", func(h *syncHarness) { | |||
| 306 | + | b := h.dmabuf() | |||
| 307 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 308 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5)) | |||
| 309 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5)) | |||
| 310 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 311 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)}, | |||
| 312 | + | {"no release point", func(h *syncHarness) { | |||
| 313 | + | b := h.dmabuf() | |||
| 314 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 315 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5)) | |||
| 316 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 317 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)}, | |||
| 318 | + | {"buffer without points", func(h *syncHarness) { | |||
| 319 | + | b := h.dmabuf() | |||
| 320 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 321 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 322 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |||
| 323 | + | {"no acquire point", func(h *syncHarness) { | |||
| 324 | + | b := h.dmabuf() | |||
| 325 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 326 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5)) | |||
| 327 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 328 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |||
| 329 | + | {"points without attach", func(h *syncHarness) { | |||
| 330 | + | b := h.dmabuf() | |||
| 331 | + | h.commit(b, 1, 2) | |||
| 332 | + | h.fire(1) | |||
| 333 | + | h.setPoints(3, 4) | |||
| 334 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 335 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)}, | |||
| 336 | + | {"points with null attach", func(h *syncHarness) { | |||
| 337 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |||
| 338 | + | h.setPoints(1, 2) | |||
| 339 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 340 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)}, | |||
| 341 | + | {"wl_shm buffer", func(h *syncHarness) { | |||
| 342 | + | b := shmBuffer(t, h.c) | |||
| 343 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 344 | + | h.setPoints(1, 2) | |||
| 345 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 346 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)}, | |||
| 347 | + | {"surface destroyed", func(h *syncHarness) { | |||
| 348 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |||
| 349 | + | h.setPoints(1, 2) | |||
| 350 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)}, | |||
| 351 | + | {"surface exists", func(h *syncHarness) { | |||
| 352 | + | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |||
| 353 | + | h.mgr = mgr | |||
| 354 | + | requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf) | |||
| 355 | + | }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)}, | |||
| 356 | + | } { | |||
| 357 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 358 | + | h := newSyncHarness(t) | |||
| 359 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 360 | + | h.t = t | |||
| 361 | + | tc.run(h) | |||
| 362 | + | expectProtocolError(t, h.c, tc.obj(h), tc.code) | |||
| 363 | + | }) | |||
| 364 | + | } | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | // An invalid timeline fd is a protocol error. | |||
| 368 | + | func TestSyncobjInvalidTimeline(t *testing.T) { | |||
| 369 | + | dev := newMocksyncobjDevice(t) | |||
| 370 | + | dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once() | |||
| 371 | + | dir := t.TempDir() | |||
| 372 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland")) | |||
| 373 | + | if err != nil { | |||
| 374 | + | t.Fatal(err) | |||
| 375 | + | } | |||
| 376 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 377 | + | done := make(chan error, 1) | |||
| 378 | + | go func() { done <- s.Run(ctx) }() | |||
| 379 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 380 | + | c := protocolClient(t, s, dir) | |||
| 381 | + | mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1") | |||
| 382 | + | fd, _ := unix.MemfdCreate("bad", 0) | |||
| 383 | + | defer unix.Close(fd) | |||
| 384 | + | if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil { | |||
| 385 | + | t.Fatal(err) | |||
| 386 | + | } | |||
| 387 | + | expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline)) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 390 | + | // A commit dropped before its acquire point has a fence signals nothing | |||
| 391 | + | // (the client has not submitted that point) and sends no wl_buffer.release; | |||
| 392 | + | // the waiter closes its eventfd and no acquire file is left open. | |||
| 393 | + | func TestSyncobjDroppedCommit(t *testing.T) { | |||
| 394 | + | h := newSyncHarness(t) | |||
| 395 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 396 | + | a := h.dmabuf() | |||
| 397 | + | before := openFDs(t) | |||
| 398 | + | h.commit(a, 1, 2) | |||
| 399 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |||
| 400 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 401 | + | t.Fatal(err) | |||
| 402 | + | } | |||
| 403 | + | // The point never gets a fence: the waiter must still stop polling it. | |||
| 404 | + | time.Sleep(250 * time.Millisecond) | |||
| 405 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 406 | + | t.Fatal(err) | |||
| 407 | + | } | |||
| 408 | + | if got := h.signalled(); len(got) != 0 { | |||
| 409 | + | t.Fatalf("signalled %v for a commit whose acquire point never arrived", got) | |||
| 410 | + | } | |||
| 411 | + | select { | |||
| 412 | + | case <-h.releases[a].released: | |||
| 413 | + | t.Fatal("wl_buffer.release sent under explicit sync") | |||
| 414 | + | default: | |||
| 415 | + | } | |||
| 416 | + | // The harness keeps its own duplicate of the eventfd. | |||
| 417 | + | if after := openFDs(t); after > before+1 { | |||
| 418 | + | t.Fatalf("eventfds and sync files %d -> %d", before, after) | |||
| 419 | + | } | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | // A commit dropped after its acquire point fired signals its release point. | |||
| 423 | + | func TestSyncobjDroppedReadyCommitReleases(t *testing.T) { | |||
| 424 | + | h := newSyncHarness(t) | |||
| 425 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 426 | + | a := h.dmabuf() | |||
| 427 | + | h.commit(a, 1, 2) | |||
| 428 | + | h.fire(1) | |||
| 429 | + | if _, ok := h.content(2 * time.Second); !ok { | |||
| 430 | + | t.Fatal("content") | |||
| 431 | + | } | |||
| 432 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |||
| 433 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 434 | + | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |||
| 435 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 436 | + | t.Fatal(err) | |||
| 437 | + | } | |||
| 438 | + | time.Sleep(2 * time.Millisecond) | |||
| 439 | + | } | |||
| 440 | + | if got := h.signalled(); len(got) != 1 || got[0] != 2 { | |||
| 441 | + | t.Fatalf("signalled %v", got) | |||
| 442 | + | } | |||
| 443 | + | } | |||
| 444 | + | ||||
| 445 | + | // openFDs counts the eventfds and sync files (pipes stand in for them) | |||
| 446 | + | // the process holds. | |||
| 447 | + | func openFDs(t *testing.T) int { | |||
| 448 | + | t.Helper() | |||
| 449 | + | ents, err := os.ReadDir("/proc/self/fd") | |||
| 450 | + | if err != nil { | |||
| 451 | + | t.Fatal(err) | |||
| 452 | + | } | |||
| 453 | + | n := 0 | |||
| 454 | + | for _, e := range ents { | |||
| 455 | + | l, _ := os.Readlink("/proc/self/fd/" + e.Name()) | |||
| 456 | + | if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") { | |||
| 457 | + | n++ | |||
| 458 | + | } | |||
| 459 | + | } | |||
| 460 | + | return n | |||
| 461 | + | } | |||
M internal/adapters/wayland/xdg.go +3 -1
| @@ -182,7 +182,8 @@ func (w *window) unmap() { | |||||
| 182 | w.xdg.acked = false | 182 | w.xdg.acked = false | ||
| 183 | w.xdg.serials = nil | 183 | w.xdg.serials = nil | ||
| 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | ||
| 185 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}) | 185 | + | w.xdg.server.syncInhibitors() |
| 186 | + | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 186 | } | 187 | } | ||
| 187 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 188 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 188 | if x.window != nil { | 189 | if x.window != nil { | ||
| @@ -215,6 +216,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||||
| 215 | // Its size comes with the commit, in WindowResized (afterCommit). | 216 | // Its size comes with the commit, in WindowResized (afterCommit). | ||
| 216 | w.floating = w.floats() | 217 | w.floating = w.floats() | ||
| 217 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH}) | 218 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH}) | ||
| 219 | + | x.server.syncInhibitors() | |||
| 218 | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped") | 220 | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped") | ||
| 219 | } else if !buffer && w.mapped { | 221 | } else if !buffer && w.mapped { | ||
| 220 | w.unmap() | 222 | w.unmap() | ||
M internal/app/app.go +18 -4
| @@ -25,6 +25,7 @@ import ( | |||||
| 25 | "github.com/bnema/nefertty/internal/logging" | 25 | "github.com/bnema/nefertty/internal/logging" | ||
| 26 | "github.com/bnema/nefertty/internal/ports" | 26 | "github.com/bnema/nefertty/internal/ports" | ||
| 27 | "github.com/bnema/zerowrap" | 27 | "github.com/bnema/zerowrap" | ||
| 28 | + | "golang.org/x/sys/unix" | |||
| 28 | ) | 29 | ) | ||
| 29 | 30 | ||||
| 30 | type Options struct { | 31 | type Options struct { | ||
| @@ -87,6 +88,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 87 | contents := make(chan ports.SurfaceContent, 64) | 88 | contents := make(chan ports.SurfaceContent, 64) | ||
| 88 | cursorChanges := make(chan ports.CursorChange, 1) | 89 | cursorChanges := make(chan ports.CursorChange, 1) | ||
| 89 | presented := make(chan ports.OutputPresented, 64) | 90 | presented := make(chan ports.OutputPresented, 64) | ||
| 91 | + | outputFormats := make(chan ports.OutputFormats, 8) | |||
| 90 | ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal} | 92 | ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal} | ||
| 91 | c, err := core.New(opts.Config, ch) | 93 | c, err := core.New(opts.Config, ch) | ||
| 92 | if err != nil { | 94 | if err != nil { | ||
| @@ -101,7 +103,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 101 | // Every output renders on the same GPU: its formats are the clients'. | 103 | // Every output renders on the same GPU: its formats are the clients'. | ||
| 102 | dmabuf := vulkan.Probe() | 104 | dmabuf := vulkan.Probe() | ||
| 103 | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | 105 | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | ||
| 104 | - | server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented}, logging.For(ctx, "wayland")) | 106 | + | server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland")) |
| 105 | if err != nil { | 107 | if err != nil { | ||
| 106 | km.Close() | 108 | km.Close() | ||
| 107 | return err | 109 | return err | ||
| @@ -159,7 +161,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 159 | return | 161 | return | ||
| 160 | } | 162 | } | ||
| 161 | done <- safe("input", func() error { | 163 | done <- safe("input", func() error { | ||
| 162 | - | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) | 164 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input) |
| 163 | }) | 165 | }) | ||
| 164 | return | 166 | return | ||
| 165 | } | 167 | } | ||
| @@ -215,8 +217,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 215 | } | 217 | } | ||
| 216 | if hw != nil { | 218 | if hw != nil { | ||
| 217 | done <- safe("output", func() error { | 219 | done <- safe("output", func() error { | ||
| 218 | - | want := func() drm.Want { return wantFromConfig(opts.Config) } | 220 | + | want := func() drm.Want { |
| 219 | - | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | 221 | + | w := wantFromConfig(opts.Config) |
| 222 | + | w.Sampled = dmabuf.Formats | |||
| 223 | + | return w | |||
| 224 | + | } | |||
| 225 | + | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, outputFormats, curs, newRenderer, logging.For(ctx, "drm")) | |||
| 220 | }) | 226 | }) | ||
| 221 | return | 227 | return | ||
| 222 | } | 228 | } | ||
| @@ -373,3 +379,11 @@ func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap. | |||||
| 373 | log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display") | 379 | log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display") | ||
| 374 | return d | 380 | return d | ||
| 375 | } | 381 | } | ||
| 382 | + | ||||
| 383 | + | // renderNode is the /dev/dri path of a render node dev_t, "" when none. | |||
| 384 | + | func renderNode(dev uint64) string { | |||
| 385 | + | if dev == 0 { | |||
| 386 | + | return "" | |||
| 387 | + | } | |||
| 388 | + | return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev)) | |||
| 389 | + | } | |||
M internal/app/drm.go +5 -2
| @@ -109,12 +109,13 @@ func (b *drmBackend) close() { | |||||
| 109 | // one flip reader per card, and a udev watcher that rescans connectors on | 109 | // one flip reader per card, and a udev watcher that rescans connectors on | ||
| 110 | // hotplug. Core learns about outputs through events. It returns when ctx | 110 | // hotplug. Core learns about outputs through events. It returns when ctx | ||
| 111 | // ends, after every output is closed. | 111 | // ends, after every output is closed. | ||
| 112 | - | func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | 112 | + | func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, formats chan<- ports.OutputFormats, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { |
| 113 | ctx, cancel := context.WithCancel(ctx) | 113 | ctx, cancel := context.WithCancel(ctx) | ||
| 114 | defer cancel() | 114 | defer cancel() | ||
| 115 | var readers sync.WaitGroup | 115 | var readers sync.WaitGroup | ||
| 116 | readerErr := make(chan error, len(b.cards)) | 116 | readerErr := make(chan error, len(b.cards)) | ||
| 117 | for _, c := range b.cards { | 117 | for _, c := range b.cards { | ||
| 118 | + | c.SetFormats(formats) | |||
| 118 | readers.Go(func() { | 119 | readers.Go(func() { | ||
| 119 | if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil { | 120 | if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil { | ||
| 120 | readerErr <- fmt.Errorf("%s: %w", c.Path(), err) | 121 | readerErr <- fmt.Errorf("%s: %w", c.Path(), err) | ||
| @@ -141,7 +142,9 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, event | |||||
| 141 | stopping := map[string]bool{} | 142 | stopping := map[string]bool{} | ||
| 142 | scan := func() { | 143 | scan := func() { | ||
| 143 | for _, c := range b.cards { | 144 | for _, c := range b.cards { | ||
| 144 | - | c.SetWant(want()) | 145 | + | w := want() |
| 146 | + | w.Device = c.Device() | |||
| 147 | + | c.SetWant(w) | |||
| 145 | added, removed, replaced, err := c.Scan() | 148 | added, removed, replaced, err := c.Scan() | ||
| 146 | if err != nil { | 149 | if err != nil { | ||
| 147 | log.Warn().Err(err).Msg("scan connectors") | 150 | log.Warn().Err(err).Msg("scan connectors") | ||
M internal/core/core.go +77 -5
| @@ -89,6 +89,12 @@ type Core struct { | |||||
| 89 | // pointer moving to another output does not. | 89 | // pointer moving to another output does not. | ||
| 90 | layerFocus WindowID | 90 | layerFocus WindowID | ||
| 91 | layerOver map[*screen]WindowID | 91 | layerOver map[*screen]WindowID | ||
| 92 | + | // inhibitors are the windows asking to keep compositor binds while | |||
| 93 | + | // focused, inhibiting the one active now (0: none); idle are those | |||
| 94 | + | // keeping the session awake. | |||
| 95 | + | inhibitors map[WindowID]bool | |||
| 96 | + | inhibiting WindowID | |||
| 97 | + | idle map[WindowID]bool | |||
| 92 | } | 98 | } | ||
| 93 | 99 | ||||
| 94 | func keyName(s string) string { | 100 | func keyName(s string) string { | ||
| @@ -271,7 +277,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||||
| 271 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | 277 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | ||
| 272 | } | 278 | } | ||
| 273 | // A placeholder screen holds windows until the first output arrives. | 279 | // A placeholder screen holds windows until the first output arrives. | ||
| 274 | - | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}} | 280 | + | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} |
| 275 | if err := c.apply(cfg); err != nil { | 281 | if err := c.apply(cfg); err != nil { | ||
| 276 | return nil, err | 282 | return nil, err | ||
| 277 | } | 283 | } | ||
| @@ -448,12 +454,37 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 448 | } | 454 | } | ||
| 449 | c.focus = focus | 455 | c.focus = focus | ||
| 450 | } | 456 | } | ||
| 457 | + | if err := c.updateInhibit(ctx); err != nil { | |||
| 458 | + | return err | |||
| 459 | + | } | |||
| 451 | c.resolveConstraint() | 460 | c.resolveConstraint() | ||
| 452 | latest(c.ch.Scenes, scenes) | 461 | latest(c.ch.Scenes, scenes) | ||
| 453 | c.publishState() | 462 | c.publishState() | ||
| 454 | return nil | 463 | return nil | ||
| 455 | } | 464 | } | ||
| 456 | 465 | ||||
| 466 | + | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | |||
| 467 | + | // active one and tells wayland when it changes. | |||
| 468 | + | func (c *Core) updateInhibit(ctx context.Context) error { | |||
| 469 | + | want := WindowID(0) | |||
| 470 | + | if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] { | |||
| 471 | + | want = f | |||
| 472 | + | } | |||
| 473 | + | if want == c.inhibiting { | |||
| 474 | + | return nil | |||
| 475 | + | } | |||
| 476 | + | if c.inhibiting != 0 { | |||
| 477 | + | if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil { | |||
| 478 | + | return err | |||
| 479 | + | } | |||
| 480 | + | } | |||
| 481 | + | c.inhibiting = want | |||
| 482 | + | if want != 0 { | |||
| 483 | + | return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true}) | |||
| 484 | + | } | |||
| 485 | + | return nil | |||
| 486 | + | } | |||
| 487 | + | ||||
| 457 | // resolveConstraint turns the active constraint into global coordinates | 488 | // resolveConstraint turns the active constraint into global coordinates | ||
| 458 | // and sends it to input when it changed. A hidden window holds nothing. | 489 | // and sends it to input when it changed. A hidden window holds nothing. | ||
| 459 | func (c *Core) resolveConstraint() { | 490 | func (c *Core) resolveConstraint() { | ||
| @@ -616,6 +647,24 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 616 | if p := c.popups[v.ID]; p != nil { | 647 | if p := c.popups[v.ID]; p != nil { | ||
| 617 | p.mapped = true | 648 | p.mapped = true | ||
| 618 | } | 649 | } | ||
| 650 | + | case ports.ShortcutsInhibit: | |||
| 651 | + | if v.Active { | |||
| 652 | + | c.inhibitors[v.Window] = true | |||
| 653 | + | } else { | |||
| 654 | + | delete(c.inhibitors, v.Window) | |||
| 655 | + | } | |||
| 656 | + | if err := c.updateInhibit(ctx); err != nil { | |||
| 657 | + | return nil | |||
| 658 | + | } | |||
| 659 | + | continue | |||
| 660 | + | case ports.IdleInhibit: | |||
| 661 | + | if v.Active { | |||
| 662 | + | c.idle[v.Window] = true | |||
| 663 | + | } else { | |||
| 664 | + | delete(c.idle, v.Window) | |||
| 665 | + | } | |||
| 666 | + | c.publishState() | |||
| 667 | + | continue | |||
| 619 | case ports.WindowAppID: | 668 | case ports.WindowAppID: | ||
| 620 | if info, ok := c.clients[v.ID]; ok { | 669 | if info, ok := c.clients[v.ID]; ok { | ||
| 621 | info.AppID = v.AppID | 670 | info.AppID = v.AppID | ||
| @@ -631,6 +680,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 631 | return nil | 680 | return nil | ||
| 632 | } | 681 | } | ||
| 633 | delete(c.clients, v.ID) | 682 | delete(c.clients, v.ID) | ||
| 683 | + | delete(c.inhibitors, v.ID) | |||
| 684 | + | delete(c.idle, v.ID) | |||
| 634 | if s, _ := c.screenOf(v.ID); s != nil { | 685 | if s, _ := c.screenOf(v.ID); s != nil { | ||
| 635 | s.mon.RemoveWindow(v.ID) | 686 | s.mon.RemoveWindow(v.ID) | ||
| 636 | } | 687 | } | ||
| @@ -797,6 +848,21 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 797 | if !ok { | 848 | if !ok { | ||
| 798 | continue | 849 | continue | ||
| 799 | } | 850 | } | ||
| 851 | + | // A focused window inhibiting shortcuts gets every key: | |||
| 852 | + | // no bind runs (emergency quit and VT switch are handled by | |||
| 853 | + | // input before core). | |||
| 854 | + | if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() { | |||
| 855 | + | if held := heldKey(key); !key.Pressed && c.pressed[held] { | |||
| 856 | + | // Its press ran a bind before inhibiting began: the | |||
| 857 | + | // window never saw it. | |||
| 858 | + | delete(c.pressed, held) | |||
| 859 | + | continue | |||
| 860 | + | } | |||
| 861 | + | if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil { | |||
| 862 | + | return nil | |||
| 863 | + | } | |||
| 864 | + | continue | |||
| 865 | + | } | |||
| 800 | name := keyName(key.Keysym) | 866 | name := keyName(key.Keysym) | ||
| 801 | // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1 | 867 | // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1 | ||
| 802 | // prints exclam on US); then the physical key (code:N). | 868 | // prints exclam on US); then the physical key (code:N). | ||
| @@ -808,10 +874,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 808 | action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}] | 874 | action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}] | ||
| 809 | } | 875 | } | ||
| 810 | // Track presses by physical key: Shift may be released before the key. | 876 | // Track presses by physical key: Shift may be released before the key. | ||
| 811 | - | held := name | 877 | + | held := heldKey(key) |
| 812 | - | if key.Keycode != 0 { | |||
| 813 | - | held = "#" + strconv.FormatUint(uint64(key.Keycode), 10) | |||
| 814 | - | } | |||
| 815 | if !key.Pressed { | 878 | if !key.Pressed { | ||
| 816 | consumed := c.pressed[held] | 879 | consumed := c.pressed[held] | ||
| 817 | delete(c.pressed, held) | 880 | delete(c.pressed, held) | ||
| @@ -902,3 +965,12 @@ func (c *Core) activate(ctx context.Context, id WindowID) error { | |||||
| 902 | c.focusScreen = c.screenIndex(s.name()) | 965 | c.focusScreen = c.screenIndex(s.name()) | ||
| 903 | return c.afterShow(ctx, before) | 966 | return c.afterShow(ctx, before) | ||
| 904 | } | 967 | } | ||
| 968 | + | ||||
| 969 | + | // heldKey names a key for press tracking: by physical key, since Shift | |||
| 970 | + | // may be released before the key. | |||
| 971 | + | func heldKey(key ports.KeyEvent) string { | |||
| 972 | + | if key.Keycode != 0 { | |||
| 973 | + | return "#" + strconv.FormatUint(uint64(key.Keycode), 10) | |||
| 974 | + | } | |||
| 975 | + | return keyName(key.Keysym) | |||
| 976 | + | } | |||
A internal/core/inhibit_test.go +127 -0
| @@ -0,0 +1,127 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/adapters/config" | |||
| 9 | + | "github.com/bnema/nefertty/internal/core" | |||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // commandOf waits for the next command of type T, skipping others. | |||
| 14 | + | func commandOf[T ports.ClientCommand](t *testing.T, commands <-chan ports.ClientCommand) T { | |||
| 15 | + | t.Helper() | |||
| 16 | + | deadline := time.After(2 * time.Second) | |||
| 17 | + | for { | |||
| 18 | + | select { | |||
| 19 | + | case c := <-commands: | |||
| 20 | + | if v, ok := c.(T); ok { | |||
| 21 | + | return v | |||
| 22 | + | } | |||
| 23 | + | case <-deadline: | |||
| 24 | + | var zero T | |||
| 25 | + | t.Fatalf("no %T", zero) | |||
| 26 | + | return zero | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // A focused window inhibiting shortcuts gets bound keys; it stops when | |||
| 32 | + | // it loses focus, and binds run again. | |||
| 33 | + | func TestShortcutsInhibit(t *testing.T) { | |||
| 34 | + | cfg := config.Defaults() | |||
| 35 | + | input := make(chan ports.InputEvent, 4) | |||
| 36 | + | client := make(chan ports.ClientEvent, 4) | |||
| 37 | + | commands := make(chan ports.ClientCommand, 64) | |||
| 38 | + | scenes := make(chan []ports.Scene, 1) | |||
| 39 | + | spawn := make(chan ports.SpawnRequest, 4) | |||
| 40 | + | state := make(chan ports.State, 1) | |||
| 41 | + | output := make(chan ports.OutputEvent, 1) | |||
| 42 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}) | |||
| 43 | + | if err != nil { | |||
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 47 | + | done := make(chan error, 1) | |||
| 48 | + | go func() { done <- c.Run(ctx) }() | |||
| 49 | + | defer func() { cancel(); receive(t, done) }() | |||
| 50 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 51 | + | scene(t, scenes) | |||
| 52 | + | client <- ports.WindowMapped{ID: 1} | |||
| 53 | + | scene(t, scenes) | |||
| 54 | + | client <- ports.ShortcutsInhibit{Window: 1, Active: true} | |||
| 55 | + | if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1, Active: true}) { | |||
| 56 | + | t.Fatal(v) | |||
| 57 | + | } | |||
| 58 | + | bound := ports.KeyEvent{Keysym: "Return", Mods: ports.ModSuper, Pressed: true} | |||
| 59 | + | input <- bound | |||
| 60 | + | if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) { | |||
| 61 | + | t.Fatal(v) | |||
| 62 | + | } | |||
| 63 | + | select { | |||
| 64 | + | case r := <-spawn: | |||
| 65 | + | t.Fatalf("bind ran: %v", r) | |||
| 66 | + | case <-time.After(30 * time.Millisecond): | |||
| 67 | + | } | |||
| 68 | + | // A new window takes focus: the inhibitor goes inactive, binds run. | |||
| 69 | + | client <- ports.WindowMapped{ID: 2} | |||
| 70 | + | if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1}) { | |||
| 71 | + | t.Fatal(v) | |||
| 72 | + | } | |||
| 73 | + | input <- bound | |||
| 74 | + | receive(t, spawn) | |||
| 75 | + | // Idle inhibitors show in the state. | |||
| 76 | + | client <- ports.IdleInhibit{Window: 2, Active: true} | |||
| 77 | + | deadline := time.After(2 * time.Second) | |||
| 78 | + | for { | |||
| 79 | + | select { | |||
| 80 | + | case st := <-state: | |||
| 81 | + | for _, w := range st.Windows { | |||
| 82 | + | if w.ID == 2 && w.IdleInhibit { | |||
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | case <-deadline: | |||
| 87 | + | t.Fatal("idle inhibit not in state") | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // A key whose press ran a bind before inhibiting began is not released to | |||
| 93 | + | // the window: it never saw the press. | |||
| 94 | + | func TestShortcutsInhibitDropsBoundRelease(t *testing.T) { | |||
| 95 | + | cfg := config.Defaults() | |||
| 96 | + | input := make(chan ports.InputEvent, 4) | |||
| 97 | + | client := make(chan ports.ClientEvent, 4) | |||
| 98 | + | commands := make(chan ports.ClientCommand, 64) | |||
| 99 | + | scenes := make(chan []ports.Scene, 1) | |||
| 100 | + | spawn := make(chan ports.SpawnRequest, 4) | |||
| 101 | + | output := make(chan ports.OutputEvent, 1) | |||
| 102 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn}) | |||
| 103 | + | if err != nil { | |||
| 104 | + | t.Fatal(err) | |||
| 105 | + | } | |||
| 106 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 107 | + | done := make(chan error, 1) | |||
| 108 | + | go func() { done <- c.Run(ctx) }() | |||
| 109 | + | defer func() { cancel(); receive(t, done) }() | |||
| 110 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 111 | + | scene(t, scenes) | |||
| 112 | + | client <- ports.WindowMapped{ID: 1} | |||
| 113 | + | scene(t, scenes) | |||
| 114 | + | press := ports.KeyEvent{Keysym: "Return", Keycode: 36, Mods: ports.ModSuper, Pressed: true} | |||
| 115 | + | input <- press | |||
| 116 | + | receive(t, spawn) | |||
| 117 | + | client <- ports.ShortcutsInhibit{Window: 1, Active: true} | |||
| 118 | + | commandOf[ports.ShortcutsInhibitState](t, commands) | |||
| 119 | + | release := press | |||
| 120 | + | release.Pressed = false | |||
| 121 | + | input <- release | |||
| 122 | + | other := ports.KeyEvent{Keysym: "a", Keycode: 38, Pressed: true} | |||
| 123 | + | input <- other | |||
| 124 | + | if v := commandOf[ports.ForwardKey](t, commands); v.Key != other { | |||
| 125 | + | t.Fatalf("forwarded %+v before %+v", v.Key, other) | |||
| 126 | + | } | |||
| 127 | + | } | |||
M internal/core/state.go +1 -1
| @@ -40,7 +40,7 @@ func (c *Core) state() ports.State { | |||||
| 40 | n := indexOf(m.Workspaces, w) + 1 | 40 | n := indexOf(m.Workspaces, w) + 1 | ||
| 41 | for _, id := range w.windows() { | 41 | for _, id := range w.windows() { | ||
| 42 | info := c.clients[id] | 42 | info := c.clients[id] | ||
| 43 | - | st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur}) | 43 | + | st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur, IdleInhibit: c.idle[id]}) |
| 44 | } | 44 | } | ||
| 45 | } | 45 | } | ||
| 46 | } | 46 | } | ||
M internal/logging/logging.go +17 -1
| @@ -20,6 +20,11 @@ var components = map[string]bool{ | |||||
| 20 | "render": true, "sync": true, "config": true, "app": true, | 20 | "render": true, "sync": true, "config": true, "app": true, | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | + | // categories are debug switches narrower than a component, only on when | |||
| 24 | + | // named: "all" leaves them off (input-motion logs every pointer motion). | |||
| 25 | + | // Naming one turns on debug for its component. | |||
| 26 | + | var categories = map[string]string{"input-motion": "input"} | |||
| 27 | + | ||||
| 23 | type debugKey struct{} | 28 | type debugKey struct{} | ||
| 24 | type levelKey struct{} | 29 | type levelKey struct{} | ||
| 25 | type debugSet map[string]bool | 30 | type debugSet map[string]bool | ||
| @@ -34,7 +39,7 @@ func ParseDebug(value string) (map[string]bool, error) { | |||||
| 34 | selected[name] = true | 39 | selected[name] = true | ||
| 35 | continue | 40 | continue | ||
| 36 | } | 41 | } | ||
| 37 | - | if !components[name] { | 42 | + | if _, ok := categories[name]; !components[name] && !ok { |
| 38 | return nil, fmt.Errorf("invalid debug component %q", name) | 43 | return nil, fmt.Errorf("invalid debug component %q", name) | ||
| 39 | } | 44 | } | ||
| 40 | selected[name] = true | 45 | selected[name] = true | ||
| @@ -104,6 +109,12 @@ func (w *consoleAndFile) Write(p []byte) (int, error) { | |||||
| 104 | return w.file.Write(p) | 109 | return w.file.Write(p) | ||
| 105 | } | 110 | } | ||
| 106 | 111 | ||||
| 112 | + | // Enabled reports whether a debug category was named in --debug. | |||
| 113 | + | func Enabled(ctx context.Context, category string) bool { | |||
| 114 | + | selected, _ := ctx.Value(debugKey{}).(debugSet) | |||
| 115 | + | return selected[category] | |||
| 116 | + | } | |||
| 117 | + | ||||
| 107 | // For returns a component logger with debug enabled only for selected components. | 118 | // For returns a component logger with debug enabled only for selected components. | ||
| 108 | func For(ctx context.Context, component string) zerowrap.Logger { | 119 | func For(ctx context.Context, component string) zerowrap.Logger { | ||
| 109 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | 120 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | ||
| @@ -120,6 +131,11 @@ func For(ctx context.Context, component string) zerowrap.Logger { | |||||
| 120 | if selected["all"] || selected[component] { | 131 | if selected["all"] || selected[component] { | ||
| 121 | level = zerolog.DebugLevel | 132 | level = zerolog.DebugLevel | ||
| 122 | } | 133 | } | ||
| 134 | + | for name, owner := range categories { | |||
| 135 | + | if selected[name] && owner == component { | |||
| 136 | + | level = zerolog.DebugLevel | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 123 | log = zerowrap.Logger{Logger: log.Level(level)} | 139 | log = zerowrap.Logger{Logger: log.Level(level)} | ||
| 124 | return log | 140 | return log | ||
| 125 | } | 141 | } | ||
M internal/logging/logging_test.go +40 -0
| @@ -18,6 +18,7 @@ func TestParseDebug(t *testing.T) { | |||||
| 18 | }{ | 18 | }{ | ||
| 19 | {"", false, nil}, {"core,app", false, []string{"core", "app"}}, | 19 | {"", false, nil}, {"core,app", false, []string{"core", "app"}}, | ||
| 20 | {"all", false, []string{"all"}}, {"unknown", true, nil}, {"core,", true, nil}, | 20 | {"all", false, []string{"all"}}, {"unknown", true, nil}, {"core,", true, nil}, | ||
| 21 | + | {"all,input-motion", false, []string{"all", "input-motion"}}, | |||
| 21 | } { | 22 | } { | ||
| 22 | got, err := ParseDebug(tc.input) | 23 | got, err := ParseDebug(tc.input) | ||
| 23 | if (err != nil) != tc.wantError { | 24 | if (err != nil) != tc.wantError { | ||
| @@ -82,3 +83,42 @@ func splitLines(data []byte) [][]byte { | |||||
| 82 | } | 83 | } | ||
| 83 | return lines | 84 | return lines | ||
| 84 | } | 85 | } | ||
| 86 | + | ||||
| 87 | + | // A category alone raises its component to debug, so its lines are written. | |||
| 88 | + | func TestCategoryEnablesComponentDebug(t *testing.T) { | |||
| 89 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |||
| 90 | + | ctx, closeLog, err := Open(context.Background(), "info", "input-motion") | |||
| 91 | + | if err != nil { | |||
| 92 | + | t.Fatal(err) | |||
| 93 | + | } | |||
| 94 | + | input, core := For(ctx, "input"), For(ctx, "core") | |||
| 95 | + | input.Debug().Msg("motion") | |||
| 96 | + | core.Debug().Msg("hidden") | |||
| 97 | + | if err := closeLog(); err != nil { | |||
| 98 | + | t.Fatal(err) | |||
| 99 | + | } | |||
| 100 | + | data, err := os.ReadFile(filepath.Join(os.Getenv("XDG_STATE_HOME"), "nefertty", "runs", "latest.log")) | |||
| 101 | + | if err != nil { | |||
| 102 | + | t.Fatal(err) | |||
| 103 | + | } | |||
| 104 | + | if lines := splitLines(data); len(lines) != 1 || !bytes.Contains(lines[0], []byte(`"motion"`)) { | |||
| 105 | + | t.Fatalf("got %q", data) | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // Narrow categories are on only when named, never through "all". | |||
| 110 | + | func TestEnabledCategories(t *testing.T) { | |||
| 111 | + | for _, tc := range []struct { | |||
| 112 | + | debug string | |||
| 113 | + | want bool | |||
| 114 | + | }{{"all", false}, {"input", false}, {"input-motion", true}, {"all,input-motion", true}} { | |||
| 115 | + | sel, err := ParseDebug(tc.debug) | |||
| 116 | + | if err != nil { | |||
| 117 | + | t.Fatal(err) | |||
| 118 | + | } | |||
| 119 | + | ctx := context.WithValue(context.Background(), debugKey{}, debugSet(sel)) | |||
| 120 | + | if got := Enabled(ctx, "input-motion"); got != tc.want { | |||
| 121 | + | t.Errorf("%q: %v", tc.debug, got) | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | } | |||
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 { | ||
A internal/ports/damage_test.go +37 -0
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | package ports | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestDamageSince(t *testing.T) { | |||
| 9 | + | a, b := Rect{X: 1, W: 2, H: 2}, Rect{Y: 5, W: 1, H: 1} | |||
| 10 | + | c := SurfaceContent{Seq: 7, DamageHistory: []SeqDamage{{Seq: 5, Rects: []Rect{a}}, {Seq: 6, Full: true}, {Seq: 7, Rects: []Rect{b}}}} | |||
| 11 | + | for _, tc := range []struct { | |||
| 12 | + | name string | |||
| 13 | + | since uint64 | |||
| 14 | + | want []Rect | |||
| 15 | + | ok bool | |||
| 16 | + | }{ | |||
| 17 | + | {"up to date", 7, nil, true}, | |||
| 18 | + | {"one behind", 6, []Rect{b}, true}, | |||
| 19 | + | {"across a full change", 5, nil, false}, | |||
| 20 | + | {"history too short", 3, nil, false}, | |||
| 21 | + | {"first content", 0, nil, false}, | |||
| 22 | + | } { | |||
| 23 | + | got, ok := c.DamageSince(tc.since) | |||
| 24 | + | if ok != tc.ok || !slices.Equal(got, tc.want) { | |||
| 25 | + | t.Errorf("%s: %v %v", tc.name, got, ok) | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | c.DamageHistory[1] = SeqDamage{Seq: 6, Rects: []Rect{a}} | |||
| 29 | + | if got, ok := c.DamageSince(4); !ok || !slices.Equal(got, []Rect{a, a, b}) { | |||
| 30 | + | t.Fatalf("union %v %v", got, ok) | |||
| 31 | + | } | |||
| 32 | + | // A history not ending at the content is stale: redraw all. | |||
| 33 | + | c.Seq = 8 | |||
| 34 | + | if _, ok := c.DamageSince(6); ok { | |||
| 35 | + | t.Fatal("stale history used") | |||
| 36 | + | } | |||
| 37 | + | } | |||
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 +113 -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() } | ||
| @@ -31,6 +34,25 @@ type WindowUnmapped struct{ ID WindowID } | |||||
| 31 | 34 | ||||
| 32 | func (WindowUnmapped) clientEvent() {} | 35 | func (WindowUnmapped) clientEvent() {} | ||
| 33 | 36 | ||||
| 37 | + | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | |||
| 38 | + | // or stops asking for all keys, compositor binds included, while it has | |||
| 39 | + | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | |||
| 40 | + | type ShortcutsInhibit struct { | |||
| 41 | + | Window WindowID | |||
| 42 | + | Active bool | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (ShortcutsInhibit) clientEvent() {} | |||
| 46 | + | ||||
| 47 | + | // IdleInhibit carries wayland → core that a window keeps (Active) or | |||
| 48 | + | // stops keeping the session from going idle (zwp_idle_inhibit_v1). | |||
| 49 | + | type IdleInhibit struct { | |||
| 50 | + | Window WindowID | |||
| 51 | + | Active bool | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | func (IdleInhibit) clientEvent() {} | |||
| 55 | + | ||||
| 34 | // WindowFullscreenRequest carries wayland → core fullscreen requests. | 56 | // WindowFullscreenRequest carries wayland → core fullscreen requests. | ||
| 35 | type WindowFullscreenRequest struct { | 57 | type WindowFullscreenRequest struct { | ||
| 36 | ID WindowID | 58 | ID WindowID | ||
| @@ -315,17 +337,47 @@ func (OutputAdded) outputEvent() {} | |||||
| 315 | // OutputPresented carries output → wayland what an output shows and reads. | 337 | // OutputPresented carries output → wayland what an output shows and reads. | ||
| 316 | // Flip is set when a page flip completed: frame callbacks of the surfaces | 338 | // 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 | 339 | // 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 | 340 | + | // (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 | 341 | + | // 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 | 342 | + | // buffer is released once every output that reports has seen a later |
| 321 | - | // queues it. | 343 | + | // content of its window and neither shows nor queues it. |
| 322 | type OutputPresented struct { | 344 | type OutputPresented struct { | ||
| 323 | Output string | 345 | Output string | ||
| 324 | - | Flip bool | 346 | + | Flip *FlipInfo |
| 325 | Shown, Queued uint64 | 347 | Shown, Queued uint64 | ||
| 326 | Seen map[WindowID]uint64 | 348 | Seen map[WindowID]uint64 | ||
| 327 | } | 349 | } | ||
| 328 | 350 | ||||
| 351 | + | // OutputFormats carries output → wayland the dmabuf formats an output can | |||
| 352 | + | // scan out directly (its primary plane's, that the renderer also samples, | |||
| 353 | + | // so a refused buffer can still be composed). Device is the KMS device | |||
| 354 | + | // (dev_t) clients allocate scanout buffers for. Sent at start and after | |||
| 355 | + | // every modeset; empty Formats means no direct scanout. | |||
| 356 | + | type OutputFormats struct { | |||
| 357 | + | Output string | |||
| 358 | + | Device uint64 | |||
| 359 | + | Formats []DMABufFormat | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | |||
| 363 | + | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | |||
| 364 | + | // refresh period (0 while variable refresh is on). ZeroCopy is the window | |||
| 365 | + | // whose buffer was shown without composition (direct scanout or overlay | |||
| 366 | + | // plane; 0: none), Async set for a tearing flip. | |||
| 367 | + | // Shows is the content Seq per window the flipped frame shows. Merged | |||
| 368 | + | // counts earlier flips folded into this one when the reader fell behind: | |||
| 369 | + | // their presentation feedback is discarded. | |||
| 370 | + | type FlipInfo struct { | |||
| 371 | + | When time.Duration | |||
| 372 | + | Seq uint64 | |||
| 373 | + | Refresh time.Duration | |||
| 374 | + | ZeroCopy WindowID | |||
| 375 | + | Async bool | |||
| 376 | + | HardwareClock bool | |||
| 377 | + | Merged int | |||
| 378 | + | Shows map[WindowID]uint64 | |||
| 379 | + | } | |||
| 380 | + | ||||
| 329 | // OutputRemoved carries output → core an unplugged display. | 381 | // OutputRemoved carries output → core an unplugged display. | ||
| 330 | type OutputRemoved struct{ Name string } | 382 | type OutputRemoved struct{ Name string } | ||
| 331 | 383 | ||||
| @@ -337,6 +389,15 @@ type ConfigChanged struct{ Config Config } | |||||
| 337 | // ClientCommand carries core → wayland commands. | 389 | // ClientCommand carries core → wayland commands. | ||
| 338 | type ClientCommand interface{ clientCommand() } | 390 | type ClientCommand interface{ clientCommand() } | ||
| 339 | 391 | ||||
| 392 | + | // ShortcutsInhibitState carries core → wayland whether a window's | |||
| 393 | + | // shortcuts inhibitor is in effect (it has keyboard focus). | |||
| 394 | + | type ShortcutsInhibitState struct { | |||
| 395 | + | Window WindowID | |||
| 396 | + | Active bool | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | func (ShortcutsInhibitState) clientCommand() {} | |||
| 400 | + | ||||
| 340 | // ConfigureWindow carries core → wayland geometry and state. Output is the | 401 | // ConfigureWindow carries core → wayland geometry and state. Output is the | ||
| 341 | // connector showing the window, empty while it is hidden. | 402 | // connector showing the window, empty while it is hidden. | ||
| 342 | type ConfigureWindow struct { | 403 | type ConfigureWindow struct { | ||
| @@ -590,6 +651,50 @@ type SurfaceContent struct { | |||||
| 590 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | 651 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | ||
| 591 | // honour it only in direct scanout. | 652 | // honour it only in direct scanout. | ||
| 592 | Async bool | 653 | Async bool | ||
| 654 | + | // Acquire is the explicit-sync fence of this content's root buffer | |||
| 655 | + | // (wp_linux_drm_syncobj_v1): readers wait on it before reading the | |||
| 656 | + | // buffer, instead of the buffer's implicit fences. Wayland owns it and | |||
| 657 | + | // closes it when the buffer is released; a reader that keeps it past | |||
| 658 | + | // the call duplicates it under Acquire.SyscallConn. nil: implicit sync. | |||
| 659 | + | // Wayland closes it only after every output reported a later content of | |||
| 660 | + | // the window as seen, so a reader never gets it already closed. | |||
| 661 | + | Acquire *os.File | |||
| 662 | + | // DamageHistory is what changed in the root surface's buffer over the | |||
| 663 | + | // window's last contents, oldest first, ending with this one (Seq). | |||
| 664 | + | // A renderer holding an older content redraws the union of the | |||
| 665 | + | // entries after it, or everything when the history does not reach it. | |||
| 666 | + | DamageHistory []SeqDamage | |||
| 667 | + | } | |||
| 668 | + | ||||
| 669 | + | // SeqDamage is what content Seq changed from the one before: Rects in | |||
| 670 | + | // root buffer pixels, or everything when Full. | |||
| 671 | + | type SeqDamage struct { | |||
| 672 | + | Seq uint64 | |||
| 673 | + | Full bool | |||
| 674 | + | Rects []Rect | |||
| 675 | + | } | |||
| 676 | + | ||||
| 677 | + | // DamageSince is the union of changes after content seq up to this one, | |||
| 678 | + | // and false when the history does not reach back to seq (redraw all). | |||
| 679 | + | func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) { | |||
| 680 | + | if seq >= c.Seq { | |||
| 681 | + | return nil, true | |||
| 682 | + | } | |||
| 683 | + | h := c.DamageHistory | |||
| 684 | + | if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq { | |||
| 685 | + | return nil, false | |||
| 686 | + | } | |||
| 687 | + | var out []Rect | |||
| 688 | + | for _, d := range h { | |||
| 689 | + | if d.Seq <= seq { | |||
| 690 | + | continue | |||
| 691 | + | } | |||
| 692 | + | if d.Full { | |||
| 693 | + | return nil, false | |||
| 694 | + | } | |||
| 695 | + | out = append(out, d.Rects...) | |||
| 696 | + | } | |||
| 697 | + | return out, true | |||
| 593 | } | 698 | } | ||
| 594 | 699 | ||||
| 595 | // Subsurface is a child surface at X, Y logical pixels from the root | 700 | // Subsurface is a child surface at X, Y logical pixels from the root | ||
| @@ -695,4 +800,6 @@ type WindowState struct { | |||||
| 695 | // Visible means on the workspace on screen (it may be behind a | 800 | // Visible means on the workspace on screen (it may be behind a | ||
| 696 | // fullscreen window). | 801 | // fullscreen window). | ||
| 697 | Visible bool | 802 | Visible bool | ||
| 803 | + | // IdleInhibit is set while the window keeps the session from idling. | |||
| 804 | + | IdleInhibit bool `json:",omitempty"` | |||
| 698 | } | 805 | } | ||