feat(wayland): wp_linux_drm_syncobj_v1 explicit sync with acquire fences for the renderer and KMS
Timelines are imported on the renderer's render node when it supports timeline syncobjs. A commit with points waits in the surface queue until its acquire point has a fence (one goroutine polls the kernel eventfds); the content then carries the fence as a sync file, which the renderer imports as a wait semaphore and direct scanout passes as IN_FENCE_FD from a duplicate. The release point is signalled when no output reads the buffer any more, instead of wl_buffer.release, and the fence is closed then. Protocol errors follow the spec.
- commit
- 6f1650908aece0f1306c4c8e7e06eb5390c3b7b5
- author
- brice · · 2026-09-26 12:11 UTC
- parent
- f0db5a6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
19 files · +1468 -27
M .mockery.yml +5 -0
| @@ -30,6 +30,11 @@ packages: | |||
| 30 | 30 | dir: internal/adapters/wayland | |
| 31 | 31 | pkgname: wayland | |
| 32 | 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 | 38 | github.com/bnema/nefertty/internal/adapters/statefile: | |
| 34 | 39 | interfaces: | |
| 35 | 40 | procTree: | |
| @@ -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 internal/adapters/drm/output.go +9 -2
| @@ -562,13 +562,20 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame | |||
| 562 | 562 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | |
| 563 | 563 | vrr := o.wantVRR(true) | |
| 564 | 564 | f.queued, f.zeroCopy = c.DMABuf.ID, true | |
| 565 | - | err := o.commitFrame(fb, nil, async, vrr, f) | |
| 565 | + | // Explicit sync: KMS waits on the client's acquire fence. Wayland may | |
| 566 | + | // close its file at any time: commit a duplicate taken while it is | |
| 567 | + | // held open (the kernel keeps its own reference). | |
| 568 | + | fence := dupFence(c.Acquire) | |
| 569 | + | if fence != nil { | |
| 570 | + | defer fence.Close() | |
| 571 | + | } | |
| 572 | + | err := o.commitFrame(fb, fence, async, vrr, f) | |
| 566 | 573 | if err != nil && async && errors.Is(err, unix.EINVAL) { | |
| 567 | 574 | // Refused (e.g. not a fast update): this buffer flips at vblank | |
| 568 | 575 | // from now on. | |
| 569 | 576 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | |
| 570 | 577 | cfb.noAsync = true | |
| 571 | - | err = o.commitFrame(fb, nil, false, vrr, f) | |
| 578 | + | err = o.commitFrame(fb, fence, false, vrr, f) | |
| 572 | 579 | } | |
| 573 | 580 | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | |
| 574 | 581 | // KMS refuses this buffer on the plane: compose it instead. | |
| @@ -562,13 +562,20 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame | |||||
| 562 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | 562 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | ||
| 563 | vrr := o.wantVRR(true) | 563 | vrr := o.wantVRR(true) | ||
| 564 | f.queued, f.zeroCopy = c.DMABuf.ID, true | 564 | f.queued, f.zeroCopy = c.DMABuf.ID, true | ||
| 565 | - | err := o.commitFrame(fb, nil, async, vrr, f) | 565 | + | // Explicit sync: KMS waits on the client's acquire fence. Wayland may |
| 566 | + | // close its file at any time: commit a duplicate taken while it is | |||
| 567 | + | // held open (the kernel keeps its own reference). | |||
| 568 | + | fence := dupFence(c.Acquire) | |||
| 569 | + | if fence != nil { | |||
| 570 | + | defer fence.Close() | |||
| 571 | + | } | |||
| 572 | + | err := o.commitFrame(fb, fence, async, vrr, f) | |||
| 566 | if err != nil && async && errors.Is(err, unix.EINVAL) { | 573 | if err != nil && async && errors.Is(err, unix.EINVAL) { | ||
| 567 | // Refused (e.g. not a fast update): this buffer flips at vblank | 574 | // Refused (e.g. not a fast update): this buffer flips at vblank | ||
| 568 | // from now on. | 575 | // from now on. | ||
| 569 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 576 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 570 | cfb.noAsync = true | 577 | cfb.noAsync = true | ||
| 571 | - | err = o.commitFrame(fb, nil, false, vrr, f) | 578 | + | err = o.commitFrame(fb, fence, false, vrr, f) |
| 572 | } | 579 | } | ||
| 573 | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | 580 | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | ||
| 574 | // KMS refuses this buffer on the plane: compose it instead. | 581 | // KMS refuses this buffer on the plane: compose it instead. | ||
M internal/adapters/drm/scanout.go +18 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "unsafe" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/nefertty/internal/ports" | |
| 11 | + | "golang.org/x/sys/unix" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| 13 | 14 | // Direct scanout (ADR 006): when one fullscreen window covers the output | |
| @@ -272,3 +273,20 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||
| 272 | 273 | delete(o.clientFBs, id) | |
| 273 | 274 | } | |
| 274 | 275 | } | |
| 276 | + | ||
| 277 | + | // dupFence duplicates a sync file under its SyscallConn, so a concurrent | |
| 278 | + | // Close cannot hand us a reused fd; nil when f is nil or closed. | |
| 279 | + | func dupFence(f *os.File) *os.File { | |
| 280 | + | if f == nil { | |
| 281 | + | return nil | |
| 282 | + | } | |
| 283 | + | raw, err := f.SyscallConn() | |
| 284 | + | if err != nil { | |
| 285 | + | return nil | |
| 286 | + | } | |
| 287 | + | fd := -1 | |
| 288 | + | if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 { | |
| 289 | + | return nil | |
| 290 | + | } | |
| 291 | + | return os.NewFile(uintptr(fd), "acquire-fence") | |
| 292 | + | } | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "unsafe" | 8 | "unsafe" | ||
| 9 | 9 | ||||
| 10 | "github.com/bnema/nefertty/internal/ports" | 10 | "github.com/bnema/nefertty/internal/ports" | ||
| 11 | + | "golang.org/x/sys/unix" | |||
| 11 | ) | 12 | ) | ||
| 12 | 13 | ||||
| 13 | // Direct scanout (ADR 006): when one fullscreen window covers the output | 14 | // Direct scanout (ADR 006): when one fullscreen window covers the output | ||
| @@ -272,3 +273,20 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||||
| 272 | delete(o.clientFBs, id) | 273 | delete(o.clientFBs, id) | ||
| 273 | } | 274 | } | ||
| 274 | } | 275 | } | ||
| 276 | + | ||||
| 277 | + | // dupFence duplicates a sync file under its SyscallConn, so a concurrent | |||
| 278 | + | // Close cannot hand us a reused fd; nil when f is nil or closed. | |||
| 279 | + | func dupFence(f *os.File) *os.File { | |||
| 280 | + | if f == nil { | |||
| 281 | + | return nil | |||
| 282 | + | } | |||
| 283 | + | raw, err := f.SyscallConn() | |||
| 284 | + | if err != nil { | |||
| 285 | + | return nil | |||
| 286 | + | } | |||
| 287 | + | fd := -1 | |||
| 288 | + | if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 { | |||
| 289 | + | return nil | |||
| 290 | + | } | |||
| 291 | + | return os.NewFile(uintptr(fd), "acquire-fence") | |||
| 292 | + | } | |||
M internal/adapters/vulkan/compose.go +3 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "encoding/binary" | |
| 6 | 6 | "image" | |
| 7 | 7 | "math" | |
| 8 | + | "os" | |
| 8 | 9 | "unsafe" | |
| 9 | 10 | ||
| 10 | 11 | vk "github.com/bnema/purego-vulkan/vulkan" | |
| @@ -51,6 +52,8 @@ type draw struct { | |||
| 51 | 52 | pc pushConstants | |
| 52 | 53 | set vk.DescriptorSet | |
| 53 | 54 | im *imported // sampled client dmabuf, for ownership barriers | |
| 55 | + | // acquire is the content's explicit-sync fence (nil: implicit). | |
| 56 | + | acquire *os.File | |
| 54 | 57 | } | |
| 55 | 58 | ||
| 56 | 59 | // composer is the pipeline state and the objects every draw can bind. | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "encoding/binary" | 5 | "encoding/binary" | ||
| 6 | "image" | 6 | "image" | ||
| 7 | "math" | 7 | "math" | ||
| 8 | + | "os" | |||
| 8 | "unsafe" | 9 | "unsafe" | ||
| 9 | 10 | ||||
| 10 | vk "github.com/bnema/purego-vulkan/vulkan" | 11 | vk "github.com/bnema/purego-vulkan/vulkan" | ||
| @@ -51,6 +52,8 @@ type draw struct { | |||||
| 51 | pc pushConstants | 52 | pc pushConstants | ||
| 52 | set vk.DescriptorSet | 53 | set vk.DescriptorSet | ||
| 53 | im *imported // sampled client dmabuf, for ownership barriers | 54 | im *imported // sampled client dmabuf, for ownership barriers | ||
| 55 | + | // acquire is the content's explicit-sync fence (nil: implicit). | |||
| 56 | + | acquire *os.File | |||
| 54 | } | 57 | } | ||
| 55 | 58 | ||||
| 56 | // composer is the pipeline state and the objects every draw can bind. | 59 | // composer is the pipeline state and the objects every draw can bind. | ||
M internal/adapters/vulkan/compose_test.go +31 -0
| @@ -3,6 +3,7 @@ package vulkan | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "image/color" | |
| 6 | + | "os" | |
| 6 | 7 | "slices" | |
| 7 | 8 | "testing" | |
| 8 | 9 | "time" | |
| @@ -182,3 +183,33 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) { | |||
| 182 | 183 | t.Fatalf("pixel %v", got) | |
| 183 | 184 | } | |
| 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 | + | } | |
| @@ -3,6 +3,7 @@ package vulkan | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "image/color" | 5 | "image/color" | ||
| 6 | + | "os" | |||
| 6 | "slices" | 7 | "slices" | ||
| 7 | "testing" | 8 | "testing" | ||
| 8 | "time" | 9 | "time" | ||
| @@ -182,3 +183,33 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) { | |||||
| 182 | t.Fatalf("pixel %v", got) | 183 | t.Fatalf("pixel %v", got) | ||
| 183 | } | 184 | } | ||
| 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 | + | } | |||
M internal/adapters/vulkan/dmabuf.go +19 -3
| @@ -287,15 +287,31 @@ func (r *Renderer) readFence(im *imported) vk.Semaphore { | |||
| 287 | 287 | if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 { | |
| 288 | 288 | return 0 | |
| 289 | 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 { | |
| 290 | 306 | var sem vk.Semaphore | |
| 291 | 307 | si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo} | |
| 292 | 308 | if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success { | |
| 293 | - | unix.Close(int(arg.fd)) | |
| 309 | + | unix.Close(fd) | |
| 294 | 310 | return 0 | |
| 295 | 311 | } | |
| 296 | - | 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)} | |
| 297 | 313 | if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success { | |
| 298 | - | unix.Close(int(arg.fd)) | |
| 314 | + | unix.Close(fd) | |
| 299 | 315 | r.dd.DestroySemaphore(r.device, sem, nil) | |
| 300 | 316 | return 0 | |
| 301 | 317 | } | |
| @@ -287,15 +287,31 @@ func (r *Renderer) readFence(im *imported) vk.Semaphore { | |||||
| 287 | 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 { | ||
| 288 | return 0 | 288 | return 0 | ||
| 289 | } | 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 { | |||
| 290 | var sem vk.Semaphore | 306 | var sem vk.Semaphore | ||
| 291 | si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo} | 307 | si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo} | ||
| 292 | if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success { | 308 | if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success { | ||
| 293 | - | unix.Close(int(arg.fd)) | 309 | + | unix.Close(fd) |
| 294 | return 0 | 310 | return 0 | ||
| 295 | } | 311 | } | ||
| 296 | - | 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)} |
| 297 | if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success { | 313 | if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success { | ||
| 298 | - | unix.Close(int(arg.fd)) | 314 | + | unix.Close(fd) |
| 299 | r.dd.DestroySemaphore(r.device, sem, nil) | 315 | r.dd.DestroySemaphore(r.device, sem, nil) | ||
| 300 | return 0 | 316 | return 0 | ||
| 301 | } | 317 | } | ||
M internal/adapters/vulkan/frame.go +13 -2
| @@ -205,11 +205,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 205 | 205 | // Client buffers come from the foreign queue family (their driver) | |
| 206 | 206 | // and go back to it after the frame, so the next frame acquires them. | |
| 207 | 207 | var dmas []*imported | |
| 208 | + | acquires := map[*imported]*os.File{} | |
| 208 | 209 | for _, dr := range ds { | |
| 209 | 210 | if dr.im != nil && !slices.Contains(dmas, dr.im) { | |
| 210 | 211 | dmas = append(dmas, dr.im) | |
| 211 | 212 | dr.im.last = r.frame | |
| 212 | 213 | } | |
| 214 | + | if dr.acquire != nil { | |
| 215 | + | acquires[dr.im] = dr.acquire | |
| 216 | + | } | |
| 213 | 217 | } | |
| 214 | 218 | r.ownership(cmd, dmas, true) | |
| 215 | 219 | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | |
| @@ -228,10 +232,17 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 228 | 232 | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil { | |
| 229 | 233 | return nil, err | |
| 230 | 234 | } | |
| 231 | - | // Implicit sync: wait for the client's GPU writes to each buffer. | |
| 235 | + | // Wait for the client's GPU writes to each buffer: its explicit | |
| 236 | + | // acquire fence, else the buffer's implicit fences. | |
| 232 | 237 | var waits []vk.Semaphore | |
| 233 | 238 | for _, im := range dmas { | |
| 234 | - | if sem := r.readFence(im); sem != 0 { | |
| 239 | + | sem := vk.Semaphore(0) | |
| 240 | + | if f := acquires[im]; f != nil { | |
| 241 | + | sem = r.importFence(f) | |
| 242 | + | } else { | |
| 243 | + | sem = r.readFence(im) | |
| 244 | + | } | |
| 245 | + | if sem != 0 { | |
| 235 | 246 | waits = append(waits, sem) | |
| 236 | 247 | } | |
| 237 | 248 | } | |
| @@ -205,11 +205,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 205 | // Client buffers come from the foreign queue family (their driver) | 205 | // Client buffers come from the foreign queue family (their driver) | ||
| 206 | // and go back to it after the frame, so the next frame acquires them. | 206 | // and go back to it after the frame, so the next frame acquires them. | ||
| 207 | var dmas []*imported | 207 | var dmas []*imported | ||
| 208 | + | acquires := map[*imported]*os.File{} | |||
| 208 | for _, dr := range ds { | 209 | for _, dr := range ds { | ||
| 209 | if dr.im != nil && !slices.Contains(dmas, dr.im) { | 210 | if dr.im != nil && !slices.Contains(dmas, dr.im) { | ||
| 210 | dmas = append(dmas, dr.im) | 211 | dmas = append(dmas, dr.im) | ||
| 211 | dr.im.last = r.frame | 212 | dr.im.last = r.frame | ||
| 212 | } | 213 | } | ||
| 214 | + | if dr.acquire != nil { | |||
| 215 | + | acquires[dr.im] = dr.acquire | |||
| 216 | + | } | |||
| 213 | } | 217 | } | ||
| 214 | r.ownership(cmd, dmas, true) | 218 | r.ownership(cmd, dmas, true) | ||
| 215 | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | 219 | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | ||
| @@ -228,10 +232,17 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 228 | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil { | 232 | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil { | ||
| 229 | return nil, err | 233 | return nil, err | ||
| 230 | } | 234 | } | ||
| 231 | - | // Implicit sync: wait for the client's GPU writes to each buffer. | 235 | + | // Wait for the client's GPU writes to each buffer: its explicit |
| 236 | + | // acquire fence, else the buffer's implicit fences. | |||
| 232 | var waits []vk.Semaphore | 237 | var waits []vk.Semaphore | ||
| 233 | for _, im := range dmas { | 238 | for _, im := range dmas { | ||
| 234 | - | if sem := r.readFence(im); sem != 0 { | 239 | + | sem := vk.Semaphore(0) |
| 240 | + | if f := acquires[im]; f != nil { | |||
| 241 | + | sem = r.importFence(f) | |||
| 242 | + | } else { | |||
| 243 | + | sem = r.readFence(im) | |||
| 244 | + | } | |||
| 245 | + | if sem != 0 { | |||
| 235 | waits = append(waits, sem) | 246 | waits = append(waits, sem) | ||
| 236 | } | 247 | } | ||
| 237 | } | 248 | } | ||
M internal/adapters/vulkan/renderer.go +3 -0
| @@ -333,6 +333,9 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 333 | 333 | } | |
| 334 | 334 | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) | |
| 335 | 335 | dr.set, dr.im = im.set, im | |
| 336 | + | if key.index == 0 { | |
| 337 | + | dr.acquire = content.Acquire | |
| 338 | + | } | |
| 336 | 339 | draws = append(draws, dr) | |
| 337 | 340 | return | |
| 338 | 341 | } | |
| @@ -333,6 +333,9 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 333 | } | 333 | } | ||
| 334 | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) | 334 | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) | ||
| 335 | dr.set, dr.im = im.set, im | 335 | dr.set, dr.im = im.set, im | ||
| 336 | + | if key.index == 0 { | |||
| 337 | + | dr.acquire = content.Acquire | |||
| 338 | + | } | |||
| 336 | draws = append(draws, dr) | 339 | draws = append(draws, dr) | ||
| 337 | return | 340 | return | ||
| 338 | } | 341 | } | ||
M internal/adapters/wayland/fifo.go +12 -4
| @@ -218,6 +218,7 @@ type update struct { | |||
| 218 | 218 | wait bool | |
| 219 | 219 | at time.Time | |
| 220 | 220 | feedback []*presentationtime.WpPresentationFeedback | |
| 221 | + | sync *commitSync | |
| 221 | 222 | damage []ports.Rect | |
| 222 | 223 | bufDamage []ports.Rect | |
| 223 | 224 | } | |
| @@ -230,9 +231,9 @@ type childMove struct { | |||
| 230 | 231 | // takePending moves the pending state into an update: one-shot state is | |
| 231 | 232 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 232 | 233 | func (s *surface) takePending() update { | |
| 233 | - | 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} | |
| 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} | |
| 234 | 235 | s.attached, s.pending, s.callbacks = false, nil, nil | |
| 235 | - | s.pendingFeedback = nil | |
| 236 | + | s.pendingFeedback, s.pendingSync = nil, nil | |
| 236 | 237 | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| 237 | 238 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | |
| 238 | 239 | if v := s.viewport; v != nil { | |
| @@ -263,7 +264,7 @@ func (s *surface) putPending(u update) { | |||
| 263 | 264 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |
| 264 | 265 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | |
| 265 | 266 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | |
| 266 | - | s.pendingFeedback = u.feedback | |
| 267 | + | s.pendingFeedback, s.pendingSync = u.feedback, u.sync | |
| 267 | 268 | if u.vp != nil && s.viewport == u.vp { | |
| 268 | 269 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | |
| 269 | 270 | } | |
| @@ -285,7 +286,7 @@ func (s *surface) putPending(u update) { | |||
| 285 | 286 | ||
| 286 | 287 | // mustWait reports whether the pending commit cannot apply now. | |
| 287 | 288 | func (s *surface) mustWait(now time.Time) bool { | |
| 288 | - | 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) | |
| 289 | 290 | } | |
| 290 | 291 | ||
| 291 | 292 | // tooEarly reports whether content applied now would show before at: | |
| @@ -347,6 +348,10 @@ func (s *surface) dropQueue() { | |||
| 347 | 348 | for _, fb := range u.feedback { | |
| 348 | 349 | discard(fb) | |
| 349 | 350 | } | |
| 351 | + | s.dropSync(u.sync) | |
| 352 | + | if u.sync != nil { | |
| 353 | + | continue // explicit sync: the release point replaced release | |
| 354 | + | } | |
| 350 | 355 | if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() { | |
| 351 | 356 | released[u.buffer.Resource] = true | |
| 352 | 357 | u.buffer.SendRelease() | |
| @@ -413,6 +418,9 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration | |||
| 413 | 418 | if u.wait && surf.barrier { | |
| 414 | 419 | break | |
| 415 | 420 | } | |
| 421 | + | if !surf.syncReady(u.sync) { | |
| 422 | + | break | |
| 423 | + | } | |
| 416 | 424 | if surf.tooEarly(u.at, now) { | |
| 417 | 425 | wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond) | |
| 418 | 426 | break | |
| @@ -218,6 +218,7 @@ type update struct { | |||||
| 218 | wait bool | 218 | wait bool | ||
| 219 | at time.Time | 219 | at time.Time | ||
| 220 | feedback []*presentationtime.WpPresentationFeedback | 220 | feedback []*presentationtime.WpPresentationFeedback | ||
| 221 | + | sync *commitSync | |||
| 221 | damage []ports.Rect | 222 | damage []ports.Rect | ||
| 222 | bufDamage []ports.Rect | 223 | bufDamage []ports.Rect | ||
| 223 | } | 224 | } | ||
| @@ -230,9 +231,9 @@ type childMove struct { | |||||
| 230 | // 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 | ||
| 231 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | 232 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | ||
| 232 | func (s *surface) takePending() update { | 233 | func (s *surface) takePending() update { | ||
| 233 | - | 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} | 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} |
| 234 | s.attached, s.pending, s.callbacks = false, nil, nil | 235 | s.attached, s.pending, s.callbacks = false, nil, nil | ||
| 235 | - | s.pendingFeedback = nil | 236 | + | s.pendingFeedback, s.pendingSync = nil, nil |
| 236 | s.pendingDamage, s.pendingBufDamage = nil, nil | 237 | s.pendingDamage, s.pendingBufDamage = nil, nil | ||
| 237 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | 238 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | ||
| 238 | if v := s.viewport; v != nil { | 239 | if v := s.viewport; v != nil { | ||
| @@ -263,7 +264,7 @@ func (s *surface) putPending(u update) { | |||||
| 263 | 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 | ||
| 264 | 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 | ||
| 265 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | 266 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | ||
| 266 | - | s.pendingFeedback = u.feedback | 267 | + | s.pendingFeedback, s.pendingSync = u.feedback, u.sync |
| 267 | if u.vp != nil && s.viewport == u.vp { | 268 | if u.vp != nil && s.viewport == u.vp { | ||
| 268 | 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 | ||
| 269 | } | 270 | } | ||
| @@ -285,7 +286,7 @@ func (s *surface) putPending(u update) { | |||||
| 285 | 286 | ||||
| 286 | // mustWait reports whether the pending commit cannot apply now. | 287 | // mustWait reports whether the pending commit cannot apply now. | ||
| 287 | func (s *surface) mustWait(now time.Time) bool { | 288 | func (s *surface) mustWait(now time.Time) bool { | ||
| 288 | - | 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) |
| 289 | } | 290 | } | ||
| 290 | 291 | ||||
| 291 | // tooEarly reports whether content applied now would show before at: | 292 | // tooEarly reports whether content applied now would show before at: | ||
| @@ -347,6 +348,10 @@ func (s *surface) dropQueue() { | |||||
| 347 | for _, fb := range u.feedback { | 348 | for _, fb := range u.feedback { | ||
| 348 | discard(fb) | 349 | discard(fb) | ||
| 349 | } | 350 | } | ||
| 351 | + | s.dropSync(u.sync) | |||
| 352 | + | if u.sync != nil { | |||
| 353 | + | continue // explicit sync: the release point replaced release | |||
| 354 | + | } | |||
| 350 | 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() { | ||
| 351 | released[u.buffer.Resource] = true | 356 | released[u.buffer.Resource] = true | ||
| 352 | u.buffer.SendRelease() | 357 | u.buffer.SendRelease() | ||
| @@ -413,6 +418,9 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration | |||||
| 413 | if u.wait && surf.barrier { | 418 | if u.wait && surf.barrier { | ||
| 414 | break | 419 | break | ||
| 415 | } | 420 | } | ||
| 421 | + | if !surf.syncReady(u.sync) { | |||
| 422 | + | break | |||
| 423 | + | } | |||
| 416 | if surf.tooEarly(u.at, now) { | 424 | if surf.tooEarly(u.at, now) { | ||
| 417 | wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond) | 425 | wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond) | ||
| 418 | break | 426 | break | ||
M internal/adapters/wayland/globals.go +1 -0
| @@ -20,6 +20,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 20 | 20 | func() error { return registerTearing(d, s) }, | |
| 21 | 21 | func() error { return registerPresentationConstraints(d, s) }, | |
| 22 | 22 | func() error { return registerPresentation(d, s) }, | |
| 23 | + | func() error { return registerSyncobj(d, s) }, | |
| 23 | 24 | func() error { return registerActivation(d, s) }, | |
| 24 | 25 | func() error { return registerVirtualKeyboard(d, s) }, | |
| 25 | 26 | func() error { | |
| @@ -20,6 +20,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 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 registerPresentationConstraints(d, s) }, | ||
| 22 | func() error { return registerPresentation(d, s) }, | 22 | func() error { return registerPresentation(d, s) }, | ||
| 23 | + | func() error { return registerSyncobj(d, s) }, | |||
| 23 | func() error { return registerActivation(d, s) }, | 24 | func() error { return registerActivation(d, s) }, | ||
| 24 | func() error { return registerVirtualKeyboard(d, s) }, | 25 | func() error { return registerVirtualKeyboard(d, s) }, | ||
| 25 | func() error { | 26 | func() error { | ||
M internal/adapters/wayland/release.go +39 -11
| @@ -21,7 +21,10 @@ import ( | |||
| 21 | 21 | const heldTimeout = 100 * time.Millisecond | |
| 22 | 22 | ||
| 23 | 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 | 28 | window ports.WindowID | |
| 26 | 29 | id uint64 // DMABuf ID, 0 for wl_shm | |
| 27 | 30 | after uint64 // the window's content Seq when the buffer was replaced | |
| @@ -30,17 +33,14 @@ type heldBuffer struct { | |||
| 30 | 33 | ||
| 31 | 34 | // releaseBuffer releases a replaced buffer, or holds it until the output | |
| 32 | 35 | // showing its window no longer reads it. | |
| 33 | - | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |
| 34 | - | if !b.Resource.Alive() { | |
| 35 | - | return | |
| 36 | - | } | |
| 36 | + | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) { | |
| 37 | 37 | window, name := surf.root().windowID(), s.frameOutput(surf) | |
| 38 | - | if window == 0 || name == "" { | |
| 38 | + | if !b.Resource.Alive() || window == 0 || name == "" { | |
| 39 | 39 | // Not drawn (unmapped, hidden, a cursor): nothing reads it. | |
| 40 | - | b.SendRelease() | |
| 40 | + | s.release(b, sync) | |
| 41 | 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 | 44 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | |
| 45 | 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 | 88 | // releaseHeld records the output reports and releases the buffers no | |
| 59 | 89 | // output reads. Every output reads every window's content, so a buffer | |
| 60 | 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 | 109 | kept = append(kept, h) | |
| 80 | 110 | continue | |
| 81 | 111 | } | |
| 82 | - | if h.res.Resource.Alive() { | |
| 83 | - | h.res.SendRelease() | |
| 84 | - | } | |
| 112 | + | s.release(h.res, h.sync) | |
| 85 | 113 | } | |
| 86 | 114 | clear(s.held[len(kept):]) | |
| 87 | 115 | s.held = kept | |
| @@ -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 +42 -1
| @@ -24,9 +24,15 @@ type Options struct { | |||
| 24 | 24 | RuntimeDir string | |
| 25 | 25 | Outputs ports.Layout | |
| 26 | 26 | // DMABuf is what the renderer imports; empty disables linux-dmabuf. | |
| 27 | - | 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 | |
| 28 | 32 | Keymap string | |
| 29 | 33 | RepeatRate, RepeatDelay int | |
| 34 | + | // syncDev replaces the render node's syncobj interface (tests). | |
| 35 | + | syncDev syncobjDevice | |
| 30 | 36 | } | |
| 31 | 37 | ||
| 32 | 38 | // Channels carries client notifications and commands. Events may be unbuffered. | |
| @@ -112,6 +118,11 @@ type Server struct { | |||
| 112 | 118 | damage map[ports.WindowID][]ports.SeqDamage | |
| 113 | 119 | // feedbacks wait for the flip that shows their content (presentation.go). | |
| 114 | 120 | feedbacks []feedbackWait | |
| 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 | |
| 115 | 126 | // flips is each output's latest flip (pacer, under Do). | |
| 116 | 127 | flips map[string]ports.FlipInfo | |
| 117 | 128 | contentNotify chan struct{} | |
| @@ -173,6 +184,26 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 173 | 184 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 174 | 185 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | |
| 175 | 186 | s.flips = map[string]ports.FlipInfo{} | |
| 187 | + | s.timelines = map[*server.Resource]*timeline{} | |
| 188 | + | var node *renderNode | |
| 189 | + | if opts.syncDev != nil { | |
| 190 | + | s.syncDev = opts.syncDev | |
| 191 | + | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |
| 192 | + | cleanup() | |
| 193 | + | d.Close() | |
| 194 | + | return nil, err | |
| 195 | + | } | |
| 196 | + | } else if opts.SyncobjNode != "" { | |
| 197 | + | if node, err = openSyncobj(opts.SyncobjNode); err != nil { | |
| 198 | + | log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off") | |
| 199 | + | node = nil | |
| 200 | + | } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |
| 201 | + | node.close() | |
| 202 | + | node = nil | |
| 203 | + | } else { | |
| 204 | + | s.syncDev = node | |
| 205 | + | } | |
| 206 | + | } | |
| 176 | 207 | s.tokens = map[string]activationToken{} | |
| 177 | 208 | if opts.Keymap != "" { | |
| 178 | 209 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -188,6 +219,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 188 | 219 | unix.Close(s.keymapFD) | |
| 189 | 220 | } | |
| 190 | 221 | s.dmabuf.close() | |
| 222 | + | if s.syncWait != nil { | |
| 223 | + | s.syncWait.close() | |
| 224 | + | } | |
| 225 | + | if node != nil { | |
| 226 | + | node.close() | |
| 227 | + | } | |
| 191 | 228 | cleanup() | |
| 192 | 229 | } | |
| 193 | 230 | if err = registerGlobals(d, opts, s); err != nil { | |
| @@ -245,6 +282,10 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 245 | 282 | }() | |
| 246 | 283 | go func() { defer wg.Done(); s.pace(ctx) }() | |
| 247 | 284 | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | |
| 285 | + | if s.syncWait != nil { | |
| 286 | + | wg.Add(1) | |
| 287 | + | go func() { defer wg.Done(); s.syncWait.run(ctx) }() | |
| 288 | + | } | |
| 248 | 289 | err := s.display.Run(ctx) | |
| 249 | 290 | wg.Wait() | |
| 250 | 291 | return err | |
| @@ -24,9 +24,15 @@ type Options struct { | |||||
| 24 | RuntimeDir string | 24 | RuntimeDir string | ||
| 25 | Outputs ports.Layout | 25 | Outputs ports.Layout | ||
| 26 | // DMABuf is what the renderer imports; empty disables linux-dmabuf. | 26 | // DMABuf is what the renderer imports; empty disables linux-dmabuf. | ||
| 27 | - | 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 | |||
| 28 | Keymap string | 32 | Keymap string | ||
| 29 | RepeatRate, RepeatDelay int | 33 | RepeatRate, RepeatDelay int | ||
| 34 | + | // syncDev replaces the render node's syncobj interface (tests). | |||
| 35 | + | syncDev syncobjDevice | |||
| 30 | } | 36 | } | ||
| 31 | 37 | ||||
| 32 | // Channels carries client notifications and commands. Events may be unbuffered. | 38 | // Channels carries client notifications and commands. Events may be unbuffered. | ||
| @@ -112,6 +118,11 @@ type Server struct { | |||||
| 112 | damage map[ports.WindowID][]ports.SeqDamage | 118 | damage map[ports.WindowID][]ports.SeqDamage | ||
| 113 | // feedbacks wait for the flip that shows their content (presentation.go). | 119 | // feedbacks wait for the flip that shows their content (presentation.go). | ||
| 114 | feedbacks []feedbackWait | 120 | feedbacks []feedbackWait | ||
| 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 | |||
| 115 | // flips is each output's latest flip (pacer, under Do). | 126 | // flips is each output's latest flip (pacer, under Do). | ||
| 116 | flips map[string]ports.FlipInfo | 127 | flips map[string]ports.FlipInfo | ||
| 117 | contentNotify chan struct{} | 128 | contentNotify chan struct{} | ||
| @@ -173,6 +184,26 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 173 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 184 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 174 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | 185 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | ||
| 175 | s.flips = map[string]ports.FlipInfo{} | 186 | s.flips = map[string]ports.FlipInfo{} | ||
| 187 | + | s.timelines = map[*server.Resource]*timeline{} | |||
| 188 | + | var node *renderNode | |||
| 189 | + | if opts.syncDev != nil { | |||
| 190 | + | s.syncDev = opts.syncDev | |||
| 191 | + | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |||
| 192 | + | cleanup() | |||
| 193 | + | d.Close() | |||
| 194 | + | return nil, err | |||
| 195 | + | } | |||
| 196 | + | } else if opts.SyncobjNode != "" { | |||
| 197 | + | if node, err = openSyncobj(opts.SyncobjNode); err != nil { | |||
| 198 | + | log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off") | |||
| 199 | + | node = nil | |||
| 200 | + | } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |||
| 201 | + | node.close() | |||
| 202 | + | node = nil | |||
| 203 | + | } else { | |||
| 204 | + | s.syncDev = node | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 176 | s.tokens = map[string]activationToken{} | 207 | s.tokens = map[string]activationToken{} | ||
| 177 | if opts.Keymap != "" { | 208 | if opts.Keymap != "" { | ||
| 178 | fd, size, e := keymapFile(opts.Keymap) | 209 | fd, size, e := keymapFile(opts.Keymap) | ||
| @@ -188,6 +219,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 188 | unix.Close(s.keymapFD) | 219 | unix.Close(s.keymapFD) | ||
| 189 | } | 220 | } | ||
| 190 | s.dmabuf.close() | 221 | s.dmabuf.close() | ||
| 222 | + | if s.syncWait != nil { | |||
| 223 | + | s.syncWait.close() | |||
| 224 | + | } | |||
| 225 | + | if node != nil { | |||
| 226 | + | node.close() | |||
| 227 | + | } | |||
| 191 | cleanup() | 228 | cleanup() | ||
| 192 | } | 229 | } | ||
| 193 | if err = registerGlobals(d, opts, s); err != nil { | 230 | if err = registerGlobals(d, opts, s); err != nil { | ||
| @@ -245,6 +282,10 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 245 | }() | 282 | }() | ||
| 246 | go func() { defer wg.Done(); s.pace(ctx) }() | 283 | go func() { defer wg.Done(); s.pace(ctx) }() | ||
| 247 | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | 284 | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | ||
| 285 | + | if s.syncWait != nil { | |||
| 286 | + | wg.Add(1) | |||
| 287 | + | go func() { defer wg.Done(); s.syncWait.run(ctx) }() | |||
| 288 | + | } | |||
| 248 | err := s.display.Run(ctx) | 289 | err := s.display.Run(ctx) | ||
| 249 | wg.Wait() | 290 | wg.Wait() | ||
| 250 | return err | 291 | return err | ||
M internal/adapters/wayland/surface.go +32 -3
| @@ -69,6 +69,18 @@ type surface struct { | |||
| 69 | 69 | committed damage | |
| 70 | 70 | // pendingFeedback are wp_presentation feedbacks for the next commit. | |
| 71 | 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 | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // commitSync is a commit's explicit-sync points and acquire wait. | |
| 81 | + | type commitSync struct { | |
| 82 | + | acquire, release syncPoint | |
| 83 | + | wait *syncWait | |
| 72 | 84 | } | |
| 73 | 85 | ||
| 74 | 86 | // damage is what one commit changed in a surface's buffer. | |
| @@ -198,8 +210,11 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 198 | 210 | } | |
| 199 | 211 | s.callbacks = nil | |
| 200 | 212 | // The client may reuse the buffer on another surface. | |
| 213 | + | s.dropSync(s.pendingSync) | |
| 214 | + | s.pendingSync = nil | |
| 201 | 215 | if s.current != nil { | |
| 202 | - | s.server.releaseBuffer(s, s.current) | |
| 216 | + | s.server.releaseBuffer(s, s.current, s.hold) | |
| 217 | + | s.hold = syncHold{} | |
| 203 | 218 | } | |
| 204 | 219 | s.current, s.pending = nil, nil | |
| 205 | 220 | if s.role != nil { | |
| @@ -228,7 +243,12 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 228 | 243 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |
| 229 | 244 | return | |
| 230 | 245 | } | |
| 231 | - | // 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). | |
| 232 | 252 | if s.mustWait(time.Now()) { | |
| 233 | 253 | s.queueUpdate() | |
| 234 | 254 | return | |
| @@ -256,14 +276,22 @@ func (s *surface) applyCommit() { | |||
| 256 | 276 | if s.viewport != nil { | |
| 257 | 277 | s.viewport.commit() | |
| 258 | 278 | } | |
| 279 | + | cs := s.pendingSync | |
| 280 | + | s.pendingSync = nil | |
| 259 | 281 | if s.attached { | |
| 260 | 282 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | |
| 261 | - | 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) | |
| 262 | 288 | } | |
| 289 | + | s.hold = syncHold{} | |
| 263 | 290 | s.current = s.pending | |
| 264 | 291 | s.pending = nil | |
| 265 | 292 | s.attached = false | |
| 266 | 293 | } | |
| 294 | + | s.applySync(cs) | |
| 267 | 295 | if len(s.callbacks) > 0 { | |
| 268 | 296 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | |
| 269 | 297 | } | |
| @@ -280,6 +308,7 @@ func (s *surface) applyCommit() { | |||
| 280 | 308 | s.lastW, s.lastH = c.Width, c.Height | |
| 281 | 309 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | |
| 282 | 310 | } | |
| 311 | + | c.Acquire = s.hold.acquire | |
| 283 | 312 | s.content, s.has = c, true | |
| 284 | 313 | } else if shm, ok := state.(*buffer); ok { | |
| 285 | 314 | shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | |
| @@ -69,6 +69,18 @@ type surface struct { | |||||
| 69 | committed damage | 69 | committed damage | ||
| 70 | // pendingFeedback are wp_presentation feedbacks for the next commit. | 70 | // pendingFeedback are wp_presentation feedbacks for the next commit. | ||
| 71 | pendingFeedback []*presentationtime.WpPresentationFeedback | 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 | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // commitSync is a commit's explicit-sync points and acquire wait. | |||
| 81 | + | type commitSync struct { | |||
| 82 | + | acquire, release syncPoint | |||
| 83 | + | wait *syncWait | |||
| 72 | } | 84 | } | ||
| 73 | 85 | ||||
| 74 | // damage is what one commit changed in a surface's buffer. | 86 | // damage is what one commit changed in a surface's buffer. | ||
| @@ -198,8 +210,11 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 198 | } | 210 | } | ||
| 199 | s.callbacks = nil | 211 | s.callbacks = nil | ||
| 200 | // 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 | |||
| 201 | if s.current != nil { | 215 | if s.current != nil { | ||
| 202 | - | s.server.releaseBuffer(s, s.current) | 216 | + | s.server.releaseBuffer(s, s.current, s.hold) |
| 217 | + | s.hold = syncHold{} | |||
| 203 | } | 218 | } | ||
| 204 | s.current, s.pending = nil, nil | 219 | s.current, s.pending = nil, nil | ||
| 205 | if s.role != nil { | 220 | if s.role != nil { | ||
| @@ -228,7 +243,12 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 228 | 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") | ||
| 229 | return | 244 | return | ||
| 230 | } | 245 | } | ||
| 231 | - | // 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). | |||
| 232 | if s.mustWait(time.Now()) { | 252 | if s.mustWait(time.Now()) { | ||
| 233 | s.queueUpdate() | 253 | s.queueUpdate() | ||
| 234 | return | 254 | return | ||
| @@ -256,14 +276,22 @@ func (s *surface) applyCommit() { | |||||
| 256 | if s.viewport != nil { | 276 | if s.viewport != nil { | ||
| 257 | s.viewport.commit() | 277 | s.viewport.commit() | ||
| 258 | } | 278 | } | ||
| 279 | + | cs := s.pendingSync | |||
| 280 | + | s.pendingSync = nil | |||
| 259 | if s.attached { | 281 | if s.attached { | ||
| 260 | 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) { | ||
| 261 | - | 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) | |||
| 262 | } | 288 | } | ||
| 289 | + | s.hold = syncHold{} | |||
| 263 | s.current = s.pending | 290 | s.current = s.pending | ||
| 264 | s.pending = nil | 291 | s.pending = nil | ||
| 265 | s.attached = false | 292 | s.attached = false | ||
| 266 | } | 293 | } | ||
| 294 | + | s.applySync(cs) | |||
| 267 | if len(s.callbacks) > 0 { | 295 | if len(s.callbacks) > 0 { | ||
| 268 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | 296 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | ||
| 269 | } | 297 | } | ||
| @@ -280,6 +308,7 @@ func (s *surface) applyCommit() { | |||||
| 280 | s.lastW, s.lastH = c.Width, c.Height | 308 | s.lastW, s.lastH = c.Width, c.Height | ||
| 281 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | 309 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | ||
| 282 | } | 310 | } | ||
| 311 | + | c.Acquire = s.hold.acquire | |||
| 283 | s.content, s.has = c, true | 312 | s.content, s.has = c, true | ||
| 284 | } else if shm, ok := state.(*buffer); ok { | 313 | } else if shm, ok := state.(*buffer); ok { | ||
| 285 | shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | 314 | shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | ||
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 | + | } | |
| @@ -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 +396 -0
| @@ -0,0 +1,396 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "os" | |
| 7 | + | "sync" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | + | "github.com/bnema/purego-libwayland/server" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // wp_linux_drm_syncobj_v1: explicit sync. A client names, per commit, the | |
| 16 | + | // timeline point its GPU signals when the buffer is drawn (acquire) and | |
| 17 | + | // the point we signal once nothing reads the buffer any more (release). | |
| 18 | + | // A commit waits (like a fifo barrier) until its acquire point has a | |
| 19 | + | // fence; the fence then travels with the content as a sync file the | |
| 20 | + | // renderer and the display wait on, so no CPU waits for the GPU. The | |
| 21 | + | // release point replaces wl_buffer.release. | |
| 22 | + | ||
| 23 | + | const syncobjVersion = 1 | |
| 24 | + | ||
| 25 | + | // timeline is an imported client timeline. | |
| 26 | + | type timeline struct { | |
| 27 | + | dev syncobjDevice | |
| 28 | + | handle uint32 | |
| 29 | + | alive bool | |
| 30 | + | // uses counts committed points still to signal or wait on. | |
| 31 | + | uses int | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // syncPoint is a point on a timeline. | |
| 35 | + | type syncPoint struct { | |
| 36 | + | tl *timeline | |
| 37 | + | point uint64 | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (p syncPoint) set() bool { return p.tl != nil } | |
| 41 | + | ||
| 42 | + | // syncState is a surface's wp_linux_drm_syncobj_surface_v1 state. | |
| 43 | + | type syncState struct { | |
| 44 | + | resource *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1 | |
| 45 | + | acquire, release syncPoint // pending, for the next commit | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func registerSyncobj(d *server.Display, s *Server) error { | |
| 49 | + | if s.syncDev == nil { | |
| 50 | + | return nil | |
| 51 | + | } | |
| 52 | + | return linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1Global(d, syncobjVersion, func(c server.Client, v, id uint32) { | |
| 53 | + | _, _ = linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1(c, int32(v), id, syncManager{s}) | |
| 54 | + | }) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | type syncManager struct{ server *Server } | |
| 58 | + | ||
| 59 | + | func (syncManager) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1) {} | |
| 60 | + | ||
| 61 | + | func (m syncManager) GetSurface(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, surf *wayland.Surface) { | |
| 62 | + | state := m.server.surfaceOf(surf) | |
| 63 | + | if state == nil { | |
| 64 | + | return | |
| 65 | + | } | |
| 66 | + | if state.sync != nil { | |
| 67 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists), "surface already has a syncobj surface") | |
| 68 | + | return | |
| 69 | + | } | |
| 70 | + | st := &syncState{} | |
| 71 | + | res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjSurfaceV1(r.Client(), r.Version(), id, syncSurface{state, st}) | |
| 72 | + | if err != nil { | |
| 73 | + | return | |
| 74 | + | } | |
| 75 | + | st.resource = res | |
| 76 | + | state.sync = st | |
| 77 | + | res.OnDestroy = func() { | |
| 78 | + | if state.sync == st { | |
| 79 | + | state.sync = nil | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func (m syncManager) ImportTimeline(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, fd int) { | |
| 85 | + | defer unix.Close(fd) | |
| 86 | + | h, err := m.server.syncDev.fdToHandle(fd) | |
| 87 | + | if err != nil { | |
| 88 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline), "invalid timeline fd") | |
| 89 | + | return | |
| 90 | + | } | |
| 91 | + | tl := &timeline{dev: m.server.syncDev, handle: h, alive: true} | |
| 92 | + | res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjTimelineV1(r.Client(), r.Version(), id, timelineHandler{}) | |
| 93 | + | if err != nil { | |
| 94 | + | _ = tl.dev.destroy(h) | |
| 95 | + | return | |
| 96 | + | } | |
| 97 | + | m.server.timelines[res.Resource] = tl | |
| 98 | + | res.OnDestroy = func() { | |
| 99 | + | delete(m.server.timelines, res.Resource) | |
| 100 | + | // Points already committed keep the syncobj: destroyed once no | |
| 101 | + | // held buffer or waiting commit uses it (timeline.drop). | |
| 102 | + | tl.alive = false | |
| 103 | + | m.server.dropTimeline(tl) | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | type timelineHandler struct{} | |
| 108 | + | ||
| 109 | + | func (timelineHandler) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1) {} | |
| 110 | + | ||
| 111 | + | type syncSurface struct { | |
| 112 | + | surf *surface | |
| 113 | + | st *syncState | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func (syncSurface) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1) {} | |
| 117 | + | ||
| 118 | + | func (h syncSurface) point(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) (syncPoint, bool) { | |
| 119 | + | if h.surf.destroyed { | |
| 120 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface), "surface destroyed") | |
| 121 | + | return syncPoint{}, false | |
| 122 | + | } | |
| 123 | + | t := h.surf.server.timelines[tl.Resource] | |
| 124 | + | if t == nil { | |
| 125 | + | return syncPoint{}, false | |
| 126 | + | } | |
| 127 | + | return syncPoint{tl: t, point: uint64(hi)<<32 | uint64(lo)}, true | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func (h syncSurface) SetAcquirePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) { | |
| 131 | + | if p, ok := h.point(r, tl, hi, lo); ok { | |
| 132 | + | h.st.acquire = p | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | func (h syncSurface) SetReleasePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) { | |
| 137 | + | if p, ok := h.point(r, tl, hi, lo); ok { | |
| 138 | + | h.st.release = p | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // checkSyncCommit applies the protocol rules to a commit with sync | |
| 143 | + | // points; false means a protocol error was posted. | |
| 144 | + | func (s *surface) checkSyncCommit() bool { | |
| 145 | + | st := s.sync | |
| 146 | + | if st == nil { | |
| 147 | + | return true | |
| 148 | + | } | |
| 149 | + | a, r := st.acquire, st.release | |
| 150 | + | hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil | |
| 151 | + | res := st.resource | |
| 152 | + | switch { | |
| 153 | + | case !a.set() && !r.set(): | |
| 154 | + | if hasBuffer && s.attached { | |
| 155 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point") | |
| 156 | + | return false | |
| 157 | + | } | |
| 158 | + | return true | |
| 159 | + | case !hasBuffer: | |
| 160 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer), "points without a buffer") | |
| 161 | + | case !a.set(): | |
| 162 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "no acquire point") | |
| 163 | + | case !r.set(): | |
| 164 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint), "no release point") | |
| 165 | + | case a.tl == r.tl && a.point >= r.point: | |
| 166 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints), "release point not after acquire point") | |
| 167 | + | case s.pendingIsSHM(): | |
| 168 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer), "explicit sync on a wl_shm buffer") | |
| 169 | + | default: | |
| 170 | + | return true | |
| 171 | + | } | |
| 172 | + | return false | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | // pendingIsSHM reports whether the buffer of the next commit is wl_shm. | |
| 176 | + | func (s *surface) pendingIsSHM() bool { | |
| 177 | + | b := s.pending | |
| 178 | + | if !s.attached { | |
| 179 | + | b = s.current | |
| 180 | + | } | |
| 181 | + | if b == nil { | |
| 182 | + | return false | |
| 183 | + | } | |
| 184 | + | _, dma := s.server.buffers[b.Resource].(*dmabufBuffer) | |
| 185 | + | return !dma | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | // syncWaiter watches acquire points: each gets an eventfd the kernel | |
| 189 | + | // writes once the point has a fence, and the pacer is woken to apply the | |
| 190 | + | // commits that waited for it. | |
| 191 | + | type syncWaiter struct { | |
| 192 | + | mu sync.Mutex | |
| 193 | + | ready map[*syncWait]bool | |
| 194 | + | wake func() | |
| 195 | + | pipeR *os.File | |
| 196 | + | pipeW *os.File | |
| 197 | + | add chan *syncWait | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | // syncWait is one acquire point a queued commit waits for. | |
| 201 | + | type syncWait struct { | |
| 202 | + | efd int | |
| 203 | + | done bool // the eventfd fired (pacer, under Do) | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | func newSyncWaiter(wake func()) (*syncWaiter, error) { | |
| 207 | + | r, w, err := os.Pipe() | |
| 208 | + | if err != nil { | |
| 209 | + | return nil, err | |
| 210 | + | } | |
| 211 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // watch registers an eventfd for p; the wait is ready once it fires. | |
| 215 | + | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |
| 216 | + | efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK) | |
| 217 | + | if err != nil { | |
| 218 | + | return nil, err | |
| 219 | + | } | |
| 220 | + | if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil { | |
| 221 | + | unix.Close(efd) | |
| 222 | + | return nil, err | |
| 223 | + | } | |
| 224 | + | w := &syncWait{efd: efd} | |
| 225 | + | sw.add <- w | |
| 226 | + | _, _ = sw.pipeW.Write([]byte{0}) | |
| 227 | + | return w, nil | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // fired reports whether w's point has a fence, and forgets w once it has. | |
| 231 | + | func (sw *syncWaiter) fired(w *syncWait) bool { | |
| 232 | + | sw.mu.Lock() | |
| 233 | + | defer sw.mu.Unlock() | |
| 234 | + | if sw.ready[w] { | |
| 235 | + | delete(sw.ready, w) | |
| 236 | + | w.done = true | |
| 237 | + | } | |
| 238 | + | return w.done | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | // cancel stops watching w (the commit was dropped). | |
| 242 | + | func (sw *syncWaiter) cancel(w *syncWait) { | |
| 243 | + | sw.mu.Lock() | |
| 244 | + | delete(sw.ready, w) | |
| 245 | + | sw.mu.Unlock() | |
| 246 | + | w.done = true | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // run polls every watched eventfd; it owns and closes them. | |
| 250 | + | func (sw *syncWaiter) run(ctx context.Context) { | |
| 251 | + | defer sw.pipeR.Close() | |
| 252 | + | var waits []*syncWait | |
| 253 | + | defer func() { | |
| 254 | + | for _, w := range waits { | |
| 255 | + | unix.Close(w.efd) | |
| 256 | + | } | |
| 257 | + | }() | |
| 258 | + | for ctx.Err() == nil { | |
| 259 | + | fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}} | |
| 260 | + | for _, w := range waits { | |
| 261 | + | fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN}) | |
| 262 | + | } | |
| 263 | + | n, err := unix.Poll(fds, 100) | |
| 264 | + | if err != nil && !errors.Is(err, unix.EINTR) { | |
| 265 | + | return | |
| 266 | + | } | |
| 267 | + | if n <= 0 { | |
| 268 | + | continue | |
| 269 | + | } | |
| 270 | + | fired := false | |
| 271 | + | kept := waits[:0] | |
| 272 | + | for i, w := range waits { | |
| 273 | + | if fds[i+1].Revents == 0 { | |
| 274 | + | kept = append(kept, w) | |
| 275 | + | continue | |
| 276 | + | } | |
| 277 | + | unix.Close(w.efd) | |
| 278 | + | sw.mu.Lock() | |
| 279 | + | sw.ready[w] = true | |
| 280 | + | sw.mu.Unlock() | |
| 281 | + | fired = true | |
| 282 | + | } | |
| 283 | + | waits = kept | |
| 284 | + | // New waits join the next poll. | |
| 285 | + | if fds[0].Revents != 0 { | |
| 286 | + | var b [64]byte | |
| 287 | + | _, _ = sw.pipeR.Read(b[:]) | |
| 288 | + | drain: | |
| 289 | + | for { | |
| 290 | + | select { | |
| 291 | + | case w := <-sw.add: | |
| 292 | + | waits = append(waits, w) | |
| 293 | + | default: | |
| 294 | + | break drain | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | if fired { | |
| 299 | + | sw.wake() | |
| 300 | + | } | |
| 301 | + | } | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | // close stops the waiter's input side. | |
| 305 | + | func (sw *syncWaiter) close() { sw.pipeW.Close() } | |
| 306 | + | ||
| 307 | + | // syncHold is a buffer's explicit-sync state while it is on screen: the | |
| 308 | + | // acquire fence handed to readers and the release point to signal. | |
| 309 | + | type syncHold struct { | |
| 310 | + | acquire *os.File | |
| 311 | + | release syncPoint | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | // releaseSync signals the release point once nothing reads the buffer, | |
| 315 | + | // and closes the acquire fence. | |
| 316 | + | func (s *Server) releaseSync(h syncHold) { | |
| 317 | + | if h.acquire != nil { | |
| 318 | + | h.acquire.Close() | |
| 319 | + | } | |
| 320 | + | if h.release.set() { | |
| 321 | + | if err := h.release.tl.dev.signal(h.release.tl.handle, h.release.point); err != nil { | |
| 322 | + | s.log.Warn().Str("component", "wayland").Err(err).Msg("release point") | |
| 323 | + | } | |
| 324 | + | h.release.tl.uses-- | |
| 325 | + | s.dropTimeline(h.release.tl) | |
| 326 | + | } | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | // dropTimeline destroys a timeline the client destroyed once no held | |
| 330 | + | // buffer still needs to signal it. | |
| 331 | + | func (s *Server) dropTimeline(tl *timeline) { | |
| 332 | + | if !tl.alive && tl.uses <= 0 && tl.handle != 0 { | |
| 333 | + | _ = tl.dev.destroy(tl.handle) | |
| 334 | + | tl.handle = 0 | |
| 335 | + | } | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | // takeSyncPoints moves the surface's requested points to the commit and | |
| 339 | + | // starts waiting for the acquire point's fence. | |
| 340 | + | func (s *surface) takeSyncPoints() { | |
| 341 | + | st := s.sync | |
| 342 | + | if st == nil || !st.acquire.set() { | |
| 343 | + | return | |
| 344 | + | } | |
| 345 | + | cs := &commitSync{acquire: st.acquire, release: st.release} | |
| 346 | + | st.acquire, st.release = syncPoint{}, syncPoint{} | |
| 347 | + | cs.acquire.tl.uses++ | |
| 348 | + | cs.release.tl.uses++ | |
| 349 | + | w, err := s.server.syncWait.watch(cs.acquire) | |
| 350 | + | if err != nil { | |
| 351 | + | // The point cannot be watched: apply now, readers wait on the | |
| 352 | + | // fence (or its absence) when they read the buffer. | |
| 353 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire point") | |
| 354 | + | } else { | |
| 355 | + | cs.wait = w | |
| 356 | + | } | |
| 357 | + | s.pendingSync = cs | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | // syncReady reports whether a commit's acquire point has its fence. | |
| 361 | + | func (s *surface) syncReady(cs *commitSync) bool { | |
| 362 | + | return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait) | |
| 363 | + | } | |
| 364 | + | ||
| 365 | + | // dropSync forgets a commit that will never apply: its points are | |
| 366 | + | // signalled so the client can reuse the buffer. | |
| 367 | + | func (s *surface) dropSync(cs *commitSync) { | |
| 368 | + | if cs == nil { | |
| 369 | + | return | |
| 370 | + | } | |
| 371 | + | if cs.wait != nil { | |
| 372 | + | s.server.syncWait.cancel(cs.wait) | |
| 373 | + | } | |
| 374 | + | cs.acquire.tl.uses-- | |
| 375 | + | s.server.dropTimeline(cs.acquire.tl) | |
| 376 | + | s.server.releaseSync(syncHold{release: cs.release}) | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // applySync makes a commit's points the current buffer's: its acquire | |
| 380 | + | // fence goes with the content, its release point waits for release. | |
| 381 | + | func (s *surface) applySync(cs *commitSync) { | |
| 382 | + | if cs == nil { | |
| 383 | + | return | |
| 384 | + | } | |
| 385 | + | if cs.wait != nil { | |
| 386 | + | s.server.syncWait.cancel(cs.wait) | |
| 387 | + | } | |
| 388 | + | f, err := cs.acquire.tl.dev.exportSyncFile(cs.acquire.tl.handle, cs.acquire.point) | |
| 389 | + | if err != nil { | |
| 390 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire fence") | |
| 391 | + | f = nil | |
| 392 | + | } | |
| 393 | + | cs.acquire.tl.uses-- | |
| 394 | + | s.server.dropTimeline(cs.acquire.tl) | |
| 395 | + | s.hold = syncHold{acquire: f, release: cs.release} | |
| 396 | + | } | |
| @@ -0,0 +1,396 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 11 | + | "github.com/bnema/purego-libwayland/server" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // wp_linux_drm_syncobj_v1: explicit sync. A client names, per commit, the | |||
| 16 | + | // timeline point its GPU signals when the buffer is drawn (acquire) and | |||
| 17 | + | // the point we signal once nothing reads the buffer any more (release). | |||
| 18 | + | // A commit waits (like a fifo barrier) until its acquire point has a | |||
| 19 | + | // fence; the fence then travels with the content as a sync file the | |||
| 20 | + | // renderer and the display wait on, so no CPU waits for the GPU. The | |||
| 21 | + | // release point replaces wl_buffer.release. | |||
| 22 | + | ||||
| 23 | + | const syncobjVersion = 1 | |||
| 24 | + | ||||
| 25 | + | // timeline is an imported client timeline. | |||
| 26 | + | type timeline struct { | |||
| 27 | + | dev syncobjDevice | |||
| 28 | + | handle uint32 | |||
| 29 | + | alive bool | |||
| 30 | + | // uses counts committed points still to signal or wait on. | |||
| 31 | + | uses int | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // syncPoint is a point on a timeline. | |||
| 35 | + | type syncPoint struct { | |||
| 36 | + | tl *timeline | |||
| 37 | + | point uint64 | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (p syncPoint) set() bool { return p.tl != nil } | |||
| 41 | + | ||||
| 42 | + | // syncState is a surface's wp_linux_drm_syncobj_surface_v1 state. | |||
| 43 | + | type syncState struct { | |||
| 44 | + | resource *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1 | |||
| 45 | + | acquire, release syncPoint // pending, for the next commit | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func registerSyncobj(d *server.Display, s *Server) error { | |||
| 49 | + | if s.syncDev == nil { | |||
| 50 | + | return nil | |||
| 51 | + | } | |||
| 52 | + | return linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1Global(d, syncobjVersion, func(c server.Client, v, id uint32) { | |||
| 53 | + | _, _ = linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1(c, int32(v), id, syncManager{s}) | |||
| 54 | + | }) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | type syncManager struct{ server *Server } | |||
| 58 | + | ||||
| 59 | + | func (syncManager) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1) {} | |||
| 60 | + | ||||
| 61 | + | func (m syncManager) GetSurface(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, surf *wayland.Surface) { | |||
| 62 | + | state := m.server.surfaceOf(surf) | |||
| 63 | + | if state == nil { | |||
| 64 | + | return | |||
| 65 | + | } | |||
| 66 | + | if state.sync != nil { | |||
| 67 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists), "surface already has a syncobj surface") | |||
| 68 | + | return | |||
| 69 | + | } | |||
| 70 | + | st := &syncState{} | |||
| 71 | + | res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjSurfaceV1(r.Client(), r.Version(), id, syncSurface{state, st}) | |||
| 72 | + | if err != nil { | |||
| 73 | + | return | |||
| 74 | + | } | |||
| 75 | + | st.resource = res | |||
| 76 | + | state.sync = st | |||
| 77 | + | res.OnDestroy = func() { | |||
| 78 | + | if state.sync == st { | |||
| 79 | + | state.sync = nil | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | func (m syncManager) ImportTimeline(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, fd int) { | |||
| 85 | + | defer unix.Close(fd) | |||
| 86 | + | h, err := m.server.syncDev.fdToHandle(fd) | |||
| 87 | + | if err != nil { | |||
| 88 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline), "invalid timeline fd") | |||
| 89 | + | return | |||
| 90 | + | } | |||
| 91 | + | tl := &timeline{dev: m.server.syncDev, handle: h, alive: true} | |||
| 92 | + | res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjTimelineV1(r.Client(), r.Version(), id, timelineHandler{}) | |||
| 93 | + | if err != nil { | |||
| 94 | + | _ = tl.dev.destroy(h) | |||
| 95 | + | return | |||
| 96 | + | } | |||
| 97 | + | m.server.timelines[res.Resource] = tl | |||
| 98 | + | res.OnDestroy = func() { | |||
| 99 | + | delete(m.server.timelines, res.Resource) | |||
| 100 | + | // Points already committed keep the syncobj: destroyed once no | |||
| 101 | + | // held buffer or waiting commit uses it (timeline.drop). | |||
| 102 | + | tl.alive = false | |||
| 103 | + | m.server.dropTimeline(tl) | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | type timelineHandler struct{} | |||
| 108 | + | ||||
| 109 | + | func (timelineHandler) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1) {} | |||
| 110 | + | ||||
| 111 | + | type syncSurface struct { | |||
| 112 | + | surf *surface | |||
| 113 | + | st *syncState | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func (syncSurface) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1) {} | |||
| 117 | + | ||||
| 118 | + | func (h syncSurface) point(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) (syncPoint, bool) { | |||
| 119 | + | if h.surf.destroyed { | |||
| 120 | + | r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface), "surface destroyed") | |||
| 121 | + | return syncPoint{}, false | |||
| 122 | + | } | |||
| 123 | + | t := h.surf.server.timelines[tl.Resource] | |||
| 124 | + | if t == nil { | |||
| 125 | + | return syncPoint{}, false | |||
| 126 | + | } | |||
| 127 | + | return syncPoint{tl: t, point: uint64(hi)<<32 | uint64(lo)}, true | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func (h syncSurface) SetAcquirePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) { | |||
| 131 | + | if p, ok := h.point(r, tl, hi, lo); ok { | |||
| 132 | + | h.st.acquire = p | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | func (h syncSurface) SetReleasePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) { | |||
| 137 | + | if p, ok := h.point(r, tl, hi, lo); ok { | |||
| 138 | + | h.st.release = p | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // checkSyncCommit applies the protocol rules to a commit with sync | |||
| 143 | + | // points; false means a protocol error was posted. | |||
| 144 | + | func (s *surface) checkSyncCommit() bool { | |||
| 145 | + | st := s.sync | |||
| 146 | + | if st == nil { | |||
| 147 | + | return true | |||
| 148 | + | } | |||
| 149 | + | a, r := st.acquire, st.release | |||
| 150 | + | hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil | |||
| 151 | + | res := st.resource | |||
| 152 | + | switch { | |||
| 153 | + | case !a.set() && !r.set(): | |||
| 154 | + | if hasBuffer && s.attached { | |||
| 155 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point") | |||
| 156 | + | return false | |||
| 157 | + | } | |||
| 158 | + | return true | |||
| 159 | + | case !hasBuffer: | |||
| 160 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer), "points without a buffer") | |||
| 161 | + | case !a.set(): | |||
| 162 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "no acquire point") | |||
| 163 | + | case !r.set(): | |||
| 164 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint), "no release point") | |||
| 165 | + | case a.tl == r.tl && a.point >= r.point: | |||
| 166 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints), "release point not after acquire point") | |||
| 167 | + | case s.pendingIsSHM(): | |||
| 168 | + | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer), "explicit sync on a wl_shm buffer") | |||
| 169 | + | default: | |||
| 170 | + | return true | |||
| 171 | + | } | |||
| 172 | + | return false | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | // pendingIsSHM reports whether the buffer of the next commit is wl_shm. | |||
| 176 | + | func (s *surface) pendingIsSHM() bool { | |||
| 177 | + | b := s.pending | |||
| 178 | + | if !s.attached { | |||
| 179 | + | b = s.current | |||
| 180 | + | } | |||
| 181 | + | if b == nil { | |||
| 182 | + | return false | |||
| 183 | + | } | |||
| 184 | + | _, dma := s.server.buffers[b.Resource].(*dmabufBuffer) | |||
| 185 | + | return !dma | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | // syncWaiter watches acquire points: each gets an eventfd the kernel | |||
| 189 | + | // writes once the point has a fence, and the pacer is woken to apply the | |||
| 190 | + | // commits that waited for it. | |||
| 191 | + | type syncWaiter struct { | |||
| 192 | + | mu sync.Mutex | |||
| 193 | + | ready map[*syncWait]bool | |||
| 194 | + | wake func() | |||
| 195 | + | pipeR *os.File | |||
| 196 | + | pipeW *os.File | |||
| 197 | + | add chan *syncWait | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | // syncWait is one acquire point a queued commit waits for. | |||
| 201 | + | type syncWait struct { | |||
| 202 | + | efd int | |||
| 203 | + | done bool // the eventfd fired (pacer, under Do) | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||
| 207 | + | r, w, err := os.Pipe() | |||
| 208 | + | if err != nil { | |||
| 209 | + | return nil, err | |||
| 210 | + | } | |||
| 211 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // watch registers an eventfd for p; the wait is ready once it fires. | |||
| 215 | + | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||
| 216 | + | efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK) | |||
| 217 | + | if err != nil { | |||
| 218 | + | return nil, err | |||
| 219 | + | } | |||
| 220 | + | if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil { | |||
| 221 | + | unix.Close(efd) | |||
| 222 | + | return nil, err | |||
| 223 | + | } | |||
| 224 | + | w := &syncWait{efd: efd} | |||
| 225 | + | sw.add <- w | |||
| 226 | + | _, _ = sw.pipeW.Write([]byte{0}) | |||
| 227 | + | return w, nil | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | // fired reports whether w's point has a fence, and forgets w once it has. | |||
| 231 | + | func (sw *syncWaiter) fired(w *syncWait) bool { | |||
| 232 | + | sw.mu.Lock() | |||
| 233 | + | defer sw.mu.Unlock() | |||
| 234 | + | if sw.ready[w] { | |||
| 235 | + | delete(sw.ready, w) | |||
| 236 | + | w.done = true | |||
| 237 | + | } | |||
| 238 | + | return w.done | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | // cancel stops watching w (the commit was dropped). | |||
| 242 | + | func (sw *syncWaiter) cancel(w *syncWait) { | |||
| 243 | + | sw.mu.Lock() | |||
| 244 | + | delete(sw.ready, w) | |||
| 245 | + | sw.mu.Unlock() | |||
| 246 | + | w.done = true | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // run polls every watched eventfd; it owns and closes them. | |||
| 250 | + | func (sw *syncWaiter) run(ctx context.Context) { | |||
| 251 | + | defer sw.pipeR.Close() | |||
| 252 | + | var waits []*syncWait | |||
| 253 | + | defer func() { | |||
| 254 | + | for _, w := range waits { | |||
| 255 | + | unix.Close(w.efd) | |||
| 256 | + | } | |||
| 257 | + | }() | |||
| 258 | + | for ctx.Err() == nil { | |||
| 259 | + | fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}} | |||
| 260 | + | for _, w := range waits { | |||
| 261 | + | fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN}) | |||
| 262 | + | } | |||
| 263 | + | n, err := unix.Poll(fds, 100) | |||
| 264 | + | if err != nil && !errors.Is(err, unix.EINTR) { | |||
| 265 | + | return | |||
| 266 | + | } | |||
| 267 | + | if n <= 0 { | |||
| 268 | + | continue | |||
| 269 | + | } | |||
| 270 | + | fired := false | |||
| 271 | + | kept := waits[:0] | |||
| 272 | + | for i, w := range waits { | |||
| 273 | + | if fds[i+1].Revents == 0 { | |||
| 274 | + | kept = append(kept, w) | |||
| 275 | + | continue | |||
| 276 | + | } | |||
| 277 | + | unix.Close(w.efd) | |||
| 278 | + | sw.mu.Lock() | |||
| 279 | + | sw.ready[w] = true | |||
| 280 | + | sw.mu.Unlock() | |||
| 281 | + | fired = true | |||
| 282 | + | } | |||
| 283 | + | waits = kept | |||
| 284 | + | // New waits join the next poll. | |||
| 285 | + | if fds[0].Revents != 0 { | |||
| 286 | + | var b [64]byte | |||
| 287 | + | _, _ = sw.pipeR.Read(b[:]) | |||
| 288 | + | drain: | |||
| 289 | + | for { | |||
| 290 | + | select { | |||
| 291 | + | case w := <-sw.add: | |||
| 292 | + | waits = append(waits, w) | |||
| 293 | + | default: | |||
| 294 | + | break drain | |||
| 295 | + | } | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | if fired { | |||
| 299 | + | sw.wake() | |||
| 300 | + | } | |||
| 301 | + | } | |||
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | // close stops the waiter's input side. | |||
| 305 | + | func (sw *syncWaiter) close() { sw.pipeW.Close() } | |||
| 306 | + | ||||
| 307 | + | // syncHold is a buffer's explicit-sync state while it is on screen: the | |||
| 308 | + | // acquire fence handed to readers and the release point to signal. | |||
| 309 | + | type syncHold struct { | |||
| 310 | + | acquire *os.File | |||
| 311 | + | release syncPoint | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // releaseSync signals the release point once nothing reads the buffer, | |||
| 315 | + | // and closes the acquire fence. | |||
| 316 | + | func (s *Server) releaseSync(h syncHold) { | |||
| 317 | + | if h.acquire != nil { | |||
| 318 | + | h.acquire.Close() | |||
| 319 | + | } | |||
| 320 | + | if h.release.set() { | |||
| 321 | + | if err := h.release.tl.dev.signal(h.release.tl.handle, h.release.point); err != nil { | |||
| 322 | + | s.log.Warn().Str("component", "wayland").Err(err).Msg("release point") | |||
| 323 | + | } | |||
| 324 | + | h.release.tl.uses-- | |||
| 325 | + | s.dropTimeline(h.release.tl) | |||
| 326 | + | } | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | // dropTimeline destroys a timeline the client destroyed once no held | |||
| 330 | + | // buffer still needs to signal it. | |||
| 331 | + | func (s *Server) dropTimeline(tl *timeline) { | |||
| 332 | + | if !tl.alive && tl.uses <= 0 && tl.handle != 0 { | |||
| 333 | + | _ = tl.dev.destroy(tl.handle) | |||
| 334 | + | tl.handle = 0 | |||
| 335 | + | } | |||
| 336 | + | } | |||
| 337 | + | ||||
| 338 | + | // takeSyncPoints moves the surface's requested points to the commit and | |||
| 339 | + | // starts waiting for the acquire point's fence. | |||
| 340 | + | func (s *surface) takeSyncPoints() { | |||
| 341 | + | st := s.sync | |||
| 342 | + | if st == nil || !st.acquire.set() { | |||
| 343 | + | return | |||
| 344 | + | } | |||
| 345 | + | cs := &commitSync{acquire: st.acquire, release: st.release} | |||
| 346 | + | st.acquire, st.release = syncPoint{}, syncPoint{} | |||
| 347 | + | cs.acquire.tl.uses++ | |||
| 348 | + | cs.release.tl.uses++ | |||
| 349 | + | w, err := s.server.syncWait.watch(cs.acquire) | |||
| 350 | + | if err != nil { | |||
| 351 | + | // The point cannot be watched: apply now, readers wait on the | |||
| 352 | + | // fence (or its absence) when they read the buffer. | |||
| 353 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire point") | |||
| 354 | + | } else { | |||
| 355 | + | cs.wait = w | |||
| 356 | + | } | |||
| 357 | + | s.pendingSync = cs | |||
| 358 | + | } | |||
| 359 | + | ||||
| 360 | + | // syncReady reports whether a commit's acquire point has its fence. | |||
| 361 | + | func (s *surface) syncReady(cs *commitSync) bool { | |||
| 362 | + | return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait) | |||
| 363 | + | } | |||
| 364 | + | ||||
| 365 | + | // dropSync forgets a commit that will never apply: its points are | |||
| 366 | + | // signalled so the client can reuse the buffer. | |||
| 367 | + | func (s *surface) dropSync(cs *commitSync) { | |||
| 368 | + | if cs == nil { | |||
| 369 | + | return | |||
| 370 | + | } | |||
| 371 | + | if cs.wait != nil { | |||
| 372 | + | s.server.syncWait.cancel(cs.wait) | |||
| 373 | + | } | |||
| 374 | + | cs.acquire.tl.uses-- | |||
| 375 | + | s.server.dropTimeline(cs.acquire.tl) | |||
| 376 | + | s.server.releaseSync(syncHold{release: cs.release}) | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // applySync makes a commit's points the current buffer's: its acquire | |||
| 380 | + | // fence goes with the content, its release point waits for release. | |||
| 381 | + | func (s *surface) applySync(cs *commitSync) { | |||
| 382 | + | if cs == nil { | |||
| 383 | + | return | |||
| 384 | + | } | |||
| 385 | + | if cs.wait != nil { | |||
| 386 | + | s.server.syncWait.cancel(cs.wait) | |||
| 387 | + | } | |||
| 388 | + | f, err := cs.acquire.tl.dev.exportSyncFile(cs.acquire.tl.handle, cs.acquire.point) | |||
| 389 | + | if err != nil { | |||
| 390 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire fence") | |||
| 391 | + | f = nil | |||
| 392 | + | } | |||
| 393 | + | cs.acquire.tl.uses-- | |||
| 394 | + | s.server.dropTimeline(cs.acquire.tl) | |||
| 395 | + | s.hold = syncHold{acquire: f, release: cs.release} | |||
| 396 | + | } | |||
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 | + | } | |
| @@ -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 +355 -0
| @@ -0,0 +1,355 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os" | |
| 6 | + | "sync" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | "unsafe" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/nefertty/internal/logging" | |
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |
| 13 | + | "github.com/bnema/purego-libwayland/protocol/linuxdmabuf" | |
| 14 | + | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |
| 15 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 16 | + | "github.com/bnema/wlturbo" | |
| 17 | + | "github.com/stretchr/testify/mock" | |
| 18 | + | "golang.org/x/sys/unix" | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | func TestSyncobjStructSizes(t *testing.T) { | |
| 22 | + | for _, tc := range []struct { | |
| 23 | + | name string | |
| 24 | + | got uintptr | |
| 25 | + | req uintptr | |
| 26 | + | }{ | |
| 27 | + | {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy}, | |
| 28 | + | {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD}, | |
| 29 | + | {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle}, | |
| 30 | + | {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal}, | |
| 31 | + | {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd}, | |
| 32 | + | {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap}, | |
| 33 | + | } { | |
| 34 | + | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| 35 | + | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // syncHarness is a server with a mocked syncobj device and a client with | |
| 41 | + | // a mapped window, a dmabuf buffer and the syncobj objects. | |
| 42 | + | type syncHarness struct { | |
| 43 | + | t *testing.T | |
| 44 | + | s *Server | |
| 45 | + | c *wlturbo.Display | |
| 46 | + | dev *mocksyncobjDevice | |
| 47 | + | contents chan ports.SurfaceContent | |
| 48 | + | presented chan ports.OutputPresented | |
| 49 | + | surf uint32 | |
| 50 | + | syncSurf uint32 | |
| 51 | + | timeline uint32 | |
| 52 | + | win ports.WindowID | |
| 53 | + | mu sync.Mutex | |
| 54 | + | efds map[uint64]int // eventfd by acquire point | |
| 55 | + | signals []uint64 | |
| 56 | + | fences []*os.File | |
| 57 | + | releases map[uint32]*syncReleaseProxy | |
| 58 | + | mgr uint32 | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func newSyncHarness(t *testing.T) *syncHarness { | |
| 62 | + | h := &syncHarness{t: t, efds: map[uint64]int{}} | |
| 63 | + | h.dev = newMocksyncobjDevice(t) | |
| 64 | + | h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe() | |
| 65 | + | h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe() | |
| 66 | + | h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error { | |
| 67 | + | // Keep a duplicate: the waiter closes its own when it fires. | |
| 68 | + | dup, err := unix.Dup(efd) | |
| 69 | + | if err != nil { | |
| 70 | + | return err | |
| 71 | + | } | |
| 72 | + | h.mu.Lock() | |
| 73 | + | h.efds[point] = dup | |
| 74 | + | h.mu.Unlock() | |
| 75 | + | return nil | |
| 76 | + | }).Maybe() | |
| 77 | + | h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) { | |
| 78 | + | r, w, err := os.Pipe() | |
| 79 | + | if err != nil { | |
| 80 | + | return nil, err | |
| 81 | + | } | |
| 82 | + | w.Close() | |
| 83 | + | h.mu.Lock() | |
| 84 | + | h.fences = append(h.fences, r) | |
| 85 | + | h.mu.Unlock() | |
| 86 | + | return r, nil | |
| 87 | + | }).Maybe() | |
| 88 | + | h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error { | |
| 89 | + | h.mu.Lock() | |
| 90 | + | h.signals = append(h.signals, point) | |
| 91 | + | h.mu.Unlock() | |
| 92 | + | return nil | |
| 93 | + | }).Maybe() | |
| 94 | + | dir := t.TempDir() | |
| 95 | + | events := make(chan ports.ClientEvent, 16) | |
| 96 | + | commands := make(chan ports.ClientCommand, 16) | |
| 97 | + | h.contents = make(chan ports.SurfaceContent, 64) | |
| 98 | + | h.presented = make(chan ports.OutputPresented, 16) | |
| 99 | + | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | |
| 100 | + | 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")) | |
| 101 | + | if err != nil { | |
| 102 | + | t.Fatal(err) | |
| 103 | + | } | |
| 104 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 105 | + | done := make(chan error, 1) | |
| 106 | + | go func() { done <- s.Run(ctx) }() | |
| 107 | + | t.Cleanup(func() { | |
| 108 | + | cancel() | |
| 109 | + | <-done | |
| 110 | + | h.mu.Lock() | |
| 111 | + | for _, fd := range h.efds { | |
| 112 | + | unix.Close(fd) | |
| 113 | + | } | |
| 114 | + | h.mu.Unlock() | |
| 115 | + | }) | |
| 116 | + | h.s = s | |
| 117 | + | h.c = protocolClient(t, s, dir) | |
| 118 | + | w, surf, xdg := surfaceMapper(t, h.c, events)() | |
| 119 | + | registerProtocol(t, h.c, xdg) | |
| 120 | + | h.win, h.surf = w.ID, surf | |
| 121 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"} | |
| 122 | + | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |
| 123 | + | h.syncSurf = h.c.AllocateID() | |
| 124 | + | requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf) | |
| 125 | + | registerProtocol(t, h.c, h.syncSurf) | |
| 126 | + | fd, err := unix.MemfdCreate("timeline", 0) | |
| 127 | + | if err != nil { | |
| 128 | + | t.Fatal(err) | |
| 129 | + | } | |
| 130 | + | defer unix.Close(fd) | |
| 131 | + | h.timeline = h.c.AllocateID() | |
| 132 | + | registerProtocol(t, h.c, h.timeline) | |
| 133 | + | if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil { | |
| 134 | + | t.Fatal(err) | |
| 135 | + | } | |
| 136 | + | for deadline := time.Now().Add(2 * time.Second); ; { | |
| 137 | + | var out string | |
| 138 | + | s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) }) | |
| 139 | + | if out != "" { | |
| 140 | + | break | |
| 141 | + | } | |
| 142 | + | if time.Now().After(deadline) { | |
| 143 | + | t.Fatal("configure not applied") | |
| 144 | + | } | |
| 145 | + | time.Sleep(2 * time.Millisecond) | |
| 146 | + | } | |
| 147 | + | return h | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it). | |
| 151 | + | func (h *syncHarness) dmabuf() uint32 { | |
| 152 | + | t, c := h.t, h.c | |
| 153 | + | dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion) | |
| 154 | + | fd, err := unix.MemfdCreate("buf", 0) | |
| 155 | + | if err != nil { | |
| 156 | + | t.Fatal(err) | |
| 157 | + | } | |
| 158 | + | defer unix.Close(fd) | |
| 159 | + | if err := unix.Ftruncate(fd, 256*16); err != nil { | |
| 160 | + | t.Fatal(err) | |
| 161 | + | } | |
| 162 | + | p := c.AllocateID() | |
| 163 | + | requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p) | |
| 164 | + | registerProtocol(t, c, p) | |
| 165 | + | if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil { | |
| 166 | + | t.Fatal(err) | |
| 167 | + | } | |
| 168 | + | b := c.AllocateID() | |
| 169 | + | rel := &syncReleaseProxy{released: make(chan struct{}, 4)} | |
| 170 | + | rel.SetID(b) | |
| 171 | + | c.Context().Register(rel) | |
| 172 | + | requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0)) | |
| 173 | + | h.releases[b] = rel | |
| 174 | + | return b | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | type syncReleaseProxy struct { | |
| 178 | + | wlturbo.BaseProxy | |
| 179 | + | released chan struct{} | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) { | |
| 183 | + | if uint32(e.Opcode) == wayland.BufferEventRelease { | |
| 184 | + | p.released <- struct{}{} | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func (h *syncHarness) commit(buf uint32, acquire, release uint64) { | |
| 189 | + | t, c := h.t, h.c | |
| 190 | + | requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 191 | + | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | |
| 192 | + | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |
| 193 | + | requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit) | |
| 194 | + | if err := c.Roundtrip(); err != nil { | |
| 195 | + | t.Fatal(err) | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // content waits for a DMABuf content of the window. | |
| 200 | + | func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) { | |
| 201 | + | deadline := time.After(wait) | |
| 202 | + | for { | |
| 203 | + | select { | |
| 204 | + | case c := <-h.contents: | |
| 205 | + | if c.ID == h.win && c.DMABuf != nil { | |
| 206 | + | return c, true | |
| 207 | + | } | |
| 208 | + | case <-deadline: | |
| 209 | + | return ports.SurfaceContent{}, false | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // fire makes the kernel's eventfd for an acquire point readable. | |
| 215 | + | func (h *syncHarness) fire(point uint64) { | |
| 216 | + | h.t.Helper() | |
| 217 | + | deadline := time.Now().Add(2 * time.Second) | |
| 218 | + | for { | |
| 219 | + | h.mu.Lock() | |
| 220 | + | fd, ok := h.efds[point] | |
| 221 | + | h.mu.Unlock() | |
| 222 | + | if ok { | |
| 223 | + | var one [8]byte | |
| 224 | + | one[0] = 1 | |
| 225 | + | if _, err := unix.Write(fd, one[:]); err != nil { | |
| 226 | + | h.t.Fatal(err) | |
| 227 | + | } | |
| 228 | + | return | |
| 229 | + | } | |
| 230 | + | if time.Now().After(deadline) { | |
| 231 | + | h.t.Fatalf("no eventfd for point %d", point) | |
| 232 | + | } | |
| 233 | + | time.Sleep(2 * time.Millisecond) | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | func (h *syncHarness) signalled() []uint64 { | |
| 238 | + | h.mu.Lock() | |
| 239 | + | defer h.mu.Unlock() | |
| 240 | + | return append([]uint64(nil), h.signals...) | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // A commit waits for its acquire point; the content carries the fence; | |
| 244 | + | // replacing it signals the release point once outputs moved on, and | |
| 245 | + | // wl_buffer.release is never sent. The acquire fence closes only then. | |
| 246 | + | func TestSyncobjAcquireAndRelease(t *testing.T) { | |
| 247 | + | h := newSyncHarness(t) | |
| 248 | + | h.releases = map[uint32]*syncReleaseProxy{} | |
| 249 | + | a, b := h.dmabuf(), h.dmabuf() | |
| 250 | + | h.commit(a, 1, 2) | |
| 251 | + | if _, ok := h.content(50 * time.Millisecond); ok { | |
| 252 | + | t.Fatal("content before the acquire point had a fence") | |
| 253 | + | } | |
| 254 | + | h.fire(1) | |
| 255 | + | first, ok := h.content(2 * time.Second) | |
| 256 | + | if !ok || first.Acquire == nil { | |
| 257 | + | t.Fatalf("content %+v", first) | |
| 258 | + | } | |
| 259 | + | h.commit(b, 3, 4) | |
| 260 | + | h.fire(3) | |
| 261 | + | second, ok := h.content(2 * time.Second) | |
| 262 | + | if !ok { | |
| 263 | + | t.Fatal("second content") | |
| 264 | + | } | |
| 265 | + | if _, err := first.Acquire.Stat(); err != nil { | |
| 266 | + | t.Fatal("acquire fence closed before release") | |
| 267 | + | } | |
| 268 | + | // The output read the second content: the first buffer is released. | |
| 269 | + | h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}} | |
| 270 | + | deadline := time.Now().Add(2 * time.Second) | |
| 271 | + | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |
| 272 | + | if err := h.c.Roundtrip(); err != nil { | |
| 273 | + | t.Fatal(err) | |
| 274 | + | } | |
| 275 | + | time.Sleep(2 * time.Millisecond) | |
| 276 | + | } | |
| 277 | + | if got := h.signalled(); len(got) != 1 || got[0] != 2 { | |
| 278 | + | t.Fatalf("signalled %v", got) | |
| 279 | + | } | |
| 280 | + | if _, err := first.Acquire.Stat(); err == nil { | |
| 281 | + | t.Fatal("acquire fence left open after release") | |
| 282 | + | } | |
| 283 | + | if err := h.c.Roundtrip(); err != nil { | |
| 284 | + | t.Fatal(err) | |
| 285 | + | } | |
| 286 | + | select { | |
| 287 | + | case <-h.releases[a].released: | |
| 288 | + | t.Fatal("wl_buffer.release sent under explicit sync") | |
| 289 | + | default: | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | func TestSyncobjProtocolErrors(t *testing.T) { | |
| 294 | + | for _, tc := range []struct { | |
| 295 | + | name string | |
| 296 | + | run func(h *syncHarness) | |
| 297 | + | obj func(h *syncHarness) uint32 | |
| 298 | + | code uint32 | |
| 299 | + | }{ | |
| 300 | + | {"conflicting points", func(h *syncHarness) { | |
| 301 | + | b := h.dmabuf() | |
| 302 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |
| 303 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5)) | |
| 304 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5)) | |
| 305 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 306 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)}, | |
| 307 | + | {"no release point", func(h *syncHarness) { | |
| 308 | + | b := h.dmabuf() | |
| 309 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |
| 310 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5)) | |
| 311 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 312 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)}, | |
| 313 | + | {"buffer without points", func(h *syncHarness) { | |
| 314 | + | b := h.dmabuf() | |
| 315 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |
| 316 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 317 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |
| 318 | + | {"surface exists", func(h *syncHarness) { | |
| 319 | + | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |
| 320 | + | h.mgr = mgr | |
| 321 | + | requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf) | |
| 322 | + | }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)}, | |
| 323 | + | } { | |
| 324 | + | t.Run(tc.name, func(t *testing.T) { | |
| 325 | + | h := newSyncHarness(t) | |
| 326 | + | h.releases = map[uint32]*syncReleaseProxy{} | |
| 327 | + | h.t = t | |
| 328 | + | tc.run(h) | |
| 329 | + | expectProtocolError(t, h.c, tc.obj(h), tc.code) | |
| 330 | + | }) | |
| 331 | + | } | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | // An invalid timeline fd is a protocol error. | |
| 335 | + | func TestSyncobjInvalidTimeline(t *testing.T) { | |
| 336 | + | dev := newMocksyncobjDevice(t) | |
| 337 | + | dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once() | |
| 338 | + | dir := t.TempDir() | |
| 339 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland")) | |
| 340 | + | if err != nil { | |
| 341 | + | t.Fatal(err) | |
| 342 | + | } | |
| 343 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 344 | + | done := make(chan error, 1) | |
| 345 | + | go func() { done <- s.Run(ctx) }() | |
| 346 | + | t.Cleanup(func() { cancel(); <-done }) | |
| 347 | + | c := protocolClient(t, s, dir) | |
| 348 | + | mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1") | |
| 349 | + | fd, _ := unix.MemfdCreate("bad", 0) | |
| 350 | + | defer unix.Close(fd) | |
| 351 | + | if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil { | |
| 352 | + | t.Fatal(err) | |
| 353 | + | } | |
| 354 | + | expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline)) | |
| 355 | + | } | |
| @@ -0,0 +1,355 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "sync" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | "unsafe" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/nefertty/internal/logging" | |||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 13 | + | "github.com/bnema/purego-libwayland/protocol/linuxdmabuf" | |||
| 14 | + | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |||
| 15 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 16 | + | "github.com/bnema/wlturbo" | |||
| 17 | + | "github.com/stretchr/testify/mock" | |||
| 18 | + | "golang.org/x/sys/unix" | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | func TestSyncobjStructSizes(t *testing.T) { | |||
| 22 | + | for _, tc := range []struct { | |||
| 23 | + | name string | |||
| 24 | + | got uintptr | |||
| 25 | + | req uintptr | |||
| 26 | + | }{ | |||
| 27 | + | {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy}, | |||
| 28 | + | {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD}, | |||
| 29 | + | {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle}, | |||
| 30 | + | {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal}, | |||
| 31 | + | {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd}, | |||
| 32 | + | {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap}, | |||
| 33 | + | } { | |||
| 34 | + | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |||
| 35 | + | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // syncHarness is a server with a mocked syncobj device and a client with | |||
| 41 | + | // a mapped window, a dmabuf buffer and the syncobj objects. | |||
| 42 | + | type syncHarness struct { | |||
| 43 | + | t *testing.T | |||
| 44 | + | s *Server | |||
| 45 | + | c *wlturbo.Display | |||
| 46 | + | dev *mocksyncobjDevice | |||
| 47 | + | contents chan ports.SurfaceContent | |||
| 48 | + | presented chan ports.OutputPresented | |||
| 49 | + | surf uint32 | |||
| 50 | + | syncSurf uint32 | |||
| 51 | + | timeline uint32 | |||
| 52 | + | win ports.WindowID | |||
| 53 | + | mu sync.Mutex | |||
| 54 | + | efds map[uint64]int // eventfd by acquire point | |||
| 55 | + | signals []uint64 | |||
| 56 | + | fences []*os.File | |||
| 57 | + | releases map[uint32]*syncReleaseProxy | |||
| 58 | + | mgr uint32 | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func newSyncHarness(t *testing.T) *syncHarness { | |||
| 62 | + | h := &syncHarness{t: t, efds: map[uint64]int{}} | |||
| 63 | + | h.dev = newMocksyncobjDevice(t) | |||
| 64 | + | h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe() | |||
| 65 | + | h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe() | |||
| 66 | + | h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error { | |||
| 67 | + | // Keep a duplicate: the waiter closes its own when it fires. | |||
| 68 | + | dup, err := unix.Dup(efd) | |||
| 69 | + | if err != nil { | |||
| 70 | + | return err | |||
| 71 | + | } | |||
| 72 | + | h.mu.Lock() | |||
| 73 | + | h.efds[point] = dup | |||
| 74 | + | h.mu.Unlock() | |||
| 75 | + | return nil | |||
| 76 | + | }).Maybe() | |||
| 77 | + | h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) { | |||
| 78 | + | r, w, err := os.Pipe() | |||
| 79 | + | if err != nil { | |||
| 80 | + | return nil, err | |||
| 81 | + | } | |||
| 82 | + | w.Close() | |||
| 83 | + | h.mu.Lock() | |||
| 84 | + | h.fences = append(h.fences, r) | |||
| 85 | + | h.mu.Unlock() | |||
| 86 | + | return r, nil | |||
| 87 | + | }).Maybe() | |||
| 88 | + | h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error { | |||
| 89 | + | h.mu.Lock() | |||
| 90 | + | h.signals = append(h.signals, point) | |||
| 91 | + | h.mu.Unlock() | |||
| 92 | + | return nil | |||
| 93 | + | }).Maybe() | |||
| 94 | + | dir := t.TempDir() | |||
| 95 | + | events := make(chan ports.ClientEvent, 16) | |||
| 96 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 97 | + | h.contents = make(chan ports.SurfaceContent, 64) | |||
| 98 | + | h.presented = make(chan ports.OutputPresented, 16) | |||
| 99 | + | sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}} | |||
| 100 | + | 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")) | |||
| 101 | + | if err != nil { | |||
| 102 | + | t.Fatal(err) | |||
| 103 | + | } | |||
| 104 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 105 | + | done := make(chan error, 1) | |||
| 106 | + | go func() { done <- s.Run(ctx) }() | |||
| 107 | + | t.Cleanup(func() { | |||
| 108 | + | cancel() | |||
| 109 | + | <-done | |||
| 110 | + | h.mu.Lock() | |||
| 111 | + | for _, fd := range h.efds { | |||
| 112 | + | unix.Close(fd) | |||
| 113 | + | } | |||
| 114 | + | h.mu.Unlock() | |||
| 115 | + | }) | |||
| 116 | + | h.s = s | |||
| 117 | + | h.c = protocolClient(t, s, dir) | |||
| 118 | + | w, surf, xdg := surfaceMapper(t, h.c, events)() | |||
| 119 | + | registerProtocol(t, h.c, xdg) | |||
| 120 | + | h.win, h.surf = w.ID, surf | |||
| 121 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"} | |||
| 122 | + | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |||
| 123 | + | h.syncSurf = h.c.AllocateID() | |||
| 124 | + | requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf) | |||
| 125 | + | registerProtocol(t, h.c, h.syncSurf) | |||
| 126 | + | fd, err := unix.MemfdCreate("timeline", 0) | |||
| 127 | + | if err != nil { | |||
| 128 | + | t.Fatal(err) | |||
| 129 | + | } | |||
| 130 | + | defer unix.Close(fd) | |||
| 131 | + | h.timeline = h.c.AllocateID() | |||
| 132 | + | registerProtocol(t, h.c, h.timeline) | |||
| 133 | + | if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil { | |||
| 134 | + | t.Fatal(err) | |||
| 135 | + | } | |||
| 136 | + | for deadline := time.Now().Add(2 * time.Second); ; { | |||
| 137 | + | var out string | |||
| 138 | + | s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) }) | |||
| 139 | + | if out != "" { | |||
| 140 | + | break | |||
| 141 | + | } | |||
| 142 | + | if time.Now().After(deadline) { | |||
| 143 | + | t.Fatal("configure not applied") | |||
| 144 | + | } | |||
| 145 | + | time.Sleep(2 * time.Millisecond) | |||
| 146 | + | } | |||
| 147 | + | return h | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it). | |||
| 151 | + | func (h *syncHarness) dmabuf() uint32 { | |||
| 152 | + | t, c := h.t, h.c | |||
| 153 | + | dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion) | |||
| 154 | + | fd, err := unix.MemfdCreate("buf", 0) | |||
| 155 | + | if err != nil { | |||
| 156 | + | t.Fatal(err) | |||
| 157 | + | } | |||
| 158 | + | defer unix.Close(fd) | |||
| 159 | + | if err := unix.Ftruncate(fd, 256*16); err != nil { | |||
| 160 | + | t.Fatal(err) | |||
| 161 | + | } | |||
| 162 | + | p := c.AllocateID() | |||
| 163 | + | requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p) | |||
| 164 | + | registerProtocol(t, c, p) | |||
| 165 | + | if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil { | |||
| 166 | + | t.Fatal(err) | |||
| 167 | + | } | |||
| 168 | + | b := c.AllocateID() | |||
| 169 | + | rel := &syncReleaseProxy{released: make(chan struct{}, 4)} | |||
| 170 | + | rel.SetID(b) | |||
| 171 | + | c.Context().Register(rel) | |||
| 172 | + | requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0)) | |||
| 173 | + | h.releases[b] = rel | |||
| 174 | + | return b | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | type syncReleaseProxy struct { | |||
| 178 | + | wlturbo.BaseProxy | |||
| 179 | + | released chan struct{} | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) { | |||
| 183 | + | if uint32(e.Opcode) == wayland.BufferEventRelease { | |||
| 184 | + | p.released <- struct{}{} | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | func (h *syncHarness) commit(buf uint32, acquire, release uint64) { | |||
| 189 | + | t, c := h.t, h.c | |||
| 190 | + | requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 191 | + | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | |||
| 192 | + | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |||
| 193 | + | requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit) | |||
| 194 | + | if err := c.Roundtrip(); err != nil { | |||
| 195 | + | t.Fatal(err) | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // content waits for a DMABuf content of the window. | |||
| 200 | + | func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) { | |||
| 201 | + | deadline := time.After(wait) | |||
| 202 | + | for { | |||
| 203 | + | select { | |||
| 204 | + | case c := <-h.contents: | |||
| 205 | + | if c.ID == h.win && c.DMABuf != nil { | |||
| 206 | + | return c, true | |||
| 207 | + | } | |||
| 208 | + | case <-deadline: | |||
| 209 | + | return ports.SurfaceContent{}, false | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // fire makes the kernel's eventfd for an acquire point readable. | |||
| 215 | + | func (h *syncHarness) fire(point uint64) { | |||
| 216 | + | h.t.Helper() | |||
| 217 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 218 | + | for { | |||
| 219 | + | h.mu.Lock() | |||
| 220 | + | fd, ok := h.efds[point] | |||
| 221 | + | h.mu.Unlock() | |||
| 222 | + | if ok { | |||
| 223 | + | var one [8]byte | |||
| 224 | + | one[0] = 1 | |||
| 225 | + | if _, err := unix.Write(fd, one[:]); err != nil { | |||
| 226 | + | h.t.Fatal(err) | |||
| 227 | + | } | |||
| 228 | + | return | |||
| 229 | + | } | |||
| 230 | + | if time.Now().After(deadline) { | |||
| 231 | + | h.t.Fatalf("no eventfd for point %d", point) | |||
| 232 | + | } | |||
| 233 | + | time.Sleep(2 * time.Millisecond) | |||
| 234 | + | } | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | func (h *syncHarness) signalled() []uint64 { | |||
| 238 | + | h.mu.Lock() | |||
| 239 | + | defer h.mu.Unlock() | |||
| 240 | + | return append([]uint64(nil), h.signals...) | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // A commit waits for its acquire point; the content carries the fence; | |||
| 244 | + | // replacing it signals the release point once outputs moved on, and | |||
| 245 | + | // wl_buffer.release is never sent. The acquire fence closes only then. | |||
| 246 | + | func TestSyncobjAcquireAndRelease(t *testing.T) { | |||
| 247 | + | h := newSyncHarness(t) | |||
| 248 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 249 | + | a, b := h.dmabuf(), h.dmabuf() | |||
| 250 | + | h.commit(a, 1, 2) | |||
| 251 | + | if _, ok := h.content(50 * time.Millisecond); ok { | |||
| 252 | + | t.Fatal("content before the acquire point had a fence") | |||
| 253 | + | } | |||
| 254 | + | h.fire(1) | |||
| 255 | + | first, ok := h.content(2 * time.Second) | |||
| 256 | + | if !ok || first.Acquire == nil { | |||
| 257 | + | t.Fatalf("content %+v", first) | |||
| 258 | + | } | |||
| 259 | + | h.commit(b, 3, 4) | |||
| 260 | + | h.fire(3) | |||
| 261 | + | second, ok := h.content(2 * time.Second) | |||
| 262 | + | if !ok { | |||
| 263 | + | t.Fatal("second content") | |||
| 264 | + | } | |||
| 265 | + | if _, err := first.Acquire.Stat(); err != nil { | |||
| 266 | + | t.Fatal("acquire fence closed before release") | |||
| 267 | + | } | |||
| 268 | + | // The output read the second content: the first buffer is released. | |||
| 269 | + | h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}} | |||
| 270 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 271 | + | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |||
| 272 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 273 | + | t.Fatal(err) | |||
| 274 | + | } | |||
| 275 | + | time.Sleep(2 * time.Millisecond) | |||
| 276 | + | } | |||
| 277 | + | if got := h.signalled(); len(got) != 1 || got[0] != 2 { | |||
| 278 | + | t.Fatalf("signalled %v", got) | |||
| 279 | + | } | |||
| 280 | + | if _, err := first.Acquire.Stat(); err == nil { | |||
| 281 | + | t.Fatal("acquire fence left open after release") | |||
| 282 | + | } | |||
| 283 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 284 | + | t.Fatal(err) | |||
| 285 | + | } | |||
| 286 | + | select { | |||
| 287 | + | case <-h.releases[a].released: | |||
| 288 | + | t.Fatal("wl_buffer.release sent under explicit sync") | |||
| 289 | + | default: | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | func TestSyncobjProtocolErrors(t *testing.T) { | |||
| 294 | + | for _, tc := range []struct { | |||
| 295 | + | name string | |||
| 296 | + | run func(h *syncHarness) | |||
| 297 | + | obj func(h *syncHarness) uint32 | |||
| 298 | + | code uint32 | |||
| 299 | + | }{ | |||
| 300 | + | {"conflicting points", func(h *syncHarness) { | |||
| 301 | + | b := h.dmabuf() | |||
| 302 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 303 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5)) | |||
| 304 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5)) | |||
| 305 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 306 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)}, | |||
| 307 | + | {"no release point", func(h *syncHarness) { | |||
| 308 | + | b := h.dmabuf() | |||
| 309 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 310 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5)) | |||
| 311 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 312 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)}, | |||
| 313 | + | {"buffer without points", func(h *syncHarness) { | |||
| 314 | + | b := h.dmabuf() | |||
| 315 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 316 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 317 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |||
| 318 | + | {"surface exists", func(h *syncHarness) { | |||
| 319 | + | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |||
| 320 | + | h.mgr = mgr | |||
| 321 | + | requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf) | |||
| 322 | + | }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)}, | |||
| 323 | + | } { | |||
| 324 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 325 | + | h := newSyncHarness(t) | |||
| 326 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 327 | + | h.t = t | |||
| 328 | + | tc.run(h) | |||
| 329 | + | expectProtocolError(t, h.c, tc.obj(h), tc.code) | |||
| 330 | + | }) | |||
| 331 | + | } | |||
| 332 | + | } | |||
| 333 | + | ||||
| 334 | + | // An invalid timeline fd is a protocol error. | |||
| 335 | + | func TestSyncobjInvalidTimeline(t *testing.T) { | |||
| 336 | + | dev := newMocksyncobjDevice(t) | |||
| 337 | + | dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once() | |||
| 338 | + | dir := t.TempDir() | |||
| 339 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland")) | |||
| 340 | + | if err != nil { | |||
| 341 | + | t.Fatal(err) | |||
| 342 | + | } | |||
| 343 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 344 | + | done := make(chan error, 1) | |||
| 345 | + | go func() { done <- s.Run(ctx) }() | |||
| 346 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 347 | + | c := protocolClient(t, s, dir) | |||
| 348 | + | mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1") | |||
| 349 | + | fd, _ := unix.MemfdCreate("bad", 0) | |||
| 350 | + | defer unix.Close(fd) | |||
| 351 | + | if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil { | |||
| 352 | + | t.Fatal(err) | |||
| 353 | + | } | |||
| 354 | + | expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline)) | |||
| 355 | + | } | |||
M internal/app/app.go +10 -1
| @@ -25,6 +25,7 @@ import ( | |||
| 25 | 25 | "github.com/bnema/nefertty/internal/logging" | |
| 26 | 26 | "github.com/bnema/nefertty/internal/ports" | |
| 27 | 27 | "github.com/bnema/zerowrap" | |
| 28 | + | "golang.org/x/sys/unix" | |
| 28 | 29 | ) | |
| 29 | 30 | ||
| 30 | 31 | type Options struct { | |
| @@ -102,7 +103,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 102 | 103 | // Every output renders on the same GPU: its formats are the clients'. | |
| 103 | 104 | dmabuf := vulkan.Probe() | |
| 104 | 105 | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | |
| 105 | - | 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, OutputFormats: outputFormats}, 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")) | |
| 106 | 107 | if err != nil { | |
| 107 | 108 | km.Close() | |
| 108 | 109 | return err | |
| @@ -378,3 +379,11 @@ func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap. | |||
| 378 | 379 | log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display") | |
| 379 | 380 | return d | |
| 380 | 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 | + | } | |
| @@ -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 { | ||
| @@ -102,7 +103,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 102 | // 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'. | ||
| 103 | dmabuf := vulkan.Probe() | 104 | dmabuf := vulkan.Probe() | ||
| 104 | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | 105 | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | ||
| 105 | - | 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, OutputFormats: outputFormats}, 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")) |
| 106 | if err != nil { | 107 | if err != nil { | ||
| 107 | km.Close() | 108 | km.Close() | ||
| 108 | return err | 109 | return err | ||
| @@ -378,3 +379,11 @@ func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap. | |||||
| 378 | log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display") | 379 | log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display") | ||
| 379 | return d | 380 | return d | ||
| 380 | } | 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/ports/messages.go +6 -0
| @@ -622,6 +622,12 @@ type SurfaceContent struct { | |||
| 622 | 622 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | |
| 623 | 623 | // honour it only in direct scanout. | |
| 624 | 624 | Async bool | |
| 625 | + | // Acquire is the explicit-sync fence of this content's root buffer | |
| 626 | + | // (wp_linux_drm_syncobj_v1): readers wait on it before reading the | |
| 627 | + | // buffer, instead of the buffer's implicit fences. Wayland owns it and | |
| 628 | + | // closes it when the buffer is released; a reader that keeps it past | |
| 629 | + | // the call duplicates it under Acquire.SyscallConn. nil: implicit sync. | |
| 630 | + | Acquire *os.File | |
| 625 | 631 | // DamageHistory is what changed in the root surface's buffer over the | |
| 626 | 632 | // window's last contents, oldest first, ending with this one (Seq). | |
| 627 | 633 | // A renderer holding an older content redraws the union of the | |
| @@ -622,6 +622,12 @@ type SurfaceContent struct { | |||||
| 622 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | 622 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | ||
| 623 | // honour it only in direct scanout. | 623 | // honour it only in direct scanout. | ||
| 624 | Async bool | 624 | Async bool | ||
| 625 | + | // Acquire is the explicit-sync fence of this content's root buffer | |||
| 626 | + | // (wp_linux_drm_syncobj_v1): readers wait on it before reading the | |||
| 627 | + | // buffer, instead of the buffer's implicit fences. Wayland owns it and | |||
| 628 | + | // closes it when the buffer is released; a reader that keeps it past | |||
| 629 | + | // the call duplicates it under Acquire.SyscallConn. nil: implicit sync. | |||
| 630 | + | Acquire *os.File | |||
| 625 | // DamageHistory is what changed in the root surface's buffer over the | 631 | // DamageHistory is what changed in the root surface's buffer over the | ||
| 626 | // window's last contents, oldest first, ending with this one (Seq). | 632 | // window's last contents, oldest first, ending with this one (Seq). | ||
| 627 | // A renderer holding an older content redraws the union of the | 633 | // A renderer holding an older content redraws the union of the | ||