fix(drm): serial-tagged commit events, cursor plane fallback and TEST_ONLY image validation
Commit events carry a per-card serial so stale events never complete a newer commit. A refused cursor plane turns the hardware cursor off instead of stopping the output or blaming scanout buffers; cursor images are never written while on screen or in flight. Output images are validated with a TEST_ONLY modeset, Close turns the CRTC off when no mode is restored, and sync file waits move to a shared syncfile package that honours ctx and poll errors.
- commit
- cb321d425a6091131085f4d2156611716e3f9063
- author
- brice · · 2026-09-26 11:03 UTC
- parent
- 7449251
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +758 -121
M internal/adapters/drm/atomic.go +1 -1
| @@ -236,7 +236,7 @@ func enableAtomic(fd int) error { | |||
| 236 | 236 | for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} { | |
| 237 | 237 | v := setClientCap{capability: c, value: 1} | |
| 238 | 238 | if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil { | |
| 239 | - | return errors.Join(errors.New("atomic modesetting unsupported"), err) | |
| 239 | + | return err | |
| 240 | 240 | } | |
| 241 | 241 | } | |
| 242 | 242 | if !hasCap(fd, capTimestampMonotonic) { | |
| @@ -236,7 +236,7 @@ func enableAtomic(fd int) error { | |||||
| 236 | for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} { | 236 | for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} { | ||
| 237 | v := setClientCap{capability: c, value: 1} | 237 | v := setClientCap{capability: c, value: 1} | ||
| 238 | if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil { | 238 | if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil { | ||
| 239 | - | return errors.Join(errors.New("atomic modesetting unsupported"), err) | 239 | + | return err |
| 240 | } | 240 | } | ||
| 241 | } | 241 | } | ||
| 242 | if !hasCap(fd, capTimestampMonotonic) { | 242 | if !hasCap(fd, capTimestampMonotonic) { | ||
M internal/adapters/drm/card.go +4 -1
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "slices" | |
| 8 | 8 | "sync" | |
| 9 | + | "sync/atomic" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/zerowrap" | |
| 11 | 12 | "golang.org/x/sys/unix" | |
| @@ -29,13 +30,15 @@ type Card struct { | |||
| 29 | 30 | taken map[uint32]bool | |
| 30 | 31 | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | |
| 31 | 32 | async bool | |
| 33 | + | // serials numbers the commits of every output (event userData). | |
| 34 | + | serials atomic.Uint64 | |
| 32 | 35 | } | |
| 33 | 36 | ||
| 34 | 37 | // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd | |
| 35 | 38 | // stays owned by the caller. A card without atomic KMS is refused. | |
| 36 | 39 | func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) { | |
| 37 | 40 | if err := enableAtomic(fd); err != nil { | |
| 38 | - | return nil, fmt.Errorf("%s: %w", path, err) | |
| 41 | + | return nil, fmt.Errorf("drm: atomic modesetting unsupported by %s: %w", path, err) | |
| 39 | 42 | } | |
| 40 | 43 | crtcs, _, err := resources(fd) | |
| 41 | 44 | if err != nil { | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "slices" | 7 | "slices" | ||
| 8 | "sync" | 8 | "sync" | ||
| 9 | + | "sync/atomic" | |||
| 9 | 10 | ||||
| 10 | "github.com/bnema/zerowrap" | 11 | "github.com/bnema/zerowrap" | ||
| 11 | "golang.org/x/sys/unix" | 12 | "golang.org/x/sys/unix" | ||
| @@ -29,13 +30,15 @@ type Card struct { | |||||
| 29 | taken map[uint32]bool | 30 | taken map[uint32]bool | ||
| 30 | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | 31 | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | ||
| 31 | async bool | 32 | async bool | ||
| 33 | + | // serials numbers the commits of every output (event userData). | |||
| 34 | + | serials atomic.Uint64 | |||
| 32 | } | 35 | } | ||
| 33 | 36 | ||||
| 34 | // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd | 37 | // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd | ||
| 35 | // stays owned by the caller. A card without atomic KMS is refused. | 38 | // stays owned by the caller. A card without atomic KMS is refused. | ||
| 36 | func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) { | 39 | func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) { | ||
| 37 | if err := enableAtomic(fd); err != nil { | 40 | if err := enableAtomic(fd); err != nil { | ||
| 38 | - | return nil, fmt.Errorf("%s: %w", path, err) | 41 | + | return nil, fmt.Errorf("drm: atomic modesetting unsupported by %s: %w", path, err) |
| 39 | } | 42 | } | ||
| 40 | crtcs, _, err := resources(fd) | 43 | crtcs, _, err := resources(fd) | ||
| 41 | if err != nil { | 44 | if err != nil { | ||
A internal/adapters/drm/commit_test.go +334 -0
| @@ -0,0 +1,334 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // An event queued before a modeset (or for an earlier output on the CRTC) | |
| 16 | + | // must not complete the commit that follows. | |
| 17 | + | func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) { | |
| 18 | + | o, _, commits := testOutput(t) | |
| 19 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |
| 20 | + | t.Fatal(err) | |
| 21 | + | } | |
| 22 | + | old := eventOf((*commits)[0]) | |
| 23 | + | o.pending = false // a modeset dropped the commit; its event is queued | |
| 24 | + | if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil { | |
| 25 | + | t.Fatal(err) | |
| 26 | + | } | |
| 27 | + | if o.completed(old, nil) || !o.pending { | |
| 28 | + | t.Fatal("stale event completed the new commit") | |
| 29 | + | } | |
| 30 | + | if !o.completed(eventOf((*commits)[1]), nil) || o.pending { | |
| 31 | + | t.Fatal("own event did not complete the commit") | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // After EBUSY no commit of ours is known: the next event of the CRTC ends | |
| 36 | + | // the wait, then the frame commits. | |
| 37 | + | func TestBusyFrameCommitsAfterEvent(t *testing.T) { | |
| 38 | + | o, _, commits := testOutput(t, unix.EBUSY) | |
| 39 | + | enabled := true | |
| 40 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) { | |
| 41 | + | t.Fatalf("EBUSY not handled: %v", err) | |
| 42 | + | } | |
| 43 | + | if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.pending { | |
| 44 | + | t.Fatal("EBUSY wait not ended by the CRTC's event") | |
| 45 | + | } | |
| 46 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.pending { | |
| 47 | + | t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits)) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // A cursor image change writes only an image the plane neither shows nor | |
| 52 | + | // is about to show; with both busy it waits for the event. | |
| 53 | + | func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |
| 54 | + | o, _, _ := testOutput(t) | |
| 55 | + | c := o.cursor | |
| 56 | + | r := portsmocks.NewMockRenderer(t) | |
| 57 | + | var written []int | |
| 58 | + | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |
| 59 | + | written = append(written, i) | |
| 60 | + | return nil | |
| 61 | + | }) | |
| 62 | + | px := []byte{1, 2, 3, 4} | |
| 63 | + | // Image 0 on screen: two changes before a commit both go to image 1. | |
| 64 | + | c.screen, c.cur, c.image = 90, 0, true | |
| 65 | + | for range 2 { | |
| 66 | + | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil { | |
| 67 | + | t.Fatal(err) | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | if len(written) != 2 || written[0] != 1 || written[1] != 1 { | |
| 71 | + | t.Fatalf("written %v", written) | |
| 72 | + | } | |
| 73 | + | // Image 1 in flight, image 0 on screen: nothing is free. | |
| 74 | + | c.committed(cursorState{on: true, fb: 91}) | |
| 75 | + | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil { | |
| 76 | + | t.Fatalf("wrote a busy image: %v %v", err, written) | |
| 77 | + | } | |
| 78 | + | // The flip frees image 0. | |
| 79 | + | c.landed() | |
| 80 | + | if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { | |
| 81 | + | t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // KMS refusing the cursor plane turns the hardware cursor off and the | |
| 86 | + | // frame commits without it; the output keeps running. | |
| 87 | + | func TestCursorRefusedFrameCommitsWithout(t *testing.T) { | |
| 88 | + | o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL) | |
| 89 | + | o.cursor.image = true | |
| 90 | + | o.cursor.Move(5, 5) | |
| 91 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |
| 92 | + | t.Fatal(err) | |
| 93 | + | } | |
| 94 | + | if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly { | |
| 95 | + | t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits)) | |
| 96 | + | } | |
| 97 | + | if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 { | |
| 98 | + | t.Fatalf("retry shows cursor fb %d", v) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // A refused cursor is not blamed on the client buffer. | |
| 103 | + | func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) { | |
| 104 | + | o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL) | |
| 105 | + | o.cursor.image = true | |
| 106 | + | o.cursor.Move(5, 5) | |
| 107 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |
| 108 | + | direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{}) | |
| 109 | + | if err != nil || !direct || o.clientFBs[9].failed != "" { | |
| 110 | + | t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // A cursor-only commit's event still reports content read meanwhile. | |
| 115 | + | func TestCursorEventReportsSeen(t *testing.T) { | |
| 116 | + | o, _, commits := testOutput(t) | |
| 117 | + | o.cursor.image = true | |
| 118 | + | o.cursor.Move(1, 1) | |
| 119 | + | if err := o.commitState(false); err != nil { | |
| 120 | + | t.Fatal(err) | |
| 121 | + | } | |
| 122 | + | seen := map[ports.WindowID]uint64{2: 5} | |
| 123 | + | if !o.completed(eventOf((*commits)[0]), seen) { | |
| 124 | + | t.Fatal("event not taken") | |
| 125 | + | } | |
| 126 | + | // Run reports Seen at the top of its loop once nothing is pending. | |
| 127 | + | if o.pending { | |
| 128 | + | t.Fatal("still pending") | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // showImages validates the images with a TEST_ONLY modeset and falls back | |
| 133 | + | // to linear images when KMS refuses the driver's. | |
| 134 | + | func TestShowImagesTestsBeforeModeset(t *testing.T) { | |
| 135 | + | o, k, commits := testOutput(t, unix.EINVAL) | |
| 136 | + | o.cursor = nil | |
| 137 | + | r := portsmocks.NewMockRenderer(t) | |
| 138 | + | buf := func() ports.DMABuf { | |
| 139 | + | f, w, _ := os.Pipe() | |
| 140 | + | w.Close() | |
| 141 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 142 | + | } | |
| 143 | + | r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 144 | + | r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 145 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 146 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| 147 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |
| 148 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 149 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 150 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 151 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 152 | + | t.Fatal(err) | |
| 153 | + | } | |
| 154 | + | want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes} | |
| 155 | + | if len(*commits) != len(want) { | |
| 156 | + | t.Fatalf("%d commits", len(*commits)) | |
| 157 | + | } | |
| 158 | + | for i, f := range want { | |
| 159 | + | if (*commits)[i].flags != f { | |
| 160 | + | t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f) | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | if o.kind != imagesLinear { | |
| 164 | + | t.Fatalf("kind %d", o.kind) | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // Enabling an output whose images were not made yet (started switched | |
| 169 | + | // away) tests them and modesets once. | |
| 170 | + | func TestEnableAfterStartAwayModesetsOnce(t *testing.T) { | |
| 171 | + | o, k, commits, commitMu := testOutputMu(t) | |
| 172 | + | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |
| 173 | + | r := portsmocks.NewMockRenderer(t) | |
| 174 | + | buf := func() ports.DMABuf { | |
| 175 | + | f, w, _ := os.Pipe() | |
| 176 | + | w.Close() | |
| 177 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 178 | + | } | |
| 179 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 180 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 181 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| 182 | + | r.EXPECT().Close().Return().Once() | |
| 183 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |
| 184 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 185 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 186 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 187 | + | active := make(chan bool, 1) | |
| 188 | + | active <- false | |
| 189 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 190 | + | done := make(chan error, 1) | |
| 191 | + | go func() { | |
| 192 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1)) | |
| 193 | + | }() | |
| 194 | + | count := func(flags uint32) int { | |
| 195 | + | commitMu.Lock() | |
| 196 | + | defer commitMu.Unlock() | |
| 197 | + | n := 0 | |
| 198 | + | for _, c := range *commits { | |
| 199 | + | if c.flags == flags { | |
| 200 | + | n++ | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | return n | |
| 204 | + | } | |
| 205 | + | time.Sleep(20 * time.Millisecond) | |
| 206 | + | if n := count(atomicAllowModes); n != 0 { | |
| 207 | + | t.Fatalf("%d modesets while away", n) | |
| 208 | + | } | |
| 209 | + | active <- true | |
| 210 | + | waitFor(t, func() bool { return count(atomicAllowModes) == 1 }) | |
| 211 | + | cancel() | |
| 212 | + | if err := <-done; err != nil { | |
| 213 | + | t.Fatal(err) | |
| 214 | + | } | |
| 215 | + | if n := count(atomicTestOnly | atomicAllowModes); n != 1 { | |
| 216 | + | t.Fatalf("%d test modesets", n) | |
| 217 | + | } | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | // Close without a mode to restore turns the CRTC off. | |
| 221 | + | func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) { | |
| 222 | + | o, k, commits := testOutput(t) | |
| 223 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 224 | + | o.Close() | |
| 225 | + | c := (*commits)[0] | |
| 226 | + | for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} { | |
| 227 | + | if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 { | |
| 228 | + | t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok) | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | // Close restores the saved mode and gives the connector its CRTC back. | |
| 234 | + | func TestCloseRestoresSavedMode(t *testing.T) { | |
| 235 | + | o, k, commits := testOutput(t) | |
| 236 | + | o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}} | |
| 237 | + | k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once() | |
| 238 | + | k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once() | |
| 239 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 240 | + | o.Close() | |
| 241 | + | c := (*commits)[0] | |
| 242 | + | if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc { | |
| 243 | + | t.Fatalf("connector crtc %d", v) | |
| 244 | + | } | |
| 245 | + | if v, _ := c.req.value(tPrimary, pFB); v != 5 { | |
| 246 | + | t.Fatalf("primary fb %d", v) | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // With the cursor off no image is loaded (no warning per change). | |
| 251 | + | func TestCursorOffSkipsLoads(t *testing.T) { | |
| 252 | + | o, _, _ := testOutput(t) | |
| 253 | + | o.cursor.off = true | |
| 254 | + | loaded := false | |
| 255 | + | o.setCursor(nil, func(ports.CursorChange, float64, int) (ports.CursorImage, error) { | |
| 256 | + | loaded = true | |
| 257 | + | return ports.CursorImage{}, nil | |
| 258 | + | }, ports.CursorChange{}, 1) | |
| 259 | + | if loaded || o.cursor.desired().on { | |
| 260 | + | t.Fatal("cursor used while off") | |
| 261 | + | } | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | // Content read while only a cursor commit is in flight is reported when | |
| 265 | + | // that commit's event arrives, not held until a later frame. | |
| 266 | + | func TestRunReportsSeenAfterCursorCommit(t *testing.T) { | |
| 267 | + | o, k, commits, commitMu := testOutputMu(t) | |
| 268 | + | o.tearing, o.cursor.fbs = false, [2]uint32{} | |
| 269 | + | flips := make(chan flipEvent, 1) | |
| 270 | + | o.flipped = flips | |
| 271 | + | r := portsmocks.NewMockRenderer(t) | |
| 272 | + | buf := func() ports.DMABuf { | |
| 273 | + | f, w, _ := os.Pipe() | |
| 274 | + | w.Close() | |
| 275 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 276 | + | } | |
| 277 | + | r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 278 | + | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil) | |
| 279 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 280 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 281 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| 282 | + | r.EXPECT().Close().Return().Once() | |
| 283 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once() | |
| 284 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once() | |
| 285 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |
| 286 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 287 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |
| 288 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 289 | + | load := func(ports.CursorChange, float64, int) (ports.CursorImage, error) { | |
| 290 | + | return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil | |
| 291 | + | } | |
| 292 | + | last := func(kind uint64) (commitRec, bool) { | |
| 293 | + | commitMu.Lock() | |
| 294 | + | defer commitMu.Unlock() | |
| 295 | + | for i := len(*commits) - 1; i >= 0; i-- { | |
| 296 | + | if c := (*commits)[i]; c.user&3 == kind { | |
| 297 | + | return c, true | |
| 298 | + | } | |
| 299 | + | } | |
| 300 | + | return commitRec{}, false | |
| 301 | + | } | |
| 302 | + | scenes := make(chan ports.Scene, 1) | |
| 303 | + | contents := make(chan ports.SurfaceContent, 1) | |
| 304 | + | presented := make(chan ports.OutputPresented, 8) | |
| 305 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 306 | + | done := make(chan error, 1) | |
| 307 | + | go func() { | |
| 308 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented) | |
| 309 | + | }() | |
| 310 | + | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |
| 311 | + | var frame commitRec | |
| 312 | + | waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok }) | |
| 313 | + | flips <- eventOf(frame) | |
| 314 | + | <-presented // the frame's flip | |
| 315 | + | o.cursor.Move(3, 3) | |
| 316 | + | var cur commitRec | |
| 317 | + | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | |
| 318 | + | // A window this output does not show commits while the cursor flies. | |
| 319 | + | contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}} | |
| 320 | + | time.Sleep(20 * time.Millisecond) | |
| 321 | + | flips <- eventOf(cur) | |
| 322 | + | select { | |
| 323 | + | case p := <-presented: | |
| 324 | + | if p.Seen[7] != 2 { | |
| 325 | + | t.Fatalf("report %+v", p) | |
| 326 | + | } | |
| 327 | + | case <-time.After(time.Second): | |
| 328 | + | t.Fatal("Seen not reported after the cursor commit") | |
| 329 | + | } | |
| 330 | + | cancel() | |
| 331 | + | if err := <-done; err != nil { | |
| 332 | + | t.Fatal(err) | |
| 333 | + | } | |
| 334 | + | } | |
| @@ -0,0 +1,334 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // An event queued before a modeset (or for an earlier output on the CRTC) | |||
| 16 | + | // must not complete the commit that follows. | |||
| 17 | + | func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) { | |||
| 18 | + | o, _, commits := testOutput(t) | |||
| 19 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 20 | + | t.Fatal(err) | |||
| 21 | + | } | |||
| 22 | + | old := eventOf((*commits)[0]) | |||
| 23 | + | o.pending = false // a modeset dropped the commit; its event is queued | |||
| 24 | + | if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil { | |||
| 25 | + | t.Fatal(err) | |||
| 26 | + | } | |||
| 27 | + | if o.completed(old, nil) || !o.pending { | |||
| 28 | + | t.Fatal("stale event completed the new commit") | |||
| 29 | + | } | |||
| 30 | + | if !o.completed(eventOf((*commits)[1]), nil) || o.pending { | |||
| 31 | + | t.Fatal("own event did not complete the commit") | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // After EBUSY no commit of ours is known: the next event of the CRTC ends | |||
| 36 | + | // the wait, then the frame commits. | |||
| 37 | + | func TestBusyFrameCommitsAfterEvent(t *testing.T) { | |||
| 38 | + | o, _, commits := testOutput(t, unix.EBUSY) | |||
| 39 | + | enabled := true | |||
| 40 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) { | |||
| 41 | + | t.Fatalf("EBUSY not handled: %v", err) | |||
| 42 | + | } | |||
| 43 | + | if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.pending { | |||
| 44 | + | t.Fatal("EBUSY wait not ended by the CRTC's event") | |||
| 45 | + | } | |||
| 46 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.pending { | |||
| 47 | + | t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits)) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // A cursor image change writes only an image the plane neither shows nor | |||
| 52 | + | // is about to show; with both busy it waits for the event. | |||
| 53 | + | func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |||
| 54 | + | o, _, _ := testOutput(t) | |||
| 55 | + | c := o.cursor | |||
| 56 | + | r := portsmocks.NewMockRenderer(t) | |||
| 57 | + | var written []int | |||
| 58 | + | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |||
| 59 | + | written = append(written, i) | |||
| 60 | + | return nil | |||
| 61 | + | }) | |||
| 62 | + | px := []byte{1, 2, 3, 4} | |||
| 63 | + | // Image 0 on screen: two changes before a commit both go to image 1. | |||
| 64 | + | c.screen, c.cur, c.image = 90, 0, true | |||
| 65 | + | for range 2 { | |||
| 66 | + | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil { | |||
| 67 | + | t.Fatal(err) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | if len(written) != 2 || written[0] != 1 || written[1] != 1 { | |||
| 71 | + | t.Fatalf("written %v", written) | |||
| 72 | + | } | |||
| 73 | + | // Image 1 in flight, image 0 on screen: nothing is free. | |||
| 74 | + | c.committed(cursorState{on: true, fb: 91}) | |||
| 75 | + | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil { | |||
| 76 | + | t.Fatalf("wrote a busy image: %v %v", err, written) | |||
| 77 | + | } | |||
| 78 | + | // The flip frees image 0. | |||
| 79 | + | c.landed() | |||
| 80 | + | if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { | |||
| 81 | + | t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // KMS refusing the cursor plane turns the hardware cursor off and the | |||
| 86 | + | // frame commits without it; the output keeps running. | |||
| 87 | + | func TestCursorRefusedFrameCommitsWithout(t *testing.T) { | |||
| 88 | + | o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 89 | + | o.cursor.image = true | |||
| 90 | + | o.cursor.Move(5, 5) | |||
| 91 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 92 | + | t.Fatal(err) | |||
| 93 | + | } | |||
| 94 | + | if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly { | |||
| 95 | + | t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits)) | |||
| 96 | + | } | |||
| 97 | + | if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 { | |||
| 98 | + | t.Fatalf("retry shows cursor fb %d", v) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // A refused cursor is not blamed on the client buffer. | |||
| 103 | + | func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) { | |||
| 104 | + | o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 105 | + | o.cursor.image = true | |||
| 106 | + | o.cursor.Move(5, 5) | |||
| 107 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |||
| 108 | + | direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{}) | |||
| 109 | + | if err != nil || !direct || o.clientFBs[9].failed != "" { | |||
| 110 | + | t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // A cursor-only commit's event still reports content read meanwhile. | |||
| 115 | + | func TestCursorEventReportsSeen(t *testing.T) { | |||
| 116 | + | o, _, commits := testOutput(t) | |||
| 117 | + | o.cursor.image = true | |||
| 118 | + | o.cursor.Move(1, 1) | |||
| 119 | + | if err := o.commitState(false); err != nil { | |||
| 120 | + | t.Fatal(err) | |||
| 121 | + | } | |||
| 122 | + | seen := map[ports.WindowID]uint64{2: 5} | |||
| 123 | + | if !o.completed(eventOf((*commits)[0]), seen) { | |||
| 124 | + | t.Fatal("event not taken") | |||
| 125 | + | } | |||
| 126 | + | // Run reports Seen at the top of its loop once nothing is pending. | |||
| 127 | + | if o.pending { | |||
| 128 | + | t.Fatal("still pending") | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // showImages validates the images with a TEST_ONLY modeset and falls back | |||
| 133 | + | // to linear images when KMS refuses the driver's. | |||
| 134 | + | func TestShowImagesTestsBeforeModeset(t *testing.T) { | |||
| 135 | + | o, k, commits := testOutput(t, unix.EINVAL) | |||
| 136 | + | o.cursor = nil | |||
| 137 | + | r := portsmocks.NewMockRenderer(t) | |||
| 138 | + | buf := func() ports.DMABuf { | |||
| 139 | + | f, w, _ := os.Pipe() | |||
| 140 | + | w.Close() | |||
| 141 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 142 | + | } | |||
| 143 | + | r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 144 | + | r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 145 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 146 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |||
| 147 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |||
| 148 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 149 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 150 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 151 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 152 | + | t.Fatal(err) | |||
| 153 | + | } | |||
| 154 | + | want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes} | |||
| 155 | + | if len(*commits) != len(want) { | |||
| 156 | + | t.Fatalf("%d commits", len(*commits)) | |||
| 157 | + | } | |||
| 158 | + | for i, f := range want { | |||
| 159 | + | if (*commits)[i].flags != f { | |||
| 160 | + | t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f) | |||
| 161 | + | } | |||
| 162 | + | } | |||
| 163 | + | if o.kind != imagesLinear { | |||
| 164 | + | t.Fatalf("kind %d", o.kind) | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // Enabling an output whose images were not made yet (started switched | |||
| 169 | + | // away) tests them and modesets once. | |||
| 170 | + | func TestEnableAfterStartAwayModesetsOnce(t *testing.T) { | |||
| 171 | + | o, k, commits, commitMu := testOutputMu(t) | |||
| 172 | + | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |||
| 173 | + | r := portsmocks.NewMockRenderer(t) | |||
| 174 | + | buf := func() ports.DMABuf { | |||
| 175 | + | f, w, _ := os.Pipe() | |||
| 176 | + | w.Close() | |||
| 177 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 178 | + | } | |||
| 179 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 180 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 181 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |||
| 182 | + | r.EXPECT().Close().Return().Once() | |||
| 183 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 184 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 185 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 186 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 187 | + | active := make(chan bool, 1) | |||
| 188 | + | active <- false | |||
| 189 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 190 | + | done := make(chan error, 1) | |||
| 191 | + | go func() { | |||
| 192 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1)) | |||
| 193 | + | }() | |||
| 194 | + | count := func(flags uint32) int { | |||
| 195 | + | commitMu.Lock() | |||
| 196 | + | defer commitMu.Unlock() | |||
| 197 | + | n := 0 | |||
| 198 | + | for _, c := range *commits { | |||
| 199 | + | if c.flags == flags { | |||
| 200 | + | n++ | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | return n | |||
| 204 | + | } | |||
| 205 | + | time.Sleep(20 * time.Millisecond) | |||
| 206 | + | if n := count(atomicAllowModes); n != 0 { | |||
| 207 | + | t.Fatalf("%d modesets while away", n) | |||
| 208 | + | } | |||
| 209 | + | active <- true | |||
| 210 | + | waitFor(t, func() bool { return count(atomicAllowModes) == 1 }) | |||
| 211 | + | cancel() | |||
| 212 | + | if err := <-done; err != nil { | |||
| 213 | + | t.Fatal(err) | |||
| 214 | + | } | |||
| 215 | + | if n := count(atomicTestOnly | atomicAllowModes); n != 1 { | |||
| 216 | + | t.Fatalf("%d test modesets", n) | |||
| 217 | + | } | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | // Close without a mode to restore turns the CRTC off. | |||
| 221 | + | func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) { | |||
| 222 | + | o, k, commits := testOutput(t) | |||
| 223 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 224 | + | o.Close() | |||
| 225 | + | c := (*commits)[0] | |||
| 226 | + | for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} { | |||
| 227 | + | if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 { | |||
| 228 | + | t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok) | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | // Close restores the saved mode and gives the connector its CRTC back. | |||
| 234 | + | func TestCloseRestoresSavedMode(t *testing.T) { | |||
| 235 | + | o, k, commits := testOutput(t) | |||
| 236 | + | o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}} | |||
| 237 | + | k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once() | |||
| 238 | + | k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once() | |||
| 239 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 240 | + | o.Close() | |||
| 241 | + | c := (*commits)[0] | |||
| 242 | + | if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc { | |||
| 243 | + | t.Fatalf("connector crtc %d", v) | |||
| 244 | + | } | |||
| 245 | + | if v, _ := c.req.value(tPrimary, pFB); v != 5 { | |||
| 246 | + | t.Fatalf("primary fb %d", v) | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | ||||
| 250 | + | // With the cursor off no image is loaded (no warning per change). | |||
| 251 | + | func TestCursorOffSkipsLoads(t *testing.T) { | |||
| 252 | + | o, _, _ := testOutput(t) | |||
| 253 | + | o.cursor.off = true | |||
| 254 | + | loaded := false | |||
| 255 | + | o.setCursor(nil, func(ports.CursorChange, float64, int) (ports.CursorImage, error) { | |||
| 256 | + | loaded = true | |||
| 257 | + | return ports.CursorImage{}, nil | |||
| 258 | + | }, ports.CursorChange{}, 1) | |||
| 259 | + | if loaded || o.cursor.desired().on { | |||
| 260 | + | t.Fatal("cursor used while off") | |||
| 261 | + | } | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | // Content read while only a cursor commit is in flight is reported when | |||
| 265 | + | // that commit's event arrives, not held until a later frame. | |||
| 266 | + | func TestRunReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 267 | + | o, k, commits, commitMu := testOutputMu(t) | |||
| 268 | + | o.tearing, o.cursor.fbs = false, [2]uint32{} | |||
| 269 | + | flips := make(chan flipEvent, 1) | |||
| 270 | + | o.flipped = flips | |||
| 271 | + | r := portsmocks.NewMockRenderer(t) | |||
| 272 | + | buf := func() ports.DMABuf { | |||
| 273 | + | f, w, _ := os.Pipe() | |||
| 274 | + | w.Close() | |||
| 275 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 276 | + | } | |||
| 277 | + | r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 278 | + | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil) | |||
| 279 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |||
| 280 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |||
| 281 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |||
| 282 | + | r.EXPECT().Close().Return().Once() | |||
| 283 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once() | |||
| 284 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once() | |||
| 285 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 286 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 287 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |||
| 288 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 289 | + | load := func(ports.CursorChange, float64, int) (ports.CursorImage, error) { | |||
| 290 | + | return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil | |||
| 291 | + | } | |||
| 292 | + | last := func(kind uint64) (commitRec, bool) { | |||
| 293 | + | commitMu.Lock() | |||
| 294 | + | defer commitMu.Unlock() | |||
| 295 | + | for i := len(*commits) - 1; i >= 0; i-- { | |||
| 296 | + | if c := (*commits)[i]; c.user&3 == kind { | |||
| 297 | + | return c, true | |||
| 298 | + | } | |||
| 299 | + | } | |||
| 300 | + | return commitRec{}, false | |||
| 301 | + | } | |||
| 302 | + | scenes := make(chan ports.Scene, 1) | |||
| 303 | + | contents := make(chan ports.SurfaceContent, 1) | |||
| 304 | + | presented := make(chan ports.OutputPresented, 8) | |||
| 305 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 306 | + | done := make(chan error, 1) | |||
| 307 | + | go func() { | |||
| 308 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented) | |||
| 309 | + | }() | |||
| 310 | + | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |||
| 311 | + | var frame commitRec | |||
| 312 | + | waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok }) | |||
| 313 | + | flips <- eventOf(frame) | |||
| 314 | + | <-presented // the frame's flip | |||
| 315 | + | o.cursor.Move(3, 3) | |||
| 316 | + | var cur commitRec | |||
| 317 | + | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | |||
| 318 | + | // A window this output does not show commits while the cursor flies. | |||
| 319 | + | contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}} | |||
| 320 | + | time.Sleep(20 * time.Millisecond) | |||
| 321 | + | flips <- eventOf(cur) | |||
| 322 | + | select { | |||
| 323 | + | case p := <-presented: | |||
| 324 | + | if p.Seen[7] != 2 { | |||
| 325 | + | t.Fatalf("report %+v", p) | |||
| 326 | + | } | |||
| 327 | + | case <-time.After(time.Second): | |||
| 328 | + | t.Fatal("Seen not reported after the cursor commit") | |||
| 329 | + | } | |||
| 330 | + | cancel() | |||
| 331 | + | if err := <-done; err != nil { | |||
| 332 | + | t.Fatal(err) | |||
| 333 | + | } | |||
| 334 | + | } | |||
M internal/adapters/drm/cursor.go +70 -5
| @@ -2,6 +2,7 @@ package drm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "slices" | |
| 5 | 6 | "sync" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/nefertty/internal/ports" | |
| @@ -38,6 +39,21 @@ type Cursor struct { | |||
| 38 | 39 | hotX, hotY int | |
| 39 | 40 | applied cursorState // what the last commit put on the plane | |
| 40 | 41 | commits int | |
| 42 | + | // screen is the image the plane scans out (0: none); flight is what | |
| 43 | + | // the pending commit puts on it when flying. An image is written only | |
| 44 | + | // when it is neither, so the plane never shows a half-written image. | |
| 45 | + | screen uint32 | |
| 46 | + | flight cursorState | |
| 47 | + | flying bool | |
| 48 | + | // later is an image that waits for a free slot (both on screen or in | |
| 49 | + | // flight); off is set when KMS refused the cursor plane. | |
| 50 | + | later *laterImage | |
| 51 | + | off bool | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | type laterImage struct { | |
| 55 | + | pixels []byte | |
| 56 | + | w, h, hotX, hotY int | |
| 41 | 57 | } | |
| 42 | 58 | ||
| 43 | 59 | // cursorState is the cursor plane as committed: off, or fb at x, y. | |
| @@ -115,9 +131,12 @@ func (c *Cursor) setup(k kms, r ports.Renderer) error { | |||
| 115 | 131 | return err | |
| 116 | 132 | } | |
| 117 | 133 | ||
| 118 | - | // setImage loads premultiplied ARGB8888 pixels into the image not on | |
| 119 | - | // screen; an empty image hides the cursor (a client asked for none). | |
| 134 | + | // setImage loads premultiplied ARGB8888 pixels into an image neither on | |
| 135 | + | // screen nor in flight; when there is none it waits for the next commit | |
| 136 | + | // event (flushLater). An empty image hides the cursor (a client asked for | |
| 137 | + | // none). | |
| 120 | 138 | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { | |
| 139 | + | c.later = nil | |
| 121 | 140 | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | |
| 122 | 141 | c.image = false | |
| 123 | 142 | return nil | |
| @@ -125,7 +144,11 @@ func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) | |||
| 125 | 144 | if c.fbs[0] == 0 { | |
| 126 | 145 | return errors.New("no cursor images") | |
| 127 | 146 | } | |
| 128 | - | next := 1 - c.cur | |
| 147 | + | next := c.freeSlot() | |
| 148 | + | if next < 0 { | |
| 149 | + | c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY} | |
| 150 | + | return nil | |
| 151 | + | } | |
| 129 | 152 | if err := r.WriteCursor(next, pixels, w, h); err != nil { | |
| 130 | 153 | return err | |
| 131 | 154 | } | |
| @@ -133,12 +156,53 @@ func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) | |||
| 133 | 156 | return nil | |
| 134 | 157 | } | |
| 135 | 158 | ||
| 159 | + | // freeSlot is an image the plane neither shows nor is about to show, -1 | |
| 160 | + | // when both are busy. | |
| 161 | + | func (c *Cursor) freeSlot() int { | |
| 162 | + | for i, fb := range c.fbs { | |
| 163 | + | if fb != c.screen && !(c.flying && c.flight.on && c.flight.fb == fb) { | |
| 164 | + | return i | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | return -1 | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | // flushLater loads an image that waited for a free slot. It reports | |
| 171 | + | // whether the cursor changed. | |
| 172 | + | func (c *Cursor) flushLater(r ports.Renderer) (bool, error) { | |
| 173 | + | l := c.later | |
| 174 | + | if l == nil || c.freeSlot() < 0 { | |
| 175 | + | return false, nil | |
| 176 | + | } | |
| 177 | + | return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY) | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // committed records that a commit carrying state s is in flight. | |
| 181 | + | func (c *Cursor) committed(s cursorState) { | |
| 182 | + | if s != c.applied { | |
| 183 | + | c.mu.Lock() | |
| 184 | + | c.commits++ | |
| 185 | + | c.mu.Unlock() | |
| 186 | + | } | |
| 187 | + | c.applied, c.flight, c.flying = s, s, true | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // landed records that the pending commit reached the screen. | |
| 191 | + | func (c *Cursor) landed() { | |
| 192 | + | if c.flying { | |
| 193 | + | c.screen, c.flying = 0, false | |
| 194 | + | if c.flight.on { | |
| 195 | + | c.screen = c.flight.fb | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | ||
| 136 | 200 | // desired is what the cursor plane should show now. | |
| 137 | 201 | func (c *Cursor) desired() cursorState { | |
| 138 | 202 | c.mu.Lock() | |
| 139 | 203 | x, y, away := c.x, c.y, c.away | |
| 140 | 204 | c.mu.Unlock() | |
| 141 | - | if !c.image || away || c.fbs[c.cur] == 0 { | |
| 205 | + | if !c.image || away || c.off || c.fbs[c.cur] == 0 { | |
| 142 | 206 | return cursorState{} | |
| 143 | 207 | } | |
| 144 | 208 | return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY} | |
| @@ -172,5 +236,6 @@ func (c *Cursor) free(k kms) { | |||
| 172 | 236 | c.fbs[i] = 0 | |
| 173 | 237 | } | |
| 174 | 238 | } | |
| 175 | - | c.image = false | |
| 239 | + | c.image, c.later = false, nil | |
| 240 | + | c.screen, c.flying = 0, false | |
| 176 | 241 | } | |
| @@ -2,6 +2,7 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "slices" | |||
| 5 | "sync" | 6 | "sync" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| @@ -38,6 +39,21 @@ type Cursor struct { | |||||
| 38 | hotX, hotY int | 39 | hotX, hotY int | ||
| 39 | applied cursorState // what the last commit put on the plane | 40 | applied cursorState // what the last commit put on the plane | ||
| 40 | commits int | 41 | commits int | ||
| 42 | + | // screen is the image the plane scans out (0: none); flight is what | |||
| 43 | + | // the pending commit puts on it when flying. An image is written only | |||
| 44 | + | // when it is neither, so the plane never shows a half-written image. | |||
| 45 | + | screen uint32 | |||
| 46 | + | flight cursorState | |||
| 47 | + | flying bool | |||
| 48 | + | // later is an image that waits for a free slot (both on screen or in | |||
| 49 | + | // flight); off is set when KMS refused the cursor plane. | |||
| 50 | + | later *laterImage | |||
| 51 | + | off bool | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | type laterImage struct { | |||
| 55 | + | pixels []byte | |||
| 56 | + | w, h, hotX, hotY int | |||
| 41 | } | 57 | } | ||
| 42 | 58 | ||||
| 43 | // cursorState is the cursor plane as committed: off, or fb at x, y. | 59 | // cursorState is the cursor plane as committed: off, or fb at x, y. | ||
| @@ -115,9 +131,12 @@ func (c *Cursor) setup(k kms, r ports.Renderer) error { | |||||
| 115 | return err | 131 | return err | ||
| 116 | } | 132 | } | ||
| 117 | 133 | ||||
| 118 | - | // setImage loads premultiplied ARGB8888 pixels into the image not on | 134 | + | // setImage loads premultiplied ARGB8888 pixels into an image neither on |
| 119 | - | // screen; an empty image hides the cursor (a client asked for none). | 135 | + | // screen nor in flight; when there is none it waits for the next commit |
| 136 | + | // event (flushLater). An empty image hides the cursor (a client asked for | |||
| 137 | + | // none). | |||
| 120 | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { | 138 | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { | ||
| 139 | + | c.later = nil | |||
| 121 | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | 140 | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | ||
| 122 | c.image = false | 141 | c.image = false | ||
| 123 | return nil | 142 | return nil | ||
| @@ -125,7 +144,11 @@ func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) | |||||
| 125 | if c.fbs[0] == 0 { | 144 | if c.fbs[0] == 0 { | ||
| 126 | return errors.New("no cursor images") | 145 | return errors.New("no cursor images") | ||
| 127 | } | 146 | } | ||
| 128 | - | next := 1 - c.cur | 147 | + | next := c.freeSlot() |
| 148 | + | if next < 0 { | |||
| 149 | + | c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY} | |||
| 150 | + | return nil | |||
| 151 | + | } | |||
| 129 | if err := r.WriteCursor(next, pixels, w, h); err != nil { | 152 | if err := r.WriteCursor(next, pixels, w, h); err != nil { | ||
| 130 | return err | 153 | return err | ||
| 131 | } | 154 | } | ||
| @@ -133,12 +156,53 @@ func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) | |||||
| 133 | return nil | 156 | return nil | ||
| 134 | } | 157 | } | ||
| 135 | 158 | ||||
| 159 | + | // freeSlot is an image the plane neither shows nor is about to show, -1 | |||
| 160 | + | // when both are busy. | |||
| 161 | + | func (c *Cursor) freeSlot() int { | |||
| 162 | + | for i, fb := range c.fbs { | |||
| 163 | + | if fb != c.screen && !(c.flying && c.flight.on && c.flight.fb == fb) { | |||
| 164 | + | return i | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | return -1 | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | // flushLater loads an image that waited for a free slot. It reports | |||
| 171 | + | // whether the cursor changed. | |||
| 172 | + | func (c *Cursor) flushLater(r ports.Renderer) (bool, error) { | |||
| 173 | + | l := c.later | |||
| 174 | + | if l == nil || c.freeSlot() < 0 { | |||
| 175 | + | return false, nil | |||
| 176 | + | } | |||
| 177 | + | return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY) | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // committed records that a commit carrying state s is in flight. | |||
| 181 | + | func (c *Cursor) committed(s cursorState) { | |||
| 182 | + | if s != c.applied { | |||
| 183 | + | c.mu.Lock() | |||
| 184 | + | c.commits++ | |||
| 185 | + | c.mu.Unlock() | |||
| 186 | + | } | |||
| 187 | + | c.applied, c.flight, c.flying = s, s, true | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // landed records that the pending commit reached the screen. | |||
| 191 | + | func (c *Cursor) landed() { | |||
| 192 | + | if c.flying { | |||
| 193 | + | c.screen, c.flying = 0, false | |||
| 194 | + | if c.flight.on { | |||
| 195 | + | c.screen = c.flight.fb | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 136 | // desired is what the cursor plane should show now. | 200 | // desired is what the cursor plane should show now. | ||
| 137 | func (c *Cursor) desired() cursorState { | 201 | func (c *Cursor) desired() cursorState { | ||
| 138 | c.mu.Lock() | 202 | c.mu.Lock() | ||
| 139 | x, y, away := c.x, c.y, c.away | 203 | x, y, away := c.x, c.y, c.away | ||
| 140 | c.mu.Unlock() | 204 | c.mu.Unlock() | ||
| 141 | - | if !c.image || away || c.fbs[c.cur] == 0 { | 205 | + | if !c.image || away || c.off || c.fbs[c.cur] == 0 { |
| 142 | return cursorState{} | 206 | return cursorState{} | ||
| 143 | } | 207 | } | ||
| 144 | return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY} | 208 | return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY} | ||
| @@ -172,5 +236,6 @@ func (c *Cursor) free(k kms) { | |||||
| 172 | c.fbs[i] = 0 | 236 | c.fbs[i] = 0 | ||
| 173 | } | 237 | } | ||
| 174 | } | 238 | } | ||
| 175 | - | c.image = false | 239 | + | c.image, c.later = false, nil |
| 240 | + | c.screen, c.flying = 0, false | |||
| 176 | } | 241 | } | ||
M internal/adapters/drm/kms_test.go +1 -0
| @@ -32,6 +32,7 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||
| 32 | 32 | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | |
| 33 | 33 | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | |
| 34 | 34 | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | |
| 35 | + | {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob}, | |
| 35 | 36 | } { | |
| 36 | 37 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| 37 | 38 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |
| @@ -32,6 +32,7 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||||
| 32 | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | 32 | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | ||
| 33 | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | 33 | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | ||
| 34 | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | 34 | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | ||
| 35 | + | {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob}, | |||
| 35 | } { | 36 | } { | ||
| 36 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | 37 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | ||
| 37 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | 38 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | ||
M internal/adapters/drm/output.go +176 -86
| @@ -7,9 +7,11 @@ import ( | |||
| 7 | 7 | "maps" | |
| 8 | 8 | "os" | |
| 9 | 9 | "slices" | |
| 10 | + | "sync/atomic" | |
| 10 | 11 | "time" | |
| 11 | 12 | "unsafe" | |
| 12 | 13 | ||
| 14 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |
| 13 | 15 | "github.com/bnema/nefertty/internal/ports" | |
| 14 | 16 | "github.com/bnema/zerowrap" | |
| 15 | 17 | "golang.org/x/sys/unix" | |
| @@ -46,8 +48,13 @@ type Output struct { | |||
| 46 | 48 | // new frame (else it only changed the cursor or VRR). | |
| 47 | 49 | pending bool | |
| 48 | 50 | pendingFrame pendingFrame | |
| 49 | - | flipStart time.Time | |
| 50 | - | flips int | |
| 51 | + | // serials numbers commits per card, so an event queued before a | |
| 52 | + | // modeset, or for an earlier output on this CRTC, never completes a | |
| 53 | + | // newer commit; pendingSerial is the pending commit's. | |
| 54 | + | serials *atomic.Uint64 | |
| 55 | + | pendingSerial, nextSerial uint64 | |
| 56 | + | flipStart time.Time | |
| 57 | + | flips int | |
| 51 | 58 | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | |
| 52 | 59 | // screen and queued (0: the composed image). | |
| 53 | 60 | scanout bool | |
| @@ -67,9 +74,8 @@ type Output struct { | |||
| 67 | 74 | vrrProp uint32 | |
| 68 | 75 | vrrOn bool | |
| 69 | 76 | composedSince time.Time | |
| 70 | - | // kind is how the images were made; validated once a modeset took them. | |
| 71 | - | kind imageKind | |
| 72 | - | validated bool | |
| 77 | + | // kind is how the images were made. | |
| 78 | + | kind imageKind | |
| 73 | 79 | } | |
| 74 | 80 | ||
| 75 | 81 | // pendingFrame is what the pending frame commit shows. | |
| @@ -80,12 +86,27 @@ type pendingFrame struct { | |||
| 80 | 86 | shows map[ports.WindowID]uint64 | |
| 81 | 87 | } | |
| 82 | 88 | ||
| 83 | - | // Commit event userData. | |
| 89 | + | // Commit event userData: the commit serial above userKindBits, the kind | |
| 90 | + | // of commit below. | |
| 84 | 91 | const ( | |
| 85 | - | userFrame = 1 | |
| 86 | - | userState = 2 | |
| 92 | + | userFrame = 1 | |
| 93 | + | userState = 2 | |
| 94 | + | userKindBits = 2 | |
| 87 | 95 | ) | |
| 88 | 96 | ||
| 97 | + | // userData takes a new serial for the next commit of kind and returns | |
| 98 | + | // what the commit carries. | |
| 99 | + | func (o *Output) userData(kind uint64) uint64 { | |
| 100 | + | o.nextSerial = o.serials.Add(1) | |
| 101 | + | return o.nextSerial<<userKindBits | kind | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // begin marks the commit made with the last userData pending. | |
| 105 | + | func (o *Output) begin(f pendingFrame) { | |
| 106 | + | o.pendingSerial = o.nextSerial | |
| 107 | + | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | |
| 108 | + | } | |
| 109 | + | ||
| 89 | 110 | // CursorLoader returns the image of a cursor at an output scale, at most | |
| 90 | 111 | // limit pixels on a side; an empty image hides the cursor. | |
| 91 | 112 | type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) | |
| @@ -94,7 +115,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||
| 94 | 115 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 95 | 116 | log := card.log | |
| 96 | 117 | pipe := slices.Index(card.crtcs, crtc) | |
| 97 | - | o := &Output{k: card.k, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} | |
| 118 | + | o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} | |
| 98 | 119 | var err error | |
| 99 | 120 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 100 | 121 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| @@ -227,11 +248,72 @@ func (o *Output) modeset() error { | |||
| 227 | 248 | o.pending = false | |
| 228 | 249 | o.pendingFrame = pendingFrame{} | |
| 229 | 250 | o.shown, o.queued = 0, 0 | |
| 230 | - | mode := o.mode | |
| 251 | + | blob, err := o.modeBlobFor(o.mode) | |
| 252 | + | if err != nil { | |
| 253 | + | return err | |
| 254 | + | } | |
| 255 | + | if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil { | |
| 256 | + | _ = o.k.destroyBlob(blob) | |
| 257 | + | return fmt.Errorf("modeset: %w", err) | |
| 258 | + | } | |
| 259 | + | if o.modeBlob != 0 { | |
| 260 | + | _ = o.k.destroyBlob(o.modeBlob) | |
| 261 | + | } | |
| 262 | + | o.modeBlob = blob | |
| 263 | + | o.vrrOn = false | |
| 264 | + | if o.cursor != nil { | |
| 265 | + | o.cursor.applied = cursorState{} | |
| 266 | + | o.cursor.screen, o.cursor.flying = 0, false | |
| 267 | + | } | |
| 268 | + | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |
| 269 | + | o.testCursor() | |
| 270 | + | return nil | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // testModeset asks KMS whether it would take the modeset of the output | |
| 274 | + | // images without applying it. | |
| 275 | + | func (o *Output) testModeset() error { | |
| 276 | + | blob, err := o.modeBlobFor(o.mode) | |
| 277 | + | if err != nil { | |
| 278 | + | return err | |
| 279 | + | } | |
| 280 | + | defer func() { _ = o.k.destroyBlob(blob) }() | |
| 281 | + | if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil { | |
| 282 | + | return fmt.Errorf("modeset test: %w", err) | |
| 283 | + | } | |
| 284 | + | return nil | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) { | |
| 231 | 288 | blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))) | |
| 232 | 289 | if err != nil { | |
| 233 | - | return fmt.Errorf("mode blob: %w", err) | |
| 290 | + | return 0, fmt.Errorf("mode blob: %w", err) | |
| 291 | + | } | |
| 292 | + | return blob, nil | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | // testCursor turns the hardware cursor off when KMS refuses its images on | |
| 296 | + | // the cursor plane (e.g. a pitch the plane cannot scan out). | |
| 297 | + | func (o *Output) testCursor() { | |
| 298 | + | c := o.cursor | |
| 299 | + | if c == nil || c.off || c.fbs[0] == 0 { | |
| 300 | + | return | |
| 234 | 301 | } | |
| 302 | + | req := &atomicReq{} | |
| 303 | + | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) | |
| 304 | + | if err := o.k.commit(req, atomicTestOnly, 0); refused(err) { | |
| 305 | + | o.cursorOff(err) | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | // cursorOff stops using the cursor plane for the rest of the output's life. | |
| 310 | + | func (o *Output) cursorOff(err error) { | |
| 311 | + | o.cursor.off = true | |
| 312 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off") | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // modesetReq is the modeset of the front image with mode blob. | |
| 316 | + | func (o *Output) modesetReq(blob uint32) *atomicReq { | |
| 235 | 317 | req := &atomicReq{} | |
| 236 | 318 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | |
| 237 | 319 | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |
| @@ -250,20 +332,7 @@ func (o *Output) modeset() error { | |||
| 250 | 332 | req.set(p.id, p.prop("FB_ID"), 0) | |
| 251 | 333 | req.set(p.id, p.prop("CRTC_ID"), 0) | |
| 252 | 334 | } | |
| 253 | - | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | |
| 254 | - | _ = o.k.destroyBlob(blob) | |
| 255 | - | return fmt.Errorf("modeset: %w", err) | |
| 256 | - | } | |
| 257 | - | if o.modeBlob != 0 { | |
| 258 | - | _ = o.k.destroyBlob(o.modeBlob) | |
| 259 | - | } | |
| 260 | - | o.modeBlob = blob | |
| 261 | - | o.vrrOn = false | |
| 262 | - | if o.cursor != nil { | |
| 263 | - | o.cursor.applied = cursorState{} | |
| 264 | - | } | |
| 265 | - | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |
| 266 | - | return nil | |
| 335 | + | return req | |
| 267 | 336 | } | |
| 268 | 337 | ||
| 269 | 338 | // probeAsync checks once whether async commits may carry IN_FENCE_FD. | |
| @@ -333,7 +402,10 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||
| 333 | 402 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 334 | 403 | } | |
| 335 | 404 | // The kernel takes its own reference on the fence. | |
| 336 | - | if err := o.k.commit(req, flags, userFrame); err != nil { | |
| 405 | + | if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil { | |
| 406 | + | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 407 | + | return o.commitFrame(fb, fence, false, vrr, f) | |
| 408 | + | } | |
| 337 | 409 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |
| 338 | 410 | // Retry without turning VRR on: if that passes, the driver | |
| 339 | 411 | // refuses VRR on this output. | |
| @@ -350,21 +422,25 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||
| 350 | 422 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |
| 351 | 423 | } | |
| 352 | 424 | o.vrrOn = vrr | |
| 353 | - | if o.cursor != nil { | |
| 354 | - | if cur != o.cursor.applied { | |
| 355 | - | o.cursor.mu.Lock() | |
| 356 | - | o.cursor.commits++ | |
| 357 | - | o.cursor.mu.Unlock() | |
| 358 | - | } | |
| 359 | - | o.cursor.applied = cur | |
| 360 | - | } | |
| 425 | + | } | |
| 426 | + | if o.cursor != nil { | |
| 427 | + | // An async commit leaves the cursor plane as applied. | |
| 428 | + | o.cursor.committed(cur) | |
| 361 | 429 | } | |
| 362 | 430 | f.frame, f.async = true, async | |
| 363 | 431 | o.setAsync(async) | |
| 364 | - | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | |
| 432 | + | o.begin(f) | |
| 365 | 433 | return nil | |
| 366 | 434 | } | |
| 367 | 435 | ||
| 436 | + | // cursorRefused reports, after a commit carrying the cursor failed with | |
| 437 | + | // EINVAL, whether the cursor plane was the cause; then the cursor is off | |
| 438 | + | // and the commit can be retried without it. | |
| 439 | + | func (o *Output) cursorRefused() bool { | |
| 440 | + | o.testCursor() | |
| 441 | + | return o.cursor.off | |
| 442 | + | } | |
| 443 | + | ||
| 368 | 444 | // commitState commits a cursor or VRR change without a new frame. | |
| 369 | 445 | func (o *Output) commitState(vrr bool) error { | |
| 370 | 446 | req := &atomicReq{} | |
| @@ -381,20 +457,20 @@ func (o *Output) commitState(vrr bool) error { | |||
| 381 | 457 | if len(req.objs) == 0 { | |
| 382 | 458 | return nil | |
| 383 | 459 | } | |
| 384 | - | if err := o.k.commit(req, atomicNonblock|flipEventFlag, userState); err != nil { | |
| 460 | + | if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil { | |
| 461 | + | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 462 | + | return o.commitState(vrr) | |
| 463 | + | } | |
| 385 | 464 | return err | |
| 386 | 465 | } | |
| 387 | - | if o.cursor != nil && cur != o.cursor.applied { | |
| 388 | - | o.cursor.applied = cur | |
| 389 | - | o.cursor.mu.Lock() | |
| 390 | - | o.cursor.commits++ | |
| 391 | - | o.cursor.mu.Unlock() | |
| 466 | + | if o.cursor != nil { | |
| 467 | + | o.cursor.committed(cur) | |
| 392 | 468 | } | |
| 393 | 469 | if vrr != o.vrrOn { | |
| 394 | 470 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |
| 395 | 471 | } | |
| 396 | 472 | o.vrrOn = vrr | |
| 397 | - | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | |
| 473 | + | o.begin(pendingFrame{}) | |
| 398 | 474 | return nil | |
| 399 | 475 | } | |
| 400 | 476 | ||
| @@ -477,13 +553,14 @@ func (o *Output) Close() { | |||
| 477 | 553 | req.set(o.crtc, o.vrrProp, 0) | |
| 478 | 554 | s := o.saved | |
| 479 | 555 | var blob uint32 | |
| 556 | + | p := o.primary | |
| 557 | + | restored := false | |
| 480 | 558 | if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 { | |
| 481 | - | mode := s.mode | |
| 482 | - | if b, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))); err == nil { | |
| 483 | - | blob = b | |
| 559 | + | if b, err := o.modeBlobFor(s.mode); err == nil { | |
| 560 | + | blob, restored = b, true | |
| 484 | 561 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b)) | |
| 485 | 562 | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |
| 486 | - | p := o.primary | |
| 563 | + | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | |
| 487 | 564 | req.set(p.id, p.prop("FB_ID"), uint64(s.fbID)) | |
| 488 | 565 | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |
| 489 | 566 | req.set(p.id, p.prop("SRC_X"), 0) | |
| @@ -496,8 +573,18 @@ func (o *Output) Close() { | |||
| 496 | 573 | req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay)) | |
| 497 | 574 | } | |
| 498 | 575 | } | |
| 576 | + | if !restored { | |
| 577 | + | // Nothing to give back: leave the CRTC off. | |
| 578 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 0) | |
| 579 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], 0) | |
| 580 | + | req.set(p.id, p.prop("FB_ID"), 0) | |
| 581 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |
| 582 | + | req.set(o.conn.id, o.connCrtc, 0) | |
| 583 | + | } | |
| 499 | 584 | // Best effort: at exit the card may already belong to another session. | |
| 500 | - | _ = o.k.commit(req, atomicAllowModes, 0) | |
| 585 | + | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | |
| 586 | + | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("restore crtc") | |
| 587 | + | } | |
| 501 | 588 | if blob != 0 { | |
| 502 | 589 | _ = o.k.destroyBlob(blob) | |
| 503 | 590 | } | |
| @@ -523,7 +610,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 523 | 610 | defer r.Close() | |
| 524 | 611 | if o.cursor != nil { | |
| 525 | 612 | if err := o.cursor.setup(o.k, r); err != nil { | |
| 526 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images") | |
| 613 | + | o.cursor.off = true | |
| 614 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") | |
| 527 | 615 | } | |
| 528 | 616 | } | |
| 529 | 617 | // The seat sends its state first; while switched away (e.g. a monitor | |
| @@ -534,16 +622,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 534 | 622 | default: | |
| 535 | 623 | } | |
| 536 | 624 | // Output images, from the best to the linear fallback (ADR 014). | |
| 537 | - | // While switched away they are validated by the first modeset on enable. | |
| 538 | - | if enabled { | |
| 539 | - | err = o.showImages(r, imagesDriver, nil) | |
| 540 | - | } else { | |
| 541 | - | o.kind, err = o.setupImages(r, imagesDriver, nil) | |
| 542 | - | } | |
| 543 | - | if err != nil { | |
| 544 | - | return err | |
| 545 | - | } | |
| 625 | + | // While switched away they are made on the first enable. | |
| 546 | 626 | if enabled { | |
| 627 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 628 | + | return err | |
| 629 | + | } | |
| 547 | 630 | o.probeAsync(r) | |
| 548 | 631 | } | |
| 549 | 632 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | |
| @@ -592,22 +675,21 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 592 | 675 | case on := <-active: | |
| 593 | 676 | // Modeset on every enable: a fast disable+enable can coalesce to one true. | |
| 594 | 677 | if on { | |
| 595 | - | err := o.modeset() | |
| 678 | + | var err error | |
| 679 | + | if o.fbs[0] == 0 { | |
| 680 | + | err = o.showImages(r, imagesDriver, nil) | |
| 681 | + | } else { | |
| 682 | + | err = o.modeset() | |
| 683 | + | } | |
| 596 | 684 | if lostMaster(err) { | |
| 597 | 685 | // The seat took DRM master back: wait for the next enable. | |
| 598 | 686 | o.log.Warn().Err(err).Msg("resume") | |
| 599 | 687 | continue | |
| 600 | 688 | } | |
| 601 | - | if refused(err) && !o.validated && o.kind < imagesLinear { | |
| 602 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images") | |
| 603 | - | o.freeImages() | |
| 604 | - | err = o.showImages(r, o.kind+1, err) | |
| 605 | - | } | |
| 606 | 689 | if err != nil { | |
| 607 | 690 | o.log.Error().Err(err).Msg("resume") | |
| 608 | 691 | return err | |
| 609 | 692 | } | |
| 610 | - | o.validated = true | |
| 611 | 693 | o.probeAsync(r) | |
| 612 | 694 | dirty = haveScene | |
| 613 | 695 | stateDirty = true | |
| @@ -615,12 +697,16 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 615 | 697 | enabled = on | |
| 616 | 698 | o.log.Info().Bool("enabled", on).Msg("output") | |
| 617 | 699 | case ev := <-o.flipped: | |
| 618 | - | if !o.pending { | |
| 700 | + | if !o.completed(ev, seen) { | |
| 619 | 701 | continue // stale event from before a modeset | |
| 620 | 702 | } | |
| 621 | - | o.completed(ev, seen) | |
| 622 | - | reportDirty = false | |
| 623 | 703 | stateDirty = true | |
| 704 | + | if o.cursor != nil { | |
| 705 | + | // An image that waited for a free slot loads now. | |
| 706 | + | if _, err := o.cursor.flushLater(r); err != nil { | |
| 707 | + | o.log.Warn().Err(err).Msg("cursor") | |
| 708 | + | } | |
| 709 | + | } | |
| 624 | 710 | case s := <-scenes: | |
| 625 | 711 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | |
| 626 | 712 | cursorScale = s.Scale | |
| @@ -707,7 +793,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 707 | 793 | func (o *Output) commitFailed(err error, enabled *bool) bool { | |
| 708 | 794 | switch { | |
| 709 | 795 | case errors.Is(err, unix.EBUSY): | |
| 796 | + | // A commit of ours is in flight (e.g. across a VT switch): wait | |
| 797 | + | // for any event of this CRTC. | |
| 710 | 798 | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | |
| 799 | + | o.pendingSerial = 0 | |
| 711 | 800 | return true | |
| 712 | 801 | case lostMaster(err): | |
| 713 | 802 | o.log.Warn().Err(err).Msg("commit refused") | |
| @@ -719,15 +808,22 @@ func (o *Output) commitFailed(err error, enabled *bool) bool { | |||
| 719 | 808 | } | |
| 720 | 809 | ||
| 721 | 810 | // completed handles the event of the pending commit: a flipped frame is | |
| 722 | - | // reported with its kernel timestamp. | |
| 723 | - | func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) { | |
| 811 | + | // reported with its kernel timestamp. It reports false for an event of | |
| 812 | + | // another commit, which is dropped. | |
| 813 | + | func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |
| 814 | + | if !o.pending || o.pendingSerial != 0 && ev.user>>userKindBits != o.pendingSerial { | |
| 815 | + | return false | |
| 816 | + | } | |
| 724 | 817 | f := o.pendingFrame | |
| 725 | 818 | o.pending, o.pendingFrame = false, pendingFrame{} | |
| 726 | 819 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| 727 | 820 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | |
| 728 | 821 | } | |
| 822 | + | if o.cursor != nil { | |
| 823 | + | o.cursor.landed() | |
| 824 | + | } | |
| 729 | 825 | if !f.frame { | |
| 730 | - | return | |
| 826 | + | return true | |
| 731 | 827 | } | |
| 732 | 828 | o.flips++ | |
| 733 | 829 | o.shown = f.queued | |
| @@ -739,10 +835,14 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) { | |||
| 739 | 835 | refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli()))) | |
| 740 | 836 | } | |
| 741 | 837 | o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen) | |
| 838 | + | return true | |
| 742 | 839 | } | |
| 743 | 840 | ||
| 744 | 841 | // setCursor loads a cursor for scale into the cursor images. | |
| 745 | 842 | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) { | |
| 843 | + | if o.cursor.off { | |
| 844 | + | return | |
| 845 | + | } | |
| 746 | 846 | img, err := load(c, scale, o.cursor.Limit()) | |
| 747 | 847 | if err == nil { | |
| 748 | 848 | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) | |
| @@ -807,7 +907,8 @@ const ( | |||
| 807 | 907 | ) | |
| 808 | 908 | ||
| 809 | 909 | // showImages sets up images from kind on and modesets them. KMS may refuse | |
| 810 | - | // an image only at modeset: then the next kind is tried. cause is why the | |
| 910 | + | // an image only in a modeset: a TEST_ONLY modeset checks it first, and the | |
| 911 | + | // next kind is tried on refusal. cause is why the | |
| 811 | 912 | // previous kind failed, for the log. | |
| 812 | 913 | func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error { | |
| 813 | 914 | for { | |
| @@ -816,11 +917,11 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error | |||
| 816 | 917 | return err | |
| 817 | 918 | } | |
| 818 | 919 | o.kind = got | |
| 819 | - | if err = o.modeset(); err == nil { | |
| 820 | - | o.validated = true | |
| 821 | - | return nil | |
| 920 | + | if err = o.testModeset(); err == nil { | |
| 921 | + | return o.modeset() | |
| 822 | 922 | } | |
| 823 | 923 | if !refused(err) || got == imagesLinear { | |
| 924 | + | o.freeImages() | |
| 824 | 925 | return err | |
| 825 | 926 | } | |
| 826 | 927 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | |
| @@ -879,7 +980,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||
| 879 | 980 | var done *os.File | |
| 880 | 981 | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | |
| 881 | 982 | // The modeset is a blocking commit: wait for the clear. | |
| 882 | - | err = errors.Join(err, waitFence(done)) | |
| 983 | + | err = errors.Join(err, syncfile.Wait(context.Background(), done)) | |
| 883 | 984 | done.Close() | |
| 884 | 985 | } | |
| 885 | 986 | } | |
| @@ -892,17 +993,6 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||
| 892 | 993 | return nil | |
| 893 | 994 | } | |
| 894 | 995 | ||
| 895 | - | // waitFence blocks until a sync file signals (setup only, never per frame). | |
| 896 | - | func waitFence(f *os.File) error { | |
| 897 | - | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |
| 898 | - | for { | |
| 899 | - | _, err := unix.Poll(fds, -1) | |
| 900 | - | if !errors.Is(err, unix.EINTR) { | |
| 901 | - | return err | |
| 902 | - | } | |
| 903 | - | } | |
| 904 | - | } | |
| 905 | - | ||
| 906 | 996 | // freeImages removes the output images' framebuffers. | |
| 907 | 997 | func (o *Output) freeImages() { | |
| 908 | 998 | for i, fb := range o.fbs { | |
| @@ -7,9 +7,11 @@ import ( | |||||
| 7 | "maps" | 7 | "maps" | ||
| 8 | "os" | 8 | "os" | ||
| 9 | "slices" | 9 | "slices" | ||
| 10 | + | "sync/atomic" | |||
| 10 | "time" | 11 | "time" | ||
| 11 | "unsafe" | 12 | "unsafe" | ||
| 12 | 13 | ||||
| 14 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |||
| 13 | "github.com/bnema/nefertty/internal/ports" | 15 | "github.com/bnema/nefertty/internal/ports" | ||
| 14 | "github.com/bnema/zerowrap" | 16 | "github.com/bnema/zerowrap" | ||
| 15 | "golang.org/x/sys/unix" | 17 | "golang.org/x/sys/unix" | ||
| @@ -46,8 +48,13 @@ type Output struct { | |||||
| 46 | // new frame (else it only changed the cursor or VRR). | 48 | // new frame (else it only changed the cursor or VRR). | ||
| 47 | pending bool | 49 | pending bool | ||
| 48 | pendingFrame pendingFrame | 50 | pendingFrame pendingFrame | ||
| 49 | - | flipStart time.Time | 51 | + | // serials numbers commits per card, so an event queued before a |
| 50 | - | flips int | 52 | + | // modeset, or for an earlier output on this CRTC, never completes a |
| 53 | + | // newer commit; pendingSerial is the pending commit's. | |||
| 54 | + | serials *atomic.Uint64 | |||
| 55 | + | pendingSerial, nextSerial uint64 | |||
| 56 | + | flipStart time.Time | |||
| 57 | + | flips int | |||
| 51 | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | 58 | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | ||
| 52 | // screen and queued (0: the composed image). | 59 | // screen and queued (0: the composed image). | ||
| 53 | scanout bool | 60 | scanout bool | ||
| @@ -67,9 +74,8 @@ type Output struct { | |||||
| 67 | vrrProp uint32 | 74 | vrrProp uint32 | ||
| 68 | vrrOn bool | 75 | vrrOn bool | ||
| 69 | composedSince time.Time | 76 | composedSince time.Time | ||
| 70 | - | // kind is how the images were made; validated once a modeset took them. | 77 | + | // kind is how the images were made. |
| 71 | - | kind imageKind | 78 | + | kind imageKind |
| 72 | - | validated bool | |||
| 73 | } | 79 | } | ||
| 74 | 80 | ||||
| 75 | // pendingFrame is what the pending frame commit shows. | 81 | // pendingFrame is what the pending frame commit shows. | ||
| @@ -80,12 +86,27 @@ type pendingFrame struct { | |||||
| 80 | shows map[ports.WindowID]uint64 | 86 | shows map[ports.WindowID]uint64 | ||
| 81 | } | 87 | } | ||
| 82 | 88 | ||||
| 83 | - | // Commit event userData. | 89 | + | // Commit event userData: the commit serial above userKindBits, the kind |
| 90 | + | // of commit below. | |||
| 84 | const ( | 91 | const ( | ||
| 85 | - | userFrame = 1 | 92 | + | userFrame = 1 |
| 86 | - | userState = 2 | 93 | + | userState = 2 |
| 94 | + | userKindBits = 2 | |||
| 87 | ) | 95 | ) | ||
| 88 | 96 | ||||
| 97 | + | // userData takes a new serial for the next commit of kind and returns | |||
| 98 | + | // what the commit carries. | |||
| 99 | + | func (o *Output) userData(kind uint64) uint64 { | |||
| 100 | + | o.nextSerial = o.serials.Add(1) | |||
| 101 | + | return o.nextSerial<<userKindBits | kind | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // begin marks the commit made with the last userData pending. | |||
| 105 | + | func (o *Output) begin(f pendingFrame) { | |||
| 106 | + | o.pendingSerial = o.nextSerial | |||
| 107 | + | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | |||
| 108 | + | } | |||
| 109 | + | ||||
| 89 | // CursorLoader returns the image of a cursor at an output scale, at most | 110 | // CursorLoader returns the image of a cursor at an output scale, at most | ||
| 90 | // limit pixels on a side; an empty image hides the cursor. | 111 | // limit pixels on a side; an empty image hides the cursor. | ||
| 91 | type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) | 112 | type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) | ||
| @@ -94,7 +115,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||||
| 94 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 115 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 95 | log := card.log | 116 | log := card.log | ||
| 96 | pipe := slices.Index(card.crtcs, crtc) | 117 | pipe := slices.Index(card.crtcs, crtc) | ||
| 97 | - | o := &Output{k: card.k, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} | 118 | + | o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} |
| 98 | var err error | 119 | var err error | ||
| 99 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 120 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 100 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 121 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| @@ -227,11 +248,72 @@ func (o *Output) modeset() error { | |||||
| 227 | o.pending = false | 248 | o.pending = false | ||
| 228 | o.pendingFrame = pendingFrame{} | 249 | o.pendingFrame = pendingFrame{} | ||
| 229 | o.shown, o.queued = 0, 0 | 250 | o.shown, o.queued = 0, 0 | ||
| 230 | - | mode := o.mode | 251 | + | blob, err := o.modeBlobFor(o.mode) |
| 252 | + | if err != nil { | |||
| 253 | + | return err | |||
| 254 | + | } | |||
| 255 | + | if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil { | |||
| 256 | + | _ = o.k.destroyBlob(blob) | |||
| 257 | + | return fmt.Errorf("modeset: %w", err) | |||
| 258 | + | } | |||
| 259 | + | if o.modeBlob != 0 { | |||
| 260 | + | _ = o.k.destroyBlob(o.modeBlob) | |||
| 261 | + | } | |||
| 262 | + | o.modeBlob = blob | |||
| 263 | + | o.vrrOn = false | |||
| 264 | + | if o.cursor != nil { | |||
| 265 | + | o.cursor.applied = cursorState{} | |||
| 266 | + | o.cursor.screen, o.cursor.flying = 0, false | |||
| 267 | + | } | |||
| 268 | + | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |||
| 269 | + | o.testCursor() | |||
| 270 | + | return nil | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | // testModeset asks KMS whether it would take the modeset of the output | |||
| 274 | + | // images without applying it. | |||
| 275 | + | func (o *Output) testModeset() error { | |||
| 276 | + | blob, err := o.modeBlobFor(o.mode) | |||
| 277 | + | if err != nil { | |||
| 278 | + | return err | |||
| 279 | + | } | |||
| 280 | + | defer func() { _ = o.k.destroyBlob(blob) }() | |||
| 281 | + | if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil { | |||
| 282 | + | return fmt.Errorf("modeset test: %w", err) | |||
| 283 | + | } | |||
| 284 | + | return nil | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) { | |||
| 231 | blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))) | 288 | blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))) | ||
| 232 | if err != nil { | 289 | if err != nil { | ||
| 233 | - | return fmt.Errorf("mode blob: %w", err) | 290 | + | return 0, fmt.Errorf("mode blob: %w", err) |
| 291 | + | } | |||
| 292 | + | return blob, nil | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | // testCursor turns the hardware cursor off when KMS refuses its images on | |||
| 296 | + | // the cursor plane (e.g. a pitch the plane cannot scan out). | |||
| 297 | + | func (o *Output) testCursor() { | |||
| 298 | + | c := o.cursor | |||
| 299 | + | if c == nil || c.off || c.fbs[0] == 0 { | |||
| 300 | + | return | |||
| 234 | } | 301 | } | ||
| 302 | + | req := &atomicReq{} | |||
| 303 | + | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) | |||
| 304 | + | if err := o.k.commit(req, atomicTestOnly, 0); refused(err) { | |||
| 305 | + | o.cursorOff(err) | |||
| 306 | + | } | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | // cursorOff stops using the cursor plane for the rest of the output's life. | |||
| 310 | + | func (o *Output) cursorOff(err error) { | |||
| 311 | + | o.cursor.off = true | |||
| 312 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off") | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | // modesetReq is the modeset of the front image with mode blob. | |||
| 316 | + | func (o *Output) modesetReq(blob uint32) *atomicReq { | |||
| 235 | req := &atomicReq{} | 317 | req := &atomicReq{} | ||
| 236 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | 318 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | ||
| 237 | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | 319 | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | ||
| @@ -250,20 +332,7 @@ func (o *Output) modeset() error { | |||||
| 250 | req.set(p.id, p.prop("FB_ID"), 0) | 332 | req.set(p.id, p.prop("FB_ID"), 0) | ||
| 251 | req.set(p.id, p.prop("CRTC_ID"), 0) | 333 | req.set(p.id, p.prop("CRTC_ID"), 0) | ||
| 252 | } | 334 | } | ||
| 253 | - | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | 335 | + | return req |
| 254 | - | _ = o.k.destroyBlob(blob) | |||
| 255 | - | return fmt.Errorf("modeset: %w", err) | |||
| 256 | - | } | |||
| 257 | - | if o.modeBlob != 0 { | |||
| 258 | - | _ = o.k.destroyBlob(o.modeBlob) | |||
| 259 | - | } | |||
| 260 | - | o.modeBlob = blob | |||
| 261 | - | o.vrrOn = false | |||
| 262 | - | if o.cursor != nil { | |||
| 263 | - | o.cursor.applied = cursorState{} | |||
| 264 | - | } | |||
| 265 | - | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |||
| 266 | - | return nil | |||
| 267 | } | 336 | } | ||
| 268 | 337 | ||||
| 269 | // probeAsync checks once whether async commits may carry IN_FENCE_FD. | 338 | // probeAsync checks once whether async commits may carry IN_FENCE_FD. | ||
| @@ -333,7 +402,10 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||||
| 333 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | 402 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | ||
| 334 | } | 403 | } | ||
| 335 | // The kernel takes its own reference on the fence. | 404 | // The kernel takes its own reference on the fence. | ||
| 336 | - | if err := o.k.commit(req, flags, userFrame); err != nil { | 405 | + | if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil { |
| 406 | + | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |||
| 407 | + | return o.commitFrame(fb, fence, false, vrr, f) | |||
| 408 | + | } | |||
| 337 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | 409 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | ||
| 338 | // Retry without turning VRR on: if that passes, the driver | 410 | // Retry without turning VRR on: if that passes, the driver | ||
| 339 | // refuses VRR on this output. | 411 | // refuses VRR on this output. | ||
| @@ -350,21 +422,25 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||||
| 350 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | 422 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | ||
| 351 | } | 423 | } | ||
| 352 | o.vrrOn = vrr | 424 | o.vrrOn = vrr | ||
| 353 | - | if o.cursor != nil { | 425 | + | } |
| 354 | - | if cur != o.cursor.applied { | 426 | + | if o.cursor != nil { |
| 355 | - | o.cursor.mu.Lock() | 427 | + | // An async commit leaves the cursor plane as applied. |
| 356 | - | o.cursor.commits++ | 428 | + | o.cursor.committed(cur) |
| 357 | - | o.cursor.mu.Unlock() | |||
| 358 | - | } | |||
| 359 | - | o.cursor.applied = cur | |||
| 360 | - | } | |||
| 361 | } | 429 | } | ||
| 362 | f.frame, f.async = true, async | 430 | f.frame, f.async = true, async | ||
| 363 | o.setAsync(async) | 431 | o.setAsync(async) | ||
| 364 | - | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | 432 | + | o.begin(f) |
| 365 | return nil | 433 | return nil | ||
| 366 | } | 434 | } | ||
| 367 | 435 | ||||
| 436 | + | // cursorRefused reports, after a commit carrying the cursor failed with | |||
| 437 | + | // EINVAL, whether the cursor plane was the cause; then the cursor is off | |||
| 438 | + | // and the commit can be retried without it. | |||
| 439 | + | func (o *Output) cursorRefused() bool { | |||
| 440 | + | o.testCursor() | |||
| 441 | + | return o.cursor.off | |||
| 442 | + | } | |||
| 443 | + | ||||
| 368 | // commitState commits a cursor or VRR change without a new frame. | 444 | // commitState commits a cursor or VRR change without a new frame. | ||
| 369 | func (o *Output) commitState(vrr bool) error { | 445 | func (o *Output) commitState(vrr bool) error { | ||
| 370 | req := &atomicReq{} | 446 | req := &atomicReq{} | ||
| @@ -381,20 +457,20 @@ func (o *Output) commitState(vrr bool) error { | |||||
| 381 | if len(req.objs) == 0 { | 457 | if len(req.objs) == 0 { | ||
| 382 | return nil | 458 | return nil | ||
| 383 | } | 459 | } | ||
| 384 | - | if err := o.k.commit(req, atomicNonblock|flipEventFlag, userState); err != nil { | 460 | + | if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil { |
| 461 | + | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |||
| 462 | + | return o.commitState(vrr) | |||
| 463 | + | } | |||
| 385 | return err | 464 | return err | ||
| 386 | } | 465 | } | ||
| 387 | - | if o.cursor != nil && cur != o.cursor.applied { | 466 | + | if o.cursor != nil { |
| 388 | - | o.cursor.applied = cur | 467 | + | o.cursor.committed(cur) |
| 389 | - | o.cursor.mu.Lock() | |||
| 390 | - | o.cursor.commits++ | |||
| 391 | - | o.cursor.mu.Unlock() | |||
| 392 | } | 468 | } | ||
| 393 | if vrr != o.vrrOn { | 469 | if vrr != o.vrrOn { | ||
| 394 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | 470 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | ||
| 395 | } | 471 | } | ||
| 396 | o.vrrOn = vrr | 472 | o.vrrOn = vrr | ||
| 397 | - | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | 473 | + | o.begin(pendingFrame{}) |
| 398 | return nil | 474 | return nil | ||
| 399 | } | 475 | } | ||
| 400 | 476 | ||||
| @@ -477,13 +553,14 @@ func (o *Output) Close() { | |||||
| 477 | req.set(o.crtc, o.vrrProp, 0) | 553 | req.set(o.crtc, o.vrrProp, 0) | ||
| 478 | s := o.saved | 554 | s := o.saved | ||
| 479 | var blob uint32 | 555 | var blob uint32 | ||
| 556 | + | p := o.primary | |||
| 557 | + | restored := false | |||
| 480 | if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 { | 558 | if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 { | ||
| 481 | - | mode := s.mode | 559 | + | if b, err := o.modeBlobFor(s.mode); err == nil { |
| 482 | - | if b, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))); err == nil { | 560 | + | blob, restored = b, true |
| 483 | - | blob = b | |||
| 484 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b)) | 561 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b)) | ||
| 485 | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | 562 | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | ||
| 486 | - | p := o.primary | 563 | + | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) |
| 487 | req.set(p.id, p.prop("FB_ID"), uint64(s.fbID)) | 564 | req.set(p.id, p.prop("FB_ID"), uint64(s.fbID)) | ||
| 488 | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | 565 | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | ||
| 489 | req.set(p.id, p.prop("SRC_X"), 0) | 566 | req.set(p.id, p.prop("SRC_X"), 0) | ||
| @@ -496,8 +573,18 @@ func (o *Output) Close() { | |||||
| 496 | req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay)) | 573 | req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay)) | ||
| 497 | } | 574 | } | ||
| 498 | } | 575 | } | ||
| 576 | + | if !restored { | |||
| 577 | + | // Nothing to give back: leave the CRTC off. | |||
| 578 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 0) | |||
| 579 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], 0) | |||
| 580 | + | req.set(p.id, p.prop("FB_ID"), 0) | |||
| 581 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |||
| 582 | + | req.set(o.conn.id, o.connCrtc, 0) | |||
| 583 | + | } | |||
| 499 | // Best effort: at exit the card may already belong to another session. | 584 | // Best effort: at exit the card may already belong to another session. | ||
| 500 | - | _ = o.k.commit(req, atomicAllowModes, 0) | 585 | + | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { |
| 586 | + | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("restore crtc") | |||
| 587 | + | } | |||
| 501 | if blob != 0 { | 588 | if blob != 0 { | ||
| 502 | _ = o.k.destroyBlob(blob) | 589 | _ = o.k.destroyBlob(blob) | ||
| 503 | } | 590 | } | ||
| @@ -523,7 +610,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 523 | defer r.Close() | 610 | defer r.Close() | ||
| 524 | if o.cursor != nil { | 611 | if o.cursor != nil { | ||
| 525 | if err := o.cursor.setup(o.k, r); err != nil { | 612 | if err := o.cursor.setup(o.k, r); err != nil { | ||
| 526 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images") | 613 | + | o.cursor.off = true |
| 614 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") | |||
| 527 | } | 615 | } | ||
| 528 | } | 616 | } | ||
| 529 | // The seat sends its state first; while switched away (e.g. a monitor | 617 | // The seat sends its state first; while switched away (e.g. a monitor | ||
| @@ -534,16 +622,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 534 | default: | 622 | default: | ||
| 535 | } | 623 | } | ||
| 536 | // Output images, from the best to the linear fallback (ADR 014). | 624 | // Output images, from the best to the linear fallback (ADR 014). | ||
| 537 | - | // While switched away they are validated by the first modeset on enable. | 625 | + | // While switched away they are made on the first enable. |
| 538 | - | if enabled { | |||
| 539 | - | err = o.showImages(r, imagesDriver, nil) | |||
| 540 | - | } else { | |||
| 541 | - | o.kind, err = o.setupImages(r, imagesDriver, nil) | |||
| 542 | - | } | |||
| 543 | - | if err != nil { | |||
| 544 | - | return err | |||
| 545 | - | } | |||
| 546 | if enabled { | 626 | if enabled { | ||
| 627 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 628 | + | return err | |||
| 629 | + | } | |||
| 547 | o.probeAsync(r) | 630 | o.probeAsync(r) | ||
| 548 | } | 631 | } | ||
| 549 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | 632 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | ||
| @@ -592,22 +675,21 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 592 | case on := <-active: | 675 | case on := <-active: | ||
| 593 | // Modeset on every enable: a fast disable+enable can coalesce to one true. | 676 | // Modeset on every enable: a fast disable+enable can coalesce to one true. | ||
| 594 | if on { | 677 | if on { | ||
| 595 | - | err := o.modeset() | 678 | + | var err error |
| 679 | + | if o.fbs[0] == 0 { | |||
| 680 | + | err = o.showImages(r, imagesDriver, nil) | |||
| 681 | + | } else { | |||
| 682 | + | err = o.modeset() | |||
| 683 | + | } | |||
| 596 | if lostMaster(err) { | 684 | if lostMaster(err) { | ||
| 597 | // The seat took DRM master back: wait for the next enable. | 685 | // The seat took DRM master back: wait for the next enable. | ||
| 598 | o.log.Warn().Err(err).Msg("resume") | 686 | o.log.Warn().Err(err).Msg("resume") | ||
| 599 | continue | 687 | continue | ||
| 600 | } | 688 | } | ||
| 601 | - | if refused(err) && !o.validated && o.kind < imagesLinear { | |||
| 602 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images") | |||
| 603 | - | o.freeImages() | |||
| 604 | - | err = o.showImages(r, o.kind+1, err) | |||
| 605 | - | } | |||
| 606 | if err != nil { | 689 | if err != nil { | ||
| 607 | o.log.Error().Err(err).Msg("resume") | 690 | o.log.Error().Err(err).Msg("resume") | ||
| 608 | return err | 691 | return err | ||
| 609 | } | 692 | } | ||
| 610 | - | o.validated = true | |||
| 611 | o.probeAsync(r) | 693 | o.probeAsync(r) | ||
| 612 | dirty = haveScene | 694 | dirty = haveScene | ||
| 613 | stateDirty = true | 695 | stateDirty = true | ||
| @@ -615,12 +697,16 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 615 | enabled = on | 697 | enabled = on | ||
| 616 | o.log.Info().Bool("enabled", on).Msg("output") | 698 | o.log.Info().Bool("enabled", on).Msg("output") | ||
| 617 | case ev := <-o.flipped: | 699 | case ev := <-o.flipped: | ||
| 618 | - | if !o.pending { | 700 | + | if !o.completed(ev, seen) { |
| 619 | continue // stale event from before a modeset | 701 | continue // stale event from before a modeset | ||
| 620 | } | 702 | } | ||
| 621 | - | o.completed(ev, seen) | |||
| 622 | - | reportDirty = false | |||
| 623 | stateDirty = true | 703 | stateDirty = true | ||
| 704 | + | if o.cursor != nil { | |||
| 705 | + | // An image that waited for a free slot loads now. | |||
| 706 | + | if _, err := o.cursor.flushLater(r); err != nil { | |||
| 707 | + | o.log.Warn().Err(err).Msg("cursor") | |||
| 708 | + | } | |||
| 709 | + | } | |||
| 624 | case s := <-scenes: | 710 | case s := <-scenes: | ||
| 625 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | 711 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | ||
| 626 | cursorScale = s.Scale | 712 | cursorScale = s.Scale | ||
| @@ -707,7 +793,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 707 | func (o *Output) commitFailed(err error, enabled *bool) bool { | 793 | func (o *Output) commitFailed(err error, enabled *bool) bool { | ||
| 708 | switch { | 794 | switch { | ||
| 709 | case errors.Is(err, unix.EBUSY): | 795 | case errors.Is(err, unix.EBUSY): | ||
| 796 | + | // A commit of ours is in flight (e.g. across a VT switch): wait | |||
| 797 | + | // for any event of this CRTC. | |||
| 710 | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | 798 | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | ||
| 799 | + | o.pendingSerial = 0 | |||
| 711 | return true | 800 | return true | ||
| 712 | case lostMaster(err): | 801 | case lostMaster(err): | ||
| 713 | o.log.Warn().Err(err).Msg("commit refused") | 802 | o.log.Warn().Err(err).Msg("commit refused") | ||
| @@ -719,15 +808,22 @@ func (o *Output) commitFailed(err error, enabled *bool) bool { | |||||
| 719 | } | 808 | } | ||
| 720 | 809 | ||||
| 721 | // completed handles the event of the pending commit: a flipped frame is | 810 | // completed handles the event of the pending commit: a flipped frame is | ||
| 722 | - | // reported with its kernel timestamp. | 811 | + | // reported with its kernel timestamp. It reports false for an event of |
| 723 | - | func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) { | 812 | + | // another commit, which is dropped. |
| 813 | + | func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |||
| 814 | + | if !o.pending || o.pendingSerial != 0 && ev.user>>userKindBits != o.pendingSerial { | |||
| 815 | + | return false | |||
| 816 | + | } | |||
| 724 | f := o.pendingFrame | 817 | f := o.pendingFrame | ||
| 725 | o.pending, o.pendingFrame = false, pendingFrame{} | 818 | o.pending, o.pendingFrame = false, pendingFrame{} | ||
| 726 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 819 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | ||
| 727 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | 820 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | ||
| 728 | } | 821 | } | ||
| 822 | + | if o.cursor != nil { | |||
| 823 | + | o.cursor.landed() | |||
| 824 | + | } | |||
| 729 | if !f.frame { | 825 | if !f.frame { | ||
| 730 | - | return | 826 | + | return true |
| 731 | } | 827 | } | ||
| 732 | o.flips++ | 828 | o.flips++ | ||
| 733 | o.shown = f.queued | 829 | o.shown = f.queued | ||
| @@ -739,10 +835,14 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) { | |||||
| 739 | refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli()))) | 835 | refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli()))) | ||
| 740 | } | 836 | } | ||
| 741 | o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen) | 837 | o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen) | ||
| 838 | + | return true | |||
| 742 | } | 839 | } | ||
| 743 | 840 | ||||
| 744 | // setCursor loads a cursor for scale into the cursor images. | 841 | // setCursor loads a cursor for scale into the cursor images. | ||
| 745 | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) { | 842 | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) { | ||
| 843 | + | if o.cursor.off { | |||
| 844 | + | return | |||
| 845 | + | } | |||
| 746 | img, err := load(c, scale, o.cursor.Limit()) | 846 | img, err := load(c, scale, o.cursor.Limit()) | ||
| 747 | if err == nil { | 847 | if err == nil { | ||
| 748 | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) | 848 | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) | ||
| @@ -807,7 +907,8 @@ const ( | |||||
| 807 | ) | 907 | ) | ||
| 808 | 908 | ||||
| 809 | // showImages sets up images from kind on and modesets them. KMS may refuse | 909 | // showImages sets up images from kind on and modesets them. KMS may refuse | ||
| 810 | - | // an image only at modeset: then the next kind is tried. cause is why the | 910 | + | // an image only in a modeset: a TEST_ONLY modeset checks it first, and the |
| 911 | + | // next kind is tried on refusal. cause is why the | |||
| 811 | // previous kind failed, for the log. | 912 | // previous kind failed, for the log. | ||
| 812 | func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error { | 913 | func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error { | ||
| 813 | for { | 914 | for { | ||
| @@ -816,11 +917,11 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error | |||||
| 816 | return err | 917 | return err | ||
| 817 | } | 918 | } | ||
| 818 | o.kind = got | 919 | o.kind = got | ||
| 819 | - | if err = o.modeset(); err == nil { | 920 | + | if err = o.testModeset(); err == nil { |
| 820 | - | o.validated = true | 921 | + | return o.modeset() |
| 821 | - | return nil | |||
| 822 | } | 922 | } | ||
| 823 | if !refused(err) || got == imagesLinear { | 923 | if !refused(err) || got == imagesLinear { | ||
| 924 | + | o.freeImages() | |||
| 824 | return err | 925 | return err | ||
| 825 | } | 926 | } | ||
| 826 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | 927 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | ||
| @@ -879,7 +980,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 879 | var done *os.File | 980 | var done *os.File | ||
| 880 | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | 981 | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | ||
| 881 | // The modeset is a blocking commit: wait for the clear. | 982 | // The modeset is a blocking commit: wait for the clear. | ||
| 882 | - | err = errors.Join(err, waitFence(done)) | 983 | + | err = errors.Join(err, syncfile.Wait(context.Background(), done)) |
| 883 | done.Close() | 984 | done.Close() | ||
| 884 | } | 985 | } | ||
| 885 | } | 986 | } | ||
| @@ -892,17 +993,6 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 892 | return nil | 993 | return nil | ||
| 893 | } | 994 | } | ||
| 894 | 995 | ||||
| 895 | - | // waitFence blocks until a sync file signals (setup only, never per frame). | |||
| 896 | - | func waitFence(f *os.File) error { | |||
| 897 | - | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |||
| 898 | - | for { | |||
| 899 | - | _, err := unix.Poll(fds, -1) | |||
| 900 | - | if !errors.Is(err, unix.EINTR) { | |||
| 901 | - | return err | |||
| 902 | - | } | |||
| 903 | - | } | |||
| 904 | - | } | |||
| 905 | - | ||||
| 906 | // freeImages removes the output images' framebuffers. | 996 | // freeImages removes the output images' framebuffers. | ||
| 907 | func (o *Output) freeImages() { | 997 | func (o *Output) freeImages() { | ||
| 908 | for i, fb := range o.fbs { | 998 | for i, fb := range o.fbs { | ||
M internal/adapters/drm/output_test.go +27 -14
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "os" | |
| 6 | 6 | "sync" | |
| 7 | + | "sync/atomic" | |
| 7 | 8 | "testing" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| @@ -23,9 +24,6 @@ const ( | |||
| 23 | 24 | ||
| 24 | 25 | var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17} | |
| 25 | 26 | ||
| 26 | - | // commitMu guards recorded commits (Run commits on its goroutine). | |
| 27 | - | var commitMu sync.Mutex | |
| 28 | - | ||
| 29 | 27 | // commitRec is one recorded atomic commit. | |
| 30 | 28 | type commitRec struct { | |
| 31 | 29 | req atomicReq | |
| @@ -36,8 +34,16 @@ type commitRec struct { | |||
| 36 | 34 | // testOutput returns an output on a mocked kms that records commits; | |
| 37 | 35 | // commit returns the next error of errs (nil when empty). | |
| 38 | 36 | func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |
| 37 | + | o, k, commits, _ := testOutputMu(t, errs...) | |
| 38 | + | return o, k, commits | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // testOutputMu is testOutput with the mutex that guards the recorded | |
| 42 | + | // commits (Run commits on its goroutine). | |
| 43 | + | func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) { | |
| 39 | 44 | k := newMockkms(t) | |
| 40 | 45 | var commits []commitRec | |
| 46 | + | commitMu := &sync.Mutex{} | |
| 41 | 47 | k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error { | |
| 42 | 48 | commitMu.Lock() | |
| 43 | 49 | defer commitMu.Unlock() | |
| @@ -57,12 +63,15 @@ func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |||
| 57 | 63 | primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps} | |
| 58 | 64 | cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64) | |
| 59 | 65 | cur.fbs = [2]uint32{90, 91} | |
| 60 | - | o := &Output{k: k, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | |
| 66 | + | o := &Output{k: k, serials: &atomic.Uint64{}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | |
| 61 | 67 | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | |
| 62 | 68 | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | |
| 63 | - | return o, k, &commits | |
| 69 | + | return o, k, &commits, commitMu | |
| 64 | 70 | } | |
| 65 | 71 | ||
| 72 | + | // eventOf is the flip event of commit c. | |
| 73 | + | func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} } | |
| 74 | + | ||
| 66 | 75 | func objCount(c commitRec) int { return len(c.req.objs) } | |
| 67 | 76 | ||
| 68 | 77 | func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | |
| @@ -79,7 +88,7 @@ func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | |||
| 79 | 88 | t.Fatal(err) | |
| 80 | 89 | } | |
| 81 | 90 | c := (*commits)[0] | |
| 82 | - | if c.flags != atomicNonblock|flipEventFlag || c.user != userFrame { | |
| 91 | + | if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.pendingSerial { | |
| 83 | 92 | t.Fatalf("flags %#x user %d", c.flags, c.user) | |
| 84 | 93 | } | |
| 85 | 94 | if v, _ := c.req.value(tPrimary, pFB); v != 70 { | |
| @@ -167,12 +176,12 @@ func TestCursorCommitWaitsForEvent(t *testing.T) { | |||
| 167 | 176 | t.Fatal(err) | |
| 168 | 177 | } | |
| 169 | 178 | c := (*commits)[0] | |
| 170 | - | if c.flags != atomicNonblock|flipEventFlag || c.user != userState || !o.pending { | |
| 179 | + | if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.pending { | |
| 171 | 180 | t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending) | |
| 172 | 181 | } | |
| 173 | 182 | // Its event is not a frame: nothing is reported. | |
| 174 | 183 | seen := map[ports.WindowID]uint64{} | |
| 175 | - | o.completed(flipEvent{user: userState}, seen) | |
| 184 | + | o.completed(eventOf(c), seen) | |
| 176 | 185 | if o.pending || len(o.unsent) != 0 || o.flips != 0 { | |
| 177 | 186 | t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips) | |
| 178 | 187 | } | |
| @@ -192,13 +201,13 @@ func TestBusyCommitStaysPending(t *testing.T) { | |||
| 192 | 201 | } | |
| 193 | 202 | ||
| 194 | 203 | func TestFlipReportsTimestampAndShownFrame(t *testing.T) { | |
| 195 | - | o, _, _ := testOutput(t) | |
| 204 | + | o, _, commits := testOutput(t) | |
| 196 | 205 | o.vrrOn = false | |
| 197 | 206 | o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0 | |
| 198 | 207 | if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil { | |
| 199 | 208 | t.Fatal(err) | |
| 200 | 209 | } | |
| 201 | - | o.completed(flipEvent{user: userFrame, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4}) | |
| 210 | + | o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4}) | |
| 202 | 211 | if len(o.unsent) != 1 { | |
| 203 | 212 | t.Fatalf("reports %d", len(o.unsent)) | |
| 204 | 213 | } | |
| @@ -344,7 +353,7 @@ func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) { | |||
| 344 | 353 | // GPU, so a newer content is reported only after the flip event; every | |
| 345 | 354 | // frame fence is closed. | |
| 346 | 355 | func TestRunReportsSeenAfterFlip(t *testing.T) { | |
| 347 | - | o, k, commits := testOutput(t) | |
| 356 | + | o, k, commits, commitMu := testOutputMu(t) | |
| 348 | 357 | o.cursor, o.tearing = nil, false | |
| 349 | 358 | flips := make(chan flipEvent, 1) | |
| 350 | 359 | o.flipped = flips | |
| @@ -360,7 +369,8 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||
| 360 | 369 | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 361 | 370 | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 362 | 371 | f, w, _ := os.Pipe() | |
| 363 | - | w.Close() // readable: signalled | |
| 372 | + | _, _ = w.Write([]byte{1}) // readable: signalled | |
| 373 | + | w.Close() | |
| 364 | 374 | mu.Lock() | |
| 365 | 375 | fences = append(fences, f) | |
| 366 | 376 | mu.Unlock() | |
| @@ -376,7 +386,7 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||
| 376 | 386 | defer commitMu.Unlock() | |
| 377 | 387 | n := 0 | |
| 378 | 388 | for _, c := range *commits { | |
| 379 | - | if c.user == userFrame { | |
| 389 | + | if c.user&3 == userFrame { | |
| 380 | 390 | n++ | |
| 381 | 391 | } | |
| 382 | 392 | } | |
| @@ -399,7 +409,10 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||
| 399 | 409 | t.Fatalf("report while a frame is in flight: %+v", p) | |
| 400 | 410 | case <-time.After(30 * time.Millisecond): | |
| 401 | 411 | } | |
| 402 | - | flips <- flipEvent{crtc: tCrtc, user: userFrame, when: time.Second, seq: 1} | |
| 412 | + | commitMu.Lock() | |
| 413 | + | last := (*commits)[len(*commits)-1] | |
| 414 | + | commitMu.Unlock() | |
| 415 | + | flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | |
| 403 | 416 | select { | |
| 404 | 417 | case p := <-presented: | |
| 405 | 418 | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | "sync" | 6 | "sync" | ||
| 7 | + | "sync/atomic" | |||
| 7 | "testing" | 8 | "testing" | ||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| @@ -23,9 +24,6 @@ const ( | |||||
| 23 | 24 | ||||
| 24 | var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17} | 25 | var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17} | ||
| 25 | 26 | ||||
| 26 | - | // commitMu guards recorded commits (Run commits on its goroutine). | |||
| 27 | - | var commitMu sync.Mutex | |||
| 28 | - | ||||
| 29 | // commitRec is one recorded atomic commit. | 27 | // commitRec is one recorded atomic commit. | ||
| 30 | type commitRec struct { | 28 | type commitRec struct { | ||
| 31 | req atomicReq | 29 | req atomicReq | ||
| @@ -36,8 +34,16 @@ type commitRec struct { | |||||
| 36 | // testOutput returns an output on a mocked kms that records commits; | 34 | // testOutput returns an output on a mocked kms that records commits; | ||
| 37 | // commit returns the next error of errs (nil when empty). | 35 | // commit returns the next error of errs (nil when empty). | ||
| 38 | func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | 36 | func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | ||
| 37 | + | o, k, commits, _ := testOutputMu(t, errs...) | |||
| 38 | + | return o, k, commits | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // testOutputMu is testOutput with the mutex that guards the recorded | |||
| 42 | + | // commits (Run commits on its goroutine). | |||
| 43 | + | func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) { | |||
| 39 | k := newMockkms(t) | 44 | k := newMockkms(t) | ||
| 40 | var commits []commitRec | 45 | var commits []commitRec | ||
| 46 | + | commitMu := &sync.Mutex{} | |||
| 41 | k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error { | 47 | k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error { | ||
| 42 | commitMu.Lock() | 48 | commitMu.Lock() | ||
| 43 | defer commitMu.Unlock() | 49 | defer commitMu.Unlock() | ||
| @@ -57,12 +63,15 @@ func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |||||
| 57 | primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps} | 63 | primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps} | ||
| 58 | cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64) | 64 | cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64) | ||
| 59 | cur.fbs = [2]uint32{90, 91} | 65 | cur.fbs = [2]uint32{90, 91} | ||
| 60 | - | o := &Output{k: k, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | 66 | + | o := &Output{k: k, serials: &atomic.Uint64{}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), |
| 61 | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | 67 | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | ||
| 62 | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | 68 | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | ||
| 63 | - | return o, k, &commits | 69 | + | return o, k, &commits, commitMu |
| 64 | } | 70 | } | ||
| 65 | 71 | ||||
| 72 | + | // eventOf is the flip event of commit c. | |||
| 73 | + | func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} } | |||
| 74 | + | ||||
| 66 | func objCount(c commitRec) int { return len(c.req.objs) } | 75 | func objCount(c commitRec) int { return len(c.req.objs) } | ||
| 67 | 76 | ||||
| 68 | func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | 77 | func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | ||
| @@ -79,7 +88,7 @@ func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | |||||
| 79 | t.Fatal(err) | 88 | t.Fatal(err) | ||
| 80 | } | 89 | } | ||
| 81 | c := (*commits)[0] | 90 | c := (*commits)[0] | ||
| 82 | - | if c.flags != atomicNonblock|flipEventFlag || c.user != userFrame { | 91 | + | if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.pendingSerial { |
| 83 | t.Fatalf("flags %#x user %d", c.flags, c.user) | 92 | t.Fatalf("flags %#x user %d", c.flags, c.user) | ||
| 84 | } | 93 | } | ||
| 85 | if v, _ := c.req.value(tPrimary, pFB); v != 70 { | 94 | if v, _ := c.req.value(tPrimary, pFB); v != 70 { | ||
| @@ -167,12 +176,12 @@ func TestCursorCommitWaitsForEvent(t *testing.T) { | |||||
| 167 | t.Fatal(err) | 176 | t.Fatal(err) | ||
| 168 | } | 177 | } | ||
| 169 | c := (*commits)[0] | 178 | c := (*commits)[0] | ||
| 170 | - | if c.flags != atomicNonblock|flipEventFlag || c.user != userState || !o.pending { | 179 | + | if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.pending { |
| 171 | t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending) | 180 | t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending) | ||
| 172 | } | 181 | } | ||
| 173 | // Its event is not a frame: nothing is reported. | 182 | // Its event is not a frame: nothing is reported. | ||
| 174 | seen := map[ports.WindowID]uint64{} | 183 | seen := map[ports.WindowID]uint64{} | ||
| 175 | - | o.completed(flipEvent{user: userState}, seen) | 184 | + | o.completed(eventOf(c), seen) |
| 176 | if o.pending || len(o.unsent) != 0 || o.flips != 0 { | 185 | if o.pending || len(o.unsent) != 0 || o.flips != 0 { | ||
| 177 | t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips) | 186 | t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips) | ||
| 178 | } | 187 | } | ||
| @@ -192,13 +201,13 @@ func TestBusyCommitStaysPending(t *testing.T) { | |||||
| 192 | } | 201 | } | ||
| 193 | 202 | ||||
| 194 | func TestFlipReportsTimestampAndShownFrame(t *testing.T) { | 203 | func TestFlipReportsTimestampAndShownFrame(t *testing.T) { | ||
| 195 | - | o, _, _ := testOutput(t) | 204 | + | o, _, commits := testOutput(t) |
| 196 | o.vrrOn = false | 205 | o.vrrOn = false | ||
| 197 | o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0 | 206 | o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0 | ||
| 198 | if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil { | 207 | if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil { | ||
| 199 | t.Fatal(err) | 208 | t.Fatal(err) | ||
| 200 | } | 209 | } | ||
| 201 | - | o.completed(flipEvent{user: userFrame, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4}) | 210 | + | o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4}) |
| 202 | if len(o.unsent) != 1 { | 211 | if len(o.unsent) != 1 { | ||
| 203 | t.Fatalf("reports %d", len(o.unsent)) | 212 | t.Fatalf("reports %d", len(o.unsent)) | ||
| 204 | } | 213 | } | ||
| @@ -344,7 +353,7 @@ func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) { | |||||
| 344 | // GPU, so a newer content is reported only after the flip event; every | 353 | // GPU, so a newer content is reported only after the flip event; every | ||
| 345 | // frame fence is closed. | 354 | // frame fence is closed. | ||
| 346 | func TestRunReportsSeenAfterFlip(t *testing.T) { | 355 | func TestRunReportsSeenAfterFlip(t *testing.T) { | ||
| 347 | - | o, k, commits := testOutput(t) | 356 | + | o, k, commits, commitMu := testOutputMu(t) |
| 348 | o.cursor, o.tearing = nil, false | 357 | o.cursor, o.tearing = nil, false | ||
| 349 | flips := make(chan flipEvent, 1) | 358 | flips := make(chan flipEvent, 1) | ||
| 350 | o.flipped = flips | 359 | o.flipped = flips | ||
| @@ -360,7 +369,8 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||||
| 360 | r.EXPECT().UseTarget(mock.Anything).Return() | 369 | r.EXPECT().UseTarget(mock.Anything).Return() | ||
| 361 | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | 370 | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | ||
| 362 | f, w, _ := os.Pipe() | 371 | f, w, _ := os.Pipe() | ||
| 363 | - | w.Close() // readable: signalled | 372 | + | _, _ = w.Write([]byte{1}) // readable: signalled |
| 373 | + | w.Close() | |||
| 364 | mu.Lock() | 374 | mu.Lock() | ||
| 365 | fences = append(fences, f) | 375 | fences = append(fences, f) | ||
| 366 | mu.Unlock() | 376 | mu.Unlock() | ||
| @@ -376,7 +386,7 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||||
| 376 | defer commitMu.Unlock() | 386 | defer commitMu.Unlock() | ||
| 377 | n := 0 | 387 | n := 0 | ||
| 378 | for _, c := range *commits { | 388 | for _, c := range *commits { | ||
| 379 | - | if c.user == userFrame { | 389 | + | if c.user&3 == userFrame { |
| 380 | n++ | 390 | n++ | ||
| 381 | } | 391 | } | ||
| 382 | } | 392 | } | ||
| @@ -399,7 +409,10 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||||
| 399 | t.Fatalf("report while a frame is in flight: %+v", p) | 409 | t.Fatalf("report while a frame is in flight: %+v", p) | ||
| 400 | case <-time.After(30 * time.Millisecond): | 410 | case <-time.After(30 * time.Millisecond): | ||
| 401 | } | 411 | } | ||
| 402 | - | flips <- flipEvent{crtc: tCrtc, user: userFrame, when: time.Second, seq: 1} | 412 | + | commitMu.Lock() |
| 413 | + | last := (*commits)[len(*commits)-1] | |||
| 414 | + | commitMu.Unlock() | |||
| 415 | + | flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1} | |||
| 403 | select { | 416 | select { | ||
| 404 | case p := <-presented: | 417 | case p := <-presented: | ||
| 405 | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | 418 | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | ||
M internal/adapters/headless/output.go +2 -14
| @@ -2,7 +2,6 @@ package headless | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "errors" | |
| 6 | 5 | "fmt" | |
| 7 | 6 | "image" | |
| 8 | 7 | "image/png" | |
| @@ -11,9 +10,9 @@ import ( | |||
| 11 | 10 | "path/filepath" | |
| 12 | 11 | "time" | |
| 13 | 12 | ||
| 13 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |
| 14 | 14 | "github.com/bnema/nefertty/internal/ports" | |
| 15 | 15 | "github.com/bnema/zerowrap" | |
| 16 | - | "golang.org/x/sys/unix" | |
| 17 | 16 | ) | |
| 18 | 17 | ||
| 19 | 18 | type Options struct { | |
| @@ -127,7 +126,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 127 | 126 | if done != nil { | |
| 128 | 127 | // No screen paces headless frames: wait for the GPU before | |
| 129 | 128 | // reporting that its buffers were read. | |
| 130 | - | err = waitFence(done) | |
| 129 | + | err = syncfile.Wait(ctx, done) | |
| 131 | 130 | done.Close() | |
| 132 | 131 | if err != nil { | |
| 133 | 132 | return fmt.Errorf("frame fence: %w", err) | |
| @@ -186,14 +185,3 @@ func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |||
| 186 | 185 | return r | |
| 187 | 186 | } | |
| 188 | 187 | } | |
| 189 | - | ||
| 190 | - | // waitFence blocks until a sync file signals. | |
| 191 | - | func waitFence(f *os.File) error { | |
| 192 | - | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |
| 193 | - | for { | |
| 194 | - | _, err := unix.Poll(fds, -1) | |
| 195 | - | if !errors.Is(err, unix.EINTR) { | |
| 196 | - | return err | |
| 197 | - | } | |
| 198 | - | } | |
| 199 | - | } | |
| @@ -2,7 +2,6 @@ package headless | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "errors" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | "image" | 6 | "image" | ||
| 8 | "image/png" | 7 | "image/png" | ||
| @@ -11,9 +10,9 @@ import ( | |||||
| 11 | "path/filepath" | 10 | "path/filepath" | ||
| 12 | "time" | 11 | "time" | ||
| 13 | 12 | ||||
| 13 | + | "github.com/bnema/nefertty/internal/adapters/syncfile" | |||
| 14 | "github.com/bnema/nefertty/internal/ports" | 14 | "github.com/bnema/nefertty/internal/ports" | ||
| 15 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| 16 | - | "golang.org/x/sys/unix" | |||
| 17 | ) | 16 | ) | ||
| 18 | 17 | ||||
| 19 | type Options struct { | 18 | type Options struct { | ||
| @@ -127,7 +126,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 127 | if done != nil { | 126 | if done != nil { | ||
| 128 | // No screen paces headless frames: wait for the GPU before | 127 | // No screen paces headless frames: wait for the GPU before | ||
| 129 | // reporting that its buffers were read. | 128 | // reporting that its buffers were read. | ||
| 130 | - | err = waitFence(done) | 129 | + | err = syncfile.Wait(ctx, done) |
| 131 | done.Close() | 130 | done.Close() | ||
| 132 | if err != nil { | 131 | if err != nil { | ||
| 133 | return fmt.Errorf("frame fence: %w", err) | 132 | return fmt.Errorf("frame fence: %w", err) | ||
| @@ -186,14 +185,3 @@ func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |||||
| 186 | return r | 185 | return r | ||
| 187 | } | 186 | } | ||
| 188 | } | 187 | } | ||
| 189 | - | ||||
| 190 | - | // waitFence blocks until a sync file signals. | |||
| 191 | - | func waitFence(f *os.File) error { | |||
| 192 | - | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |||
| 193 | - | for { | |||
| 194 | - | _, err := unix.Poll(fds, -1) | |||
| 195 | - | if !errors.Is(err, unix.EINTR) { | |||
| 196 | - | return err | |||
| 197 | - | } | |||
| 198 | - | } | |||
| 199 | - | } | |||
A internal/adapters/syncfile/syncfile.go +38 -0
| @@ -0,0 +1,38 @@ | |||
| 1 | + | // Package syncfile waits for Linux sync files (dma-fence fds), such as the | |
| 2 | + | // GPU fence a frame render returns. | |
| 3 | + | package syncfile | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "fmt" | |
| 9 | + | "os" | |
| 10 | + | ||
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // pollSlice bounds one poll so a canceled ctx is seen. | |
| 15 | + | const pollSlice = 100 // ms | |
| 16 | + | ||
| 17 | + | // Wait blocks until f signals or ctx ends. A sync file is signalled when | |
| 18 | + | // it polls readable; an error or invalid fd is reported, not taken as | |
| 19 | + | // signalled. | |
| 20 | + | func Wait(ctx context.Context, f *os.File) error { | |
| 21 | + | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |
| 22 | + | for { | |
| 23 | + | n, err := unix.Poll(fds, pollSlice) | |
| 24 | + | switch { | |
| 25 | + | case errors.Is(err, unix.EINTR): | |
| 26 | + | continue | |
| 27 | + | case err != nil: | |
| 28 | + | return fmt.Errorf("poll sync file: %w", err) | |
| 29 | + | case n > 0 && fds[0].Revents&unix.POLLIN != 0: | |
| 30 | + | return nil | |
| 31 | + | case n > 0: | |
| 32 | + | return fmt.Errorf("sync file poll events %#x", fds[0].Revents) | |
| 33 | + | } | |
| 34 | + | if err := ctx.Err(); err != nil { | |
| 35 | + | return err | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | } | |
| @@ -0,0 +1,38 @@ | |||||
| 1 | + | // Package syncfile waits for Linux sync files (dma-fence fds), such as the | |||
| 2 | + | // GPU fence a frame render returns. | |||
| 3 | + | package syncfile | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "errors" | |||
| 8 | + | "fmt" | |||
| 9 | + | "os" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // pollSlice bounds one poll so a canceled ctx is seen. | |||
| 15 | + | const pollSlice = 100 // ms | |||
| 16 | + | ||||
| 17 | + | // Wait blocks until f signals or ctx ends. A sync file is signalled when | |||
| 18 | + | // it polls readable; an error or invalid fd is reported, not taken as | |||
| 19 | + | // signalled. | |||
| 20 | + | func Wait(ctx context.Context, f *os.File) error { | |||
| 21 | + | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |||
| 22 | + | for { | |||
| 23 | + | n, err := unix.Poll(fds, pollSlice) | |||
| 24 | + | switch { | |||
| 25 | + | case errors.Is(err, unix.EINTR): | |||
| 26 | + | continue | |||
| 27 | + | case err != nil: | |||
| 28 | + | return fmt.Errorf("poll sync file: %w", err) | |||
| 29 | + | case n > 0 && fds[0].Revents&unix.POLLIN != 0: | |||
| 30 | + | return nil | |||
| 31 | + | case n > 0: | |||
| 32 | + | return fmt.Errorf("sync file poll events %#x", fds[0].Revents) | |||
| 33 | + | } | |||
| 34 | + | if err := ctx.Err(); err != nil { | |||
| 35 | + | return err | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
A internal/adapters/syncfile/syncfile_test.go +53 -0
| @@ -0,0 +1,53 @@ | |||
| 1 | + | package syncfile | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "os" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // A pipe stands in for a sync file: readable means signalled. | |
| 12 | + | func TestWaitReturnsWhenReadable(t *testing.T) { | |
| 13 | + | r, w, err := os.Pipe() | |
| 14 | + | if err != nil { | |
| 15 | + | t.Fatal(err) | |
| 16 | + | } | |
| 17 | + | defer r.Close() | |
| 18 | + | go func() { | |
| 19 | + | time.Sleep(10 * time.Millisecond) | |
| 20 | + | _, _ = w.Write([]byte{1}) | |
| 21 | + | w.Close() | |
| 22 | + | }() | |
| 23 | + | if err := Wait(context.Background(), r); err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func TestWaitStopsOnCancel(t *testing.T) { | |
| 29 | + | r, w, err := os.Pipe() | |
| 30 | + | if err != nil { | |
| 31 | + | t.Fatal(err) | |
| 32 | + | } | |
| 33 | + | defer r.Close() | |
| 34 | + | defer w.Close() | |
| 35 | + | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) | |
| 36 | + | defer cancel() | |
| 37 | + | if err := Wait(ctx, r); !errors.Is(err, context.DeadlineExceeded) { | |
| 38 | + | t.Fatalf("err %v", err) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // POLLERR alone (a write end whose reader is gone) is not a signal. | |
| 43 | + | func TestWaitReportsErrorEvents(t *testing.T) { | |
| 44 | + | r, w, err := os.Pipe() | |
| 45 | + | if err != nil { | |
| 46 | + | t.Fatal(err) | |
| 47 | + | } | |
| 48 | + | defer w.Close() | |
| 49 | + | r.Close() | |
| 50 | + | if err := Wait(context.Background(), w); err == nil { | |
| 51 | + | t.Fatal("POLLERR taken as signalled") | |
| 52 | + | } | |
| 53 | + | } | |
| @@ -0,0 +1,53 @@ | |||||
| 1 | + | package syncfile | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // A pipe stands in for a sync file: readable means signalled. | |||
| 12 | + | func TestWaitReturnsWhenReadable(t *testing.T) { | |||
| 13 | + | r, w, err := os.Pipe() | |||
| 14 | + | if err != nil { | |||
| 15 | + | t.Fatal(err) | |||
| 16 | + | } | |||
| 17 | + | defer r.Close() | |||
| 18 | + | go func() { | |||
| 19 | + | time.Sleep(10 * time.Millisecond) | |||
| 20 | + | _, _ = w.Write([]byte{1}) | |||
| 21 | + | w.Close() | |||
| 22 | + | }() | |||
| 23 | + | if err := Wait(context.Background(), r); err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func TestWaitStopsOnCancel(t *testing.T) { | |||
| 29 | + | r, w, err := os.Pipe() | |||
| 30 | + | if err != nil { | |||
| 31 | + | t.Fatal(err) | |||
| 32 | + | } | |||
| 33 | + | defer r.Close() | |||
| 34 | + | defer w.Close() | |||
| 35 | + | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) | |||
| 36 | + | defer cancel() | |||
| 37 | + | if err := Wait(ctx, r); !errors.Is(err, context.DeadlineExceeded) { | |||
| 38 | + | t.Fatalf("err %v", err) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // POLLERR alone (a write end whose reader is gone) is not a signal. | |||
| 43 | + | func TestWaitReportsErrorEvents(t *testing.T) { | |||
| 44 | + | r, w, err := os.Pipe() | |||
| 45 | + | if err != nil { | |||
| 46 | + | t.Fatal(err) | |||
| 47 | + | } | |||
| 48 | + | defer w.Close() | |||
| 49 | + | r.Close() | |||
| 50 | + | if err := Wait(context.Background(), w); err == nil { | |||
| 51 | + | t.Fatal("POLLERR taken as signalled") | |||
| 52 | + | } | |||
| 53 | + | } | |||
M internal/adapters/vulkan/cursor.go +4 -0
| @@ -79,6 +79,10 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) { | |||
| 79 | 79 | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectColorBit} | |
| 80 | 80 | d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout) | |
| 81 | 81 | c.pitch = int(layout.RowPitch) | |
| 82 | + | // Cursor planes scan out packed rows from the start of the buffer. | |
| 83 | + | if c.pitch != size*4 || layout.Offset != 0 { | |
| 84 | + | return nil, ports.DMABuf{}, fmt.Errorf("cursor image layout pitch %d offset %d, want pitch %d offset 0", c.pitch, layout.Offset, size*4) | |
| 85 | + | } | |
| 82 | 86 | if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil { | |
| 83 | 87 | return nil, ports.DMABuf{}, err | |
| 84 | 88 | } | |
| @@ -79,6 +79,10 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) { | |||||
| 79 | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectColorBit} | 79 | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectColorBit} | ||
| 80 | d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout) | 80 | d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout) | ||
| 81 | c.pitch = int(layout.RowPitch) | 81 | c.pitch = int(layout.RowPitch) | ||
| 82 | + | // Cursor planes scan out packed rows from the start of the buffer. | |||
| 83 | + | if c.pitch != size*4 || layout.Offset != 0 { | |||
| 84 | + | return nil, ports.DMABuf{}, fmt.Errorf("cursor image layout pitch %d offset %d, want pitch %d offset 0", c.pitch, layout.Offset, size*4) | |||
| 85 | + | } | |||
| 82 | if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil { | 86 | if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil { | ||
| 83 | return nil, ports.DMABuf{}, err | 87 | return nil, ports.DMABuf{}, err | ||
| 84 | } | 88 | } | ||
A internal/adapters/vulkan/cursor_test.go +48 -0
| @@ -0,0 +1,48 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "unsafe" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // Cursor images are packed (pitch = size*4), exported, and WriteCursor | |
| 9 | + | // crops to the image and clears around the pixels. | |
| 10 | + | func TestCursorBuffersWriteCursor(t *testing.T) { | |
| 11 | + | r, err := New(64, 48) | |
| 12 | + | if err != nil { | |
| 13 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 14 | + | } | |
| 15 | + | defer r.Close() | |
| 16 | + | bufs, err := r.CursorBuffers(64) | |
| 17 | + | if err != nil { | |
| 18 | + | t.Skipf("no exportable cursor images: %v", err) | |
| 19 | + | } | |
| 20 | + | for _, b := range bufs { | |
| 21 | + | if p := b.Planes[0]; p.Stride != 64*4 || p.Offset != 0 || b.Format != fourccARGB { | |
| 22 | + | t.Fatalf("layout %+v format %#x", p, b.Format) | |
| 23 | + | } | |
| 24 | + | b.Planes[0].File.Close() | |
| 25 | + | } | |
| 26 | + | mem := func() []byte { c := r.cursors[1]; return unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) } | |
| 27 | + | // A first image fills the corner; a second, smaller one must clear it. | |
| 28 | + | big := make([]byte, 100*100*4) | |
| 29 | + | for i := range big { | |
| 30 | + | big[i] = 0xff | |
| 31 | + | } | |
| 32 | + | if err := r.WriteCursor(1, big, 100, 100); err != nil { // cropped to 64 | |
| 33 | + | t.Fatal(err) | |
| 34 | + | } | |
| 35 | + | if m := mem(); m[63*256+63*4] != 0xff { | |
| 36 | + | t.Fatal("cropped image not written") | |
| 37 | + | } | |
| 38 | + | if err := r.WriteCursor(1, []byte{1, 2, 3, 4}, 1, 1); err != nil { | |
| 39 | + | t.Fatal(err) | |
| 40 | + | } | |
| 41 | + | m := mem() | |
| 42 | + | if m[0] != 1 || m[3] != 4 || m[4] != 0 || m[256] != 0 || m[63*256+63*4] != 0 { | |
| 43 | + | t.Fatal("image not cleared around the new pixels") | |
| 44 | + | } | |
| 45 | + | if err := r.WriteCursor(1, []byte{1}, 1, 1); err == nil { | |
| 46 | + | t.Fatal("short image accepted") | |
| 47 | + | } | |
| 48 | + | } | |
| @@ -0,0 +1,48 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "unsafe" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // Cursor images are packed (pitch = size*4), exported, and WriteCursor | |||
| 9 | + | // crops to the image and clears around the pixels. | |||
| 10 | + | func TestCursorBuffersWriteCursor(t *testing.T) { | |||
| 11 | + | r, err := New(64, 48) | |||
| 12 | + | if err != nil { | |||
| 13 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 14 | + | } | |||
| 15 | + | defer r.Close() | |||
| 16 | + | bufs, err := r.CursorBuffers(64) | |||
| 17 | + | if err != nil { | |||
| 18 | + | t.Skipf("no exportable cursor images: %v", err) | |||
| 19 | + | } | |||
| 20 | + | for _, b := range bufs { | |||
| 21 | + | if p := b.Planes[0]; p.Stride != 64*4 || p.Offset != 0 || b.Format != fourccARGB { | |||
| 22 | + | t.Fatalf("layout %+v format %#x", p, b.Format) | |||
| 23 | + | } | |||
| 24 | + | b.Planes[0].File.Close() | |||
| 25 | + | } | |||
| 26 | + | mem := func() []byte { c := r.cursors[1]; return unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) } | |||
| 27 | + | // A first image fills the corner; a second, smaller one must clear it. | |||
| 28 | + | big := make([]byte, 100*100*4) | |||
| 29 | + | for i := range big { | |||
| 30 | + | big[i] = 0xff | |||
| 31 | + | } | |||
| 32 | + | if err := r.WriteCursor(1, big, 100, 100); err != nil { // cropped to 64 | |||
| 33 | + | t.Fatal(err) | |||
| 34 | + | } | |||
| 35 | + | if m := mem(); m[63*256+63*4] != 0xff { | |||
| 36 | + | t.Fatal("cropped image not written") | |||
| 37 | + | } | |||
| 38 | + | if err := r.WriteCursor(1, []byte{1, 2, 3, 4}, 1, 1); err != nil { | |||
| 39 | + | t.Fatal(err) | |||
| 40 | + | } | |||
| 41 | + | m := mem() | |||
| 42 | + | if m[0] != 1 || m[3] != 4 || m[4] != 0 || m[256] != 0 || m[63*256+63*4] != 0 { | |||
| 43 | + | t.Fatal("image not cleared around the new pixels") | |||
| 44 | + | } | |||
| 45 | + | if err := r.WriteCursor(1, []byte{1}, 1, 1); err == nil { | |||
| 46 | + | t.Fatal("short image accepted") | |||
| 47 | + | } | |||
| 48 | + | } | |||