detached f0c31c4

refactor(drm): simplify GBM image lifecycle and harden the fence gate

Drop redundant image state (imageAlloc, dropLive), share the framebuffer
setup tail between GBM and Vulkan images, and require
gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane. On gated
nvidia-drm, poll client fences without dup, compose when the poll fails,
wait once per commit, and count and log fence timeouts. Cache the
renderer's importable target modifiers and make the fence and import
tests deterministic.
commit
f0c31c4b25a0570b34928bb1f86760bad05ba192
author
brice · · 2026-10-21 20:49 UTC
parent
53269c8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +635 -402
M docs/dev/architecture.md +2 -2
@@ -28,9 +28,9 @@ About 35 channels connect them, all created in one place, so the full data flow
2828
2929 A DRM output flips between two images the renderer draws into, with no CPU copy.
3030
31-- **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), the allocator of the display driver, with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. NVIDIA shows images allocated by Vulkan corrupted, which is why every driver uses GBM.
31+- **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. GBM is the display driver's own allocator, so the layouts it picks scan out correctly on every driver.
3232 - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`).
33-- **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one.
33+- **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable.
3434
3535 I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them:
3636
@@ -28,9 +28,9 @@ About 35 channels connect them, all created in one place, so the full data flow
28 28
29 A DRM output flips between two images the renderer draws into, with no CPU copy.29 A DRM output flips between two images the renderer draws into, with no CPU copy.
30 30
31-- **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), the allocator of the display driver, with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. NVIDIA shows images allocated by Vulkan corrupted, which is why every driver uses GBM.31+- **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. GBM is the display driver's own allocator, so the layouts it picks scan out correctly on every driver.
32 - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`).32 - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`).
33-- **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one.33+- **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable.
34 34
35 I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them:35 I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them:
36 36
M internal/adapters/drm/alloc.go +5 -15
@@ -3,7 +3,6 @@ package drm
33 import (
44 "github.com/bnema/neferwl/internal/adapters/drm/gbm"
55 "github.com/bnema/neferwl/internal/ports"
6- "github.com/bnema/zerowrap"
76 )
87
98 // imageAllocator makes the output images with the display driver's own
@@ -24,35 +23,26 @@ type imageBuffer interface {
2423 }
2524
2625 // gbmAllocator allocates scanout-capable, renderable images with libgbm.
27-type gbmAllocator struct {
28- dev *gbm.Device
29- log zerowrap.Logger
30- // flaglessLogged: the missing usage flags were logged once.
31- flaglessLogged bool
32-}
26+type gbmAllocator struct{ dev *gbm.Device }
3327
3428 // openGBM opens a GBM allocator on a duplicate of the card fd.
35-func openGBM(fd int, log zerowrap.Logger) (imageAllocator, error) {
29+func openGBM(fd int) (imageAllocator, error) {
3630 dev, err := gbm.Open(fd)
3731 if err != nil {
3832 return nil, err
3933 }
40- return &gbmAllocator{dev: dev, log: log}, nil
34+ return gbmAllocator{dev}, nil
4135 }
4236
43-func (a *gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
37+func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
4438 b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)
4539 if err != nil {
4640 return nil, err
4741 }
48- if b.Flagless && !a.flaglessLogged {
49- a.flaglessLogged = true
50- a.log.Info().Msg("libgbm lacks gbm_bo_create_with_modifiers2: images are allocated without the scanout and rendering flags")
51- }
5242 return gbmImage{b}, nil
5343 }
5444
55-func (a *gbmAllocator) close() { a.dev.Close() }
45+func (a gbmAllocator) close() { a.dev.Close() }
5646
5747 type gbmImage struct{ b *gbm.Buffer }
5848
@@ -3,7 +3,6 @@ package drm
3 import (3 import (
4 "github.com/bnema/neferwl/internal/adapters/drm/gbm"4 "github.com/bnema/neferwl/internal/adapters/drm/gbm"
5 "github.com/bnema/neferwl/internal/ports"5 "github.com/bnema/neferwl/internal/ports"
6- "github.com/bnema/zerowrap"
7 )6 )
8 7
9 // imageAllocator makes the output images with the display driver's own8 // imageAllocator makes the output images with the display driver's own
@@ -24,35 +23,26 @@ type imageBuffer interface {
24 }23 }
25 24
26 // gbmAllocator allocates scanout-capable, renderable images with libgbm.25 // gbmAllocator allocates scanout-capable, renderable images with libgbm.
27-type gbmAllocator struct {26+type gbmAllocator struct{ dev *gbm.Device }
28- dev *gbm.Device
29- log zerowrap.Logger
30- // flaglessLogged: the missing usage flags were logged once.
31- flaglessLogged bool
32-}
33 27
34 // openGBM opens a GBM allocator on a duplicate of the card fd.28 // openGBM opens a GBM allocator on a duplicate of the card fd.
35-func openGBM(fd int, log zerowrap.Logger) (imageAllocator, error) {29+func openGBM(fd int) (imageAllocator, error) {
36 dev, err := gbm.Open(fd)30 dev, err := gbm.Open(fd)
37 if err != nil {31 if err != nil {
38 return nil, err32 return nil, err
39 }33 }
40- return &gbmAllocator{dev: dev, log: log}, nil34+ return gbmAllocator{dev}, nil
41 }35 }
42 36
43-func (a *gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {37+func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
44 b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)38 b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)
45 if err != nil {39 if err != nil {
46 return nil, err40 return nil, err
47 }41 }
48- if b.Flagless && !a.flaglessLogged {
49- a.flaglessLogged = true
50- a.log.Info().Msg("libgbm lacks gbm_bo_create_with_modifiers2: images are allocated without the scanout and rendering flags")
51- }
52 return gbmImage{b}, nil42 return gbmImage{b}, nil
53 }43 }
54 44
55-func (a *gbmAllocator) close() { a.dev.Close() }45+func (a gbmAllocator) close() { a.dev.Close() }
56 46
57 type gbmImage struct{ b *gbm.Buffer }47 type gbmImage struct{ b *gbm.Buffer }
58 48
M internal/adapters/drm/alloc_test.go +49 -93
@@ -22,19 +22,19 @@ func gbmBuf(planes int) ports.DMABuf {
2222 return b
2323 }
2424
25-// gbmImages makes an allocator that hands out two images; each image
26-// closes exactly once.
27-func gbmImages(t *testing.T, planes int) (imageAllocator, *int) {
28- a := newMockimageAllocator(t)
29- closed := new(int)
30- a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) {
25+// twoGBMImages makes an allocator that hands out two images of the given
26+// plane count; each image closes exactly once, counted in closed.
27+func twoGBMImages(t *testing.T, planes int) (a imageAllocator, closed *int) {
28+ m := newMockimageAllocator(t)
29+ closed = new(int)
30+ m.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) {
3131 img := newMockimageBuffer(t)
3232 img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil })
3333 img.EXPECT().close().Run(func() { *closed++ }).Once()
3434 return img, nil
3535 }).Twice()
36- a.EXPECT().close().Return().Maybe()
37- return a, closed
36+ m.EXPECT().close().Return().Maybe()
37+ return m, closed
3838 }
3939
4040 func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {
@@ -50,7 +50,7 @@ func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {
5050 func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {
5151 o, k, _ := testOutput(t)
5252 o.cursor = nil
53- a, closed := gbmImages(t, 1)
53+ a, closed := twoGBMImages(t, 1)
5454 o.openAlloc = func() (imageAllocator, error) { return a, nil }
5555 r := gbmRenderer(t)
5656 r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
@@ -62,69 +62,71 @@ func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {
6262 if err := o.showImages(r, imagesDriver, nil); err != nil {
6363 t.Fatal(err)
6464 }
65- if o.imageAlloc != allocGBM || o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 {
66- t.Fatalf("alloc %q kind %d modifier %#x images %d closed %d", o.imageAlloc, o.kind, o.imageMod, len(o.images), *closed)
65+ if o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 {
66+ t.Fatalf("kind %d modifier %#x images %d closed %d", o.kind, o.imageMod, len(o.images), *closed)
6767 }
68- o.freeImages()
69- if *closed != 2 || o.images != nil {
70- t.Fatalf("closed %d images, %d left", *closed, len(o.images))
68+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
69+ o.dropImages(r)
70+ if *closed != 2 || o.images != nil || o.fbs != [2]uint32{} {
71+ t.Fatalf("closed %d images, %d left, fbs %v", *closed, len(o.images), o.fbs)
7172 }
7273 }
7374
74-// The plane's modifiers reach the renderer with the single-plane rule, and
75-// a GBM failure falls back to the renderer's own images of the same kind.
75+// A GBM failure at any step falls back to the renderer's own images of the same kind.
7676 func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
7777 for name, tc := range map[string]struct {
78- setup func(t *testing.T, o *Output, r *portsmocks.MockRenderer)
78+ setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer)
7979 }{
80- "gbm unavailable": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
80+ "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) {
8181 o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") }
8282 }},
83- "no common modifier": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
83+ "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
8484 a := newMockimageAllocator(t)
8585 a.EXPECT().close().Return().Maybe()
8686 o.openAlloc = func() (imageAllocator, error) { return a, nil }
8787 r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once()
8888 }},
89- "allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
89+ "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
9090 a := newMockimageAllocator(t)
9191 a.EXPECT().close().Return().Maybe()
9292 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once()
9393 o.openAlloc = func() (imageAllocator, error) { return a, nil }
9494 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
9595 }},
96- "second allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
96+ "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
9797 a := newMockimageAllocator(t)
9898 a.EXPECT().close().Return().Maybe()
9999 img := newMockimageBuffer(t)
100100 img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once()
101101 img.EXPECT().close().Return().Once() // freed with the failed attempt
102+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
102103 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()
103104 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()
104105 o.openAlloc = func() (imageAllocator, error) { return a, nil }
105106 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
106107 }},
107- "import refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
108- a, closed := gbmImages(t, 1)
109- _ = closed
108+ "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
109+ a, _ := twoGBMImages(t, 1)
110110 o.openAlloc = func() (imageAllocator, error) { return a, nil }
111111 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
112112 r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once()
113+ // The images made before the refusal go with a renderer drop.
114+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
113115 }},
114- "framebuffer refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
115- a, _ := gbmImages(t, 1)
116+ "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
117+ a, _ := twoGBMImages(t, 1)
116118 o.openAlloc = func() (imageAllocator, error) { return a, nil }
117119 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
118120 r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()
119121 r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
120- o.k.(*mockkms).EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
122+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
121123 }},
122124 } {
123125 t.Run(name, func(t *testing.T) {
124126 o, k, _ := testOutput(t)
125127 o.cursor = nil
126128 r := gbmRenderer(t)
127- tc.setup(t, o, r)
129+ tc.setup(t, o, k, r)
128130 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
129131 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
130132 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
@@ -133,8 +135,9 @@ func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
133135 if err := o.showImages(r, imagesDriver, nil); err != nil {
134136 t.Fatal(err)
135137 }
136- if o.imageAlloc != allocVulkan || o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 {
137- t.Fatalf("alloc %q kind %d images %d fbs %v", o.imageAlloc, o.kind, len(o.images), o.fbs)
138+ // The renderer's own images: no GBM image is alive.
139+ if o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 {
140+ t.Fatalf("kind %d images %d fbs %v", o.kind, len(o.images), o.fbs)
138141 }
139142 })
140143 }
@@ -167,7 +170,7 @@ func TestGBMOpenFailureIsRemembered(t *testing.T) {
167170 func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
168171 o, k, commits := testOutput(t, unix.EINVAL)
169172 o.cursor = nil
170- a, closed := gbmImages(t, 1)
173+ a, closed := twoGBMImages(t, 1)
171174 o.openAlloc = func() (imageAllocator, error) { return a, nil }
172175 r := gbmRenderer(t)
173176 r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
@@ -182,28 +185,33 @@ func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
182185 if err := o.showImages(r, imagesDriver, nil); err != nil {
183186 t.Fatal(err)
184187 }
185- if o.imageAlloc != allocVulkan || o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 {
186- t.Fatalf("alloc %q kind %d closed %d images %d commits %d", o.imageAlloc, o.kind, *closed, len(o.images), len(*commits))
188+ if o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 {
189+ t.Fatalf("kind %d closed %d images %d commits %d", o.kind, *closed, len(o.images), len(*commits))
187190 }
188191 }
189192
190-// Closing the output frees the GBM images and the allocator.
191-func TestOutputFreeImagesClosesGBMImages(t *testing.T) {
193+// Closing the output frees the GBM images and closes the allocator once.
194+func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) {
192195 o, k, _ := testOutput(t)
196+ o.cursor = nil
193197 img := newMockimageBuffer(t)
194198 img.EXPECT().close().Return().Twice()
199+ alloc := newMockimageAllocator(t)
200+ alloc.EXPECT().close().Return().Once()
195201 o.images = []imageBuffer{img, img}
202+ o.alloc = alloc
196203 o.fbs = [2]uint32{70, 71}
197- k.EXPECT().rmFB(mock.Anything).Return(nil).Twice()
198- o.freeImages()
199- if o.images != nil || o.fbs != [2]uint32{} {
200- t.Fatalf("images %v fbs %v", o.images, o.fbs)
204+ k.EXPECT().rmFB(uint32(70)).Return(nil).Once()
205+ k.EXPECT().rmFB(uint32(71)).Return(nil).Once()
206+ o.Close()
207+ if o.images != nil || o.alloc != nil || o.fbs != [2]uint32{} {
208+ t.Fatalf("images %v alloc %v fbs %v", o.images, o.alloc, o.fbs)
201209 }
202210 }
203211
204212 // A setup that finds images left over frees them first, with the renderer's
205-// targets over GBM ones, and a refused modeset of GBM images (no fallback
206-// left) drops the targets with the buffers.
213+// targets over GBM ones: the renderer never keeps a target over a freed
214+// image.
207215 func TestSetupStartsFromCleanState(t *testing.T) {
208216 o, k, _ := testOutput(t)
209217 o.cursor = nil
@@ -224,55 +232,3 @@ func TestSetupStartsFromCleanState(t *testing.T) {
224232 t.Fatalf("images %v fbs %v", o.images, o.fbs)
225233 }
226234 }
227-
228-// Without CPU fence waits every fence is ready; with them only a signalled
229-// client fence is (a pipe stands in: readable means signalled).
230-func TestClientFenceReady(t *testing.T) {
231- r, w, err := os.Pipe()
232- if err != nil {
233- t.Fatal(err)
234- }
235- defer r.Close()
236- defer w.Close()
237- o, _, _ := testOutput(t)
238- if !o.clientFenceReady(r) || !o.clientFenceReady(nil) {
239- t.Fatal("fence not ready without CPU waits")
240- }
241- o.cpuFences = true
242- if !o.clientFenceReady(nil) {
243- t.Fatal("no fence must be ready")
244- }
245- if o.clientFenceReady(r) {
246- t.Fatal("pending client fence ready")
247- }
248- _, _ = w.Write([]byte{1})
249- if !o.clientFenceReady(r) {
250- t.Fatal("signalled client fence not ready")
251- }
252-}
253-
254-// With CPU waits a client buffer whose fence is pending is composed, not
255-// scanned out and not put on the overlay: neither plane waits for it. Once
256-// the fence signalled the same frame scans out.
257-func TestPendingClientFenceComposes(t *testing.T) {
258- r, w, err := os.Pipe()
259- if err != nil {
260- t.Fatal(err)
261- }
262- defer r.Close()
263- defer w.Close()
264- o, k, _ := testOutput(t)
265- k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
266- o.cpuFences = true
267- o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
268- s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}}
269- c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, Acquire: r, DMABuf: &ports.DMABuf{ID: 5, Format: fourccXRGB, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{File: r}}}}
270- contents := map[ports.WindowID]ports.SurfaceContent{1: c}
271- if fb, _, _ := o.scanoutFrame(s, contents); fb != 0 || o.reason != "fence_pending" {
272- t.Fatalf("pending fence: fb %d reason %q", fb, o.reason)
273- }
274- _, _ = w.Write([]byte{1})
275- if fb, _, _ := o.scanoutFrame(s, contents); fb == 0 {
276- t.Fatalf("signalled fence: reason %q", o.reason)
277- }
278-}
@@ -22,19 +22,19 @@ func gbmBuf(planes int) ports.DMABuf {
22 return b22 return b
23 }23 }
24 24
25-// gbmImages makes an allocator that hands out two images; each image25+// twoGBMImages makes an allocator that hands out two images of the given
26-// closes exactly once.26+// plane count; each image closes exactly once, counted in closed.
27-func gbmImages(t *testing.T, planes int) (imageAllocator, *int) {27+func twoGBMImages(t *testing.T, planes int) (a imageAllocator, closed *int) {
28- a := newMockimageAllocator(t)28+ m := newMockimageAllocator(t)
29- closed := new(int)29+ closed = new(int)
30- a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) {30+ m.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) {
31 img := newMockimageBuffer(t)31 img := newMockimageBuffer(t)
32 img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil })32 img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil })
33 img.EXPECT().close().Run(func() { *closed++ }).Once()33 img.EXPECT().close().Run(func() { *closed++ }).Once()
34 return img, nil34 return img, nil
35 }).Twice()35 }).Twice()
36- a.EXPECT().close().Return().Maybe()36+ m.EXPECT().close().Return().Maybe()
37- return a, closed37+ return m, closed
38 }38 }
39 39
40 func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {40 func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {
@@ -50,7 +50,7 @@ func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {
50 func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {50 func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {
51 o, k, _ := testOutput(t)51 o, k, _ := testOutput(t)
52 o.cursor = nil52 o.cursor = nil
53- a, closed := gbmImages(t, 1)53+ a, closed := twoGBMImages(t, 1)
54 o.openAlloc = func() (imageAllocator, error) { return a, nil }54 o.openAlloc = func() (imageAllocator, error) { return a, nil }
55 r := gbmRenderer(t)55 r := gbmRenderer(t)
56 r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()56 r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
@@ -62,69 +62,71 @@ func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {
62 if err := o.showImages(r, imagesDriver, nil); err != nil {62 if err := o.showImages(r, imagesDriver, nil); err != nil {
63 t.Fatal(err)63 t.Fatal(err)
64 }64 }
65- if o.imageAlloc != allocGBM || o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 {65+ if o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 {
66- t.Fatalf("alloc %q kind %d modifier %#x images %d closed %d", o.imageAlloc, o.kind, o.imageMod, len(o.images), *closed)66+ t.Fatalf("kind %d modifier %#x images %d closed %d", o.kind, o.imageMod, len(o.images), *closed)
67 }67 }
68- o.freeImages()68+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
69- if *closed != 2 || o.images != nil {69+ o.dropImages(r)
70- t.Fatalf("closed %d images, %d left", *closed, len(o.images))70+ if *closed != 2 || o.images != nil || o.fbs != [2]uint32{} {
71+ t.Fatalf("closed %d images, %d left, fbs %v", *closed, len(o.images), o.fbs)
71 }72 }
72 }73 }
73 74
74-// The plane's modifiers reach the renderer with the single-plane rule, and75+// A GBM failure at any step falls back to the renderer's own images of the same kind.
75-// a GBM failure falls back to the renderer's own images of the same kind.
76 func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {76 func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
77 for name, tc := range map[string]struct {77 for name, tc := range map[string]struct {
78- setup func(t *testing.T, o *Output, r *portsmocks.MockRenderer)78+ setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer)
79 }{79 }{
80- "gbm unavailable": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {80+ "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) {
81 o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") }81 o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") }
82 }},82 }},
83- "no common modifier": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {83+ "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
84 a := newMockimageAllocator(t)84 a := newMockimageAllocator(t)
85 a.EXPECT().close().Return().Maybe()85 a.EXPECT().close().Return().Maybe()
86 o.openAlloc = func() (imageAllocator, error) { return a, nil }86 o.openAlloc = func() (imageAllocator, error) { return a, nil }
87 r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once()87 r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once()
88 }},88 }},
89- "allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {89+ "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
90 a := newMockimageAllocator(t)90 a := newMockimageAllocator(t)
91 a.EXPECT().close().Return().Maybe()91 a.EXPECT().close().Return().Maybe()
92 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once()92 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once()
93 o.openAlloc = func() (imageAllocator, error) { return a, nil }93 o.openAlloc = func() (imageAllocator, error) { return a, nil }
94 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()94 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
95 }},95 }},
96- "second allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {96+ "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
97 a := newMockimageAllocator(t)97 a := newMockimageAllocator(t)
98 a.EXPECT().close().Return().Maybe()98 a.EXPECT().close().Return().Maybe()
99 img := newMockimageBuffer(t)99 img := newMockimageBuffer(t)
100 img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once()100 img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once()
101 img.EXPECT().close().Return().Once() // freed with the failed attempt101 img.EXPECT().close().Return().Once() // freed with the failed attempt
102+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
102 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()103 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()
103 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()104 a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()
104 o.openAlloc = func() (imageAllocator, error) { return a, nil }105 o.openAlloc = func() (imageAllocator, error) { return a, nil }
105 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()106 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
106 }},107 }},
107- "import refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {108+ "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
108- a, closed := gbmImages(t, 1)109+ a, _ := twoGBMImages(t, 1)
109- _ = closed
110 o.openAlloc = func() (imageAllocator, error) { return a, nil }110 o.openAlloc = func() (imageAllocator, error) { return a, nil }
111 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()111 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
112 r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once()112 r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once()
113+ // The images made before the refusal go with a renderer drop.
114+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
113 }},115 }},
114- "framebuffer refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {116+ "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
115- a, _ := gbmImages(t, 1)117+ a, _ := twoGBMImages(t, 1)
116 o.openAlloc = func() (imageAllocator, error) { return a, nil }118 o.openAlloc = func() (imageAllocator, error) { return a, nil }
117 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()119 r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
118 r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()120 r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()
119 r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()121 r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
120- o.k.(*mockkms).EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()122+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
121 }},123 }},
122 } {124 } {
123 t.Run(name, func(t *testing.T) {125 t.Run(name, func(t *testing.T) {
124 o, k, _ := testOutput(t)126 o, k, _ := testOutput(t)
125 o.cursor = nil127 o.cursor = nil
126 r := gbmRenderer(t)128 r := gbmRenderer(t)
127- tc.setup(t, o, r)129+ tc.setup(t, o, k, r)
128 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()130 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
129 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()131 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
130 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()132 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
@@ -133,8 +135,9 @@ func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
133 if err := o.showImages(r, imagesDriver, nil); err != nil {135 if err := o.showImages(r, imagesDriver, nil); err != nil {
134 t.Fatal(err)136 t.Fatal(err)
135 }137 }
136- if o.imageAlloc != allocVulkan || o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 {138+ // The renderer's own images: no GBM image is alive.
137- t.Fatalf("alloc %q kind %d images %d fbs %v", o.imageAlloc, o.kind, len(o.images), o.fbs)139+ if o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 {
140+ t.Fatalf("kind %d images %d fbs %v", o.kind, len(o.images), o.fbs)
138 }141 }
139 })142 })
140 }143 }
@@ -167,7 +170,7 @@ func TestGBMOpenFailureIsRemembered(t *testing.T) {
167 func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {170 func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
168 o, k, commits := testOutput(t, unix.EINVAL)171 o, k, commits := testOutput(t, unix.EINVAL)
169 o.cursor = nil172 o.cursor = nil
170- a, closed := gbmImages(t, 1)173+ a, closed := twoGBMImages(t, 1)
171 o.openAlloc = func() (imageAllocator, error) { return a, nil }174 o.openAlloc = func() (imageAllocator, error) { return a, nil }
172 r := gbmRenderer(t)175 r := gbmRenderer(t)
173 r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()176 r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
@@ -182,28 +185,33 @@ func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
182 if err := o.showImages(r, imagesDriver, nil); err != nil {185 if err := o.showImages(r, imagesDriver, nil); err != nil {
183 t.Fatal(err)186 t.Fatal(err)
184 }187 }
185- if o.imageAlloc != allocVulkan || o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 {188+ if o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 {
186- t.Fatalf("alloc %q kind %d closed %d images %d commits %d", o.imageAlloc, o.kind, *closed, len(o.images), len(*commits))189+ t.Fatalf("kind %d closed %d images %d commits %d", o.kind, *closed, len(o.images), len(*commits))
187 }190 }
188 }191 }
189 192
190-// Closing the output frees the GBM images and the allocator.193+// Closing the output frees the GBM images and closes the allocator once.
191-func TestOutputFreeImagesClosesGBMImages(t *testing.T) {194+func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) {
192 o, k, _ := testOutput(t)195 o, k, _ := testOutput(t)
196+ o.cursor = nil
193 img := newMockimageBuffer(t)197 img := newMockimageBuffer(t)
194 img.EXPECT().close().Return().Twice()198 img.EXPECT().close().Return().Twice()
199+ alloc := newMockimageAllocator(t)
200+ alloc.EXPECT().close().Return().Once()
195 o.images = []imageBuffer{img, img}201 o.images = []imageBuffer{img, img}
202+ o.alloc = alloc
196 o.fbs = [2]uint32{70, 71}203 o.fbs = [2]uint32{70, 71}
197- k.EXPECT().rmFB(mock.Anything).Return(nil).Twice()204+ k.EXPECT().rmFB(uint32(70)).Return(nil).Once()
198- o.freeImages()205+ k.EXPECT().rmFB(uint32(71)).Return(nil).Once()
199- if o.images != nil || o.fbs != [2]uint32{} {206+ o.Close()
200- t.Fatalf("images %v fbs %v", o.images, o.fbs)207+ if o.images != nil || o.alloc != nil || o.fbs != [2]uint32{} {
208+ t.Fatalf("images %v alloc %v fbs %v", o.images, o.alloc, o.fbs)
201 }209 }
202 }210 }
203 211
204 // A setup that finds images left over frees them first, with the renderer's212 // A setup that finds images left over frees them first, with the renderer's
205-// targets over GBM ones, and a refused modeset of GBM images (no fallback213+// targets over GBM ones: the renderer never keeps a target over a freed
206-// left) drops the targets with the buffers.214+// image.
207 func TestSetupStartsFromCleanState(t *testing.T) {215 func TestSetupStartsFromCleanState(t *testing.T) {
208 o, k, _ := testOutput(t)216 o, k, _ := testOutput(t)
209 o.cursor = nil217 o.cursor = nil
@@ -224,55 +232,3 @@ func TestSetupStartsFromCleanState(t *testing.T) {
224 t.Fatalf("images %v fbs %v", o.images, o.fbs)232 t.Fatalf("images %v fbs %v", o.images, o.fbs)
225 }233 }
226 }234 }
227-
228-// Without CPU fence waits every fence is ready; with them only a signalled
229-// client fence is (a pipe stands in: readable means signalled).
230-func TestClientFenceReady(t *testing.T) {
231- r, w, err := os.Pipe()
232- if err != nil {
233- t.Fatal(err)
234- }
235- defer r.Close()
236- defer w.Close()
237- o, _, _ := testOutput(t)
238- if !o.clientFenceReady(r) || !o.clientFenceReady(nil) {
239- t.Fatal("fence not ready without CPU waits")
240- }
241- o.cpuFences = true
242- if !o.clientFenceReady(nil) {
243- t.Fatal("no fence must be ready")
244- }
245- if o.clientFenceReady(r) {
246- t.Fatal("pending client fence ready")
247- }
248- _, _ = w.Write([]byte{1})
249- if !o.clientFenceReady(r) {
250- t.Fatal("signalled client fence not ready")
251- }
252-}
253-
254-// With CPU waits a client buffer whose fence is pending is composed, not
255-// scanned out and not put on the overlay: neither plane waits for it. Once
256-// the fence signalled the same frame scans out.
257-func TestPendingClientFenceComposes(t *testing.T) {
258- r, w, err := os.Pipe()
259- if err != nil {
260- t.Fatal(err)
261- }
262- defer r.Close()
263- defer w.Close()
264- o, k, _ := testOutput(t)
265- k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
266- o.cpuFences = true
267- o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
268- s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}}
269- c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, Acquire: r, DMABuf: &ports.DMABuf{ID: 5, Format: fourccXRGB, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{File: r}}}}
270- contents := map[ports.WindowID]ports.SurfaceContent{1: c}
271- if fb, _, _ := o.scanoutFrame(s, contents); fb != 0 || o.reason != "fence_pending" {
272- t.Fatalf("pending fence: fb %d reason %q", fb, o.reason)
273- }
274- _, _ = w.Write([]byte{1})
275- if fb, _, _ := o.scanoutFrame(s, contents); fb == 0 {
276- t.Fatalf("signalled fence: reason %q", o.reason)
277- }
278-}
M internal/adapters/drm/fencegate.go +67 -23
@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/bnema/neferwl/internal/adapters/syncfile"
1414 "github.com/bnema/zerowrap"
15+ "golang.org/x/sys/unix"
1516 )
1617
1718 // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
@@ -52,20 +53,19 @@ func driverName(fd int) (string, error) {
5253 }
5354
5455 // parseVersion reads a dotted version such as "560.35.03" (missing parts
55-// are 0).
56+// are 0). Parts are plain digits: no sign.
5657 func parseVersion(s string) ([3]int, error) {
5758 var v [3]int
5859 parts := strings.Split(strings.TrimSpace(s), ".")
59- if len(parts) == 0 || len(parts) > 3 || parts[0] == "" {
60+ if len(parts) > len(v) {
6061 return v, fmt.Errorf("version %q", s)
6162 }
6263 for i, p := range parts {
63- // Digits only: Atoi would also take a sign.
64- n, err := strconv.Atoi(p)
65- if err != nil || p[0] < '0' || p[0] > '9' {
64+ n, err := strconv.ParseUint(p, 10, 31)
65+ if err != nil {
6666 return v, fmt.Errorf("version %q", s)
6767 }
68- v[i] = n
68+ v[i] = int(n)
6969 }
7070 return v, nil
7171 }
@@ -110,7 +110,7 @@ func fenceGate(fd int, log zerowrap.Logger) bool {
110110 if !brokenInFence(driver, version, verr) {
111111 return false
112112 }
113- log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU")
113+ log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU")
114114 return true
115115 }
116116
@@ -123,32 +123,63 @@ func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) {
123123 req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd()))
124124 }
125125
126-// fenceWaitLimit bounds a CPU wait on a fence of our own render.
126+// fenceWaitLimit bounds the CPU wait of one commit, whatever its number of
127+// fences.
127128 const fenceWaitLimit = time.Second
128129
130+// fenceWarnEvery is the least time between two warnings about a fence wait
131+// that hit fenceWaitLimit.
132+const fenceWarnEvery = 10 * time.Second
133+
134+// fencePoll polls one fence without blocking. fn is built once, so polling
135+// allocates nothing; the output goroutine owns it.
136+type fencePoll struct {
137+ fds [1]unix.PollFd
138+ ready bool
139+ fn func(fd uintptr)
140+}
141+
142+func (p *fencePoll) init() {
143+ p.fn = func(fd uintptr) {
144+ p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN}
145+ n, err := unix.Poll(p.fds[:], 0)
146+ p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0
147+ }
148+}
149+
129150 // clientFenceReady reports whether a client's acquire fence lets its buffer
130151 // be shown by a plane this frame. Where fences are waited for on the CPU, a
131152 // client fence that has not signalled must not stall the output: the buffer
132-// is composed instead (the renderer waits for it on the GPU). A nil or
133-// unpollable fence counts as signalled, as in signalled.
153+// is composed instead (the renderer waits for it on the GPU). Without CPU
154+// waits, and for no fence, it is ready; with them, a fence that cannot be
155+// polled is not ready.
134156 func (o *Output) clientFenceReady(acquire *os.File) bool {
135157 if !o.cpuFences || acquire == nil {
136158 return true
137159 }
138- // Wayland may close its file at any time: poll a duplicate.
139- d := dupFence(acquire)
140- if d == nil {
141- return true
160+ if o.fencePoll.fn == nil {
161+ o.fencePoll.init()
142162 }
143- defer d.Close()
144- return signalled([]*os.File{d})
163+ raw, err := acquire.SyscallConn()
164+ if err != nil {
165+ return false
166+ }
167+ o.fencePoll.ready = false
168+ // Control keeps the descriptor open for the call: Wayland closing its
169+ // file meanwhile cannot hand us a reused number.
170+ if raw.Control(o.fencePoll.fn) != nil {
171+ return false
172+ }
173+ return o.fencePoll.ready
145174 }
146175
147-// waitFences blocks until our own render fences signalled when the driver
148-// cannot wait for them in the commit, for at most fenceWaitLimit each, or
149-// until the output stops. Client fences are not waited for here: planes take
150-// only buffers whose fence signalled (clientFenceReady). A failed wait is not
151-// fatal: the frame may then show early, as it would on any such driver.
176+// waitFences blocks until the fences signalled when the driver cannot wait
177+// for them in the commit, for at most fenceWaitLimit in all, or until the
178+// output stops. A client fence passed here (direct scanout) already
179+// signalled: planes take only such buffers (clientFenceReady), so it
180+// returns at once. A failed or timed out wait is not fatal: the frame may
181+// then show early, as it would on any such driver. A timeout is counted in
182+// the stats and warned about at most every fenceWarnEvery.
152183 func (o *Output) waitFences(fences ...*os.File) {
153184 if !o.cpuFences {
154185 return
@@ -157,13 +188,26 @@ func (o *Output) waitFences(fences ...*os.File) {
157188 if ctx == nil {
158189 ctx = context.Background()
159190 }
160- ctx, cancel := context.WithTimeout(ctx, fenceWaitLimit)
191+ limit := o.fenceLimit
192+ if limit == 0 {
193+ limit = fenceWaitLimit
194+ }
195+ ctx, cancel := context.WithTimeout(ctx, limit)
161196 defer cancel()
162197 for _, f := range fences {
163198 if f == nil {
164199 continue
165200 }
166- if err := syncfile.Wait(ctx, f); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
201+ err := syncfile.Wait(ctx, f)
202+ switch {
203+ case err == nil || errors.Is(err, context.Canceled):
204+ case errors.Is(err, context.DeadlineExceeded):
205+ o.flipStats.fenceTimeouts++
206+ if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery {
207+ o.fenceWarnAt = now
208+ o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway")
209+ }
210+ default:
167211 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
168212 }
169213 }
@@ -12,6 +12,7 @@ import (
12 12
13 "github.com/bnema/neferwl/internal/adapters/syncfile"13 "github.com/bnema/neferwl/internal/adapters/syncfile"
14 "github.com/bnema/zerowrap"14 "github.com/bnema/zerowrap"
15+ "golang.org/x/sys/unix"
15 )16 )
16 17
17 // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for18 // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
@@ -52,20 +53,19 @@ func driverName(fd int) (string, error) {
52 }53 }
53 54
54 // parseVersion reads a dotted version such as "560.35.03" (missing parts55 // parseVersion reads a dotted version such as "560.35.03" (missing parts
55-// are 0).56+// are 0). Parts are plain digits: no sign.
56 func parseVersion(s string) ([3]int, error) {57 func parseVersion(s string) ([3]int, error) {
57 var v [3]int58 var v [3]int
58 parts := strings.Split(strings.TrimSpace(s), ".")59 parts := strings.Split(strings.TrimSpace(s), ".")
59- if len(parts) == 0 || len(parts) > 3 || parts[0] == "" {60+ if len(parts) > len(v) {
60 return v, fmt.Errorf("version %q", s)61 return v, fmt.Errorf("version %q", s)
61 }62 }
62 for i, p := range parts {63 for i, p := range parts {
63- // Digits only: Atoi would also take a sign.64+ n, err := strconv.ParseUint(p, 10, 31)
64- n, err := strconv.Atoi(p)65+ if err != nil {
65- if err != nil || p[0] < '0' || p[0] > '9' {
66 return v, fmt.Errorf("version %q", s)66 return v, fmt.Errorf("version %q", s)
67 }67 }
68- v[i] = n68+ v[i] = int(n)
69 }69 }
70 return v, nil70 return v, nil
71 }71 }
@@ -110,7 +110,7 @@ func fenceGate(fd int, log zerowrap.Logger) bool {
110 if !brokenInFence(driver, version, verr) {110 if !brokenInFence(driver, version, verr) {
111 return false111 return false
112 }112 }
113- log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU")113+ log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU")
114 return true114 return true
115 }115 }
116 116
@@ -123,32 +123,63 @@ func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) {
123 req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd()))123 req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd()))
124 }124 }
125 125
126-// fenceWaitLimit bounds a CPU wait on a fence of our own render.126+// fenceWaitLimit bounds the CPU wait of one commit, whatever its number of
127+// fences.
127 const fenceWaitLimit = time.Second128 const fenceWaitLimit = time.Second
128 129
130+// fenceWarnEvery is the least time between two warnings about a fence wait
131+// that hit fenceWaitLimit.
132+const fenceWarnEvery = 10 * time.Second
133+
134+// fencePoll polls one fence without blocking. fn is built once, so polling
135+// allocates nothing; the output goroutine owns it.
136+type fencePoll struct {
137+ fds [1]unix.PollFd
138+ ready bool
139+ fn func(fd uintptr)
140+}
141+
142+func (p *fencePoll) init() {
143+ p.fn = func(fd uintptr) {
144+ p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN}
145+ n, err := unix.Poll(p.fds[:], 0)
146+ p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0
147+ }
148+}
149+
129 // clientFenceReady reports whether a client's acquire fence lets its buffer150 // clientFenceReady reports whether a client's acquire fence lets its buffer
130 // be shown by a plane this frame. Where fences are waited for on the CPU, a151 // be shown by a plane this frame. Where fences are waited for on the CPU, a
131 // client fence that has not signalled must not stall the output: the buffer152 // client fence that has not signalled must not stall the output: the buffer
132-// is composed instead (the renderer waits for it on the GPU). A nil or153+// is composed instead (the renderer waits for it on the GPU). Without CPU
133-// unpollable fence counts as signalled, as in signalled.154+// waits, and for no fence, it is ready; with them, a fence that cannot be
155+// polled is not ready.
134 func (o *Output) clientFenceReady(acquire *os.File) bool {156 func (o *Output) clientFenceReady(acquire *os.File) bool {
135 if !o.cpuFences || acquire == nil {157 if !o.cpuFences || acquire == nil {
136 return true158 return true
137 }159 }
138- // Wayland may close its file at any time: poll a duplicate.160+ if o.fencePoll.fn == nil {
139- d := dupFence(acquire)161+ o.fencePoll.init()
140- if d == nil {
141- return true
142 }162 }
143- defer d.Close()163+ raw, err := acquire.SyscallConn()
144- return signalled([]*os.File{d})164+ if err != nil {
165+ return false
166+ }
167+ o.fencePoll.ready = false
168+ // Control keeps the descriptor open for the call: Wayland closing its
169+ // file meanwhile cannot hand us a reused number.
170+ if raw.Control(o.fencePoll.fn) != nil {
171+ return false
172+ }
173+ return o.fencePoll.ready
145 }174 }
146 175
147-// waitFences blocks until our own render fences signalled when the driver176+// waitFences blocks until the fences signalled when the driver cannot wait
148-// cannot wait for them in the commit, for at most fenceWaitLimit each, or177+// for them in the commit, for at most fenceWaitLimit in all, or until the
149-// until the output stops. Client fences are not waited for here: planes take178+// output stops. A client fence passed here (direct scanout) already
150-// only buffers whose fence signalled (clientFenceReady). A failed wait is not179+// signalled: planes take only such buffers (clientFenceReady), so it
151-// fatal: the frame may then show early, as it would on any such driver.180+// returns at once. A failed or timed out wait is not fatal: the frame may
181+// then show early, as it would on any such driver. A timeout is counted in
182+// the stats and warned about at most every fenceWarnEvery.
152 func (o *Output) waitFences(fences ...*os.File) {183 func (o *Output) waitFences(fences ...*os.File) {
153 if !o.cpuFences {184 if !o.cpuFences {
154 return185 return
@@ -157,13 +188,26 @@ func (o *Output) waitFences(fences ...*os.File) {
157 if ctx == nil {188 if ctx == nil {
158 ctx = context.Background()189 ctx = context.Background()
159 }190 }
160- ctx, cancel := context.WithTimeout(ctx, fenceWaitLimit)191+ limit := o.fenceLimit
192+ if limit == 0 {
193+ limit = fenceWaitLimit
194+ }
195+ ctx, cancel := context.WithTimeout(ctx, limit)
161 defer cancel()196 defer cancel()
162 for _, f := range fences {197 for _, f := range fences {
163 if f == nil {198 if f == nil {
164 continue199 continue
165 }200 }
166- if err := syncfile.Wait(ctx, f); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {201+ err := syncfile.Wait(ctx, f)
202+ switch {
203+ case err == nil || errors.Is(err, context.Canceled):
204+ case errors.Is(err, context.DeadlineExceeded):
205+ o.flipStats.fenceTimeouts++
206+ if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery {
207+ o.fenceWarnAt = now
208+ o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway")
209+ }
210+ default:
167 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")211 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
168 }212 }
169 }213 }
M internal/adapters/drm/fencegate_test.go +206 -45
@@ -1,14 +1,31 @@
11 package drm
22
33 import (
4+ "context"
45 "errors"
56 "os"
7+ "path/filepath"
68 "testing"
79 "time"
810
9- "github.com/bnema/zerowrap"
11+ "github.com/bnema/neferwl/internal/ports"
12+ "github.com/stretchr/testify/mock"
13+ "golang.org/x/sys/unix"
1014 )
1115
16+// pipeFence returns a pipe standing in for a sync file: its read end is
17+// readable (signalled) once the write end got a byte. Both ends close with
18+// the test.
19+func pipeFence(t *testing.T) (fence, signal *os.File) {
20+ t.Helper()
21+ r, w, err := os.Pipe()
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ t.Cleanup(func() { r.Close(); w.Close() })
26+ return r, w
27+}
28+
1229 func TestParseVersion(t *testing.T) {
1330 for _, tc := range []struct {
1431 in string
@@ -26,7 +43,9 @@ func TestParseVersion(t *testing.T) {
2643 {"-1.2", [3]int{}, false},
2744 {"+560.35.3", [3]int{}, false},
2845 {"560.+35.3", [3]int{}, false},
46+ {"560.-5.3", [3]int{}, false},
2947 {"560..3", [3]int{}, false},
48+ {"99999999999.1.1", [3]int{}, false},
3049 } {
3150 got, err := parseVersion(tc.in)
3251 if (err == nil) != tc.ok || tc.ok && got != tc.want {
@@ -35,25 +54,8 @@ func TestParseVersion(t *testing.T) {
3554 }
3655 }
3756
38-func TestVersionLess(t *testing.T) {
39- for _, tc := range []struct {
40- a, b [3]int
41- want bool
42- }{
43- {[3]int{560, 35, 2}, [3]int{560, 35, 3}, true},
44- {[3]int{560, 35, 3}, [3]int{560, 35, 3}, false},
45- {[3]int{560, 35, 5}, [3]int{560, 35, 3}, false},
46- {[3]int{550, 120, 0}, [3]int{560, 35, 3}, true},
47- {[3]int{560, 36, 0}, [3]int{560, 35, 3}, false},
48- {[3]int{615, 78, 8}, [3]int{560, 35, 3}, false},
49- {[3]int{470, 99, 99}, [3]int{560, 0, 0}, true},
50- } {
51- if got := versionLess(tc.a, tc.b); got != tc.want {
52- t.Errorf("versionLess(%v, %v) = %v", tc.a, tc.b, got)
53- }
54- }
55-}
56-
57+// The gate opens for nvidia-drm before 560.35.3 or of unknown version, and
58+// for no other driver.
5759 func TestBrokenInFence(t *testing.T) {
5860 for _, tc := range []struct {
5961 name, driver, version string
@@ -63,8 +65,12 @@ func TestBrokenInFence(t *testing.T) {
6365 {"amd", "amdgpu", "", nil, false},
6466 {"intel", "i915", "garbage", nil, false},
6567 {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
66- {"just before", driverNVIDIA, "560.35.2", nil, true},
68+ {"one patch before", driverNVIDIA, "560.35.2", nil, true},
69+ {"minor before", driverNVIDIA, "560.34.99", nil, true},
6770 {"fixed", driverNVIDIA, "560.35.03\n", nil, false},
71+ {"patch after", driverNVIDIA, "560.35.5", nil, false},
72+ {"minor after", driverNVIDIA, "560.36.0", nil, false},
73+ {"major after", driverNVIDIA, "561.0.0", nil, false},
6874 {"newer", driverNVIDIA, "615.78.08", nil, false},
6975 {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
7076 {"unparsable", driverNVIDIA, "n/a", nil, true},
@@ -75,14 +81,32 @@ func TestBrokenInFence(t *testing.T) {
7581 }
7682 }
7783
84+// The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node
85+// answers it too: skips without one.
86+func TestDriverName(t *testing.T) {
87+ nodes, _ := filepath.Glob("/dev/dri/renderD*")
88+ if len(nodes) == 0 {
89+ t.Skip("no /dev/dri/renderD* node")
90+ }
91+ for _, n := range nodes {
92+ f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
93+ if err != nil {
94+ continue
95+ }
96+ defer f.Close()
97+ name, err := driverName(int(f.Fd()))
98+ if err != nil || name == "" {
99+ t.Fatalf("driverName(%s) = %q, %v", n, name, err)
100+ }
101+ t.Logf("%s: %s", n, name)
102+ return
103+ }
104+ t.Skip("no render node can be opened")
105+}
106+
78107 // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
79108 func TestFenceProp(t *testing.T) {
80- f, w, err := os.Pipe()
81- if err != nil {
82- t.Fatal(err)
83- }
84- defer f.Close()
85- defer w.Close()
109+ f, _ := pipeFence(t)
86110 for _, cpu := range []bool{false, true} {
87111 o, _, _ := testOutput(t)
88112 o.cpuFences = cpu
@@ -96,27 +120,164 @@ func TestFenceProp(t *testing.T) {
96120 }
97121 }
98122
99-// The CPU wait returns once the fence signals (a pipe stands in: readable
100-// means signalled) and gives up after fenceWaitLimit.
123+// waitFences returns for a fence that signalled, for one that never will
124+// once the output stops, and at once where fences go to the kernel.
101125 func TestWaitFences(t *testing.T) {
126+ returns := func(t *testing.T, o *Output, fences ...*os.File) {
127+ t.Helper()
128+ done := make(chan struct{})
129+ go func() { o.waitFences(fences...); close(done) }()
130+ select {
131+ case <-done:
132+ case <-time.After(30 * time.Second): // failure guard only
133+ t.Fatal("waitFences did not return")
134+ }
135+ }
136+ t.Run("signalled", func(t *testing.T) {
137+ o, _, _ := testOutput(t)
138+ o.cpuFences = true
139+ f, w := pipeFence(t)
140+ _, _ = w.Write([]byte{1})
141+ returns(t, o, nil, f)
142+ })
143+ t.Run("never signalled, output stopped", func(t *testing.T) {
144+ o, _, _ := testOutput(t)
145+ o.cpuFences = true
146+ ctx, cancel := context.WithCancel(t.Context())
147+ cancel()
148+ o.runContext = ctx
149+ f, _ := pipeFence(t)
150+ returns(t, o, f)
151+ if o.flipStats.fenceTimeouts != 0 {
152+ t.Fatal("a stopped output counted a fence timeout")
153+ }
154+ })
155+ t.Run("kernel waits", func(t *testing.T) {
156+ o, _, _ := testOutput(t)
157+ f, _ := pipeFence(t)
158+ returns(t, o, f)
159+ })
160+}
161+
162+// A fence wait that hit its limit counts in the stats, and warns at most
163+// once per fenceWarnEvery.
164+func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) {
102165 o, _, _ := testOutput(t)
103166 o.cpuFences = true
104- o.log = zerowrap.Default()
105- r, w, err := os.Pipe()
106- if err != nil {
167+ o.fenceLimit = time.Millisecond
168+ f, _ := pipeFence(t)
169+ o.waitFences(f)
170+ if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() {
171+ t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt)
172+ }
173+ first := o.fenceWarnAt
174+ o.waitFences(f)
175+ if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) {
176+ t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt)
177+ }
178+}
179+
180+// Without CPU fence waits every fence is ready; with them only a signalled
181+// client fence is, and one that cannot be polled is not.
182+func TestClientFenceReady(t *testing.T) {
183+ f, w := pipeFence(t)
184+ o, _, _ := testOutput(t)
185+ if !o.clientFenceReady(f) || !o.clientFenceReady(nil) {
186+ t.Fatal("fence not ready without CPU waits")
187+ }
188+ o.cpuFences = true
189+ if !o.clientFenceReady(nil) {
190+ t.Fatal("no fence must be ready")
191+ }
192+ if o.clientFenceReady(f) {
193+ t.Fatal("pending client fence ready")
194+ }
195+ _, _ = w.Write([]byte{1})
196+ if !o.clientFenceReady(f) {
197+ t.Fatal("signalled client fence not ready")
198+ }
199+ closed, cw := pipeFence(t)
200+ closed.Close()
201+ cw.Close()
202+ if o.clientFenceReady(closed) {
203+ t.Fatal("closed fence ready")
204+ }
205+}
206+
207+// Polling a fence allocates nothing.
208+func TestClientFenceReadyAllocatesNothing(t *testing.T) {
209+ f, _ := pipeFence(t)
210+ o, _, _ := testOutput(t)
211+ o.cpuFences = true
212+ o.clientFenceReady(f) // builds the poll closure
213+ if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 {
214+ t.Fatalf("%v allocations per poll", n)
215+ }
216+}
217+
218+// With CPU waits a client buffer whose fence is pending is composed, on the
219+// primary plane and on the overlay alike: neither plane waits for it. Once
220+// the fence signalled the same frame uses the plane.
221+func TestPendingClientFenceComposes(t *testing.T) {
222+ for _, tc := range []struct {
223+ name string
224+ reason func(o *Output) string
225+ // decide returns whether the plane took the buffer.
226+ decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool
227+ }{
228+ {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
229+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
230+ s, c := fullscreenScene()
231+ content := c[1]
232+ content.Acquire = fence
233+ fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content})
234+ return fb != 0
235+ }},
236+ {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
237+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
238+ s, c := overlayScene()
239+ content := c[2]
240+ content.Acquire = fence
241+ ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content})
242+ ov.close()
243+ return ov.fb != 0
244+ }},
245+ } {
246+ t.Run(tc.name, func(t *testing.T) {
247+ o, k, _ := overlayOutput(t)
248+ k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
249+ o.cpuFences = true
250+ fence, w := pipeFence(t)
251+ if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" {
252+ t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o))
253+ }
254+ _, _ = w.Write([]byte{1})
255+ if !tc.decide(t, o, k, fence) {
256+ t.Fatalf("signalled fence: reason %q", tc.reason(o))
257+ }
258+ })
259+ }
260+}
261+
262+// A commit that is retried (here: VRR refused, then the same frame without
263+// it) waits for its fence once, not once per attempt.
264+func TestCommitWaitsForItsFenceOnce(t *testing.T) {
265+ o, _, commits := testOutput(t, unix.EINVAL)
266+ o.cpuFences = true
267+ o.fenceLimit = time.Millisecond
268+ fence, _ := pipeFence(t)
269+ if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
107270 t.Fatal(err)
108271 }
109- defer r.Close()
110- go func() {
111- time.Sleep(20 * time.Millisecond)
112- _, _ = w.Write([]byte{1})
113- }()
114- start := time.Now()
115- o.waitFences(nil, r)
116- if d := time.Since(start); d < 15*time.Millisecond || d > fenceWaitLimit/2 {
117- t.Fatalf("waited %v for the fence", d)
118- }
119- w.Close()
120- o.cpuFences = false
121- o.waitFences(r) // not waited for: returns at once
272+ if len(*commits) < 2 {
273+ t.Fatalf("%d commits: no retry happened", len(*commits))
274+ }
275+ if o.flipStats.fenceTimeouts != 1 {
276+ t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits))
277+ }
278+ for i, c := range *commits {
279+ if _, ok := c.req.value(tPrimary, pFence); ok {
280+ t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i)
281+ }
282+ }
122283 }
@@ -1,14 +1,31 @@
1 package drm1 package drm
2 2
3 import (3 import (
4+ "context"
4 "errors"5 "errors"
5 "os"6 "os"
7+ "path/filepath"
6 "testing"8 "testing"
7 "time"9 "time"
8 10
9- "github.com/bnema/zerowrap"11+ "github.com/bnema/neferwl/internal/ports"
12+ "github.com/stretchr/testify/mock"
13+ "golang.org/x/sys/unix"
10 )14 )
11 15
16+// pipeFence returns a pipe standing in for a sync file: its read end is
17+// readable (signalled) once the write end got a byte. Both ends close with
18+// the test.
19+func pipeFence(t *testing.T) (fence, signal *os.File) {
20+ t.Helper()
21+ r, w, err := os.Pipe()
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ t.Cleanup(func() { r.Close(); w.Close() })
26+ return r, w
27+}
28+
12 func TestParseVersion(t *testing.T) {29 func TestParseVersion(t *testing.T) {
13 for _, tc := range []struct {30 for _, tc := range []struct {
14 in string31 in string
@@ -26,7 +43,9 @@ func TestParseVersion(t *testing.T) {
26 {"-1.2", [3]int{}, false},43 {"-1.2", [3]int{}, false},
27 {"+560.35.3", [3]int{}, false},44 {"+560.35.3", [3]int{}, false},
28 {"560.+35.3", [3]int{}, false},45 {"560.+35.3", [3]int{}, false},
46+ {"560.-5.3", [3]int{}, false},
29 {"560..3", [3]int{}, false},47 {"560..3", [3]int{}, false},
48+ {"99999999999.1.1", [3]int{}, false},
30 } {49 } {
31 got, err := parseVersion(tc.in)50 got, err := parseVersion(tc.in)
32 if (err == nil) != tc.ok || tc.ok && got != tc.want {51 if (err == nil) != tc.ok || tc.ok && got != tc.want {
@@ -35,25 +54,8 @@ func TestParseVersion(t *testing.T) {
35 }54 }
36 }55 }
37 56
38-func TestVersionLess(t *testing.T) {57+// The gate opens for nvidia-drm before 560.35.3 or of unknown version, and
39- for _, tc := range []struct {58+// for no other driver.
40- a, b [3]int
41- want bool
42- }{
43- {[3]int{560, 35, 2}, [3]int{560, 35, 3}, true},
44- {[3]int{560, 35, 3}, [3]int{560, 35, 3}, false},
45- {[3]int{560, 35, 5}, [3]int{560, 35, 3}, false},
46- {[3]int{550, 120, 0}, [3]int{560, 35, 3}, true},
47- {[3]int{560, 36, 0}, [3]int{560, 35, 3}, false},
48- {[3]int{615, 78, 8}, [3]int{560, 35, 3}, false},
49- {[3]int{470, 99, 99}, [3]int{560, 0, 0}, true},
50- } {
51- if got := versionLess(tc.a, tc.b); got != tc.want {
52- t.Errorf("versionLess(%v, %v) = %v", tc.a, tc.b, got)
53- }
54- }
55-}
56-
57 func TestBrokenInFence(t *testing.T) {59 func TestBrokenInFence(t *testing.T) {
58 for _, tc := range []struct {60 for _, tc := range []struct {
59 name, driver, version string61 name, driver, version string
@@ -63,8 +65,12 @@ func TestBrokenInFence(t *testing.T) {
63 {"amd", "amdgpu", "", nil, false},65 {"amd", "amdgpu", "", nil, false},
64 {"intel", "i915", "garbage", nil, false},66 {"intel", "i915", "garbage", nil, false},
65 {"old nvidia", driverNVIDIA, "555.58.02", nil, true},67 {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
66- {"just before", driverNVIDIA, "560.35.2", nil, true},68+ {"one patch before", driverNVIDIA, "560.35.2", nil, true},
69+ {"minor before", driverNVIDIA, "560.34.99", nil, true},
67 {"fixed", driverNVIDIA, "560.35.03\n", nil, false},70 {"fixed", driverNVIDIA, "560.35.03\n", nil, false},
71+ {"patch after", driverNVIDIA, "560.35.5", nil, false},
72+ {"minor after", driverNVIDIA, "560.36.0", nil, false},
73+ {"major after", driverNVIDIA, "561.0.0", nil, false},
68 {"newer", driverNVIDIA, "615.78.08", nil, false},74 {"newer", driverNVIDIA, "615.78.08", nil, false},
69 {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},75 {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
70 {"unparsable", driverNVIDIA, "n/a", nil, true},76 {"unparsable", driverNVIDIA, "n/a", nil, true},
@@ -75,14 +81,32 @@ func TestBrokenInFence(t *testing.T) {
75 }81 }
76 }82 }
77 83
84+// The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node
85+// answers it too: skips without one.
86+func TestDriverName(t *testing.T) {
87+ nodes, _ := filepath.Glob("/dev/dri/renderD*")
88+ if len(nodes) == 0 {
89+ t.Skip("no /dev/dri/renderD* node")
90+ }
91+ for _, n := range nodes {
92+ f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
93+ if err != nil {
94+ continue
95+ }
96+ defer f.Close()
97+ name, err := driverName(int(f.Fd()))
98+ if err != nil || name == "" {
99+ t.Fatalf("driverName(%s) = %q, %v", n, name, err)
100+ }
101+ t.Logf("%s: %s", n, name)
102+ return
103+ }
104+ t.Skip("no render node can be opened")
105+}
106+
78 // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.107 // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
79 func TestFenceProp(t *testing.T) {108 func TestFenceProp(t *testing.T) {
80- f, w, err := os.Pipe()109+ f, _ := pipeFence(t)
81- if err != nil {
82- t.Fatal(err)
83- }
84- defer f.Close()
85- defer w.Close()
86 for _, cpu := range []bool{false, true} {110 for _, cpu := range []bool{false, true} {
87 o, _, _ := testOutput(t)111 o, _, _ := testOutput(t)
88 o.cpuFences = cpu112 o.cpuFences = cpu
@@ -96,27 +120,164 @@ func TestFenceProp(t *testing.T) {
96 }120 }
97 }121 }
98 122
99-// The CPU wait returns once the fence signals (a pipe stands in: readable123+// waitFences returns for a fence that signalled, for one that never will
100-// means signalled) and gives up after fenceWaitLimit.124+// once the output stops, and at once where fences go to the kernel.
101 func TestWaitFences(t *testing.T) {125 func TestWaitFences(t *testing.T) {
126+ returns := func(t *testing.T, o *Output, fences ...*os.File) {
127+ t.Helper()
128+ done := make(chan struct{})
129+ go func() { o.waitFences(fences...); close(done) }()
130+ select {
131+ case <-done:
132+ case <-time.After(30 * time.Second): // failure guard only
133+ t.Fatal("waitFences did not return")
134+ }
135+ }
136+ t.Run("signalled", func(t *testing.T) {
137+ o, _, _ := testOutput(t)
138+ o.cpuFences = true
139+ f, w := pipeFence(t)
140+ _, _ = w.Write([]byte{1})
141+ returns(t, o, nil, f)
142+ })
143+ t.Run("never signalled, output stopped", func(t *testing.T) {
144+ o, _, _ := testOutput(t)
145+ o.cpuFences = true
146+ ctx, cancel := context.WithCancel(t.Context())
147+ cancel()
148+ o.runContext = ctx
149+ f, _ := pipeFence(t)
150+ returns(t, o, f)
151+ if o.flipStats.fenceTimeouts != 0 {
152+ t.Fatal("a stopped output counted a fence timeout")
153+ }
154+ })
155+ t.Run("kernel waits", func(t *testing.T) {
156+ o, _, _ := testOutput(t)
157+ f, _ := pipeFence(t)
158+ returns(t, o, f)
159+ })
160+}
161+
162+// A fence wait that hit its limit counts in the stats, and warns at most
163+// once per fenceWarnEvery.
164+func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) {
102 o, _, _ := testOutput(t)165 o, _, _ := testOutput(t)
103 o.cpuFences = true166 o.cpuFences = true
104- o.log = zerowrap.Default()167+ o.fenceLimit = time.Millisecond
105- r, w, err := os.Pipe()168+ f, _ := pipeFence(t)
106- if err != nil {169+ o.waitFences(f)
170+ if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() {
171+ t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt)
172+ }
173+ first := o.fenceWarnAt
174+ o.waitFences(f)
175+ if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) {
176+ t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt)
177+ }
178+}
179+
180+// Without CPU fence waits every fence is ready; with them only a signalled
181+// client fence is, and one that cannot be polled is not.
182+func TestClientFenceReady(t *testing.T) {
183+ f, w := pipeFence(t)
184+ o, _, _ := testOutput(t)
185+ if !o.clientFenceReady(f) || !o.clientFenceReady(nil) {
186+ t.Fatal("fence not ready without CPU waits")
187+ }
188+ o.cpuFences = true
189+ if !o.clientFenceReady(nil) {
190+ t.Fatal("no fence must be ready")
191+ }
192+ if o.clientFenceReady(f) {
193+ t.Fatal("pending client fence ready")
194+ }
195+ _, _ = w.Write([]byte{1})
196+ if !o.clientFenceReady(f) {
197+ t.Fatal("signalled client fence not ready")
198+ }
199+ closed, cw := pipeFence(t)
200+ closed.Close()
201+ cw.Close()
202+ if o.clientFenceReady(closed) {
203+ t.Fatal("closed fence ready")
204+ }
205+}
206+
207+// Polling a fence allocates nothing.
208+func TestClientFenceReadyAllocatesNothing(t *testing.T) {
209+ f, _ := pipeFence(t)
210+ o, _, _ := testOutput(t)
211+ o.cpuFences = true
212+ o.clientFenceReady(f) // builds the poll closure
213+ if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 {
214+ t.Fatalf("%v allocations per poll", n)
215+ }
216+}
217+
218+// With CPU waits a client buffer whose fence is pending is composed, on the
219+// primary plane and on the overlay alike: neither plane waits for it. Once
220+// the fence signalled the same frame uses the plane.
221+func TestPendingClientFenceComposes(t *testing.T) {
222+ for _, tc := range []struct {
223+ name string
224+ reason func(o *Output) string
225+ // decide returns whether the plane took the buffer.
226+ decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool
227+ }{
228+ {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
229+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
230+ s, c := fullscreenScene()
231+ content := c[1]
232+ content.Acquire = fence
233+ fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content})
234+ return fb != 0
235+ }},
236+ {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool {
237+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
238+ s, c := overlayScene()
239+ content := c[2]
240+ content.Acquire = fence
241+ ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content})
242+ ov.close()
243+ return ov.fb != 0
244+ }},
245+ } {
246+ t.Run(tc.name, func(t *testing.T) {
247+ o, k, _ := overlayOutput(t)
248+ k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
249+ o.cpuFences = true
250+ fence, w := pipeFence(t)
251+ if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" {
252+ t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o))
253+ }
254+ _, _ = w.Write([]byte{1})
255+ if !tc.decide(t, o, k, fence) {
256+ t.Fatalf("signalled fence: reason %q", tc.reason(o))
257+ }
258+ })
259+ }
260+}
261+
262+// A commit that is retried (here: VRR refused, then the same frame without
263+// it) waits for its fence once, not once per attempt.
264+func TestCommitWaitsForItsFenceOnce(t *testing.T) {
265+ o, _, commits := testOutput(t, unix.EINVAL)
266+ o.cpuFences = true
267+ o.fenceLimit = time.Millisecond
268+ fence, _ := pipeFence(t)
269+ if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
107 t.Fatal(err)270 t.Fatal(err)
108 }271 }
109- defer r.Close()272+ if len(*commits) < 2 {
110- go func() {273+ t.Fatalf("%d commits: no retry happened", len(*commits))
111- time.Sleep(20 * time.Millisecond)274+ }
112- _, _ = w.Write([]byte{1})275+ if o.flipStats.fenceTimeouts != 1 {
113- }()276+ t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits))
114- start := time.Now()277+ }
115- o.waitFences(nil, r)278+ for i, c := range *commits {
116- if d := time.Since(start); d < 15*time.Millisecond || d > fenceWaitLimit/2 {279+ if _, ok := c.req.value(tPrimary, pFence); ok {
117- t.Fatalf("waited %v for the fence", d)280+ t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i)
118- }281+ }
119- w.Close()282+ }
120- o.cpuFences = false
121- o.waitFences(r) // not waited for: returns at once
122 }283 }
M internal/adapters/drm/flip_stats.go +3 -0
@@ -30,6 +30,9 @@ type flipStats struct {
3030 // stateCarried counts deferred state changes a frame carried instead.
3131 // Many state commits while a video plays point at a lost vblank each.
3232 stateCommits, stateCarried int
33+ // fenceTimeouts counts commits whose CPU fence wait hit its limit
34+ // (fencegate.go).
35+ fenceTimeouts int
3336 }
3437
3538 // nominalPeriod is the time between two vblanks at the mode's refresh
@@ -30,6 +30,9 @@ type flipStats struct {
30 // stateCarried counts deferred state changes a frame carried instead.30 // stateCarried counts deferred state changes a frame carried instead.
31 // Many state commits while a video plays point at a lost vblank each.31 // Many state commits while a video plays point at a lost vblank each.
32 stateCommits, stateCarried int32 stateCommits, stateCarried int
33+ // fenceTimeouts counts commits whose CPU fence wait hit its limit
34+ // (fencegate.go).
35+ fenceTimeouts int
33 }36 }
34 37
35 // nominalPeriod is the time between two vblanks at the mode's refresh38 // nominalPeriod is the time between two vblanks at the mode's refresh
M internal/adapters/drm/gbm/gbm.go +23 -59
@@ -18,9 +18,6 @@ import (
1818 "golang.org/x/sys/unix"
1919 )
2020
21-// ErrUnavailable marks a missing or unusable libgbm.
22-var ErrUnavailable = errors.New("gbm unavailable")
23-
2421 // Usage is a set of GBM_BO_USE_* flags.
2522 type Usage uint32
2623
@@ -29,8 +26,8 @@ const (
2926 UseScanout Usage = 1 << 0
3027 // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.
3128 UseRendering Usage = 1 << 2
32- // UseLinear (GBM_BO_USE_LINEAR): the layout is linear.
33- UseLinear Usage = 1 << 4
29+ // useLinear (GBM_BO_USE_LINEAR): the layout is linear.
30+ useLinear Usage = 1 << 4
3431 )
3532
3633 const (
@@ -49,56 +46,42 @@ var (
4946 createDevice func(fd int32) uintptr
5047 destroyDevice func(dev uintptr)
5148 createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr
52- createWithModifiers func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32) uintptr
5349 createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr
5450 boModifier func(bo uintptr) uint64
5551 boPlaneCount func(bo uintptr) int32
5652 boFDForPlane func(bo uintptr, plane int32) int32
57- boFD func(bo uintptr) int32
5853 boStrideForPlane func(bo uintptr, plane int32) uint32
5954 boOffset func(bo uintptr, plane int32) uint32
6055 boDestroy func(bo uintptr)
6156 )
6257
63-// load opens libgbm once. Without it (or with a lacking one) the allocator
64-// is unavailable and callers fall back to another way to make images.
58+// load opens libgbm once. Without it, or with one that lacks a function
59+// below (gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane came with
60+// Mesa 21.1), the allocator is unavailable and callers fall back to another
61+// way to make images.
6562 func load() error {
6663 loadOnce.Do(func() {
6764 lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
6865 if err != nil {
69- loadErr = fmt.Errorf("%w: open libgbm.so.1: %v", ErrUnavailable, err)
66+ loadErr = fmt.Errorf("open libgbm.so.1: %w", err)
7067 return
7168 }
7269 defer func() {
7370 if r := recover(); r != nil {
74- loadErr = fmt.Errorf("%w: libgbm: %v", ErrUnavailable, r)
71+ loadErr = fmt.Errorf("libgbm: %v", r)
7572 }
7673 }()
7774 reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }
78- have := func(name string) bool { p, err := purego.Dlsym(lib, name); return err == nil && p != 0 }
7975 reg(&createDevice, "gbm_create_device")
8076 reg(&destroyDevice, "gbm_device_destroy")
77+ reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
8178 reg(&createPlain, "gbm_bo_create")
8279 reg(&boModifier, "gbm_bo_get_modifier")
8380 reg(&boPlaneCount, "gbm_bo_get_plane_count")
84- if have("gbm_bo_get_fd_for_plane") {
85- reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
86- } else {
87- // Older libgbm: one fd for the whole object.
88- reg(&boFD, "gbm_bo_get_fd")
89- }
81+ reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
9082 reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")
9183 reg(&boOffset, "gbm_bo_get_offset")
9284 reg(&boDestroy, "gbm_bo_destroy")
93- if have("gbm_bo_create_with_modifiers2") {
94- reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
95- }
96- if have("gbm_bo_create_with_modifiers") {
97- reg(&createWithModifiers, "gbm_bo_create_with_modifiers")
98- }
99- if createWithModifiers2 == nil && createWithModifiers == nil {
100- loadErr = fmt.Errorf("%w: libgbm lacks gbm_bo_create_with_modifiers", ErrUnavailable)
101- }
10285 })
10386 return loadErr
10487 }
@@ -141,13 +124,10 @@ func (d *Device) Close() {
141124 // object, kept alive for as long as the image is used.
142125 type Buffer struct {
143126 bo uintptr
144- Width, Height int
145- Format uint32
146- Modifier uint64
147- // Flagless: the buffer came from gbm_bo_create_with_modifiers, which
148- // takes no usage flags (libgbm before 21.1): scanout was not asked for.
149- Flagless bool
150- planes []plane
127+ width, height int
128+ format uint32
129+ modifier uint64
130+ planes []plane
151131 }
152132
153133 type plane struct {
@@ -158,7 +138,7 @@ type plane struct {
158138 // Create allocates an image whose modifier is one of modifiers (a
159139 // non-empty list). A list holding only linear falls back to a plain
160140 // linear allocation when GBM refuses the modifier call, and accepts an
161-// implicit layout (DRM_FORMAT_MOD_INVALID) as linear on either path. The
141+// implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The
162142 // result must be one memory object, because the renderer imports one
163143 // dmabuf for all planes.
164144 func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {
@@ -168,21 +148,11 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us
168148 if width <= 0 || height <= 0 || len(modifiers) == 0 {
169149 return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))
170150 }
171- var bo uintptr
172- flagless := false
173- switch {
174- case createWithModifiers2 != nil:
175- bo = createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
176- case createWithModifiers != nil:
177- // This entry point cannot ask for scanout.
178- flagless = true
179- bo = createWithModifiers(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)))
180- }
151+ bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
181152 runtime.KeepAlive(modifiers)
182153 onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear
183154 if bo == 0 && onlyLinear {
184- bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|UseLinear))
185- flagless = false
155+ bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear))
186156 }
187157 if bo == 0 {
188158 return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)
@@ -192,10 +162,9 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us
192162 boDestroy(bo)
193163 return nil, err
194164 }
195- if onlyLinear && b.Modifier == modInvalid {
196- b.Modifier = modLinear
165+ if onlyLinear && b.modifier == modInvalid {
166+ b.modifier = modLinear
197167 }
198- b.Flagless = flagless
199168 return b, nil
200169 }
201170
@@ -211,8 +180,8 @@ func acceptedModifiers(requested []uint64) []uint64 {
211180 // readBuffer reads the layout of bo and validates it against the request.
212181 // It does not destroy bo on error.
213182 func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {
214- b := &Buffer{bo: bo, Width: width, Height: height, Format: format, Modifier: boModifier(bo)}
215- if err := checkModifier(modifiers, b.Modifier); err != nil {
183+ b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)}
184+ if err := checkModifier(modifiers, b.modifier); err != nil {
216185 return nil, err
217186 }
218187 n := int(boPlaneCount(bo))
@@ -220,12 +189,7 @@ func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64
220189 return nil, fmt.Errorf("gbm: buffer has %d planes", n)
221190 }
222191 for i := range n {
223- var fd int32
224- if boFDForPlane != nil {
225- fd = boFDForPlane(bo, int32(i))
226- } else {
227- fd = boFD(bo)
228- }
192+ fd := boFDForPlane(bo, int32(i))
229193 if fd < 0 {
230194 b.closeFDs()
231195 return nil, fmt.Errorf("gbm: no fd for plane %d", i)
@@ -287,7 +251,7 @@ func (b *Buffer) DMABuf() (ports.DMABuf, error) {
287251 if b == nil || b.bo == 0 {
288252 return ports.DMABuf{}, errors.New("gbm buffer closed")
289253 }
290- out := ports.DMABuf{Width: b.Width, Height: b.Height, Format: b.Format, Modifier: b.Modifier}
254+ out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier}
291255 for _, p := range b.planes {
292256 fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)
293257 if err != nil {
@@ -18,9 +18,6 @@ import (
18 "golang.org/x/sys/unix"18 "golang.org/x/sys/unix"
19 )19 )
20 20
21-// ErrUnavailable marks a missing or unusable libgbm.
22-var ErrUnavailable = errors.New("gbm unavailable")
23-
24 // Usage is a set of GBM_BO_USE_* flags.21 // Usage is a set of GBM_BO_USE_* flags.
25 type Usage uint3222 type Usage uint32
26 23
@@ -29,8 +26,8 @@ const (
29 UseScanout Usage = 1 << 026 UseScanout Usage = 1 << 0
30 // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.27 // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.
31 UseRendering Usage = 1 << 228 UseRendering Usage = 1 << 2
32- // UseLinear (GBM_BO_USE_LINEAR): the layout is linear.29+ // useLinear (GBM_BO_USE_LINEAR): the layout is linear.
33- UseLinear Usage = 1 << 430+ useLinear Usage = 1 << 4
34 )31 )
35 32
36 const (33 const (
@@ -49,56 +46,42 @@ var (
49 createDevice func(fd int32) uintptr46 createDevice func(fd int32) uintptr
50 destroyDevice func(dev uintptr)47 destroyDevice func(dev uintptr)
51 createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr48 createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr
52- createWithModifiers func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32) uintptr
53 createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr49 createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr
54 boModifier func(bo uintptr) uint6450 boModifier func(bo uintptr) uint64
55 boPlaneCount func(bo uintptr) int3251 boPlaneCount func(bo uintptr) int32
56 boFDForPlane func(bo uintptr, plane int32) int3252 boFDForPlane func(bo uintptr, plane int32) int32
57- boFD func(bo uintptr) int32
58 boStrideForPlane func(bo uintptr, plane int32) uint3253 boStrideForPlane func(bo uintptr, plane int32) uint32
59 boOffset func(bo uintptr, plane int32) uint3254 boOffset func(bo uintptr, plane int32) uint32
60 boDestroy func(bo uintptr)55 boDestroy func(bo uintptr)
61 )56 )
62 57
63-// load opens libgbm once. Without it (or with a lacking one) the allocator58+// load opens libgbm once. Without it, or with one that lacks a function
64-// is unavailable and callers fall back to another way to make images.59+// below (gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane came with
60+// Mesa 21.1), the allocator is unavailable and callers fall back to another
61+// way to make images.
65 func load() error {62 func load() error {
66 loadOnce.Do(func() {63 loadOnce.Do(func() {
67 lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)64 lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
68 if err != nil {65 if err != nil {
69- loadErr = fmt.Errorf("%w: open libgbm.so.1: %v", ErrUnavailable, err)66+ loadErr = fmt.Errorf("open libgbm.so.1: %w", err)
70 return67 return
71 }68 }
72 defer func() {69 defer func() {
73 if r := recover(); r != nil {70 if r := recover(); r != nil {
74- loadErr = fmt.Errorf("%w: libgbm: %v", ErrUnavailable, r)71+ loadErr = fmt.Errorf("libgbm: %v", r)
75 }72 }
76 }()73 }()
77 reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }74 reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }
78- have := func(name string) bool { p, err := purego.Dlsym(lib, name); return err == nil && p != 0 }
79 reg(&createDevice, "gbm_create_device")75 reg(&createDevice, "gbm_create_device")
80 reg(&destroyDevice, "gbm_device_destroy")76 reg(&destroyDevice, "gbm_device_destroy")
77+ reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
81 reg(&createPlain, "gbm_bo_create")78 reg(&createPlain, "gbm_bo_create")
82 reg(&boModifier, "gbm_bo_get_modifier")79 reg(&boModifier, "gbm_bo_get_modifier")
83 reg(&boPlaneCount, "gbm_bo_get_plane_count")80 reg(&boPlaneCount, "gbm_bo_get_plane_count")
84- if have("gbm_bo_get_fd_for_plane") {81+ reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
85- reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
86- } else {
87- // Older libgbm: one fd for the whole object.
88- reg(&boFD, "gbm_bo_get_fd")
89- }
90 reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")82 reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")
91 reg(&boOffset, "gbm_bo_get_offset")83 reg(&boOffset, "gbm_bo_get_offset")
92 reg(&boDestroy, "gbm_bo_destroy")84 reg(&boDestroy, "gbm_bo_destroy")
93- if have("gbm_bo_create_with_modifiers2") {
94- reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
95- }
96- if have("gbm_bo_create_with_modifiers") {
97- reg(&createWithModifiers, "gbm_bo_create_with_modifiers")
98- }
99- if createWithModifiers2 == nil && createWithModifiers == nil {
100- loadErr = fmt.Errorf("%w: libgbm lacks gbm_bo_create_with_modifiers", ErrUnavailable)
101- }
102 })85 })
103 return loadErr86 return loadErr
104 }87 }
@@ -141,13 +124,10 @@ func (d *Device) Close() {
141 // object, kept alive for as long as the image is used.124 // object, kept alive for as long as the image is used.
142 type Buffer struct {125 type Buffer struct {
143 bo uintptr126 bo uintptr
144- Width, Height int127+ width, height int
145- Format uint32128+ format uint32
146- Modifier uint64129+ modifier uint64
147- // Flagless: the buffer came from gbm_bo_create_with_modifiers, which130+ planes []plane
148- // takes no usage flags (libgbm before 21.1): scanout was not asked for.
149- Flagless bool
150- planes []plane
151 }131 }
152 132
153 type plane struct {133 type plane struct {
@@ -158,7 +138,7 @@ type plane struct {
158 // Create allocates an image whose modifier is one of modifiers (a138 // Create allocates an image whose modifier is one of modifiers (a
159 // non-empty list). A list holding only linear falls back to a plain139 // non-empty list). A list holding only linear falls back to a plain
160 // linear allocation when GBM refuses the modifier call, and accepts an140 // linear allocation when GBM refuses the modifier call, and accepts an
161-// implicit layout (DRM_FORMAT_MOD_INVALID) as linear on either path. The141+// implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The
162 // result must be one memory object, because the renderer imports one142 // result must be one memory object, because the renderer imports one
163 // dmabuf for all planes.143 // dmabuf for all planes.
164 func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {144 func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {
@@ -168,21 +148,11 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us
168 if width <= 0 || height <= 0 || len(modifiers) == 0 {148 if width <= 0 || height <= 0 || len(modifiers) == 0 {
169 return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))149 return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))
170 }150 }
171- var bo uintptr151+ bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
172- flagless := false
173- switch {
174- case createWithModifiers2 != nil:
175- bo = createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
176- case createWithModifiers != nil:
177- // This entry point cannot ask for scanout.
178- flagless = true
179- bo = createWithModifiers(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)))
180- }
181 runtime.KeepAlive(modifiers)152 runtime.KeepAlive(modifiers)
182 onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear153 onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear
183 if bo == 0 && onlyLinear {154 if bo == 0 && onlyLinear {
184- bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|UseLinear))155+ bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear))
185- flagless = false
186 }156 }
187 if bo == 0 {157 if bo == 0 {
188 return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)158 return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)
@@ -192,10 +162,9 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us
192 boDestroy(bo)162 boDestroy(bo)
193 return nil, err163 return nil, err
194 }164 }
195- if onlyLinear && b.Modifier == modInvalid {165+ if onlyLinear && b.modifier == modInvalid {
196- b.Modifier = modLinear166+ b.modifier = modLinear
197 }167 }
198- b.Flagless = flagless
199 return b, nil168 return b, nil
200 }169 }
201 170
@@ -211,8 +180,8 @@ func acceptedModifiers(requested []uint64) []uint64 {
211 // readBuffer reads the layout of bo and validates it against the request.180 // readBuffer reads the layout of bo and validates it against the request.
212 // It does not destroy bo on error.181 // It does not destroy bo on error.
213 func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {182 func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {
214- b := &Buffer{bo: bo, Width: width, Height: height, Format: format, Modifier: boModifier(bo)}183+ b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)}
215- if err := checkModifier(modifiers, b.Modifier); err != nil {184+ if err := checkModifier(modifiers, b.modifier); err != nil {
216 return nil, err185 return nil, err
217 }186 }
218 n := int(boPlaneCount(bo))187 n := int(boPlaneCount(bo))
@@ -220,12 +189,7 @@ func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64
220 return nil, fmt.Errorf("gbm: buffer has %d planes", n)189 return nil, fmt.Errorf("gbm: buffer has %d planes", n)
221 }190 }
222 for i := range n {191 for i := range n {
223- var fd int32192+ fd := boFDForPlane(bo, int32(i))
224- if boFDForPlane != nil {
225- fd = boFDForPlane(bo, int32(i))
226- } else {
227- fd = boFD(bo)
228- }
229 if fd < 0 {193 if fd < 0 {
230 b.closeFDs()194 b.closeFDs()
231 return nil, fmt.Errorf("gbm: no fd for plane %d", i)195 return nil, fmt.Errorf("gbm: no fd for plane %d", i)
@@ -287,7 +251,7 @@ func (b *Buffer) DMABuf() (ports.DMABuf, error) {
287 if b == nil || b.bo == 0 {251 if b == nil || b.bo == 0 {
288 return ports.DMABuf{}, errors.New("gbm buffer closed")252 return ports.DMABuf{}, errors.New("gbm buffer closed")
289 }253 }
290- out := ports.DMABuf{Width: b.Width, Height: b.Height, Format: b.Format, Modifier: b.Modifier}254+ out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier}
291 for _, p := range b.planes {255 for _, p := range b.planes {
292 fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)256 fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)
293 if err != nil {257 if err != nil {
M internal/adapters/drm/gbm/gbm_test.go +10 -17
@@ -2,15 +2,15 @@ package gbm
22
33 import (
44 "os"
5- "slices"
65 "strings"
76 "testing"
87
98 "golang.org/x/sys/unix"
109 )
1110
12-// An allocator answer outside the requested list is refused, including an
13-// implicit layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import.
11+// An allocator answer outside the request is refused, including an implicit
12+// layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import; only a
13+// linear-only request takes it, as linear.
1414 func TestCheckModifier(t *testing.T) {
1515 const tiled = 0x300000000606014
1616 for _, tc := range []struct {
@@ -23,10 +23,15 @@ func TestCheckModifier(t *testing.T) {
2323 {"linear listed", []uint64{tiled, 0}, 0, true},
2424 {"not listed", []uint64{tiled}, 0, false},
2525 {"other vendor", []uint64{tiled}, 0x200000000401b03, false},
26- {"invalid", []uint64{tiled, 0}, modInvalid, false},
2726 {"empty list", nil, 0, false},
27+ {"linear only takes linear", []uint64{0}, 0, true},
28+ {"linear only takes invalid", []uint64{0}, modInvalid, true},
29+ {"linear only refuses others", []uint64{0}, tiled, false},
30+ {"tiled refuses invalid", []uint64{tiled}, modInvalid, false},
31+ {"tiled or linear refuses invalid", []uint64{tiled, 0}, modInvalid, false},
32+ {"0 and 5 refuses invalid", []uint64{0, 5}, modInvalid, false},
2833 } {
29- if err := checkModifier(tc.requested, tc.got); (err == nil) != tc.ok {
34+ if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok {
3035 t.Errorf("%s: %v", tc.name, err)
3136 }
3237 }
@@ -81,15 +86,3 @@ func TestCreateRejectsBadRequests(t *testing.T) {
8186 }
8287 }
8388 }
84-
85-// Only a linear-only request accepts an implicit layout, as linear.
86-func TestAcceptedModifiers(t *testing.T) {
87- if got := acceptedModifiers([]uint64{0}); checkModifier(got, modInvalid) != nil || checkModifier(got, 0) != nil || checkModifier(got, 5) == nil {
88- t.Errorf("linear-only request accepts %#x", got)
89- }
90- for _, req := range [][]uint64{{5}, {0, 5}, {modInvalid}} {
91- if got := acceptedModifiers(req); checkModifier(got, modInvalid) == nil && !slices.Contains(req, modInvalid) {
92- t.Errorf("request %#x accepts an implicit layout", req)
93- }
94- }
95-}
@@ -2,15 +2,15 @@ package gbm
2 2
3 import (3 import (
4 "os"4 "os"
5- "slices"
6 "strings"5 "strings"
7 "testing"6 "testing"
8 7
9 "golang.org/x/sys/unix"8 "golang.org/x/sys/unix"
10 )9 )
11 10
12-// An allocator answer outside the requested list is refused, including an11+// An allocator answer outside the request is refused, including an implicit
13-// implicit layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import.12+// layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import; only a
13+// linear-only request takes it, as linear.
14 func TestCheckModifier(t *testing.T) {14 func TestCheckModifier(t *testing.T) {
15 const tiled = 0x30000000060601415 const tiled = 0x300000000606014
16 for _, tc := range []struct {16 for _, tc := range []struct {
@@ -23,10 +23,15 @@ func TestCheckModifier(t *testing.T) {
23 {"linear listed", []uint64{tiled, 0}, 0, true},23 {"linear listed", []uint64{tiled, 0}, 0, true},
24 {"not listed", []uint64{tiled}, 0, false},24 {"not listed", []uint64{tiled}, 0, false},
25 {"other vendor", []uint64{tiled}, 0x200000000401b03, false},25 {"other vendor", []uint64{tiled}, 0x200000000401b03, false},
26- {"invalid", []uint64{tiled, 0}, modInvalid, false},
27 {"empty list", nil, 0, false},26 {"empty list", nil, 0, false},
27+ {"linear only takes linear", []uint64{0}, 0, true},
28+ {"linear only takes invalid", []uint64{0}, modInvalid, true},
29+ {"linear only refuses others", []uint64{0}, tiled, false},
30+ {"tiled refuses invalid", []uint64{tiled}, modInvalid, false},
31+ {"tiled or linear refuses invalid", []uint64{tiled, 0}, modInvalid, false},
32+ {"0 and 5 refuses invalid", []uint64{0, 5}, modInvalid, false},
28 } {33 } {
29- if err := checkModifier(tc.requested, tc.got); (err == nil) != tc.ok {34+ if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok {
30 t.Errorf("%s: %v", tc.name, err)35 t.Errorf("%s: %v", tc.name, err)
31 }36 }
32 }37 }
@@ -81,15 +86,3 @@ func TestCreateRejectsBadRequests(t *testing.T) {
81 }86 }
82 }87 }
83 }88 }
84-
85-// Only a linear-only request accepts an implicit layout, as linear.
86-func TestAcceptedModifiers(t *testing.T) {
87- if got := acceptedModifiers([]uint64{0}); checkModifier(got, modInvalid) != nil || checkModifier(got, 0) != nil || checkModifier(got, 5) == nil {
88- t.Errorf("linear-only request accepts %#x", got)
89- }
90- for _, req := range [][]uint64{{5}, {0, 5}, {modInvalid}} {
91- if got := acceptedModifiers(req); checkModifier(got, modInvalid) == nil && !slices.Contains(req, modInvalid) {
92- t.Errorf("request %#x accepts an implicit layout", req)
93- }
94- }
95-}
M internal/adapters/drm/images.go +44 -64
@@ -112,7 +112,8 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
112112 o.dropImages(r)
113113 return err
114114 }
115- if o.imageAlloc == allocGBM {
115+ if len(o.images) > 0 {
116+ // GBM images (the renderer imported them).
116117 // The renderer's own images of this kind may still pass.
117118 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
118119 o.dropImages(r)
@@ -147,9 +148,9 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
147148 // GPU that cannot export a scanout-capable image cannot drive the output.
148149 func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
149150 err := cause
150- for first := true; kind <= imagesLinear; kind++ {
151- noGBM := first && skipGBM
152- first = false
151+ start := kind
152+ for ; kind <= imagesLinear; kind++ {
153+ noGBM := skipGBM && kind == start
153154 if o.runContext != nil && o.runContext.Err() != nil {
154155 return kind, o.runContext.Err()
155156 }
@@ -220,38 +221,52 @@ func (o *Output) primaryModifiers(format uint32) []uint64 {
220221 return mods
221222 }
222223
224+// closeBufs closes every plane file of bufs. The callers own them: the
225+// renderer and the framebuffers keep their own references to the memory.
226+func closeBufs(bufs []ports.DMABuf) {
227+ for _, b := range bufs {
228+ for _, p := range b.Planes {
229+ _ = p.File.Close()
230+ }
231+ }
232+}
233+
223234 // exportImages makes the renderer's exported targets the output images.
224235 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
225- o.dropLive(r)
236+ o.dropImages(r)
226237 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
227238 if err != nil {
228239 return err
229240 }
241+ defer closeBufs(bufs)
242+ if err = o.showBufs(r, bufs, allocVulkan); err != nil {
243+ o.dropImages(r)
244+ _, _ = r.ExportTargets(0, nil, false)
245+ return err
246+ }
247+ return nil
248+}
249+
250+// showBufs makes framebuffers of the output images bufs (already the
251+// renderer's targets), clears them and records their layout. The caller
252+// closes the plane files and drops the images on error.
253+func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error {
230254 o.planes, o.imageMod = 0, 0
231255 for i := range bufs {
232- if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
233- err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
234- }
235- if err == nil {
236- o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
256+ if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
257+ return fmt.Errorf("output image has %d planes", n)
237258 }
238- // Every plane owns its file; the framebuffer keeps its own
239- // reference to the buffer.
240- for _, p := range bufs[i].Planes {
241- p.File.Close()
259+ fb, err := o.k.addFB(&bufs[i], o.imageFormat())
260+ if err != nil {
261+ return err
242262 }
263+ o.fbs[i] = fb
243264 }
244- if err == nil {
245- err = o.clearImages(r)
246- }
247- if err != nil {
248- o.dropImages(r)
249- _, _ = r.ExportTargets(0, nil, false)
265+ if err := o.clearImages(r); err != nil {
250266 return err
251267 }
252268 o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
253- o.imageAlloc = allocVulkan
254- o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocVulkan).Msg("zero-copy output")
269+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output")
255270 return nil
256271 }
257272
@@ -284,7 +299,7 @@ func (o *Output) clearImages(r ports.Renderer) error {
284299 // those the plane lists (mods, as for exportImages) that the renderer can
285300 // render into and import. On failure nothing is left allocated.
286301 func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
287- o.dropLive(r)
302+ o.dropImages(r)
288303 if o.alloc == nil {
289304 if o.allocErr != nil {
290305 return o.allocErr
@@ -299,21 +314,10 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e
299314 return errors.New("no modifier both the plane and the renderer accept for GBM images")
300315 }
301316 var bufs []ports.DMABuf
302- imported := false
303317 defer func() {
304- for _, b := range bufs {
305- for _, p := range b.Planes {
306- p.File.Close()
307- }
308- }
318+ closeBufs(bufs)
309319 if err != nil {
310- // A failed import leaves no renderer targets; a later failure
311- // leaves ones over the images about to be freed.
312- if imported {
313- o.dropImages(r)
314- } else {
315- o.freeImages()
316- }
320+ o.dropImages(r)
317321 }
318322 }()
319323 for range o.fbs {
@@ -331,35 +335,12 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e
331335 if err = r.ImportTargets(bufs); err != nil {
332336 return err
333337 }
334- imported = true
335- for i := range bufs {
336- if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
337- return fmt.Errorf("output image has %d planes", n)
338- }
339- if o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()); err != nil {
340- return err
341- }
342- }
343- if err = o.clearImages(r); err != nil {
344- return err
345- }
346- o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
347- o.imageAlloc = allocGBM
348- o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocGBM).Msg("zero-copy output")
349- return nil
350-}
351-
352-// dropLive makes sure no image is left from an earlier setup: setup starts
353-// from nothing, so what is still there is freed first.
354-func (o *Output) dropLive(r ports.Renderer) {
355- if o.fbs != [2]uint32{} || len(o.images) > 0 {
356- o.dropImages(r)
357- }
338+ return o.showBufs(r, bufs, allocGBM)
358339 }
359340
360-// dropImages frees the output images with the renderer targets built over
361-// the GBM ones: they would keep their memory (VRAM) alive after the
362-// buffers are gone.
341+// dropImages frees the output images: their framebuffers, the GBM buffers
342+// and the renderer targets built over them (they would keep the memory
343+// alive after the buffers are gone). It is a no-op when there are none.
363344 func (o *Output) dropImages(r ports.Renderer) {
364345 if len(o.images) > 0 {
365346 _ = r.ImportTargets(nil)
@@ -384,5 +365,4 @@ func (o *Output) freeImages() {
384365 img.close()
385366 }
386367 o.images = nil
387- o.imageAlloc = ""
388368 }
@@ -112,7 +112,8 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
112 o.dropImages(r)112 o.dropImages(r)
113 return err113 return err
114 }114 }
115- if o.imageAlloc == allocGBM {115+ if len(o.images) > 0 {
116+ // GBM images (the renderer imported them).
116 // The renderer's own images of this kind may still pass.117 // The renderer's own images of this kind may still pass.
117 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")118 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
118 o.dropImages(r)119 o.dropImages(r)
@@ -147,9 +148,9 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
147 // GPU that cannot export a scanout-capable image cannot drive the output.148 // GPU that cannot export a scanout-capable image cannot drive the output.
148 func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {149 func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
149 err := cause150 err := cause
150- for first := true; kind <= imagesLinear; kind++ {151+ start := kind
151- noGBM := first && skipGBM152+ for ; kind <= imagesLinear; kind++ {
152- first = false153+ noGBM := skipGBM && kind == start
153 if o.runContext != nil && o.runContext.Err() != nil {154 if o.runContext != nil && o.runContext.Err() != nil {
154 return kind, o.runContext.Err()155 return kind, o.runContext.Err()
155 }156 }
@@ -220,38 +221,52 @@ func (o *Output) primaryModifiers(format uint32) []uint64 {
220 return mods221 return mods
221 }222 }
222 223
224+// closeBufs closes every plane file of bufs. The callers own them: the
225+// renderer and the framebuffers keep their own references to the memory.
226+func closeBufs(bufs []ports.DMABuf) {
227+ for _, b := range bufs {
228+ for _, p := range b.Planes {
229+ _ = p.File.Close()
230+ }
231+ }
232+}
233+
223 // exportImages makes the renderer's exported targets the output images.234 // exportImages makes the renderer's exported targets the output images.
224 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {235 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
225- o.dropLive(r)236+ o.dropImages(r)
226 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)237 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
227 if err != nil {238 if err != nil {
228 return err239 return err
229 }240 }
241+ defer closeBufs(bufs)
242+ if err = o.showBufs(r, bufs, allocVulkan); err != nil {
243+ o.dropImages(r)
244+ _, _ = r.ExportTargets(0, nil, false)
245+ return err
246+ }
247+ return nil
248+}
249+
250+// showBufs makes framebuffers of the output images bufs (already the
251+// renderer's targets), clears them and records their layout. The caller
252+// closes the plane files and drops the images on error.
253+func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error {
230 o.planes, o.imageMod = 0, 0254 o.planes, o.imageMod = 0, 0
231 for i := range bufs {255 for i := range bufs {
232- if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {256+ if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
233- err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))257+ return fmt.Errorf("output image has %d planes", n)
234- }
235- if err == nil {
236- o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
237 }258 }
238- // Every plane owns its file; the framebuffer keeps its own259+ fb, err := o.k.addFB(&bufs[i], o.imageFormat())
239- // reference to the buffer.260+ if err != nil {
240- for _, p := range bufs[i].Planes {261+ return err
241- p.File.Close()
242 }262 }
263+ o.fbs[i] = fb
243 }264 }
244- if err == nil {265+ if err := o.clearImages(r); err != nil {
245- err = o.clearImages(r)
246- }
247- if err != nil {
248- o.dropImages(r)
249- _, _ = r.ExportTargets(0, nil, false)
250 return err266 return err
251 }267 }
252 o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier268 o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
253- o.imageAlloc = allocVulkan269+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output")
254- o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocVulkan).Msg("zero-copy output")
255 return nil270 return nil
256 }271 }
257 272
@@ -284,7 +299,7 @@ func (o *Output) clearImages(r ports.Renderer) error {
284 // those the plane lists (mods, as for exportImages) that the renderer can299 // those the plane lists (mods, as for exportImages) that the renderer can
285 // render into and import. On failure nothing is left allocated.300 // render into and import. On failure nothing is left allocated.
286 func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {301 func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
287- o.dropLive(r)302+ o.dropImages(r)
288 if o.alloc == nil {303 if o.alloc == nil {
289 if o.allocErr != nil {304 if o.allocErr != nil {
290 return o.allocErr305 return o.allocErr
@@ -299,21 +314,10 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e
299 return errors.New("no modifier both the plane and the renderer accept for GBM images")314 return errors.New("no modifier both the plane and the renderer accept for GBM images")
300 }315 }
301 var bufs []ports.DMABuf316 var bufs []ports.DMABuf
302- imported := false
303 defer func() {317 defer func() {
304- for _, b := range bufs {318+ closeBufs(bufs)
305- for _, p := range b.Planes {
306- p.File.Close()
307- }
308- }
309 if err != nil {319 if err != nil {
310- // A failed import leaves no renderer targets; a later failure320+ o.dropImages(r)
311- // leaves ones over the images about to be freed.
312- if imported {
313- o.dropImages(r)
314- } else {
315- o.freeImages()
316- }
317 }321 }
318 }()322 }()
319 for range o.fbs {323 for range o.fbs {
@@ -331,35 +335,12 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e
331 if err = r.ImportTargets(bufs); err != nil {335 if err = r.ImportTargets(bufs); err != nil {
332 return err336 return err
333 }337 }
334- imported = true338+ return o.showBufs(r, bufs, allocGBM)
335- for i := range bufs {
336- if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
337- return fmt.Errorf("output image has %d planes", n)
338- }
339- if o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()); err != nil {
340- return err
341- }
342- }
343- if err = o.clearImages(r); err != nil {
344- return err
345- }
346- o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
347- o.imageAlloc = allocGBM
348- o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocGBM).Msg("zero-copy output")
349- return nil
350-}
351-
352-// dropLive makes sure no image is left from an earlier setup: setup starts
353-// from nothing, so what is still there is freed first.
354-func (o *Output) dropLive(r ports.Renderer) {
355- if o.fbs != [2]uint32{} || len(o.images) > 0 {
356- o.dropImages(r)
357- }
358 }339 }
359 340
360-// dropImages frees the output images with the renderer targets built over341+// dropImages frees the output images: their framebuffers, the GBM buffers
361-// the GBM ones: they would keep their memory (VRAM) alive after the342+// and the renderer targets built over them (they would keep the memory
362-// buffers are gone.343+// alive after the buffers are gone). It is a no-op when there are none.
363 func (o *Output) dropImages(r ports.Renderer) {344 func (o *Output) dropImages(r ports.Renderer) {
364 if len(o.images) > 0 {345 if len(o.images) > 0 {
365 _ = r.ImportTargets(nil)346 _ = r.ImportTargets(nil)
@@ -384,5 +365,4 @@ func (o *Output) freeImages() {
384 img.close()365 img.close()
385 }366 }
386 o.images = nil367 o.images = nil
387- o.imageAlloc = ""
388 }368 }
M internal/adapters/drm/output.go +27 -12
@@ -197,12 +197,10 @@ type Output struct {
197197 protectNotBefore time.Time
198198 // kind is how the images were made.
199199 kind imageKind
200- // planes is the memory plane count of the last exported images.
200+ // planes is the memory plane count of the output images.
201201 planes int
202- // imageMod is the modifier of the exported images (0: linear).
202+ // imageMod is the modifier of the output images (0: linear).
203203 imageMod uint64
204- // imageAlloc is who allocated the images (allocGBM, allocVulkan).
205- imageAlloc string
206204 // openAlloc opens the GBM image allocator, once per output (nil: GBM is
207205 // not used); alloc is it, allocErr why it failed. images are the GBM
208206 // images alive while they are shown.
@@ -213,6 +211,12 @@ type Output struct {
213211 // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so
214212 // commits wait for their fences on the CPU instead.
215213 cpuFences bool
214+ // fencePoll polls client fences; fenceWarnAt is when a fence wait
215+ // timeout was last warned about.
216+ // fenceLimit overrides fenceWaitLimit (zero: no override; tests).
217+ fencePoll fencePoll
218+ fenceWarnAt time.Time
219+ fenceLimit time.Duration
216220 // formats receives the direct scanout formats after each modeset;
217221 // sampled and device are what they are built from (Want).
218222 formats chan<- ports.OutputFormats
@@ -234,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf
234238 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
235239 log := card.log
236240 pipe := slices.Index(card.crtcs, crtc)
237- o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd, card.log) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}
241+ o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}
238242 var err error
239243 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
240244 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -650,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p
650654 // (overlay) say. A frame that changes a plane's colour state is never async:
651655 // only FB_ID may change in an async commit.
652656 func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error {
657+ // Where fences are waited for on the CPU, once per commit and before
658+ // any state is read: the retries below go through commitRect. The
659+ // fence is our own render's, or a client's that already signalled
660+ // (clientFenceReady).
661+ o.waitFences(fence)
662+ return o.commitRect(fb, fence, async, vrr, f, ov, rect, pc)
663+}
664+
665+// commitRect is commitWithRect once the fence was waited for; its retries
666+// call it again, without waiting again.
667+func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error {
653668 o.observeSecurity()
654669 if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) {
655670 return errSecurityScene
@@ -661,9 +676,6 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
661676 if o.protected {
662677 async, vrr, ov, pc = false, false, overlayWin{}, colorUse{}
663678 }
664- // A client fence (direct scanout, overlay) signalled already
665- // (clientFenceReady): this waits only for our own render.
666- o.waitFences(fence)
667679 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) {
668680 async = false
669681 }
@@ -704,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
704716 return errColorRefused
705717 }
706718 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
707- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)
719+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
708720 }
709721 // The same frame without the connector hint tells whether the hint
710722 // is the cause.
711723 if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) {
712724 o.log.Warn().Err(err).Msg("content type refused; disabled")
713725 o.contentProp = 0
714- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)
726+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
715727 }
716728 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
717729 // Retry without turning VRR on: if that passes, the driver
718730 // refuses VRR on this output. The overlay stays: the composed
719731 // image left its window out.
720- if o.commitWithRect(fb, fence, false, false, f, ov, rect, pc) == nil {
732+ if o.commitRect(fb, fence, false, false, f, ov, rect, pc) == nil {
721733 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
722734 o.vrrProp = 0
723735 return nil
@@ -923,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF
923935 // from now on.
924936 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
925937 cfb.noAsync = true
926- err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
938+ err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
927939 }
928940 if errors.Is(err, errColorRefused) {
929941 // KMS takes the buffer without the pipeline: compose it; the
@@ -1513,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
15131525 ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval))
15141526 ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead))
15151527 ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried)
1528+ if fs.fenceTimeouts > 0 {
1529+ ev = ev.Int("fence_timeouts", fs.fenceTimeouts)
1530+ }
15161531 ev = ev.Int("redrawn_pixels", r.TakeRedrawn())
15171532 if o.traceFlips {
15181533 ev = ev.Int("late_fences", fs.lateFences)
@@ -197,12 +197,10 @@ type Output struct {
197 protectNotBefore time.Time197 protectNotBefore time.Time
198 // kind is how the images were made.198 // kind is how the images were made.
199 kind imageKind199 kind imageKind
200- // planes is the memory plane count of the last exported images.200+ // planes is the memory plane count of the output images.
201 planes int201 planes int
202- // imageMod is the modifier of the exported images (0: linear).202+ // imageMod is the modifier of the output images (0: linear).
203 imageMod uint64203 imageMod uint64
204- // imageAlloc is who allocated the images (allocGBM, allocVulkan).
205- imageAlloc string
206 // openAlloc opens the GBM image allocator, once per output (nil: GBM is204 // openAlloc opens the GBM image allocator, once per output (nil: GBM is
207 // not used); alloc is it, allocErr why it failed. images are the GBM205 // not used); alloc is it, allocErr why it failed. images are the GBM
208 // images alive while they are shown.206 // images alive while they are shown.
@@ -213,6 +211,12 @@ type Output struct {
213 // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so211 // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so
214 // commits wait for their fences on the CPU instead.212 // commits wait for their fences on the CPU instead.
215 cpuFences bool213 cpuFences bool
214+ // fencePoll polls client fences; fenceWarnAt is when a fence wait
215+ // timeout was last warned about.
216+ // fenceLimit overrides fenceWaitLimit (zero: no override; tests).
217+ fencePoll fencePoll
218+ fenceWarnAt time.Time
219+ fenceLimit time.Duration
216 // formats receives the direct scanout formats after each modeset;220 // formats receives the direct scanout formats after each modeset;
217 // sampled and device are what they are built from (Want).221 // sampled and device are what they are built from (Want).
218 formats chan<- ports.OutputFormats222 formats chan<- ports.OutputFormats
@@ -234,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf
234 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {238 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
235 log := card.log239 log := card.log
236 pipe := slices.Index(card.crtcs, crtc)240 pipe := slices.Index(card.crtcs, crtc)
237- o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd, card.log) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}241+ o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}
238 var err error242 var err error
239 if o.saved, err = getCrtc(card.fd, crtc); err != nil {243 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
240 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")244 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -650,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p
650 // (overlay) say. A frame that changes a plane's colour state is never async:654 // (overlay) say. A frame that changes a plane's colour state is never async:
651 // only FB_ID may change in an async commit.655 // only FB_ID may change in an async commit.
652 func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error {656 func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error {
657+ // Where fences are waited for on the CPU, once per commit and before
658+ // any state is read: the retries below go through commitRect. The
659+ // fence is our own render's, or a client's that already signalled
660+ // (clientFenceReady).
661+ o.waitFences(fence)
662+ return o.commitRect(fb, fence, async, vrr, f, ov, rect, pc)
663+}
664+
665+// commitRect is commitWithRect once the fence was waited for; its retries
666+// call it again, without waiting again.
667+func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error {
653 o.observeSecurity()668 o.observeSecurity()
654 if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) {669 if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) {
655 return errSecurityScene670 return errSecurityScene
@@ -661,9 +676,6 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
661 if o.protected {676 if o.protected {
662 async, vrr, ov, pc = false, false, overlayWin{}, colorUse{}677 async, vrr, ov, pc = false, false, overlayWin{}, colorUse{}
663 }678 }
664- // A client fence (direct scanout, overlay) signalled already
665- // (clientFenceReady): this waits only for our own render.
666- o.waitFences(fence)
667 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) {679 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) {
668 async = false680 async = false
669 }681 }
@@ -704,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
704 return errColorRefused716 return errColorRefused
705 }717 }
706 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {718 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
707- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)719+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
708 }720 }
709 // The same frame without the connector hint tells whether the hint721 // The same frame without the connector hint tells whether the hint
710 // is the cause.722 // is the cause.
711 if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) {723 if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) {
712 o.log.Warn().Err(err).Msg("content type refused; disabled")724 o.log.Warn().Err(err).Msg("content type refused; disabled")
713 o.contentProp = 0725 o.contentProp = 0
714- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)726+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
715 }727 }
716 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {728 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
717 // Retry without turning VRR on: if that passes, the driver729 // Retry without turning VRR on: if that passes, the driver
718 // refuses VRR on this output. The overlay stays: the composed730 // refuses VRR on this output. The overlay stays: the composed
719 // image left its window out.731 // image left its window out.
720- if o.commitWithRect(fb, fence, false, false, f, ov, rect, pc) == nil {732+ if o.commitRect(fb, fence, false, false, f, ov, rect, pc) == nil {
721 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")733 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
722 o.vrrProp = 0734 o.vrrProp = 0
723 return nil735 return nil
@@ -923,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF
923 // from now on.935 // from now on.
924 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")936 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
925 cfb.noAsync = true937 cfb.noAsync = true
926- err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)938+ err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
927 }939 }
928 if errors.Is(err, errColorRefused) {940 if errors.Is(err, errColorRefused) {
929 // KMS takes the buffer without the pipeline: compose it; the941 // KMS takes the buffer without the pipeline: compose it; the
@@ -1513,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1513 ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval))1525 ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval))
1514 ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead))1526 ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead))
1515 ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried)1527 ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried)
1528+ if fs.fenceTimeouts > 0 {
1529+ ev = ev.Int("fence_timeouts", fs.fenceTimeouts)
1530+ }
1516 ev = ev.Int("redrawn_pixels", r.TakeRedrawn())1531 ev = ev.Int("redrawn_pixels", r.TakeRedrawn())
1517 if o.traceFlips {1532 if o.traceFlips {
1518 ev = ev.Int("late_fences", fs.lateFences)1533 ev = ev.Int("late_fences", fs.lateFences)
M internal/adapters/vulkan/renderer.go +3 -0
@@ -34,6 +34,9 @@ type Renderer struct {
3434 current int
3535 renderMods []uint64
3636 hdrMods []uint64
37+ // importMods caches the modifiers a target of a format can be imported
38+ // with (probeImportModifiers), by what the answer depends on.
39+ importMods map[importKey][]uint64
3740 hdrNits float64
3841 hdr hdrPass
3942 hdrError error
@@ -34,6 +34,9 @@ type Renderer struct {
34 current int34 current int
35 renderMods []uint6435 renderMods []uint64
36 hdrMods []uint6436 hdrMods []uint64
37+ // importMods caches the modifiers a target of a format can be imported
38+ // with (probeImportModifiers), by what the answer depends on.
39+ importMods map[importKey][]uint64
37 hdrNits float6440 hdrNits float64
38 hdr hdrPass41 hdr hdrPass
39 hdrError error42 hdrError error
M internal/adapters/vulkan/targets.go +35 -9
@@ -214,11 +214,28 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
214214 return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit)
215215 }
216216
217+// importKey is what the importable modifiers of a target depend on: its
218+// format and whether test readback adds the transfer-source requirement.
219+type importKey struct {
220+ format vk.Format
221+ readback bool
222+}
223+
217224 // probeImportModifiers lists the modifiers of format a target can be
218225 // imported with: probeModifiers with the importable bit, for images the
219-// display allocator made.
226+// display allocator made. The answer is probed once per key: it does not
227+// change while the device lives.
220228 func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
221- return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit)
229+ key := importKey{format, r.hdrReadback}
230+ if mods, ok := r.importMods[key]; ok {
231+ return mods
232+ }
233+ mods := r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit)
234+ if r.importMods == nil {
235+ r.importMods = make(map[importKey][]uint64)
236+ }
237+ r.importMods[key] = mods
238+ return mods
222239 }
223240
224241 func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {
@@ -425,6 +442,14 @@ func (r *Renderer) dropTargets() {
425442 r.targets, r.current, r.lastHDRTarget = nil, 0, -1
426443 }
427444
445+// What ImportTargets refuses an image for, before asking the driver.
446+var (
447+ errImageGeometry = errors.New("image size or format does not match the target")
448+ errImageModifier = errors.New("image modifier is not an importable target modifier")
449+ errImagePlanes = errors.New("image plane count does not match its modifier")
450+ errImageDMABuf = errors.New("image planes are not in one dmabuf")
451+)
452+
428453 // importTarget makes a target of an image allocated outside the renderer.
429454 // The target is shared with the display like an exported one: frames
430455 // acquire it from and release it to the foreign queue family.
@@ -432,15 +457,16 @@ func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) {
432457 d := r.dd
433458 format, fourcc := r.targetFormat()
434459 n := len(b.Planes)
460+ want, known := r.modPlanes[b.Modifier]
435461 switch {
436462 case b.Format != fourcc || b.Width != r.width || b.Height != r.height:
437- return nil, fmt.Errorf("image %dx%d format %#x does not match target %dx%d format %#x", b.Width, b.Height, b.Format, r.width, r.height, fourcc)
438- case n < 1 || n > maxModifierPlanes:
439- return nil, fmt.Errorf("image has %d planes", n)
440- case r.modPlanes[b.Modifier] != uint32(n):
441- return nil, fmt.Errorf("modifier %#x: image has %d planes, device expects %d", b.Modifier, n, r.modPlanes[b.Modifier])
463+ return nil, fmt.Errorf("%w: image %dx%d format %#x, target %dx%d format %#x", errImageGeometry, b.Width, b.Height, b.Format, r.width, r.height, fourcc)
464+ case !known:
465+ return nil, fmt.Errorf("%w: %#x is not a target modifier of the device", errImageModifier, b.Modifier)
466+ case n < 1 || n > maxModifierPlanes || want != uint32(n):
467+ return nil, fmt.Errorf("%w: modifier %#x: image has %d planes, device expects %d", errImagePlanes, b.Modifier, n, want)
442468 case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):
443- return nil, fmt.Errorf("modifier %#x cannot be imported as a target", b.Modifier)
469+ return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier)
444470 }
445471 if err := sameDMABuf(b.Planes); err != nil {
446472 return nil, err
@@ -523,7 +549,7 @@ func sameDMABuf(planes []ports.DMABufPlane) error {
523549 if i == 0 {
524550 first = st
525551 } else if st.Dev != first.Dev || st.Ino != first.Ino {
526- return fmt.Errorf("plane %d is not in the same dmabuf as plane 0", i)
552+ return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i)
527553 }
528554 }
529555 return nil
@@ -214,11 +214,28 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
214 return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit)214 return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit)
215 }215 }
216 216
217+// importKey is what the importable modifiers of a target depend on: its
218+// format and whether test readback adds the transfer-source requirement.
219+type importKey struct {
220+ format vk.Format
221+ readback bool
222+}
223+
217 // probeImportModifiers lists the modifiers of format a target can be224 // probeImportModifiers lists the modifiers of format a target can be
218 // imported with: probeModifiers with the importable bit, for images the225 // imported with: probeModifiers with the importable bit, for images the
219-// display allocator made.226+// display allocator made. The answer is probed once per key: it does not
227+// change while the device lives.
220 func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {228 func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
221- return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit)229+ key := importKey{format, r.hdrReadback}
230+ if mods, ok := r.importMods[key]; ok {
231+ return mods
232+ }
233+ mods := r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit)
234+ if r.importMods == nil {
235+ r.importMods = make(map[importKey][]uint64)
236+ }
237+ r.importMods[key] = mods
238+ return mods
222 }239 }
223 240
224 func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {241 func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {
@@ -425,6 +442,14 @@ func (r *Renderer) dropTargets() {
425 r.targets, r.current, r.lastHDRTarget = nil, 0, -1442 r.targets, r.current, r.lastHDRTarget = nil, 0, -1
426 }443 }
427 444
445+// What ImportTargets refuses an image for, before asking the driver.
446+var (
447+ errImageGeometry = errors.New("image size or format does not match the target")
448+ errImageModifier = errors.New("image modifier is not an importable target modifier")
449+ errImagePlanes = errors.New("image plane count does not match its modifier")
450+ errImageDMABuf = errors.New("image planes are not in one dmabuf")
451+)
452+
428 // importTarget makes a target of an image allocated outside the renderer.453 // importTarget makes a target of an image allocated outside the renderer.
429 // The target is shared with the display like an exported one: frames454 // The target is shared with the display like an exported one: frames
430 // acquire it from and release it to the foreign queue family.455 // acquire it from and release it to the foreign queue family.
@@ -432,15 +457,16 @@ func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) {
432 d := r.dd457 d := r.dd
433 format, fourcc := r.targetFormat()458 format, fourcc := r.targetFormat()
434 n := len(b.Planes)459 n := len(b.Planes)
460+ want, known := r.modPlanes[b.Modifier]
435 switch {461 switch {
436 case b.Format != fourcc || b.Width != r.width || b.Height != r.height:462 case b.Format != fourcc || b.Width != r.width || b.Height != r.height:
437- return nil, fmt.Errorf("image %dx%d format %#x does not match target %dx%d format %#x", b.Width, b.Height, b.Format, r.width, r.height, fourcc)463+ return nil, fmt.Errorf("%w: image %dx%d format %#x, target %dx%d format %#x", errImageGeometry, b.Width, b.Height, b.Format, r.width, r.height, fourcc)
438- case n < 1 || n > maxModifierPlanes:464+ case !known:
439- return nil, fmt.Errorf("image has %d planes", n)465+ return nil, fmt.Errorf("%w: %#x is not a target modifier of the device", errImageModifier, b.Modifier)
440- case r.modPlanes[b.Modifier] != uint32(n):466+ case n < 1 || n > maxModifierPlanes || want != uint32(n):
441- return nil, fmt.Errorf("modifier %#x: image has %d planes, device expects %d", b.Modifier, n, r.modPlanes[b.Modifier])467+ return nil, fmt.Errorf("%w: modifier %#x: image has %d planes, device expects %d", errImagePlanes, b.Modifier, n, want)
442 case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):468 case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):
443- return nil, fmt.Errorf("modifier %#x cannot be imported as a target", b.Modifier)469+ return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier)
444 }470 }
445 if err := sameDMABuf(b.Planes); err != nil {471 if err := sameDMABuf(b.Planes); err != nil {
446 return nil, err472 return nil, err
@@ -523,7 +549,7 @@ func sameDMABuf(planes []ports.DMABufPlane) error {
523 if i == 0 {549 if i == 0 {
524 first = st550 first = st
525 } else if st.Dev != first.Dev || st.Ino != first.Ino {551 } else if st.Dev != first.Dev || st.Ino != first.Ino {
526- return fmt.Errorf("plane %d is not in the same dmabuf as plane 0", i)552+ return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i)
527 }553 }
528 }554 }
529 return nil555 return nil
M internal/adapters/vulkan/targets_test.go +149 -54
@@ -1,6 +1,7 @@
11 package vulkan
22
33 import (
4+ "errors"
45 "image/color"
56 "math"
67 "os"
@@ -369,10 +370,46 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
369370 }
370371 }
371372
372-// ImportTargets refuses images that do not fit the renderer (size, format,
373-// plane count, modifier) before touching the driver, and a refusal leaves
374-// no targets: frames go back to the renderer's own image.
375-func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
373+// pipeFile is a file for plane tests: a distinct inode per call.
374+func pipeFile(t *testing.T) *os.File {
375+ t.Helper()
376+ f, w, err := os.Pipe()
377+ if err != nil {
378+ t.Fatal(err)
379+ }
380+ w.Close()
381+ t.Cleanup(func() { f.Close() })
382+ return f
383+}
384+
385+// Planes must be in one dmabuf: the same file or duplicates of it are, two
386+// files are not. No device needed.
387+func TestSameDMABuf(t *testing.T) {
388+ one, other := pipeFile(t), pipeFile(t)
389+ for _, tc := range []struct {
390+ name string
391+ planes []ports.DMABufPlane
392+ ok bool
393+ }{
394+ {"one plane", []ports.DMABufPlane{{File: one}}, true},
395+ {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true},
396+ {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false},
397+ {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false},
398+ {"no file", []ports.DMABufPlane{{File: one}, {}}, false},
399+ } {
400+ err := sameDMABuf(tc.planes)
401+ if (err == nil) != tc.ok {
402+ t.Errorf("%s: %v", tc.name, err)
403+ }
404+ if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) {
405+ t.Errorf("%s: %v is not errImageDMABuf", tc.name, err)
406+ }
407+ }
408+}
409+
410+// TargetModifiers keeps the modifiers the display lists and drops
411+// multi-plane ones for singlePlane.
412+func TestTargetModifiersFiltering(t *testing.T) {
376413 r, err := New(64, 32)
377414 if err != nil {
378415 t.Skipf("Vulkan unavailable: %v", err)
@@ -382,56 +419,6 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
382419 if len(mods) == 0 {
383420 t.Skip("no importable target modifier")
384421 }
385- pipe := func() *os.File {
386- f, w, err := os.Pipe()
387- if err != nil {
388- t.Fatal(err)
389- }
390- w.Close()
391- t.Cleanup(func() { f.Close() })
392- return f
393- }
394- good := func() ports.DMABuf {
395- return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: mods[0], Planes: make([]ports.DMABufPlane, r.modPlanes[mods[0]])}
396- }
397- for name, mutate := range map[string]func(*ports.DMABuf){
398- "width": func(b *ports.DMABuf) { b.Width = 65 },
399- "height": func(b *ports.DMABuf) { b.Height = 31 },
400- "format": func(b *ports.DMABuf) { b.Format = fourccXR30 },
401- "planes": func(b *ports.DMABuf) { b.Planes = nil },
402- "modifier": func(b *ports.DMABuf) { b.Modifier = 0x1234567 },
403- } {
404- b := good()
405- for i := range b.Planes {
406- b.Planes[i].File = pipe()
407- }
408- mutate(&b)
409- if err := r.ImportTargets([]ports.DMABuf{b}); err == nil {
410- t.Errorf("%s: mismatched image imported", name)
411- }
412- if len(r.targets) != 0 {
413- t.Errorf("%s: %d targets left", name, len(r.targets))
414- }
415- }
416- // Planes in two different dmabufs: refused whatever the plane count.
417- if n := r.modPlanes[mods[0]]; n == 1 {
418- two := good()
419- two.Planes = []ports.DMABufPlane{{File: pipe()}, {File: pipe()}}
420- if err := r.ImportTargets([]ports.DMABuf{two}); err == nil {
421- t.Error("two-plane image imported for a one-plane modifier")
422- }
423- }
424- if err := sameDMABuf([]ports.DMABufPlane{{File: pipe()}, {File: pipe()}}); err == nil {
425- t.Error("planes in different files accepted")
426- }
427- same := pipe()
428- if err := sameDMABuf([]ports.DMABufPlane{{File: same}, {File: same}}); err != nil {
429- t.Errorf("planes in one file refused: %v", err)
430- }
431- if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
432- t.Errorf("dropping targets: %v, %d left", err, len(r.targets))
433- }
434- // A display list without the renderer's modifiers leaves nothing.
435422 if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
436423 t.Errorf("modifiers outside the display list: %#x", got)
437424 }
@@ -443,4 +430,112 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
443430 t.Errorf("single-plane list holds multi-plane modifier %#x", m)
444431 }
445432 }
433+ // The answer is probed once and kept.
434+ if len(r.importMods) == 0 {
435+ t.Error("import modifiers not cached")
436+ }
437+ again := r.TargetModifiers(nil, false)
438+ if !slices.Equal(mods, again) {
439+ t.Errorf("cached list differs: %#x vs %#x", mods, again)
440+ }
441+}
442+
443+// ImportTargets refuses an image that does not fit the renderer before
444+// asking the driver, says why, and leaves no targets: frames go back to the
445+// renderer's own image.
446+func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
447+ r, err := New(64, 32)
448+ if err != nil {
449+ t.Skipf("Vulkan unavailable: %v", err)
450+ }
451+ defer r.Close()
452+ var single uint64
453+ found := false
454+ for _, m := range r.TargetModifiers(nil, true) {
455+ single, found = m, true
456+ break
457+ }
458+ if !found {
459+ t.Skip("no importable one-plane target modifier")
460+ }
461+ good := func() ports.DMABuf {
462+ return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}}
463+ }
464+ for _, tc := range []struct {
465+ name string
466+ mutate func(*ports.DMABuf)
467+ want error
468+ }{
469+ {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry},
470+ {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry},
471+ {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry},
472+ {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier},
473+ {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes},
474+ {"too many planes", func(b *ports.DMABuf) {
475+ b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)})
476+ }, errImagePlanes},
477+ } {
478+ t.Run(tc.name, func(t *testing.T) {
479+ b := good()
480+ tc.mutate(&b)
481+ err := r.ImportTargets([]ports.DMABuf{b})
482+ if !errors.Is(err, tc.want) {
483+ t.Fatalf("error %v, want %v", err, tc.want)
484+ }
485+ if len(r.targets) != 0 {
486+ t.Fatalf("%d targets left", len(r.targets))
487+ }
488+ })
489+ }
490+ t.Run("second image refused drops the first", func(t *testing.T) {
491+ // A real dmabuf for the first image: the renderer's own export.
492+ exported, err := r.ExportTargets(1, []uint64{single}, true)
493+ if err != nil {
494+ t.Skipf("no exportable target to reuse: %v", err)
495+ }
496+ defer closeBufs(exported)
497+ bad := good()
498+ bad.Width = 1
499+ err = r.ImportTargets([]ports.DMABuf{exported[0], bad})
500+ if !errors.Is(err, errImageGeometry) {
501+ t.Fatalf("error %v, want the second image's geometry refusal", err)
502+ }
503+ if len(r.targets) != 0 {
504+ t.Fatalf("%d targets left", len(r.targets))
505+ }
506+ })
507+ t.Run("dropping", func(t *testing.T) {
508+ if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
509+ t.Fatalf("%v, %d targets", err, len(r.targets))
510+ }
511+ })
512+ t.Run("planes in different dmabufs", func(t *testing.T) {
513+ multi := uint64(0)
514+ for _, m := range r.TargetModifiers(nil, false) {
515+ if r.modPlanes[m] > 1 {
516+ multi = m
517+ break
518+ }
519+ }
520+ if multi == 0 {
521+ t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf")
522+ }
523+ b := good()
524+ b.Modifier = multi
525+ b.Planes = nil
526+ for range r.modPlanes[multi] {
527+ b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)})
528+ }
529+ if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) {
530+ t.Fatalf("error %v", err)
531+ }
532+ })
533+}
534+
535+func closeBufs(bufs []ports.DMABuf) {
536+ for _, b := range bufs {
537+ for _, p := range b.Planes {
538+ p.File.Close()
539+ }
540+ }
446541 }
@@ -1,6 +1,7 @@
1 package vulkan1 package vulkan
2 2
3 import (3 import (
4+ "errors"
4 "image/color"5 "image/color"
5 "math"6 "math"
6 "os"7 "os"
@@ -369,10 +370,46 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
369 }370 }
370 }371 }
371 372
372-// ImportTargets refuses images that do not fit the renderer (size, format,373+// pipeFile is a file for plane tests: a distinct inode per call.
373-// plane count, modifier) before touching the driver, and a refusal leaves374+func pipeFile(t *testing.T) *os.File {
374-// no targets: frames go back to the renderer's own image.375+ t.Helper()
375-func TestImportTargetsRefusesMismatchedImages(t *testing.T) {376+ f, w, err := os.Pipe()
377+ if err != nil {
378+ t.Fatal(err)
379+ }
380+ w.Close()
381+ t.Cleanup(func() { f.Close() })
382+ return f
383+}
384+
385+// Planes must be in one dmabuf: the same file or duplicates of it are, two
386+// files are not. No device needed.
387+func TestSameDMABuf(t *testing.T) {
388+ one, other := pipeFile(t), pipeFile(t)
389+ for _, tc := range []struct {
390+ name string
391+ planes []ports.DMABufPlane
392+ ok bool
393+ }{
394+ {"one plane", []ports.DMABufPlane{{File: one}}, true},
395+ {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true},
396+ {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false},
397+ {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false},
398+ {"no file", []ports.DMABufPlane{{File: one}, {}}, false},
399+ } {
400+ err := sameDMABuf(tc.planes)
401+ if (err == nil) != tc.ok {
402+ t.Errorf("%s: %v", tc.name, err)
403+ }
404+ if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) {
405+ t.Errorf("%s: %v is not errImageDMABuf", tc.name, err)
406+ }
407+ }
408+}
409+
410+// TargetModifiers keeps the modifiers the display lists and drops
411+// multi-plane ones for singlePlane.
412+func TestTargetModifiersFiltering(t *testing.T) {
376 r, err := New(64, 32)413 r, err := New(64, 32)
377 if err != nil {414 if err != nil {
378 t.Skipf("Vulkan unavailable: %v", err)415 t.Skipf("Vulkan unavailable: %v", err)
@@ -382,56 +419,6 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
382 if len(mods) == 0 {419 if len(mods) == 0 {
383 t.Skip("no importable target modifier")420 t.Skip("no importable target modifier")
384 }421 }
385- pipe := func() *os.File {
386- f, w, err := os.Pipe()
387- if err != nil {
388- t.Fatal(err)
389- }
390- w.Close()
391- t.Cleanup(func() { f.Close() })
392- return f
393- }
394- good := func() ports.DMABuf {
395- return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: mods[0], Planes: make([]ports.DMABufPlane, r.modPlanes[mods[0]])}
396- }
397- for name, mutate := range map[string]func(*ports.DMABuf){
398- "width": func(b *ports.DMABuf) { b.Width = 65 },
399- "height": func(b *ports.DMABuf) { b.Height = 31 },
400- "format": func(b *ports.DMABuf) { b.Format = fourccXR30 },
401- "planes": func(b *ports.DMABuf) { b.Planes = nil },
402- "modifier": func(b *ports.DMABuf) { b.Modifier = 0x1234567 },
403- } {
404- b := good()
405- for i := range b.Planes {
406- b.Planes[i].File = pipe()
407- }
408- mutate(&b)
409- if err := r.ImportTargets([]ports.DMABuf{b}); err == nil {
410- t.Errorf("%s: mismatched image imported", name)
411- }
412- if len(r.targets) != 0 {
413- t.Errorf("%s: %d targets left", name, len(r.targets))
414- }
415- }
416- // Planes in two different dmabufs: refused whatever the plane count.
417- if n := r.modPlanes[mods[0]]; n == 1 {
418- two := good()
419- two.Planes = []ports.DMABufPlane{{File: pipe()}, {File: pipe()}}
420- if err := r.ImportTargets([]ports.DMABuf{two}); err == nil {
421- t.Error("two-plane image imported for a one-plane modifier")
422- }
423- }
424- if err := sameDMABuf([]ports.DMABufPlane{{File: pipe()}, {File: pipe()}}); err == nil {
425- t.Error("planes in different files accepted")
426- }
427- same := pipe()
428- if err := sameDMABuf([]ports.DMABufPlane{{File: same}, {File: same}}); err != nil {
429- t.Errorf("planes in one file refused: %v", err)
430- }
431- if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
432- t.Errorf("dropping targets: %v, %d left", err, len(r.targets))
433- }
434- // A display list without the renderer's modifiers leaves nothing.
435 if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {422 if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
436 t.Errorf("modifiers outside the display list: %#x", got)423 t.Errorf("modifiers outside the display list: %#x", got)
437 }424 }
@@ -443,4 +430,112 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
443 t.Errorf("single-plane list holds multi-plane modifier %#x", m)430 t.Errorf("single-plane list holds multi-plane modifier %#x", m)
444 }431 }
445 }432 }
433+ // The answer is probed once and kept.
434+ if len(r.importMods) == 0 {
435+ t.Error("import modifiers not cached")
436+ }
437+ again := r.TargetModifiers(nil, false)
438+ if !slices.Equal(mods, again) {
439+ t.Errorf("cached list differs: %#x vs %#x", mods, again)
440+ }
441+}
442+
443+// ImportTargets refuses an image that does not fit the renderer before
444+// asking the driver, says why, and leaves no targets: frames go back to the
445+// renderer's own image.
446+func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
447+ r, err := New(64, 32)
448+ if err != nil {
449+ t.Skipf("Vulkan unavailable: %v", err)
450+ }
451+ defer r.Close()
452+ var single uint64
453+ found := false
454+ for _, m := range r.TargetModifiers(nil, true) {
455+ single, found = m, true
456+ break
457+ }
458+ if !found {
459+ t.Skip("no importable one-plane target modifier")
460+ }
461+ good := func() ports.DMABuf {
462+ return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}}
463+ }
464+ for _, tc := range []struct {
465+ name string
466+ mutate func(*ports.DMABuf)
467+ want error
468+ }{
469+ {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry},
470+ {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry},
471+ {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry},
472+ {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier},
473+ {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes},
474+ {"too many planes", func(b *ports.DMABuf) {
475+ b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)})
476+ }, errImagePlanes},
477+ } {
478+ t.Run(tc.name, func(t *testing.T) {
479+ b := good()
480+ tc.mutate(&b)
481+ err := r.ImportTargets([]ports.DMABuf{b})
482+ if !errors.Is(err, tc.want) {
483+ t.Fatalf("error %v, want %v", err, tc.want)
484+ }
485+ if len(r.targets) != 0 {
486+ t.Fatalf("%d targets left", len(r.targets))
487+ }
488+ })
489+ }
490+ t.Run("second image refused drops the first", func(t *testing.T) {
491+ // A real dmabuf for the first image: the renderer's own export.
492+ exported, err := r.ExportTargets(1, []uint64{single}, true)
493+ if err != nil {
494+ t.Skipf("no exportable target to reuse: %v", err)
495+ }
496+ defer closeBufs(exported)
497+ bad := good()
498+ bad.Width = 1
499+ err = r.ImportTargets([]ports.DMABuf{exported[0], bad})
500+ if !errors.Is(err, errImageGeometry) {
501+ t.Fatalf("error %v, want the second image's geometry refusal", err)
502+ }
503+ if len(r.targets) != 0 {
504+ t.Fatalf("%d targets left", len(r.targets))
505+ }
506+ })
507+ t.Run("dropping", func(t *testing.T) {
508+ if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
509+ t.Fatalf("%v, %d targets", err, len(r.targets))
510+ }
511+ })
512+ t.Run("planes in different dmabufs", func(t *testing.T) {
513+ multi := uint64(0)
514+ for _, m := range r.TargetModifiers(nil, false) {
515+ if r.modPlanes[m] > 1 {
516+ multi = m
517+ break
518+ }
519+ }
520+ if multi == 0 {
521+ t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf")
522+ }
523+ b := good()
524+ b.Modifier = multi
525+ b.Planes = nil
526+ for range r.modPlanes[multi] {
527+ b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)})
528+ }
529+ if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) {
530+ t.Fatalf("error %v", err)
531+ }
532+ })
533+}
534+
535+func closeBufs(bufs []ports.DMABuf) {
536+ for _, b := range bufs {
537+ for _, p := range b.Planes {
538+ p.File.Close()
539+ }
540+ }
446 }541 }
M internal/app/gbm_targets_test.go +4 -1
@@ -12,6 +12,9 @@ import (
1212 "golang.org/x/sys/unix"
1313 )
1414
15+// fourccXRGB8888 is DRM_FORMAT_XRGB8888.
16+const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
17+
1518 // renderNodeFor opens the render node of the GPU the renderer runs on.
1619 func renderNodeFor(t *testing.T, dev uint64) *os.File {
1720 t.Helper()
@@ -63,7 +66,7 @@ func TestGBMImagesAreRenderTargets(t *testing.T) {
6366 for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
6467 var cerr error
6568 for i := range bufs {
66- if bufs[i], cerr = dev.Create(w, h, 'X'|'R'<<8|'2'<<16|'4'<<24, mods, usage); cerr != nil {
69+ if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil {
6770 break
6871 }
6972 }
@@ -12,6 +12,9 @@ import (
12 "golang.org/x/sys/unix"12 "golang.org/x/sys/unix"
13 )13 )
14 14
15+// fourccXRGB8888 is DRM_FORMAT_XRGB8888.
16+const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
17+
15 // renderNodeFor opens the render node of the GPU the renderer runs on.18 // renderNodeFor opens the render node of the GPU the renderer runs on.
16 func renderNodeFor(t *testing.T, dev uint64) *os.File {19 func renderNodeFor(t *testing.T, dev uint64) *os.File {
17 t.Helper()20 t.Helper()
@@ -63,7 +66,7 @@ func TestGBMImagesAreRenderTargets(t *testing.T) {
63 for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {66 for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
64 var cerr error67 var cerr error
65 for i := range bufs {68 for i := range bufs {
66- if bufs[i], cerr = dev.Create(w, h, 'X'|'R'<<8|'2'<<16|'4'<<24, mods, usage); cerr != nil {69+ if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil {
67 break70 break
68 }71 }
69 }72 }
M internal/ports/devices.go +8 -8
@@ -41,7 +41,7 @@ type CaptureFrame interface {
4141 // (ADR 014: no CPU pass).
4242 type Renderer interface {
4343 // SetHDR selects the HDR10 output transform (0 disables it).
44- // The DRM output calls this before ExportTargets.
44+ // The DRM output calls this before ExportTargets or ImportTargets.
4545 SetHDR(sdrNits float64)
4646 // Render draws the scene. done is a sync file signalled when the GPU
4747 // finished the frame (nil: already finished); the caller closes it.
@@ -62,13 +62,13 @@ type Renderer interface {
6262 // cannot import dmabufs.
6363 TargetModifiers(display []uint64, singlePlane bool) []uint64
6464 // ImportTargets makes images allocated outside the renderer its
65- // targets, replacing the previous ones (no buffers: drops them). Each is
66- // one memory object (every plane file is the same dmabuf, the planes
67- // differing by offset; disjoint planes are refused) of the renderer's
68- // size and target format, with a modifier from TargetModifiers. The
69- // caller keeps its files and must
70- // keep the memory alive while the targets are in use. An error leaves
71- // the renderer without targets.
65+ // targets, replacing the previous ones (no buffers: drops them). Each
66+ // is one memory object of the renderer's size and target format, with
67+ // a modifier from TargetModifiers: every plane file is the same
68+ // dmabuf (the planes differ by offset), and disjoint planes are
69+ // refused. The caller keeps its files and must keep the memory alive
70+ // while the targets are in use. An error leaves the renderer without
71+ // targets.
7272 ImportTargets(bufs []DMABuf) error
7373 // UseTarget selects the exported image the next Render draws into.
7474 UseTarget(i int)
@@ -41,7 +41,7 @@ type CaptureFrame interface {
41 // (ADR 014: no CPU pass).41 // (ADR 014: no CPU pass).
42 type Renderer interface {42 type Renderer interface {
43 // SetHDR selects the HDR10 output transform (0 disables it).43 // SetHDR selects the HDR10 output transform (0 disables it).
44- // The DRM output calls this before ExportTargets.44+ // The DRM output calls this before ExportTargets or ImportTargets.
45 SetHDR(sdrNits float64)45 SetHDR(sdrNits float64)
46 // Render draws the scene. done is a sync file signalled when the GPU46 // Render draws the scene. done is a sync file signalled when the GPU
47 // finished the frame (nil: already finished); the caller closes it.47 // finished the frame (nil: already finished); the caller closes it.
@@ -62,13 +62,13 @@ type Renderer interface {
62 // cannot import dmabufs.62 // cannot import dmabufs.
63 TargetModifiers(display []uint64, singlePlane bool) []uint6463 TargetModifiers(display []uint64, singlePlane bool) []uint64
64 // ImportTargets makes images allocated outside the renderer its64 // ImportTargets makes images allocated outside the renderer its
65- // targets, replacing the previous ones (no buffers: drops them). Each is65+ // targets, replacing the previous ones (no buffers: drops them). Each
66- // one memory object (every plane file is the same dmabuf, the planes66+ // is one memory object of the renderer's size and target format, with
67- // differing by offset; disjoint planes are refused) of the renderer's67+ // a modifier from TargetModifiers: every plane file is the same
68- // size and target format, with a modifier from TargetModifiers. The68+ // dmabuf (the planes differ by offset), and disjoint planes are
69- // caller keeps its files and must69+ // refused. The caller keeps its files and must keep the memory alive
70- // keep the memory alive while the targets are in use. An error leaves70+ // while the targets are in use. An error leaves the renderer without
71- // the renderer without targets.71+ // targets.
72 ImportTargets(bufs []DMABuf) error72 ImportTargets(bufs []DMABuf) error
73 // UseTarget selects the exported image the next Render draws into.73 // UseTarget selects the exported image the next Render draws into.
74 UseTarget(i int)74 UseTarget(i int)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close