detached 3c13b3d

Merge branch 'fix/gbm-scanout-images' into main (!24)

fix(drm): allocate output images with GBM and import them into Vulkan
commit
3c13b3d3cf1deae3f997dd5bb3e9e0a55049380f
author
ermite · · 2026-10-22 08:45 UTC
merge of
48800da + 760a012 · diff against the first parent
signature
SSH VerifiedSHA256:lY9fILbB/A+hln6Vh/iHoiiog74bbw/4qWrf/lrTU3U · ermite instance key Verified for the committer email
checks
1/1
30 files · +2354 -69
M .mockery.yml +10 -0
@@ -34,6 +34,16 @@ packages:
3434 dir: internal/adapters/drm
3535 pkgname: drm
3636 filename: kms_mock_test.go
37+ imageAllocator:
38+ config:
39+ dir: internal/adapters/drm
40+ pkgname: drm
41+ filename: image_allocator_mock_test.go
42+ imageBuffer:
43+ config:
44+ dir: internal/adapters/drm
45+ pkgname: drm
46+ filename: image_buffer_mock_test.go
3747 github.com/bnema/neferwl/internal/adapters/seat:
3848 interfaces:
3949 disabler:
@@ -34,6 +34,16 @@ packages:
34 dir: internal/adapters/drm34 dir: internal/adapters/drm
35 pkgname: drm35 pkgname: drm
36 filename: kms_mock_test.go36 filename: kms_mock_test.go
37+ imageAllocator:
38+ config:
39+ dir: internal/adapters/drm
40+ pkgname: drm
41+ filename: image_allocator_mock_test.go
42+ imageBuffer:
43+ config:
44+ dir: internal/adapters/drm
45+ pkgname: drm
46+ filename: image_buffer_mock_test.go
37 github.com/bnema/neferwl/internal/adapters/seat:47 github.com/bnema/neferwl/internal/adapters/seat:
38 interfaces:48 interfaces:
39 disabler:49 disabler:
M docs/dev/architecture.md +9 -1
@@ -20,10 +20,18 @@ About 35 channels connect them, all created in one place, so the full data flow
2020
2121 - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating.
2222 - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](../performance.md).
23-- **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary.
23+- **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan, libinput and GBM are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary.
2424 - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing.
2525 - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate.
2626
27+### Output images
28+
29+A DRM output flips between two images the renderer draws into, with no CPU copy.
30+
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`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23).
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+
2735 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:
2836
2937 - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo.
@@ -20,10 +20,18 @@ About 35 channels connect them, all created in one place, so the full data flow
20 20
21 - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating.21 - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating.
22 - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](../performance.md).22 - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](../performance.md).
23-- **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary.23+- **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan, libinput and GBM are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary.
24 - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing.24 - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing.
25 - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate.25 - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate.
26 26
27+### Output images
28+
29+A DRM output flips between two images the renderer draws into, with no CPU copy.
30+
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`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23).
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+
27 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:
28 36
29 - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo.37 - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo.
A internal/adapters/drm/alloc.go +50 -0
@@ -0,0 +1,50 @@
1+package drm
2+
3+import (
4+ "github.com/bnema/neferwl/internal/adapters/drm/gbm"
5+ "github.com/bnema/neferwl/internal/ports"
6+)
7+
8+// imageAllocator makes the output images with the display driver's own
9+// allocator (GBM), which knows the layouts the driver scans out correctly:
10+// images allocated by the renderer show corrupted on NVIDIA. One per
11+// output, used by its goroutine only.
12+type imageAllocator interface {
13+ // create allocates an image whose modifier is one of modifiers.
14+ create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error)
15+ close()
16+}
17+
18+// imageBuffer is one allocated image, kept alive while it is shown.
19+type imageBuffer interface {
20+ // dmabuf returns the image with fresh descriptors the caller closes.
21+ dmabuf() (ports.DMABuf, error)
22+ close()
23+}
24+
25+// gbmAllocator allocates scanout-capable, renderable images with libgbm.
26+type gbmAllocator struct{ dev *gbm.Device }
27+
28+// openGBM opens a GBM allocator on a duplicate of the card fd.
29+func openGBM(fd int) (imageAllocator, error) {
30+ dev, err := gbm.Open(fd)
31+ if err != nil {
32+ return nil, err
33+ }
34+ return gbmAllocator{dev}, nil
35+}
36+
37+func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
38+ b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)
39+ if err != nil {
40+ return nil, err
41+ }
42+ return gbmImage{b}, nil
43+}
44+
45+func (a gbmAllocator) close() { a.dev.Close() }
46+
47+type gbmImage struct{ b *gbm.Buffer }
48+
49+func (i gbmImage) dmabuf() (ports.DMABuf, error) { return i.b.DMABuf() }
50+func (i gbmImage) close() { i.b.Close() }
@@ -0,0 +1,50 @@
1+package drm
2+
3+import (
4+ "github.com/bnema/neferwl/internal/adapters/drm/gbm"
5+ "github.com/bnema/neferwl/internal/ports"
6+)
7+
8+// imageAllocator makes the output images with the display driver's own
9+// allocator (GBM), which knows the layouts the driver scans out correctly:
10+// images allocated by the renderer show corrupted on NVIDIA. One per
11+// output, used by its goroutine only.
12+type imageAllocator interface {
13+ // create allocates an image whose modifier is one of modifiers.
14+ create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error)
15+ close()
16+}
17+
18+// imageBuffer is one allocated image, kept alive while it is shown.
19+type imageBuffer interface {
20+ // dmabuf returns the image with fresh descriptors the caller closes.
21+ dmabuf() (ports.DMABuf, error)
22+ close()
23+}
24+
25+// gbmAllocator allocates scanout-capable, renderable images with libgbm.
26+type gbmAllocator struct{ dev *gbm.Device }
27+
28+// openGBM opens a GBM allocator on a duplicate of the card fd.
29+func openGBM(fd int) (imageAllocator, error) {
30+ dev, err := gbm.Open(fd)
31+ if err != nil {
32+ return nil, err
33+ }
34+ return gbmAllocator{dev}, nil
35+}
36+
37+func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
38+ b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)
39+ if err != nil {
40+ return nil, err
41+ }
42+ return gbmImage{b}, nil
43+}
44+
45+func (a gbmAllocator) close() { a.dev.Close() }
46+
47+type gbmImage struct{ b *gbm.Buffer }
48+
49+func (i gbmImage) dmabuf() (ports.DMABuf, error) { return i.b.DMABuf() }
50+func (i gbmImage) close() { i.b.Close() }
A internal/adapters/drm/alloc_test.go +234 -0
@@ -0,0 +1,234 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+
8+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// gbmBuf is a dmabuf over a closed pipe, as an image buffer hands out.
15+func gbmBuf(planes int) ports.DMABuf {
16+ b := ports.DMABuf{Modifier: tiledMod}
17+ for range planes {
18+ f, w, _ := os.Pipe()
19+ w.Close()
20+ b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
21+ }
22+ return b
23+}
24+
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) {
31+ img := newMockimageBuffer(t)
32+ img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil })
33+ img.EXPECT().close().Run(func() { *closed++ }).Once()
34+ return img, nil
35+ }).Twice()
36+ m.EXPECT().close().Return().Maybe()
37+ return m, closed
38+}
39+
40+func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {
41+ r := portsmocks.NewMockRenderer(t)
42+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
43+ r.EXPECT().UseTarget(mock.Anything).Return().Maybe()
44+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
45+ return r
46+}
47+
48+// GBM images come first: the renderer imports them, the framebuffers are
49+// made from them, and no image is exported.
50+func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {
51+ o, k, _ := testOutput(t)
52+ o.cursor = nil
53+ a, closed := twoGBMImages(t, 1)
54+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
55+ r := gbmRenderer(t)
56+ r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
57+ r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 && b[0].Modifier == tiledMod })).Return(nil).Once()
58+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
59+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
60+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
61+ k.EXPECT().rmFB(mock.Anything).Return(nil)
62+ if err := o.showImages(r, imagesDriver, nil); err != nil {
63+ t.Fatal(err)
64+ }
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)
67+ }
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)
72+ }
73+}
74+
75+// A GBM failure at any step falls back to the renderer's own images of the same kind.
76+func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
77+ for name, tc := range map[string]struct {
78+ setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer)
79+ }{
80+ "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) {
81+ o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") }
82+ }},
83+ "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
84+ a := newMockimageAllocator(t)
85+ a.EXPECT().close().Return().Maybe()
86+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
87+ r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once()
88+ }},
89+ "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
90+ a := newMockimageAllocator(t)
91+ a.EXPECT().close().Return().Maybe()
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 }
94+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
95+ }},
96+ "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
97+ a := newMockimageAllocator(t)
98+ a.EXPECT().close().Return().Maybe()
99+ img := newMockimageBuffer(t)
100+ img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once()
101+ img.EXPECT().close().Return().Once() // freed with the failed attempt
102+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
103+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()
104+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()
105+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
106+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
107+ }},
108+ "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
109+ a, _ := twoGBMImages(t, 1)
110+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
111+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).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()
115+ }},
116+ "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
117+ a, _ := twoGBMImages(t, 1)
118+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
119+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
120+ r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()
121+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
122+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
123+ }},
124+ } {
125+ t.Run(name, func(t *testing.T) {
126+ o, k, _ := testOutput(t)
127+ o.cursor = nil
128+ r := gbmRenderer(t)
129+ tc.setup(t, o, k, r)
130+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
131+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
132+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
133+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
134+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
135+ if err := o.showImages(r, imagesDriver, nil); err != nil {
136+ t.Fatal(err)
137+ }
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)
141+ }
142+ })
143+ }
144+}
145+
146+// A failed GBM open is remembered: the next kind and the next setup do not
147+// open the library again.
148+func TestGBMOpenFailureIsRemembered(t *testing.T) {
149+ o, k, _ := testOutput(t, unix.EINVAL)
150+ o.cursor = nil
151+ opens := 0
152+ o.openAlloc = func() (imageAllocator, error) { opens++; return nil, errors.New("no libgbm") }
153+ r := gbmRenderer(t)
154+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(2), gbmBuf(2)}, nil).Once()
155+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
156+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
157+ k.EXPECT().rmFB(mock.Anything).Return(nil)
158+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
159+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
160+ if err := o.showImages(r, imagesDriver, nil); err != nil {
161+ t.Fatal(err)
162+ }
163+ if opens != 1 || o.kind != imagesSinglePlane {
164+ t.Fatalf("%d opens, kind %d", opens, o.kind)
165+ }
166+}
167+
168+// A modeset test that refuses GBM images tries the renderer's images of the
169+// same kind before the next kind.
170+func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
171+ o, k, commits := testOutput(t, unix.EINVAL)
172+ o.cursor = nil
173+ a, closed := twoGBMImages(t, 1)
174+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
175+ r := gbmRenderer(t)
176+ r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
177+ r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 })).Return(nil).Once()
178+ // The refused GBM images leave no renderer target behind.
179+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
180+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
181+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
182+ k.EXPECT().rmFB(mock.Anything).Return(nil)
183+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
184+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
185+ if err := o.showImages(r, imagesDriver, nil); err != nil {
186+ t.Fatal(err)
187+ }
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))
190+ }
191+}
192+
193+// Closing the output frees the GBM images and closes the allocator once.
194+func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) {
195+ o, k, _ := testOutput(t)
196+ o.cursor = nil
197+ img := newMockimageBuffer(t)
198+ img.EXPECT().close().Return().Twice()
199+ alloc := newMockimageAllocator(t)
200+ alloc.EXPECT().close().Return().Once()
201+ o.images = []imageBuffer{img, img}
202+ o.alloc = alloc
203+ o.fbs = [2]uint32{70, 71}
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)
209+ }
210+}
211+
212+// A setup that finds images left over frees them first, with the renderer's
213+// targets over GBM ones: the renderer never keeps a target over a freed
214+// image.
215+func TestSetupStartsFromCleanState(t *testing.T) {
216+ o, k, _ := testOutput(t)
217+ o.cursor = nil
218+ img := newMockimageBuffer(t)
219+ img.EXPECT().close().Return().Once()
220+ o.images = []imageBuffer{img}
221+ o.fbs = [2]uint32{70, 71}
222+ k.EXPECT().rmFB(uint32(70)).Return(nil).Once()
223+ k.EXPECT().rmFB(uint32(71)).Return(nil).Once()
224+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(80, nil)
225+ r := gbmRenderer(t)
226+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
227+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
228+ if err := o.exportImages(r, nil, false); err != nil {
229+ t.Fatal(err)
230+ }
231+ if o.images != nil || o.fbs != [2]uint32{80, 80} {
232+ t.Fatalf("images %v fbs %v", o.images, o.fbs)
233+ }
234+}
@@ -0,0 +1,234 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+
8+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// gbmBuf is a dmabuf over a closed pipe, as an image buffer hands out.
15+func gbmBuf(planes int) ports.DMABuf {
16+ b := ports.DMABuf{Modifier: tiledMod}
17+ for range planes {
18+ f, w, _ := os.Pipe()
19+ w.Close()
20+ b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
21+ }
22+ return b
23+}
24+
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) {
31+ img := newMockimageBuffer(t)
32+ img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil })
33+ img.EXPECT().close().Run(func() { *closed++ }).Once()
34+ return img, nil
35+ }).Twice()
36+ m.EXPECT().close().Return().Maybe()
37+ return m, closed
38+}
39+
40+func gbmRenderer(t *testing.T) *portsmocks.MockRenderer {
41+ r := portsmocks.NewMockRenderer(t)
42+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
43+ r.EXPECT().UseTarget(mock.Anything).Return().Maybe()
44+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
45+ return r
46+}
47+
48+// GBM images come first: the renderer imports them, the framebuffers are
49+// made from them, and no image is exported.
50+func TestShowImagesAllocatesWithGBMFirst(t *testing.T) {
51+ o, k, _ := testOutput(t)
52+ o.cursor = nil
53+ a, closed := twoGBMImages(t, 1)
54+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
55+ r := gbmRenderer(t)
56+ r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
57+ r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 && b[0].Modifier == tiledMod })).Return(nil).Once()
58+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
59+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
60+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
61+ k.EXPECT().rmFB(mock.Anything).Return(nil)
62+ if err := o.showImages(r, imagesDriver, nil); err != nil {
63+ t.Fatal(err)
64+ }
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)
67+ }
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)
72+ }
73+}
74+
75+// A GBM failure at any step falls back to the renderer's own images of the same kind.
76+func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
77+ for name, tc := range map[string]struct {
78+ setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer)
79+ }{
80+ "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) {
81+ o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") }
82+ }},
83+ "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
84+ a := newMockimageAllocator(t)
85+ a.EXPECT().close().Return().Maybe()
86+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
87+ r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once()
88+ }},
89+ "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
90+ a := newMockimageAllocator(t)
91+ a.EXPECT().close().Return().Maybe()
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 }
94+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
95+ }},
96+ "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
97+ a := newMockimageAllocator(t)
98+ a.EXPECT().close().Return().Maybe()
99+ img := newMockimageBuffer(t)
100+ img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once()
101+ img.EXPECT().close().Return().Once() // freed with the failed attempt
102+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
103+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()
104+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()
105+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
106+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
107+ }},
108+ "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
109+ a, _ := twoGBMImages(t, 1)
110+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
111+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).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()
115+ }},
116+ "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) {
117+ a, _ := twoGBMImages(t, 1)
118+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
119+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
120+ r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()
121+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
122+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
123+ }},
124+ } {
125+ t.Run(name, func(t *testing.T) {
126+ o, k, _ := testOutput(t)
127+ o.cursor = nil
128+ r := gbmRenderer(t)
129+ tc.setup(t, o, k, r)
130+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
131+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
132+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
133+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
134+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
135+ if err := o.showImages(r, imagesDriver, nil); err != nil {
136+ t.Fatal(err)
137+ }
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)
141+ }
142+ })
143+ }
144+}
145+
146+// A failed GBM open is remembered: the next kind and the next setup do not
147+// open the library again.
148+func TestGBMOpenFailureIsRemembered(t *testing.T) {
149+ o, k, _ := testOutput(t, unix.EINVAL)
150+ o.cursor = nil
151+ opens := 0
152+ o.openAlloc = func() (imageAllocator, error) { opens++; return nil, errors.New("no libgbm") }
153+ r := gbmRenderer(t)
154+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(2), gbmBuf(2)}, nil).Once()
155+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
156+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
157+ k.EXPECT().rmFB(mock.Anything).Return(nil)
158+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
159+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
160+ if err := o.showImages(r, imagesDriver, nil); err != nil {
161+ t.Fatal(err)
162+ }
163+ if opens != 1 || o.kind != imagesSinglePlane {
164+ t.Fatalf("%d opens, kind %d", opens, o.kind)
165+ }
166+}
167+
168+// A modeset test that refuses GBM images tries the renderer's images of the
169+// same kind before the next kind.
170+func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
171+ o, k, commits := testOutput(t, unix.EINVAL)
172+ o.cursor = nil
173+ a, closed := twoGBMImages(t, 1)
174+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
175+ r := gbmRenderer(t)
176+ r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
177+ r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 })).Return(nil).Once()
178+ // The refused GBM images leave no renderer target behind.
179+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
180+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
181+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
182+ k.EXPECT().rmFB(mock.Anything).Return(nil)
183+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
184+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
185+ if err := o.showImages(r, imagesDriver, nil); err != nil {
186+ t.Fatal(err)
187+ }
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))
190+ }
191+}
192+
193+// Closing the output frees the GBM images and closes the allocator once.
194+func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) {
195+ o, k, _ := testOutput(t)
196+ o.cursor = nil
197+ img := newMockimageBuffer(t)
198+ img.EXPECT().close().Return().Twice()
199+ alloc := newMockimageAllocator(t)
200+ alloc.EXPECT().close().Return().Once()
201+ o.images = []imageBuffer{img, img}
202+ o.alloc = alloc
203+ o.fbs = [2]uint32{70, 71}
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)
209+ }
210+}
211+
212+// A setup that finds images left over frees them first, with the renderer's
213+// targets over GBM ones: the renderer never keeps a target over a freed
214+// image.
215+func TestSetupStartsFromCleanState(t *testing.T) {
216+ o, k, _ := testOutput(t)
217+ o.cursor = nil
218+ img := newMockimageBuffer(t)
219+ img.EXPECT().close().Return().Once()
220+ o.images = []imageBuffer{img}
221+ o.fbs = [2]uint32{70, 71}
222+ k.EXPECT().rmFB(uint32(70)).Return(nil).Once()
223+ k.EXPECT().rmFB(uint32(71)).Return(nil).Once()
224+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(80, nil)
225+ r := gbmRenderer(t)
226+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
227+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
228+ if err := o.exportImages(r, nil, false); err != nil {
229+ t.Fatal(err)
230+ }
231+ if o.images != nil || o.fbs != [2]uint32{80, 80} {
232+ t.Fatalf("images %v fbs %v", o.images, o.fbs)
233+ }
234+}
M internal/adapters/drm/amdgpu_rules_test.go +1 -0
@@ -407,6 +407,7 @@ func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T)
407407 }
408408 return nil
409409 })
410+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
410411 o.cursor = nil
411412 var state atomic.Uint64
412413 state.Store(3)
@@ -407,6 +407,7 @@ func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T)
407 }407 }
408 return nil408 return nil
409 })409 })
410+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
410 o.cursor = nil411 o.cursor = nil
411 var state atomic.Uint64412 var state atomic.Uint64
412 state.Store(3)413 state.Store(3)
M internal/adapters/drm/card.go +4 -1
@@ -38,6 +38,9 @@ type Card struct {
3838 taken map[uint32]bool
3939 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.
4040 async bool
41+ // cpuFences: commits wait for fences on the CPU instead of IN_FENCE_FD
42+ // (the driver mishandles it, see fencegate.go).
43+ cpuFences bool
4144 // serials numbers the commits of every output (event userData).
4245 serials atomic.Uint64
4346 // formats receives each output's direct scanout formats (nil: none
@@ -65,7 +68,7 @@ func OpenCard(fd int, path string, want Want, log, renderLog zerowrap.Logger) (*
6568 flips[c] = make(chan flipEvent, 4)
6669 }
6770 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}
68- return &Card{fd: fd, path: path, want: want, log: log, renderLog: renderLog, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil
71+ return &Card{fd: fd, path: path, want: want, log: log, renderLog: renderLog, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync), cpuFences: fenceGate(fd, log)}, nil
6972 }
7073
7174 // Path is the device path, e.g. /dev/dri/card1.
@@ -38,6 +38,9 @@ type Card struct {
38 taken map[uint32]bool38 taken map[uint32]bool
39 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.39 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.
40 async bool40 async bool
41+ // cpuFences: commits wait for fences on the CPU instead of IN_FENCE_FD
42+ // (the driver mishandles it, see fencegate.go).
43+ cpuFences bool
41 // serials numbers the commits of every output (event userData).44 // serials numbers the commits of every output (event userData).
42 serials atomic.Uint6445 serials atomic.Uint64
43 // formats receives each output's direct scanout formats (nil: none46 // formats receives each output's direct scanout formats (nil: none
@@ -65,7 +68,7 @@ func OpenCard(fd int, path string, want Want, log, renderLog zerowrap.Logger) (*
65 flips[c] = make(chan flipEvent, 4)68 flips[c] = make(chan flipEvent, 4)
66 }69 }
67 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}70 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}
68- return &Card{fd: fd, path: path, want: want, log: log, renderLog: renderLog, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil71+ return &Card{fd: fd, path: path, want: want, log: log, renderLog: renderLog, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync), cpuFences: fenceGate(fd, log)}, nil
69 }72 }
70 73
71 // Path is the device path, e.g. /dev/dri/card1.74 // Path is the device path, e.g. /dev/dri/card1.
M internal/adapters/drm/commit_test.go +1 -0
@@ -284,6 +284,7 @@ func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
284284 // single-plane attempt KMS would refuse.
285285 func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {
286286 o, k, commits := testOutput(t)
287+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
287288 o.cursor = nil
288289 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
289290 r := portsmocks.NewMockRenderer(t)
@@ -284,6 +284,7 @@ func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
284 // single-plane attempt KMS would refuse.284 // single-plane attempt KMS would refuse.
285 func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {285 func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {
286 o, k, commits := testOutput(t)286 o, k, commits := testOutput(t)
287+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
287 o.cursor = nil288 o.cursor = nil
288 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}289 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
289 r := portsmocks.NewMockRenderer(t)290 r := portsmocks.NewMockRenderer(t)
A internal/adapters/drm/fencegate.go +214 -0
@@ -0,0 +1,214 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "os"
8+ "strconv"
9+ "strings"
10+ "time"
11+ "unsafe"
12+
13+ "github.com/bnema/neferwl/internal/adapters/syncfile"
14+ "github.com/bnema/zerowrap"
15+ "golang.org/x/sys/unix"
16+)
17+
18+// Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
19+// the fence and shows a frame the GPU still draws
20+// (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the
21+// output waits for each fence on the CPU and commits without it.
22+
23+const (
24+ driverNVIDIA = "nvidia-drm"
25+ // nvidiaDRMVersionFile holds the loaded module's version.
26+ nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version"
27+ // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version).
28+ ioctlVersion = 0xC0406400
29+)
30+
31+// nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD.
32+var nvidiaFenceFixed = [3]int{560, 35, 3}
33+
34+// drmVersion is struct drm_version.
35+type drmVersion struct {
36+ major, minor, patch int32
37+ nameLen uint64
38+ name unsafe.Pointer
39+ dateLen uint64
40+ date unsafe.Pointer
41+ descLen uint64
42+ desc unsafe.Pointer
43+}
44+
45+// driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION).
46+func driverName(fd int) (string, error) {
47+ var buf [64]byte
48+ v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])}
49+ if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil {
50+ return "", err
51+ }
52+ return string(buf[:min(int(v.nameLen), len(buf))]), nil
53+}
54+
55+// parseVersion reads a dotted version such as "560.35.03" (missing parts
56+// are 0). Parts are plain digits: no sign.
57+func parseVersion(s string) ([3]int, error) {
58+ var v [3]int
59+ parts := strings.Split(strings.TrimSpace(s), ".")
60+ if len(parts) > len(v) {
61+ return v, fmt.Errorf("version %q", s)
62+ }
63+ for i, p := range parts {
64+ n, err := strconv.ParseUint(p, 10, 31)
65+ if err != nil {
66+ return v, fmt.Errorf("version %q", s)
67+ }
68+ v[i] = int(n)
69+ }
70+ return v, nil
71+}
72+
73+// versionLess reports whether a < b.
74+func versionLess(a, b [3]int) bool {
75+ for i := range a {
76+ if a[i] != b[i] {
77+ return a[i] < b[i]
78+ }
79+ }
80+ return false
81+}
82+
83+// brokenInFence reports whether the driver may mishandle IN_FENCE_FD:
84+// nvidia-drm before 560.35.3, or whose version is unknown (version is the
85+// module version file's content, err its read error).
86+func brokenInFence(driver, version string, err error) bool {
87+ if driver != driverNVIDIA {
88+ return false
89+ }
90+ if err != nil {
91+ return true
92+ }
93+ v, perr := parseVersion(version)
94+ return perr != nil || versionLess(v, nvidiaFenceFixed)
95+}
96+
97+// fenceGate decides, once per card, whether commits carry no IN_FENCE_FD.
98+func fenceGate(fd int, log zerowrap.Logger) bool {
99+ driver, err := driverName(fd)
100+ if err != nil {
101+ log.Debug().Err(err).Msg("drm driver name unavailable")
102+ return false
103+ }
104+ var version string
105+ var verr error
106+ if driver == driverNVIDIA {
107+ b, rerr := os.ReadFile(nvidiaDRMVersionFile)
108+ version, verr = string(b), rerr
109+ }
110+ if !brokenInFence(driver, version, verr) {
111+ return false
112+ }
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 true
115+}
116+
117+// fenceProp sets IN_FENCE_FD on plane p to the fence, unless there is none
118+// or the fences are waited for on the CPU (waitFences).
119+func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) {
120+ if fence == nil || o.cpuFences {
121+ return
122+ }
123+ req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd()))
124+}
125+
126+// fenceWaitLimit bounds the CPU wait of one commit, whatever its number of
127+// fences.
128+const fenceWaitLimit = time.Second
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+
150+// clientFenceReady reports whether a client's acquire fence lets its buffer
151+// be shown by a plane this frame. Where fences are waited for on the CPU, a
152+// client fence that has not signalled must not stall the output: the buffer
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.
156+func (o *Output) clientFenceReady(acquire *os.File) bool {
157+ if !o.cpuFences || acquire == nil {
158+ return true
159+ }
160+ if o.fencePoll.fn == nil {
161+ o.fencePoll.init()
162+ }
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
174+}
175+
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.
183+func (o *Output) waitFences(fences ...*os.File) {
184+ if !o.cpuFences {
185+ return
186+ }
187+ ctx := o.runContext
188+ if ctx == nil {
189+ ctx = context.Background()
190+ }
191+ limit := o.fenceLimit
192+ if limit == 0 {
193+ limit = fenceWaitLimit
194+ }
195+ ctx, cancel := context.WithTimeout(ctx, limit)
196+ defer cancel()
197+ for _, f := range fences {
198+ if f == nil {
199+ continue
200+ }
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:
211+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
212+ }
213+ }
214+}
@@ -0,0 +1,214 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "os"
8+ "strconv"
9+ "strings"
10+ "time"
11+ "unsafe"
12+
13+ "github.com/bnema/neferwl/internal/adapters/syncfile"
14+ "github.com/bnema/zerowrap"
15+ "golang.org/x/sys/unix"
16+)
17+
18+// Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
19+// the fence and shows a frame the GPU still draws
20+// (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the
21+// output waits for each fence on the CPU and commits without it.
22+
23+const (
24+ driverNVIDIA = "nvidia-drm"
25+ // nvidiaDRMVersionFile holds the loaded module's version.
26+ nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version"
27+ // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version).
28+ ioctlVersion = 0xC0406400
29+)
30+
31+// nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD.
32+var nvidiaFenceFixed = [3]int{560, 35, 3}
33+
34+// drmVersion is struct drm_version.
35+type drmVersion struct {
36+ major, minor, patch int32
37+ nameLen uint64
38+ name unsafe.Pointer
39+ dateLen uint64
40+ date unsafe.Pointer
41+ descLen uint64
42+ desc unsafe.Pointer
43+}
44+
45+// driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION).
46+func driverName(fd int) (string, error) {
47+ var buf [64]byte
48+ v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])}
49+ if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil {
50+ return "", err
51+ }
52+ return string(buf[:min(int(v.nameLen), len(buf))]), nil
53+}
54+
55+// parseVersion reads a dotted version such as "560.35.03" (missing parts
56+// are 0). Parts are plain digits: no sign.
57+func parseVersion(s string) ([3]int, error) {
58+ var v [3]int
59+ parts := strings.Split(strings.TrimSpace(s), ".")
60+ if len(parts) > len(v) {
61+ return v, fmt.Errorf("version %q", s)
62+ }
63+ for i, p := range parts {
64+ n, err := strconv.ParseUint(p, 10, 31)
65+ if err != nil {
66+ return v, fmt.Errorf("version %q", s)
67+ }
68+ v[i] = int(n)
69+ }
70+ return v, nil
71+}
72+
73+// versionLess reports whether a < b.
74+func versionLess(a, b [3]int) bool {
75+ for i := range a {
76+ if a[i] != b[i] {
77+ return a[i] < b[i]
78+ }
79+ }
80+ return false
81+}
82+
83+// brokenInFence reports whether the driver may mishandle IN_FENCE_FD:
84+// nvidia-drm before 560.35.3, or whose version is unknown (version is the
85+// module version file's content, err its read error).
86+func brokenInFence(driver, version string, err error) bool {
87+ if driver != driverNVIDIA {
88+ return false
89+ }
90+ if err != nil {
91+ return true
92+ }
93+ v, perr := parseVersion(version)
94+ return perr != nil || versionLess(v, nvidiaFenceFixed)
95+}
96+
97+// fenceGate decides, once per card, whether commits carry no IN_FENCE_FD.
98+func fenceGate(fd int, log zerowrap.Logger) bool {
99+ driver, err := driverName(fd)
100+ if err != nil {
101+ log.Debug().Err(err).Msg("drm driver name unavailable")
102+ return false
103+ }
104+ var version string
105+ var verr error
106+ if driver == driverNVIDIA {
107+ b, rerr := os.ReadFile(nvidiaDRMVersionFile)
108+ version, verr = string(b), rerr
109+ }
110+ if !brokenInFence(driver, version, verr) {
111+ return false
112+ }
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 true
115+}
116+
117+// fenceProp sets IN_FENCE_FD on plane p to the fence, unless there is none
118+// or the fences are waited for on the CPU (waitFences).
119+func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) {
120+ if fence == nil || o.cpuFences {
121+ return
122+ }
123+ req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd()))
124+}
125+
126+// fenceWaitLimit bounds the CPU wait of one commit, whatever its number of
127+// fences.
128+const fenceWaitLimit = time.Second
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+
150+// clientFenceReady reports whether a client's acquire fence lets its buffer
151+// be shown by a plane this frame. Where fences are waited for on the CPU, a
152+// client fence that has not signalled must not stall the output: the buffer
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.
156+func (o *Output) clientFenceReady(acquire *os.File) bool {
157+ if !o.cpuFences || acquire == nil {
158+ return true
159+ }
160+ if o.fencePoll.fn == nil {
161+ o.fencePoll.init()
162+ }
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
174+}
175+
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.
183+func (o *Output) waitFences(fences ...*os.File) {
184+ if !o.cpuFences {
185+ return
186+ }
187+ ctx := o.runContext
188+ if ctx == nil {
189+ ctx = context.Background()
190+ }
191+ limit := o.fenceLimit
192+ if limit == 0 {
193+ limit = fenceWaitLimit
194+ }
195+ ctx, cancel := context.WithTimeout(ctx, limit)
196+ defer cancel()
197+ for _, f := range fences {
198+ if f == nil {
199+ continue
200+ }
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:
211+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
212+ }
213+ }
214+}
A internal/adapters/drm/fencegate_test.go +283 -0
@@ -0,0 +1,283 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "path/filepath"
8+ "testing"
9+ "time"
10+
11+ "github.com/bnema/neferwl/internal/ports"
12+ "github.com/stretchr/testify/mock"
13+ "golang.org/x/sys/unix"
14+)
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+
29+func TestParseVersion(t *testing.T) {
30+ for _, tc := range []struct {
31+ in string
32+ want [3]int
33+ ok bool
34+ }{
35+ {"560.35.03\n", [3]int{560, 35, 3}, true},
36+ {"615.78.08", [3]int{615, 78, 8}, true},
37+ {"550.120", [3]int{550, 120, 0}, true},
38+ {"570", [3]int{570, 0, 0}, true},
39+ {"", [3]int{}, false},
40+ {"abc", [3]int{}, false},
41+ {"560.x.3", [3]int{}, false},
42+ {"1.2.3.4", [3]int{}, false},
43+ {"-1.2", [3]int{}, false},
44+ {"+560.35.3", [3]int{}, false},
45+ {"560.+35.3", [3]int{}, false},
46+ {"560.-5.3", [3]int{}, false},
47+ {"560..3", [3]int{}, false},
48+ {"99999999999.1.1", [3]int{}, false},
49+ } {
50+ got, err := parseVersion(tc.in)
51+ if (err == nil) != tc.ok || tc.ok && got != tc.want {
52+ t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err)
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.
59+func TestBrokenInFence(t *testing.T) {
60+ for _, tc := range []struct {
61+ name, driver, version string
62+ err error
63+ want bool
64+ }{
65+ {"amd", "amdgpu", "", nil, false},
66+ {"intel", "i915", "garbage", nil, false},
67+ {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
68+ {"one patch before", driverNVIDIA, "560.35.2", nil, true},
69+ {"minor before", driverNVIDIA, "560.34.99", nil, true},
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},
74+ {"newer", driverNVIDIA, "615.78.08", nil, false},
75+ {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
76+ {"unparsable", driverNVIDIA, "n/a", nil, true},
77+ } {
78+ if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want {
79+ t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want)
80+ }
81+ }
82+}
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+
107+// With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
108+func TestFenceProp(t *testing.T) {
109+ f, _ := pipeFence(t)
110+ for _, cpu := range []bool{false, true} {
111+ o, _, _ := testOutput(t)
112+ o.cpuFences = cpu
113+ req := &atomicReq{}
114+ o.fenceProp(req, o.primary, f)
115+ o.fenceProp(req, o.primary, nil)
116+ v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD"))
117+ if ok == cpu || ok && v != uint64(f.Fd()) {
118+ t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok)
119+ }
120+ }
121+}
122+
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.
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) {
165+ o, _, _ := testOutput(t)
166+ o.cpuFences = true
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 {
270+ t.Fatal(err)
271+ }
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+ }
283+}
@@ -0,0 +1,283 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "path/filepath"
8+ "testing"
9+ "time"
10+
11+ "github.com/bnema/neferwl/internal/ports"
12+ "github.com/stretchr/testify/mock"
13+ "golang.org/x/sys/unix"
14+)
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+
29+func TestParseVersion(t *testing.T) {
30+ for _, tc := range []struct {
31+ in string
32+ want [3]int
33+ ok bool
34+ }{
35+ {"560.35.03\n", [3]int{560, 35, 3}, true},
36+ {"615.78.08", [3]int{615, 78, 8}, true},
37+ {"550.120", [3]int{550, 120, 0}, true},
38+ {"570", [3]int{570, 0, 0}, true},
39+ {"", [3]int{}, false},
40+ {"abc", [3]int{}, false},
41+ {"560.x.3", [3]int{}, false},
42+ {"1.2.3.4", [3]int{}, false},
43+ {"-1.2", [3]int{}, false},
44+ {"+560.35.3", [3]int{}, false},
45+ {"560.+35.3", [3]int{}, false},
46+ {"560.-5.3", [3]int{}, false},
47+ {"560..3", [3]int{}, false},
48+ {"99999999999.1.1", [3]int{}, false},
49+ } {
50+ got, err := parseVersion(tc.in)
51+ if (err == nil) != tc.ok || tc.ok && got != tc.want {
52+ t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err)
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.
59+func TestBrokenInFence(t *testing.T) {
60+ for _, tc := range []struct {
61+ name, driver, version string
62+ err error
63+ want bool
64+ }{
65+ {"amd", "amdgpu", "", nil, false},
66+ {"intel", "i915", "garbage", nil, false},
67+ {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
68+ {"one patch before", driverNVIDIA, "560.35.2", nil, true},
69+ {"minor before", driverNVIDIA, "560.34.99", nil, true},
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},
74+ {"newer", driverNVIDIA, "615.78.08", nil, false},
75+ {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
76+ {"unparsable", driverNVIDIA, "n/a", nil, true},
77+ } {
78+ if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want {
79+ t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want)
80+ }
81+ }
82+}
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+
107+// With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
108+func TestFenceProp(t *testing.T) {
109+ f, _ := pipeFence(t)
110+ for _, cpu := range []bool{false, true} {
111+ o, _, _ := testOutput(t)
112+ o.cpuFences = cpu
113+ req := &atomicReq{}
114+ o.fenceProp(req, o.primary, f)
115+ o.fenceProp(req, o.primary, nil)
116+ v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD"))
117+ if ok == cpu || ok && v != uint64(f.Fd()) {
118+ t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok)
119+ }
120+ }
121+}
122+
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.
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) {
165+ o, _, _ := testOutput(t)
166+ o.cpuFences = true
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 {
270+ t.Fatal(err)
271+ }
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+ }
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
A internal/adapters/drm/gbm/gbm.go +276 -0
@@ -0,0 +1,276 @@
1+// Package gbm allocates dmabuf images with libgbm, loaded at runtime with
2+// purego. Display drivers know best how to lay out a buffer they will scan
3+// out (NVIDIA's block-linear layouts in particular), so the DRM output asks
4+// GBM for its images and the renderer imports them.
5+package gbm
6+
7+import (
8+ "errors"
9+ "fmt"
10+ "os"
11+ "runtime"
12+ "slices"
13+ "sync"
14+ "unsafe"
15+
16+ "github.com/bnema/neferwl/internal/ports"
17+ "github.com/bnema/purego"
18+ "golang.org/x/sys/unix"
19+)
20+
21+// Usage is a set of GBM_BO_USE_* flags.
22+type Usage uint32
23+
24+const (
25+ // UseScanout (GBM_BO_USE_SCANOUT): the buffer can be shown by KMS.
26+ UseScanout Usage = 1 << 0
27+ // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.
28+ UseRendering Usage = 1 << 2
29+ // useLinear (GBM_BO_USE_LINEAR): the layout is linear.
30+ useLinear Usage = 1 << 4
31+)
32+
33+const (
34+ // modLinear is DRM_FORMAT_MOD_LINEAR.
35+ modLinear = 0
36+ // modInvalid is DRM_FORMAT_MOD_INVALID: an implicit layout.
37+ modInvalid = 1<<56 - 1
38+ // maxPlanes is the DRM limit of planes in a framebuffer.
39+ maxPlanes = 4
40+)
41+
42+var (
43+ loadOnce sync.Once
44+ loadErr error
45+
46+ createDevice func(fd int32) uintptr
47+ destroyDevice func(dev uintptr)
48+ createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr
49+ createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr
50+ boModifier func(bo uintptr) uint64
51+ boPlaneCount func(bo uintptr) int32
52+ boFDForPlane func(bo uintptr, plane int32) int32
53+ boStrideForPlane func(bo uintptr, plane int32) uint32
54+ boOffset func(bo uintptr, plane int32) uint32
55+ boDestroy func(bo uintptr)
56+)
57+
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.
62+func load() error {
63+ loadOnce.Do(func() {
64+ lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
65+ if err != nil {
66+ loadErr = fmt.Errorf("open libgbm.so.1: %w", err)
67+ return
68+ }
69+ defer func() {
70+ if r := recover(); r != nil {
71+ loadErr = fmt.Errorf("libgbm: %v", r)
72+ }
73+ }()
74+ reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }
75+ reg(&createDevice, "gbm_create_device")
76+ reg(&destroyDevice, "gbm_device_destroy")
77+ reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
78+ reg(&createPlain, "gbm_bo_create")
79+ reg(&boModifier, "gbm_bo_get_modifier")
80+ reg(&boPlaneCount, "gbm_bo_get_plane_count")
81+ reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
82+ reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")
83+ reg(&boOffset, "gbm_bo_get_offset")
84+ reg(&boDestroy, "gbm_bo_destroy")
85+ })
86+ return loadErr
87+}
88+
89+// Device is a GBM device on a duplicate of a DRM device fd. Close its
90+// buffers before the device.
91+type Device struct {
92+ dev uintptr
93+ fd int
94+}
95+
96+// Open creates a GBM device on a duplicate of fd, which stays the caller's.
97+func Open(fd int) (*Device, error) {
98+ if err := load(); err != nil {
99+ return nil, err
100+ }
101+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
102+ if err != nil {
103+ return nil, fmt.Errorf("duplicate drm fd: %w", err)
104+ }
105+ dev := createDevice(int32(dup))
106+ if dev == 0 {
107+ _ = unix.Close(dup)
108+ return nil, errors.New("gbm_create_device failed")
109+ }
110+ return &Device{dev: dev, fd: dup}, nil
111+}
112+
113+// Close destroys the device and its fd.
114+func (d *Device) Close() {
115+ if d == nil || d.dev == 0 {
116+ return
117+ }
118+ destroyDevice(d.dev)
119+ _ = unix.Close(d.fd)
120+ d.dev = 0
121+}
122+
123+// Buffer is an allocated image. It owns one fd per plane and the GBM
124+// object, kept alive for as long as the image is used.
125+type Buffer struct {
126+ bo uintptr
127+ width, height int
128+ format uint32
129+ modifier uint64
130+ planes []plane
131+}
132+
133+type plane struct {
134+ fd int
135+ offset, stride uint32
136+}
137+
138+// Create allocates an image whose modifier is one of modifiers (a
139+// non-empty list). A list holding only linear falls back to a plain
140+// linear allocation when GBM refuses the modifier call, and accepts an
141+// implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The
142+// result must be one memory object, because the renderer imports one
143+// dmabuf for all planes.
144+func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {
145+ if d == nil || d.dev == 0 {
146+ return nil, errors.New("gbm device closed")
147+ }
148+ if width <= 0 || height <= 0 || len(modifiers) == 0 {
149+ return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))
150+ }
151+ bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
152+ runtime.KeepAlive(modifiers)
153+ onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear
154+ if bo == 0 && onlyLinear {
155+ bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear))
156+ }
157+ if bo == 0 {
158+ return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)
159+ }
160+ b, err := readBuffer(bo, width, height, format, acceptedModifiers(modifiers))
161+ if err != nil {
162+ boDestroy(bo)
163+ return nil, err
164+ }
165+ if onlyLinear && b.modifier == modInvalid {
166+ b.modifier = modLinear
167+ }
168+ return b, nil
169+}
170+
171+// acceptedModifiers is what an allocation may report for a request: the
172+// request itself, plus an implicit layout when only linear was asked for.
173+func acceptedModifiers(requested []uint64) []uint64 {
174+ if len(requested) == 1 && requested[0] == modLinear {
175+ return []uint64{modLinear, modInvalid}
176+ }
177+ return requested
178+}
179+
180+// readBuffer reads the layout of bo and validates it against the request.
181+// It does not destroy bo on error.
182+func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {
183+ b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)}
184+ if err := checkModifier(modifiers, b.modifier); err != nil {
185+ return nil, err
186+ }
187+ n := int(boPlaneCount(bo))
188+ if n < 1 || n > maxPlanes {
189+ return nil, fmt.Errorf("gbm: buffer has %d planes", n)
190+ }
191+ for i := range n {
192+ fd := boFDForPlane(bo, int32(i))
193+ if fd < 0 {
194+ b.closeFDs()
195+ return nil, fmt.Errorf("gbm: no fd for plane %d", i)
196+ }
197+ b.planes = append(b.planes, plane{fd: int(fd), offset: boOffset(bo, int32(i)), stride: boStrideForPlane(bo, int32(i))})
198+ }
199+ fds := make([]int, len(b.planes))
200+ for i, p := range b.planes {
201+ fds[i] = p.fd
202+ }
203+ if err := checkOneObject(fds); err != nil {
204+ b.closeFDs()
205+ return nil, err
206+ }
207+ b.bo = bo
208+ return b, nil
209+}
210+
211+// checkModifier fails when the allocator returned a modifier that was not
212+// asked for (including DRM_FORMAT_MOD_INVALID, an implicit layout the
213+// renderer cannot import).
214+func checkModifier(requested []uint64, got uint64) error {
215+ if !slices.Contains(requested, got) {
216+ return fmt.Errorf("gbm: modifier %#x not in the requested list %#x", got, requested)
217+ }
218+ return nil
219+}
220+
221+// checkOneObject fails when the plane fds are not all the same dmabuf:
222+// disjoint planes cannot be imported as one memory object.
223+func checkOneObject(fds []int) error {
224+ var first unix.Stat_t
225+ for i, fd := range fds {
226+ var st unix.Stat_t
227+ if err := unix.Fstat(fd, &st); err != nil {
228+ return fmt.Errorf("gbm: stat plane %d: %w", i, err)
229+ }
230+ if i == 0 {
231+ first = st
232+ continue
233+ }
234+ if st.Dev != first.Dev || st.Ino != first.Ino {
235+ return fmt.Errorf("gbm: plane %d is a different dmabuf than plane 0", i)
236+ }
237+ }
238+ return nil
239+}
240+
241+func (b *Buffer) closeFDs() {
242+ for _, p := range b.planes {
243+ _ = unix.Close(p.fd)
244+ }
245+ b.planes = nil
246+}
247+
248+// DMABuf returns the image as a dmabuf with fresh descriptors (the caller
249+// closes them); the buffer keeps its own.
250+func (b *Buffer) DMABuf() (ports.DMABuf, error) {
251+ if b == nil || b.bo == 0 {
252+ return ports.DMABuf{}, errors.New("gbm buffer closed")
253+ }
254+ out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier}
255+ for _, p := range b.planes {
256+ fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)
257+ if err != nil {
258+ for _, q := range out.Planes {
259+ _ = q.File.Close()
260+ }
261+ return ports.DMABuf{}, fmt.Errorf("duplicate gbm plane fd: %w", err)
262+ }
263+ out.Planes = append(out.Planes, ports.DMABufPlane{File: os.NewFile(uintptr(fd), "neferwl-gbm"), Offset: p.offset, Stride: p.stride})
264+ }
265+ return out, nil
266+}
267+
268+// Close frees the buffer and its descriptors.
269+func (b *Buffer) Close() {
270+ if b == nil || b.bo == 0 {
271+ return
272+ }
273+ b.closeFDs()
274+ boDestroy(b.bo)
275+ b.bo = 0
276+}
@@ -0,0 +1,276 @@
1+// Package gbm allocates dmabuf images with libgbm, loaded at runtime with
2+// purego. Display drivers know best how to lay out a buffer they will scan
3+// out (NVIDIA's block-linear layouts in particular), so the DRM output asks
4+// GBM for its images and the renderer imports them.
5+package gbm
6+
7+import (
8+ "errors"
9+ "fmt"
10+ "os"
11+ "runtime"
12+ "slices"
13+ "sync"
14+ "unsafe"
15+
16+ "github.com/bnema/neferwl/internal/ports"
17+ "github.com/bnema/purego"
18+ "golang.org/x/sys/unix"
19+)
20+
21+// Usage is a set of GBM_BO_USE_* flags.
22+type Usage uint32
23+
24+const (
25+ // UseScanout (GBM_BO_USE_SCANOUT): the buffer can be shown by KMS.
26+ UseScanout Usage = 1 << 0
27+ // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.
28+ UseRendering Usage = 1 << 2
29+ // useLinear (GBM_BO_USE_LINEAR): the layout is linear.
30+ useLinear Usage = 1 << 4
31+)
32+
33+const (
34+ // modLinear is DRM_FORMAT_MOD_LINEAR.
35+ modLinear = 0
36+ // modInvalid is DRM_FORMAT_MOD_INVALID: an implicit layout.
37+ modInvalid = 1<<56 - 1
38+ // maxPlanes is the DRM limit of planes in a framebuffer.
39+ maxPlanes = 4
40+)
41+
42+var (
43+ loadOnce sync.Once
44+ loadErr error
45+
46+ createDevice func(fd int32) uintptr
47+ destroyDevice func(dev uintptr)
48+ createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr
49+ createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr
50+ boModifier func(bo uintptr) uint64
51+ boPlaneCount func(bo uintptr) int32
52+ boFDForPlane func(bo uintptr, plane int32) int32
53+ boStrideForPlane func(bo uintptr, plane int32) uint32
54+ boOffset func(bo uintptr, plane int32) uint32
55+ boDestroy func(bo uintptr)
56+)
57+
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.
62+func load() error {
63+ loadOnce.Do(func() {
64+ lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
65+ if err != nil {
66+ loadErr = fmt.Errorf("open libgbm.so.1: %w", err)
67+ return
68+ }
69+ defer func() {
70+ if r := recover(); r != nil {
71+ loadErr = fmt.Errorf("libgbm: %v", r)
72+ }
73+ }()
74+ reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }
75+ reg(&createDevice, "gbm_create_device")
76+ reg(&destroyDevice, "gbm_device_destroy")
77+ reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
78+ reg(&createPlain, "gbm_bo_create")
79+ reg(&boModifier, "gbm_bo_get_modifier")
80+ reg(&boPlaneCount, "gbm_bo_get_plane_count")
81+ reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
82+ reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")
83+ reg(&boOffset, "gbm_bo_get_offset")
84+ reg(&boDestroy, "gbm_bo_destroy")
85+ })
86+ return loadErr
87+}
88+
89+// Device is a GBM device on a duplicate of a DRM device fd. Close its
90+// buffers before the device.
91+type Device struct {
92+ dev uintptr
93+ fd int
94+}
95+
96+// Open creates a GBM device on a duplicate of fd, which stays the caller's.
97+func Open(fd int) (*Device, error) {
98+ if err := load(); err != nil {
99+ return nil, err
100+ }
101+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
102+ if err != nil {
103+ return nil, fmt.Errorf("duplicate drm fd: %w", err)
104+ }
105+ dev := createDevice(int32(dup))
106+ if dev == 0 {
107+ _ = unix.Close(dup)
108+ return nil, errors.New("gbm_create_device failed")
109+ }
110+ return &Device{dev: dev, fd: dup}, nil
111+}
112+
113+// Close destroys the device and its fd.
114+func (d *Device) Close() {
115+ if d == nil || d.dev == 0 {
116+ return
117+ }
118+ destroyDevice(d.dev)
119+ _ = unix.Close(d.fd)
120+ d.dev = 0
121+}
122+
123+// Buffer is an allocated image. It owns one fd per plane and the GBM
124+// object, kept alive for as long as the image is used.
125+type Buffer struct {
126+ bo uintptr
127+ width, height int
128+ format uint32
129+ modifier uint64
130+ planes []plane
131+}
132+
133+type plane struct {
134+ fd int
135+ offset, stride uint32
136+}
137+
138+// Create allocates an image whose modifier is one of modifiers (a
139+// non-empty list). A list holding only linear falls back to a plain
140+// linear allocation when GBM refuses the modifier call, and accepts an
141+// implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The
142+// result must be one memory object, because the renderer imports one
143+// dmabuf for all planes.
144+func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {
145+ if d == nil || d.dev == 0 {
146+ return nil, errors.New("gbm device closed")
147+ }
148+ if width <= 0 || height <= 0 || len(modifiers) == 0 {
149+ return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))
150+ }
151+ bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
152+ runtime.KeepAlive(modifiers)
153+ onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear
154+ if bo == 0 && onlyLinear {
155+ bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear))
156+ }
157+ if bo == 0 {
158+ return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)
159+ }
160+ b, err := readBuffer(bo, width, height, format, acceptedModifiers(modifiers))
161+ if err != nil {
162+ boDestroy(bo)
163+ return nil, err
164+ }
165+ if onlyLinear && b.modifier == modInvalid {
166+ b.modifier = modLinear
167+ }
168+ return b, nil
169+}
170+
171+// acceptedModifiers is what an allocation may report for a request: the
172+// request itself, plus an implicit layout when only linear was asked for.
173+func acceptedModifiers(requested []uint64) []uint64 {
174+ if len(requested) == 1 && requested[0] == modLinear {
175+ return []uint64{modLinear, modInvalid}
176+ }
177+ return requested
178+}
179+
180+// readBuffer reads the layout of bo and validates it against the request.
181+// It does not destroy bo on error.
182+func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {
183+ b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)}
184+ if err := checkModifier(modifiers, b.modifier); err != nil {
185+ return nil, err
186+ }
187+ n := int(boPlaneCount(bo))
188+ if n < 1 || n > maxPlanes {
189+ return nil, fmt.Errorf("gbm: buffer has %d planes", n)
190+ }
191+ for i := range n {
192+ fd := boFDForPlane(bo, int32(i))
193+ if fd < 0 {
194+ b.closeFDs()
195+ return nil, fmt.Errorf("gbm: no fd for plane %d", i)
196+ }
197+ b.planes = append(b.planes, plane{fd: int(fd), offset: boOffset(bo, int32(i)), stride: boStrideForPlane(bo, int32(i))})
198+ }
199+ fds := make([]int, len(b.planes))
200+ for i, p := range b.planes {
201+ fds[i] = p.fd
202+ }
203+ if err := checkOneObject(fds); err != nil {
204+ b.closeFDs()
205+ return nil, err
206+ }
207+ b.bo = bo
208+ return b, nil
209+}
210+
211+// checkModifier fails when the allocator returned a modifier that was not
212+// asked for (including DRM_FORMAT_MOD_INVALID, an implicit layout the
213+// renderer cannot import).
214+func checkModifier(requested []uint64, got uint64) error {
215+ if !slices.Contains(requested, got) {
216+ return fmt.Errorf("gbm: modifier %#x not in the requested list %#x", got, requested)
217+ }
218+ return nil
219+}
220+
221+// checkOneObject fails when the plane fds are not all the same dmabuf:
222+// disjoint planes cannot be imported as one memory object.
223+func checkOneObject(fds []int) error {
224+ var first unix.Stat_t
225+ for i, fd := range fds {
226+ var st unix.Stat_t
227+ if err := unix.Fstat(fd, &st); err != nil {
228+ return fmt.Errorf("gbm: stat plane %d: %w", i, err)
229+ }
230+ if i == 0 {
231+ first = st
232+ continue
233+ }
234+ if st.Dev != first.Dev || st.Ino != first.Ino {
235+ return fmt.Errorf("gbm: plane %d is a different dmabuf than plane 0", i)
236+ }
237+ }
238+ return nil
239+}
240+
241+func (b *Buffer) closeFDs() {
242+ for _, p := range b.planes {
243+ _ = unix.Close(p.fd)
244+ }
245+ b.planes = nil
246+}
247+
248+// DMABuf returns the image as a dmabuf with fresh descriptors (the caller
249+// closes them); the buffer keeps its own.
250+func (b *Buffer) DMABuf() (ports.DMABuf, error) {
251+ if b == nil || b.bo == 0 {
252+ return ports.DMABuf{}, errors.New("gbm buffer closed")
253+ }
254+ out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier}
255+ for _, p := range b.planes {
256+ fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)
257+ if err != nil {
258+ for _, q := range out.Planes {
259+ _ = q.File.Close()
260+ }
261+ return ports.DMABuf{}, fmt.Errorf("duplicate gbm plane fd: %w", err)
262+ }
263+ out.Planes = append(out.Planes, ports.DMABufPlane{File: os.NewFile(uintptr(fd), "neferwl-gbm"), Offset: p.offset, Stride: p.stride})
264+ }
265+ return out, nil
266+}
267+
268+// Close frees the buffer and its descriptors.
269+func (b *Buffer) Close() {
270+ if b == nil || b.bo == 0 {
271+ return
272+ }
273+ b.closeFDs()
274+ boDestroy(b.bo)
275+ b.bo = 0
276+}
A internal/adapters/drm/gbm/gbm_test.go +88 -0
@@ -0,0 +1,88 @@
1+package gbm
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+
8+ "golang.org/x/sys/unix"
9+)
10+
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.
14+func TestCheckModifier(t *testing.T) {
15+ const tiled = 0x300000000606014
16+ for _, tc := range []struct {
17+ name string
18+ requested []uint64
19+ got uint64
20+ ok bool
21+ }{
22+ {"listed", []uint64{tiled, 0}, tiled, true},
23+ {"linear listed", []uint64{tiled, 0}, 0, true},
24+ {"not listed", []uint64{tiled}, 0, false},
25+ {"other vendor", []uint64{tiled}, 0x200000000401b03, 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},
33+ } {
34+ if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok {
35+ t.Errorf("%s: %v", tc.name, err)
36+ }
37+ }
38+}
39+
40+// Planes must be one dmabuf: the renderer imports a single memory object.
41+// Duplicates of one descriptor are the same inode; two files are not.
42+func TestCheckOneObject(t *testing.T) {
43+ f, err := os.CreateTemp(t.TempDir(), "a")
44+ if err != nil {
45+ t.Fatal(err)
46+ }
47+ defer f.Close()
48+ g, err := os.CreateTemp(t.TempDir(), "b")
49+ if err != nil {
50+ t.Fatal(err)
51+ }
52+ defer g.Close()
53+ dup, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
54+ if err != nil {
55+ t.Fatal(err)
56+ }
57+ defer unix.Close(dup)
58+ fd := int(f.Fd())
59+ if err := checkOneObject([]int{fd}); err != nil {
60+ t.Errorf("one plane: %v", err)
61+ }
62+ if err := checkOneObject([]int{fd, dup, fd}); err != nil {
63+ t.Errorf("shared planes: %v", err)
64+ }
65+ err = checkOneObject([]int{fd, int(g.Fd())})
66+ if err == nil || !strings.Contains(err.Error(), "plane 1") {
67+ t.Errorf("disjoint planes: %v", err)
68+ }
69+ if err := checkOneObject([]int{fd, -1}); err == nil {
70+ t.Error("bad descriptor accepted")
71+ }
72+}
73+
74+// A closed device and bad requests are refused without touching libgbm.
75+func TestCreateRejectsBadRequests(t *testing.T) {
76+ if _, err := (*Device)(nil).Create(8, 8, 0, []uint64{0}, UseRendering); err == nil {
77+ t.Error("nil device accepted")
78+ }
79+ d := &Device{dev: 1}
80+ for _, tc := range []struct {
81+ w, h int
82+ mods []uint64
83+ }{{0, 8, []uint64{0}}, {8, 0, []uint64{0}}, {8, 8, nil}} {
84+ if _, err := d.Create(tc.w, tc.h, 0, tc.mods, UseRendering); err == nil {
85+ t.Errorf("%dx%d with %d modifiers accepted", tc.w, tc.h, len(tc.mods))
86+ }
87+ }
88+}
@@ -0,0 +1,88 @@
1+package gbm
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+
8+ "golang.org/x/sys/unix"
9+)
10+
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.
14+func TestCheckModifier(t *testing.T) {
15+ const tiled = 0x300000000606014
16+ for _, tc := range []struct {
17+ name string
18+ requested []uint64
19+ got uint64
20+ ok bool
21+ }{
22+ {"listed", []uint64{tiled, 0}, tiled, true},
23+ {"linear listed", []uint64{tiled, 0}, 0, true},
24+ {"not listed", []uint64{tiled}, 0, false},
25+ {"other vendor", []uint64{tiled}, 0x200000000401b03, 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},
33+ } {
34+ if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok {
35+ t.Errorf("%s: %v", tc.name, err)
36+ }
37+ }
38+}
39+
40+// Planes must be one dmabuf: the renderer imports a single memory object.
41+// Duplicates of one descriptor are the same inode; two files are not.
42+func TestCheckOneObject(t *testing.T) {
43+ f, err := os.CreateTemp(t.TempDir(), "a")
44+ if err != nil {
45+ t.Fatal(err)
46+ }
47+ defer f.Close()
48+ g, err := os.CreateTemp(t.TempDir(), "b")
49+ if err != nil {
50+ t.Fatal(err)
51+ }
52+ defer g.Close()
53+ dup, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
54+ if err != nil {
55+ t.Fatal(err)
56+ }
57+ defer unix.Close(dup)
58+ fd := int(f.Fd())
59+ if err := checkOneObject([]int{fd}); err != nil {
60+ t.Errorf("one plane: %v", err)
61+ }
62+ if err := checkOneObject([]int{fd, dup, fd}); err != nil {
63+ t.Errorf("shared planes: %v", err)
64+ }
65+ err = checkOneObject([]int{fd, int(g.Fd())})
66+ if err == nil || !strings.Contains(err.Error(), "plane 1") {
67+ t.Errorf("disjoint planes: %v", err)
68+ }
69+ if err := checkOneObject([]int{fd, -1}); err == nil {
70+ t.Error("bad descriptor accepted")
71+ }
72+}
73+
74+// A closed device and bad requests are refused without touching libgbm.
75+func TestCreateRejectsBadRequests(t *testing.T) {
76+ if _, err := (*Device)(nil).Create(8, 8, 0, []uint64{0}, UseRendering); err == nil {
77+ t.Error("nil device accepted")
78+ }
79+ d := &Device{dev: 1}
80+ for _, tc := range []struct {
81+ w, h int
82+ mods []uint64
83+ }{{0, 8, []uint64{0}}, {8, 0, []uint64{0}}, {8, 8, nil}} {
84+ if _, err := d.Create(tc.w, tc.h, 0, tc.mods, UseRendering); err == nil {
85+ t.Errorf("%dx%d with %d modifiers accepted", tc.w, tc.h, len(tc.mods))
86+ }
87+ }
88+}
M internal/adapters/drm/hdr_test.go +1 -0
@@ -148,6 +148,7 @@ func TestHDRSettingsRestartComparison(t *testing.T) {
148148
149149 func TestHDRSuccessfulModeset(t *testing.T) {
150150 o, k, commits := testOutput(t)
151+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
151152 o.cursor = nil
152153 o.hdr.cap = hdrCapability{Capable: true}
153154 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
@@ -148,6 +148,7 @@ func TestHDRSettingsRestartComparison(t *testing.T) {
148 148
149 func TestHDRSuccessfulModeset(t *testing.T) {149 func TestHDRSuccessfulModeset(t *testing.T) {
150 o, k, commits := testOutput(t)150 o, k, commits := testOutput(t)
151+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
151 o.cursor = nil152 o.cursor = nil
152 o.hdr.cap = hdrCapability{Capable: true}153 o.hdr.cap = hdrCapability{Capable: true}
153 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}154 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness}
A internal/adapters/drm/image_allocator_mock_test.go +158 -0
@@ -0,0 +1,158 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockimageAllocator creates a new instance of mockimageAllocator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockimageAllocator(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockimageAllocator {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockimageAllocator{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockimageAllocator is an autogenerated mock type for the imageAllocator type
35+type mockimageAllocator struct {
36+ mock.Mock
37+}
38+
39+type mockimageAllocator_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockimageAllocator) EXPECT() *mockimageAllocator_Expecter {
44+ return &mockimageAllocator_Expecter{mock: &_m.Mock}
45+}
46+
47+// close provides a mock function for the type mockimageAllocator
48+func (_mock *mockimageAllocator) close() {
49+ _mock.Called()
50+ return
51+}
52+
53+// mockimageAllocator_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close'
54+type mockimageAllocator_close_Call struct {
55+ *mock.Call
56+}
57+
58+// close is a helper method to define mock.On call
59+func (_e *mockimageAllocator_Expecter) close() *mockimageAllocator_close_Call {
60+ return &mockimageAllocator_close_Call{Call: _e.mock.On("close")}
61+}
62+
63+func (_c *mockimageAllocator_close_Call) Run(run func()) *mockimageAllocator_close_Call {
64+ _c.Call.Run(func(args mock.Arguments) {
65+ run()
66+ })
67+ return _c
68+}
69+
70+func (_c *mockimageAllocator_close_Call) Return() *mockimageAllocator_close_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *mockimageAllocator_close_Call) RunAndReturn(run func()) *mockimageAllocator_close_Call {
76+ _c.Run(run)
77+ return _c
78+}
79+
80+// create provides a mock function for the type mockimageAllocator
81+func (_mock *mockimageAllocator) create(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
82+ ret := _mock.Called(width, height, format, modifiers)
83+
84+ if len(ret) == 0 {
85+ panic("no return value specified for create")
86+ }
87+
88+ var r0 imageBuffer
89+ var r1 error
90+ if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) (imageBuffer, error)); ok {
91+ return returnFunc(width, height, format, modifiers)
92+ }
93+ if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) imageBuffer); ok {
94+ r0 = returnFunc(width, height, format, modifiers)
95+ } else {
96+ if ret.Get(0) != nil {
97+ r0 = ret.Get(0).(imageBuffer)
98+ }
99+ }
100+ if returnFunc, ok := ret.Get(1).(func(int, int, uint32, []uint64) error); ok {
101+ r1 = returnFunc(width, height, format, modifiers)
102+ } else {
103+ r1 = ret.Error(1)
104+ }
105+ return r0, r1
106+}
107+
108+// mockimageAllocator_create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'create'
109+type mockimageAllocator_create_Call struct {
110+ *mock.Call
111+}
112+
113+// create is a helper method to define mock.On call
114+// - width int
115+// - height int
116+// - format uint32
117+// - modifiers []uint64
118+func (_e *mockimageAllocator_Expecter) create(width any, height any, format any, modifiers any) *mockimageAllocator_create_Call {
119+ return &mockimageAllocator_create_Call{Call: _e.mock.On("create", width, height, format, modifiers)}
120+}
121+
122+func (_c *mockimageAllocator_create_Call) Run(run func(width int, height int, format uint32, modifiers []uint64)) *mockimageAllocator_create_Call {
123+ _c.Call.Run(func(args mock.Arguments) {
124+ var arg0 int
125+ if args[0] != nil {
126+ arg0 = args[0].(int)
127+ }
128+ var arg1 int
129+ if args[1] != nil {
130+ arg1 = args[1].(int)
131+ }
132+ var arg2 uint32
133+ if args[2] != nil {
134+ arg2 = args[2].(uint32)
135+ }
136+ var arg3 []uint64
137+ if args[3] != nil {
138+ arg3 = args[3].([]uint64)
139+ }
140+ run(
141+ arg0,
142+ arg1,
143+ arg2,
144+ arg3,
145+ )
146+ })
147+ return _c
148+}
149+
150+func (_c *mockimageAllocator_create_Call) Return(imageBufferMoqParam imageBuffer, err error) *mockimageAllocator_create_Call {
151+ _c.Call.Return(imageBufferMoqParam, err)
152+ return _c
153+}
154+
155+func (_c *mockimageAllocator_create_Call) RunAndReturn(run func(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error)) *mockimageAllocator_create_Call {
156+ _c.Call.Return(run)
157+ return _c
158+}
@@ -0,0 +1,158 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockimageAllocator creates a new instance of mockimageAllocator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockimageAllocator(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockimageAllocator {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockimageAllocator{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockimageAllocator is an autogenerated mock type for the imageAllocator type
35+type mockimageAllocator struct {
36+ mock.Mock
37+}
38+
39+type mockimageAllocator_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockimageAllocator) EXPECT() *mockimageAllocator_Expecter {
44+ return &mockimageAllocator_Expecter{mock: &_m.Mock}
45+}
46+
47+// close provides a mock function for the type mockimageAllocator
48+func (_mock *mockimageAllocator) close() {
49+ _mock.Called()
50+ return
51+}
52+
53+// mockimageAllocator_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close'
54+type mockimageAllocator_close_Call struct {
55+ *mock.Call
56+}
57+
58+// close is a helper method to define mock.On call
59+func (_e *mockimageAllocator_Expecter) close() *mockimageAllocator_close_Call {
60+ return &mockimageAllocator_close_Call{Call: _e.mock.On("close")}
61+}
62+
63+func (_c *mockimageAllocator_close_Call) Run(run func()) *mockimageAllocator_close_Call {
64+ _c.Call.Run(func(args mock.Arguments) {
65+ run()
66+ })
67+ return _c
68+}
69+
70+func (_c *mockimageAllocator_close_Call) Return() *mockimageAllocator_close_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *mockimageAllocator_close_Call) RunAndReturn(run func()) *mockimageAllocator_close_Call {
76+ _c.Run(run)
77+ return _c
78+}
79+
80+// create provides a mock function for the type mockimageAllocator
81+func (_mock *mockimageAllocator) create(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
82+ ret := _mock.Called(width, height, format, modifiers)
83+
84+ if len(ret) == 0 {
85+ panic("no return value specified for create")
86+ }
87+
88+ var r0 imageBuffer
89+ var r1 error
90+ if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) (imageBuffer, error)); ok {
91+ return returnFunc(width, height, format, modifiers)
92+ }
93+ if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) imageBuffer); ok {
94+ r0 = returnFunc(width, height, format, modifiers)
95+ } else {
96+ if ret.Get(0) != nil {
97+ r0 = ret.Get(0).(imageBuffer)
98+ }
99+ }
100+ if returnFunc, ok := ret.Get(1).(func(int, int, uint32, []uint64) error); ok {
101+ r1 = returnFunc(width, height, format, modifiers)
102+ } else {
103+ r1 = ret.Error(1)
104+ }
105+ return r0, r1
106+}
107+
108+// mockimageAllocator_create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'create'
109+type mockimageAllocator_create_Call struct {
110+ *mock.Call
111+}
112+
113+// create is a helper method to define mock.On call
114+// - width int
115+// - height int
116+// - format uint32
117+// - modifiers []uint64
118+func (_e *mockimageAllocator_Expecter) create(width any, height any, format any, modifiers any) *mockimageAllocator_create_Call {
119+ return &mockimageAllocator_create_Call{Call: _e.mock.On("create", width, height, format, modifiers)}
120+}
121+
122+func (_c *mockimageAllocator_create_Call) Run(run func(width int, height int, format uint32, modifiers []uint64)) *mockimageAllocator_create_Call {
123+ _c.Call.Run(func(args mock.Arguments) {
124+ var arg0 int
125+ if args[0] != nil {
126+ arg0 = args[0].(int)
127+ }
128+ var arg1 int
129+ if args[1] != nil {
130+ arg1 = args[1].(int)
131+ }
132+ var arg2 uint32
133+ if args[2] != nil {
134+ arg2 = args[2].(uint32)
135+ }
136+ var arg3 []uint64
137+ if args[3] != nil {
138+ arg3 = args[3].([]uint64)
139+ }
140+ run(
141+ arg0,
142+ arg1,
143+ arg2,
144+ arg3,
145+ )
146+ })
147+ return _c
148+}
149+
150+func (_c *mockimageAllocator_create_Call) Return(imageBufferMoqParam imageBuffer, err error) *mockimageAllocator_create_Call {
151+ _c.Call.Return(imageBufferMoqParam, err)
152+ return _c
153+}
154+
155+func (_c *mockimageAllocator_create_Call) RunAndReturn(run func(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error)) *mockimageAllocator_create_Call {
156+ _c.Call.Return(run)
157+ return _c
158+}
A internal/adapters/drm/image_buffer_mock_test.go +132 -0
@@ -0,0 +1,132 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// newMockimageBuffer creates a new instance of mockimageBuffer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func newMockimageBuffer(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *mockimageBuffer {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &mockimageBuffer{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// mockimageBuffer is an autogenerated mock type for the imageBuffer type
36+type mockimageBuffer struct {
37+ mock.Mock
38+}
39+
40+type mockimageBuffer_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *mockimageBuffer) EXPECT() *mockimageBuffer_Expecter {
45+ return &mockimageBuffer_Expecter{mock: &_m.Mock}
46+}
47+
48+// close provides a mock function for the type mockimageBuffer
49+func (_mock *mockimageBuffer) close() {
50+ _mock.Called()
51+ return
52+}
53+
54+// mockimageBuffer_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close'
55+type mockimageBuffer_close_Call struct {
56+ *mock.Call
57+}
58+
59+// close is a helper method to define mock.On call
60+func (_e *mockimageBuffer_Expecter) close() *mockimageBuffer_close_Call {
61+ return &mockimageBuffer_close_Call{Call: _e.mock.On("close")}
62+}
63+
64+func (_c *mockimageBuffer_close_Call) Run(run func()) *mockimageBuffer_close_Call {
65+ _c.Call.Run(func(args mock.Arguments) {
66+ run()
67+ })
68+ return _c
69+}
70+
71+func (_c *mockimageBuffer_close_Call) Return() *mockimageBuffer_close_Call {
72+ _c.Call.Return()
73+ return _c
74+}
75+
76+func (_c *mockimageBuffer_close_Call) RunAndReturn(run func()) *mockimageBuffer_close_Call {
77+ _c.Run(run)
78+ return _c
79+}
80+
81+// dmabuf provides a mock function for the type mockimageBuffer
82+func (_mock *mockimageBuffer) dmabuf() (ports.DMABuf, error) {
83+ ret := _mock.Called()
84+
85+ if len(ret) == 0 {
86+ panic("no return value specified for dmabuf")
87+ }
88+
89+ var r0 ports.DMABuf
90+ var r1 error
91+ if returnFunc, ok := ret.Get(0).(func() (ports.DMABuf, error)); ok {
92+ return returnFunc()
93+ }
94+ if returnFunc, ok := ret.Get(0).(func() ports.DMABuf); ok {
95+ r0 = returnFunc()
96+ } else {
97+ r0 = ret.Get(0).(ports.DMABuf)
98+ }
99+ if returnFunc, ok := ret.Get(1).(func() error); ok {
100+ r1 = returnFunc()
101+ } else {
102+ r1 = ret.Error(1)
103+ }
104+ return r0, r1
105+}
106+
107+// mockimageBuffer_dmabuf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'dmabuf'
108+type mockimageBuffer_dmabuf_Call struct {
109+ *mock.Call
110+}
111+
112+// dmabuf is a helper method to define mock.On call
113+func (_e *mockimageBuffer_Expecter) dmabuf() *mockimageBuffer_dmabuf_Call {
114+ return &mockimageBuffer_dmabuf_Call{Call: _e.mock.On("dmabuf")}
115+}
116+
117+func (_c *mockimageBuffer_dmabuf_Call) Run(run func()) *mockimageBuffer_dmabuf_Call {
118+ _c.Call.Run(func(args mock.Arguments) {
119+ run()
120+ })
121+ return _c
122+}
123+
124+func (_c *mockimageBuffer_dmabuf_Call) Return(dMABuf ports.DMABuf, err error) *mockimageBuffer_dmabuf_Call {
125+ _c.Call.Return(dMABuf, err)
126+ return _c
127+}
128+
129+func (_c *mockimageBuffer_dmabuf_Call) RunAndReturn(run func() (ports.DMABuf, error)) *mockimageBuffer_dmabuf_Call {
130+ _c.Call.Return(run)
131+ return _c
132+}
@@ -0,0 +1,132 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// newMockimageBuffer creates a new instance of mockimageBuffer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func newMockimageBuffer(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *mockimageBuffer {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &mockimageBuffer{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// mockimageBuffer is an autogenerated mock type for the imageBuffer type
36+type mockimageBuffer struct {
37+ mock.Mock
38+}
39+
40+type mockimageBuffer_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *mockimageBuffer) EXPECT() *mockimageBuffer_Expecter {
45+ return &mockimageBuffer_Expecter{mock: &_m.Mock}
46+}
47+
48+// close provides a mock function for the type mockimageBuffer
49+func (_mock *mockimageBuffer) close() {
50+ _mock.Called()
51+ return
52+}
53+
54+// mockimageBuffer_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close'
55+type mockimageBuffer_close_Call struct {
56+ *mock.Call
57+}
58+
59+// close is a helper method to define mock.On call
60+func (_e *mockimageBuffer_Expecter) close() *mockimageBuffer_close_Call {
61+ return &mockimageBuffer_close_Call{Call: _e.mock.On("close")}
62+}
63+
64+func (_c *mockimageBuffer_close_Call) Run(run func()) *mockimageBuffer_close_Call {
65+ _c.Call.Run(func(args mock.Arguments) {
66+ run()
67+ })
68+ return _c
69+}
70+
71+func (_c *mockimageBuffer_close_Call) Return() *mockimageBuffer_close_Call {
72+ _c.Call.Return()
73+ return _c
74+}
75+
76+func (_c *mockimageBuffer_close_Call) RunAndReturn(run func()) *mockimageBuffer_close_Call {
77+ _c.Run(run)
78+ return _c
79+}
80+
81+// dmabuf provides a mock function for the type mockimageBuffer
82+func (_mock *mockimageBuffer) dmabuf() (ports.DMABuf, error) {
83+ ret := _mock.Called()
84+
85+ if len(ret) == 0 {
86+ panic("no return value specified for dmabuf")
87+ }
88+
89+ var r0 ports.DMABuf
90+ var r1 error
91+ if returnFunc, ok := ret.Get(0).(func() (ports.DMABuf, error)); ok {
92+ return returnFunc()
93+ }
94+ if returnFunc, ok := ret.Get(0).(func() ports.DMABuf); ok {
95+ r0 = returnFunc()
96+ } else {
97+ r0 = ret.Get(0).(ports.DMABuf)
98+ }
99+ if returnFunc, ok := ret.Get(1).(func() error); ok {
100+ r1 = returnFunc()
101+ } else {
102+ r1 = ret.Error(1)
103+ }
104+ return r0, r1
105+}
106+
107+// mockimageBuffer_dmabuf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'dmabuf'
108+type mockimageBuffer_dmabuf_Call struct {
109+ *mock.Call
110+}
111+
112+// dmabuf is a helper method to define mock.On call
113+func (_e *mockimageBuffer_Expecter) dmabuf() *mockimageBuffer_dmabuf_Call {
114+ return &mockimageBuffer_dmabuf_Call{Call: _e.mock.On("dmabuf")}
115+}
116+
117+func (_c *mockimageBuffer_dmabuf_Call) Run(run func()) *mockimageBuffer_dmabuf_Call {
118+ _c.Call.Run(func(args mock.Arguments) {
119+ run()
120+ })
121+ return _c
122+}
123+
124+func (_c *mockimageBuffer_dmabuf_Call) Return(dMABuf ports.DMABuf, err error) *mockimageBuffer_dmabuf_Call {
125+ _c.Call.Return(dMABuf, err)
126+ return _c
127+}
128+
129+func (_c *mockimageBuffer_dmabuf_Call) RunAndReturn(run func() (ports.DMABuf, error)) *mockimageBuffer_dmabuf_Call {
130+ _c.Call.Return(run)
131+ return _c
132+}
M internal/adapters/drm/images.go +168 -37
@@ -13,13 +13,21 @@ import (
1313
1414 // Output images: the renderer targets frames alternate between.
1515
16-// imageKind is how output images are made, best first.
16+// imageKind is how output images are made, best first. Each kind is tried
17+// with GBM allocation first, then with images the renderer allocates and
18+// exports.
1719 type imageKind int
1820
1921 const (
20- imagesDriver imageKind = iota // exported, modifier chosen by the driver (DCC planes allowed)
21- imagesSinglePlane // exported, driver modifier with one memory plane
22- imagesLinear // exported, linear
22+ imagesDriver imageKind = iota // modifier chosen by the driver (DCC planes allowed)
23+ imagesSinglePlane // driver modifier with one memory plane
24+ imagesLinear // linear
25+)
26+
27+// Who allocated the output images (the "alloc" log field).
28+const (
29+ allocGBM = "gbm"
30+ allocVulkan = "vulkan"
2331 )
2432
2533 // maxFBPlanes is the number of planes a KMS framebuffer holds (fbCmd2).
@@ -59,7 +67,7 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error
5967 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR")
6068 o.hdr.failed = true
6169 o.hdr.on = false
62- o.freeImages()
70+ o.dropImages(r)
6371 r.SetHDR(0)
6472 _, _ = r.ExportTargets(0, nil, false)
6573 // A failed test commit can leave the old HDR mode on screen;
@@ -76,8 +84,11 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error
7684 }
7785
7886 func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) error {
87+ // skipGBM makes the next setup use renderer-allocated images for kind:
88+ // GBM images of this kind were refused by the modeset test.
89+ skipGBM := false
7990 for {
80- got, err := o.setupImages(r, kind, cause)
91+ got, err := o.setupImages(r, kind, cause, skipGBM)
8192 if err != nil {
8293 return err
8394 }
@@ -97,19 +108,31 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
97108 // are fine; retry the protected modeset later.
98109 return protectedCommitError{err}
99110 }
100- if !refused(err) || got == imagesLinear {
101- o.freeImages()
111+ if !refused(err) {
112+ o.dropImages(r)
113+ return err
114+ }
115+ if len(o.images) > 0 {
116+ // GBM images (the renderer imported them).
117+ // The renderer's own images of this kind may still pass.
118+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
119+ o.dropImages(r)
120+ kind, cause, skipGBM = got, err, true
121+ continue
122+ }
123+ if got == imagesLinear {
124+ o.dropImages(r)
102125 return err
103126 }
104127 if o.imageMod == 0 {
105128 // The refused images were already linear: the later kinds
106129 // would export the same ones again.
107- o.freeImages()
130+ o.dropImages(r)
108131 return err
109132 }
110- o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
111- o.freeImages()
112- kind, cause = got+1, err
133+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocVulkan).Msg("modeset refused the output images")
134+ o.dropImages(r)
135+ kind, cause, skipGBM = got+1, err, false
113136 if kind == imagesSinglePlane && o.planes == 1 {
114137 // The refused images already had one plane: single-plane
115138 // images would be the same ones.
@@ -119,11 +142,15 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
119142 }
120143
121144 // setupImages gives the output the two renderer images it flips between,
122-// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that
123-// cannot export a scanout-capable image cannot drive the output.
124-func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
145+// as dmabufs: allocated with GBM and imported by the renderer, else
146+// allocated and exported by the renderer, for each kind in turn. skipGBM
147+// leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a
148+// GPU that cannot export a scanout-capable image cannot drive the output.
149+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
125150 err := cause
151+ start := kind
126152 for ; kind <= imagesLinear; kind++ {
153+ noGBM := skipGBM && kind == start
127154 if o.runContext != nil && o.runContext.Err() != nil {
128155 return kind, o.runContext.Err()
129156 }
@@ -159,10 +186,20 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
159186 // attempt was the single-plane one.
160187 continue
161188 }
189+ if !noGBM && o.openAlloc != nil {
190+ gerr := o.gbmImages(r, mods, kind == imagesSinglePlane)
191+ if gerr == nil {
192+ return kind, nil
193+ }
194+ o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation")
195+ }
196+ // Deprecated fallback (neferwl#23): renderer-exported images, kept
197+ // while GBM is validated on few setups. Remove once no report shows
198+ // "alloc":"vulkan" for a few releases.
162199 if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {
163200 return kind, nil
164201 }
165- o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
202+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export")
166203 }
167204 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
168205 }
@@ -187,32 +224,69 @@ func (o *Output) primaryModifiers(format uint32) []uint64 {
187224 return mods
188225 }
189226
227+// closeBufs closes every plane file of bufs. The callers own them: the
228+// renderer and the framebuffers keep their own references to the memory.
229+func closeBufs(bufs []ports.DMABuf) {
230+ for _, b := range bufs {
231+ for _, p := range b.Planes {
232+ _ = p.File.Close()
233+ }
234+ }
235+}
236+
190237 // exportImages makes the renderer's exported targets the output images.
238+//
239+// Deprecated fallback for gbmImages, to remove with neferwl#23. Exported
240+// Vulkan images corrupt scanout on nvidia-drm.
191241 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
242+ o.dropImages(r)
192243 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
193244 if err != nil {
194245 return err
195246 }
247+ defer closeBufs(bufs)
248+ if err = o.showBufs(r, bufs, allocVulkan); err != nil {
249+ o.dropImages(r)
250+ _, _ = r.ExportTargets(0, nil, false)
251+ return err
252+ }
253+ return nil
254+}
255+
256+// showBufs makes framebuffers of the output images bufs (already the
257+// renderer's targets), clears them and records their layout. The caller
258+// closes the plane files and drops the images on error.
259+func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error {
196260 o.planes, o.imageMod = 0, 0
197261 for i := range bufs {
198- if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
199- err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
200- }
201- if err == nil {
202- o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
262+ if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
263+ return fmt.Errorf("output image has %d planes", n)
203264 }
204- // Every plane owns its file; the framebuffer keeps its own
205- // reference to the buffer.
206- for _, p := range bufs[i].Planes {
207- p.File.Close()
265+ fb, err := o.k.addFB(&bufs[i], o.imageFormat())
266+ if err != nil {
267+ return err
208268 }
269+ o.fbs[i] = fb
270+ }
271+ if err := o.clearImages(r); err != nil {
272+ return err
209273 }
210- // GPU memory starts undefined (old VRAM contents): clear both images
211- // before the modeset shows one.
274+ o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
275+ ev := o.log.Info()
276+ if alloc == allocVulkan {
277+ // The deprecated fallback is in use: make it visible in reports.
278+ ev = o.log.Warn()
279+ }
280+ ev.Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output")
281+ return nil
282+}
283+
284+// clearImages draws black into both output images and waits for the GPU.
285+// GPU memory starts undefined (old VRAM contents): both are cleared before
286+// the modeset shows one.
287+func (o *Output) clearImages(r ports.Renderer) error {
288+ var err error
212289 for i := range o.fbs {
213- if err != nil {
214- break
215- }
216290 r.UseTarget(i)
217291 var done *os.File
218292 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
@@ -224,18 +298,71 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
224298 err = errors.Join(err, syncfile.Wait(waitCtx, done))
225299 done.Close()
226300 }
301+ if err != nil {
302+ return err
303+ }
227304 }
228- if err != nil {
229- o.freeImages()
230- _, _ = r.ExportTargets(0, nil, false)
305+ return nil
306+}
307+
308+// gbmImages allocates the output images with GBM, makes them the
309+// renderer's targets and framebuffers, and clears them. The modifiers are
310+// those the plane lists (mods, as for exportImages) that the renderer can
311+// render into and import. On failure nothing is left allocated.
312+func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
313+ o.dropImages(r)
314+ if o.alloc == nil {
315+ if o.allocErr != nil {
316+ return o.allocErr
317+ }
318+ if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil {
319+ o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images")
320+ return o.allocErr
321+ }
322+ }
323+ usable := r.TargetModifiers(mods, singlePlane)
324+ if len(usable) == 0 {
325+ return errors.New("no modifier both the plane and the renderer accept for GBM images")
326+ }
327+ var bufs []ports.DMABuf
328+ defer func() {
329+ closeBufs(bufs)
330+ if err != nil {
331+ o.dropImages(r)
332+ }
333+ }()
334+ for range o.fbs {
335+ img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable)
336+ if cerr != nil {
337+ return cerr
338+ }
339+ o.images = append(o.images, img)
340+ b, derr := img.dmabuf()
341+ if derr != nil {
342+ return derr
343+ }
344+ bufs = append(bufs, b)
345+ }
346+ if err = r.ImportTargets(bufs); err != nil {
231347 return err
232348 }
233- o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
234- o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")
235- return nil
349+ return o.showBufs(r, bufs, allocGBM)
350+}
351+
352+// dropImages frees the output images: their framebuffers, the GBM buffers
353+// and the renderer targets built over them (they would keep the memory
354+// alive after the buffers are gone). It is a no-op when there are none.
355+func (o *Output) dropImages(r ports.Renderer) {
356+ if len(o.images) > 0 {
357+ _ = r.ImportTargets(nil)
358+ }
359+ o.freeImages()
236360 }
237361
238-// freeImages removes the output images' framebuffers.
362+// freeImages removes the output images' framebuffers and frees the images
363+// GBM allocated. Callers that still have a renderer use dropImages: this
364+// alone leaves the renderer's targets over the freed images (the renderer
365+// drops them when it closes, before Close frees the output's images).
239366 func (o *Output) freeImages() {
240367 o.front.forget()
241368 o.forgetOverlayProbes()
@@ -245,4 +372,8 @@ func (o *Output) freeImages() {
245372 }
246373 o.fbs[i] = 0
247374 }
375+ for _, img := range o.images {
376+ img.close()
377+ }
378+ o.images = nil
248379 }
@@ -13,13 +13,21 @@ import (
13 13
14 // Output images: the renderer targets frames alternate between.14 // Output images: the renderer targets frames alternate between.
15 15
16-// imageKind is how output images are made, best first.16+// imageKind is how output images are made, best first. Each kind is tried
17+// with GBM allocation first, then with images the renderer allocates and
18+// exports.
17 type imageKind int19 type imageKind int
18 20
19 const (21 const (
20- imagesDriver imageKind = iota // exported, modifier chosen by the driver (DCC planes allowed)22+ imagesDriver imageKind = iota // modifier chosen by the driver (DCC planes allowed)
21- imagesSinglePlane // exported, driver modifier with one memory plane23+ imagesSinglePlane // driver modifier with one memory plane
22- imagesLinear // exported, linear24+ imagesLinear // linear
25+)
26+
27+// Who allocated the output images (the "alloc" log field).
28+const (
29+ allocGBM = "gbm"
30+ allocVulkan = "vulkan"
23 )31 )
24 32
25 // maxFBPlanes is the number of planes a KMS framebuffer holds (fbCmd2).33 // maxFBPlanes is the number of planes a KMS framebuffer holds (fbCmd2).
@@ -59,7 +67,7 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error
59 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR")67 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR")
60 o.hdr.failed = true68 o.hdr.failed = true
61 o.hdr.on = false69 o.hdr.on = false
62- o.freeImages()70+ o.dropImages(r)
63 r.SetHDR(0)71 r.SetHDR(0)
64 _, _ = r.ExportTargets(0, nil, false)72 _, _ = r.ExportTargets(0, nil, false)
65 // A failed test commit can leave the old HDR mode on screen;73 // A failed test commit can leave the old HDR mode on screen;
@@ -76,8 +84,11 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error
76 }84 }
77 85
78 func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) error {86 func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) error {
87+ // skipGBM makes the next setup use renderer-allocated images for kind:
88+ // GBM images of this kind were refused by the modeset test.
89+ skipGBM := false
79 for {90 for {
80- got, err := o.setupImages(r, kind, cause)91+ got, err := o.setupImages(r, kind, cause, skipGBM)
81 if err != nil {92 if err != nil {
82 return err93 return err
83 }94 }
@@ -97,19 +108,31 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
97 // are fine; retry the protected modeset later.108 // are fine; retry the protected modeset later.
98 return protectedCommitError{err}109 return protectedCommitError{err}
99 }110 }
100- if !refused(err) || got == imagesLinear {111+ if !refused(err) {
101- o.freeImages()112+ o.dropImages(r)
113+ return err
114+ }
115+ if len(o.images) > 0 {
116+ // GBM images (the renderer imported them).
117+ // The renderer's own images of this kind may still pass.
118+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
119+ o.dropImages(r)
120+ kind, cause, skipGBM = got, err, true
121+ continue
122+ }
123+ if got == imagesLinear {
124+ o.dropImages(r)
102 return err125 return err
103 }126 }
104 if o.imageMod == 0 {127 if o.imageMod == 0 {
105 // The refused images were already linear: the later kinds128 // The refused images were already linear: the later kinds
106 // would export the same ones again.129 // would export the same ones again.
107- o.freeImages()130+ o.dropImages(r)
108 return err131 return err
109 }132 }
110- o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")133+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocVulkan).Msg("modeset refused the output images")
111- o.freeImages()134+ o.dropImages(r)
112- kind, cause = got+1, err135+ kind, cause, skipGBM = got+1, err, false
113 if kind == imagesSinglePlane && o.planes == 1 {136 if kind == imagesSinglePlane && o.planes == 1 {
114 // The refused images already had one plane: single-plane137 // The refused images already had one plane: single-plane
115 // images would be the same ones.138 // images would be the same ones.
@@ -119,11 +142,15 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
119 }142 }
120 143
121 // setupImages gives the output the two renderer images it flips between,144 // setupImages gives the output the two renderer images it flips between,
122-// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that145+// as dmabufs: allocated with GBM and imported by the renderer, else
123-// cannot export a scanout-capable image cannot drive the output.146+// allocated and exported by the renderer, for each kind in turn. skipGBM
124-func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {147+// leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a
148+// GPU that cannot export a scanout-capable image cannot drive the output.
149+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
125 err := cause150 err := cause
151+ start := kind
126 for ; kind <= imagesLinear; kind++ {152 for ; kind <= imagesLinear; kind++ {
153+ noGBM := skipGBM && kind == start
127 if o.runContext != nil && o.runContext.Err() != nil {154 if o.runContext != nil && o.runContext.Err() != nil {
128 return kind, o.runContext.Err()155 return kind, o.runContext.Err()
129 }156 }
@@ -159,10 +186,20 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
159 // attempt was the single-plane one.186 // attempt was the single-plane one.
160 continue187 continue
161 }188 }
189+ if !noGBM && o.openAlloc != nil {
190+ gerr := o.gbmImages(r, mods, kind == imagesSinglePlane)
191+ if gerr == nil {
192+ return kind, nil
193+ }
194+ o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation")
195+ }
196+ // Deprecated fallback (neferwl#23): renderer-exported images, kept
197+ // while GBM is validated on few setups. Remove once no report shows
198+ // "alloc":"vulkan" for a few releases.
162 if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {199 if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {
163 return kind, nil200 return kind, nil
164 }201 }
165- o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")202+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export")
166 }203 }
167 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)204 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
168 }205 }
@@ -187,32 +224,69 @@ func (o *Output) primaryModifiers(format uint32) []uint64 {
187 return mods224 return mods
188 }225 }
189 226
227+// closeBufs closes every plane file of bufs. The callers own them: the
228+// renderer and the framebuffers keep their own references to the memory.
229+func closeBufs(bufs []ports.DMABuf) {
230+ for _, b := range bufs {
231+ for _, p := range b.Planes {
232+ _ = p.File.Close()
233+ }
234+ }
235+}
236+
190 // exportImages makes the renderer's exported targets the output images.237 // exportImages makes the renderer's exported targets the output images.
238+//
239+// Deprecated fallback for gbmImages, to remove with neferwl#23. Exported
240+// Vulkan images corrupt scanout on nvidia-drm.
191 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {241 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
242+ o.dropImages(r)
192 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)243 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
193 if err != nil {244 if err != nil {
194 return err245 return err
195 }246 }
247+ defer closeBufs(bufs)
248+ if err = o.showBufs(r, bufs, allocVulkan); err != nil {
249+ o.dropImages(r)
250+ _, _ = r.ExportTargets(0, nil, false)
251+ return err
252+ }
253+ return nil
254+}
255+
256+// showBufs makes framebuffers of the output images bufs (already the
257+// renderer's targets), clears them and records their layout. The caller
258+// closes the plane files and drops the images on error.
259+func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error {
196 o.planes, o.imageMod = 0, 0260 o.planes, o.imageMod = 0, 0
197 for i := range bufs {261 for i := range bufs {
198- if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {262+ if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
199- err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))263+ return fmt.Errorf("output image has %d planes", n)
200- }
201- if err == nil {
202- o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
203 }264 }
204- // Every plane owns its file; the framebuffer keeps its own265+ fb, err := o.k.addFB(&bufs[i], o.imageFormat())
205- // reference to the buffer.266+ if err != nil {
206- for _, p := range bufs[i].Planes {267+ return err
207- p.File.Close()
208 }268 }
269+ o.fbs[i] = fb
270+ }
271+ if err := o.clearImages(r); err != nil {
272+ return err
209 }273 }
210- // GPU memory starts undefined (old VRAM contents): clear both images274+ o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
211- // before the modeset shows one.275+ ev := o.log.Info()
276+ if alloc == allocVulkan {
277+ // The deprecated fallback is in use: make it visible in reports.
278+ ev = o.log.Warn()
279+ }
280+ ev.Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output")
281+ return nil
282+}
283+
284+// clearImages draws black into both output images and waits for the GPU.
285+// GPU memory starts undefined (old VRAM contents): both are cleared before
286+// the modeset shows one.
287+func (o *Output) clearImages(r ports.Renderer) error {
288+ var err error
212 for i := range o.fbs {289 for i := range o.fbs {
213- if err != nil {
214- break
215- }
216 r.UseTarget(i)290 r.UseTarget(i)
217 var done *os.File291 var done *os.File
218 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {292 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
@@ -224,18 +298,71 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
224 err = errors.Join(err, syncfile.Wait(waitCtx, done))298 err = errors.Join(err, syncfile.Wait(waitCtx, done))
225 done.Close()299 done.Close()
226 }300 }
301+ if err != nil {
302+ return err
303+ }
227 }304 }
228- if err != nil {305+ return nil
229- o.freeImages()306+}
230- _, _ = r.ExportTargets(0, nil, false)307+
308+// gbmImages allocates the output images with GBM, makes them the
309+// renderer's targets and framebuffers, and clears them. The modifiers are
310+// those the plane lists (mods, as for exportImages) that the renderer can
311+// render into and import. On failure nothing is left allocated.
312+func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
313+ o.dropImages(r)
314+ if o.alloc == nil {
315+ if o.allocErr != nil {
316+ return o.allocErr
317+ }
318+ if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil {
319+ o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images")
320+ return o.allocErr
321+ }
322+ }
323+ usable := r.TargetModifiers(mods, singlePlane)
324+ if len(usable) == 0 {
325+ return errors.New("no modifier both the plane and the renderer accept for GBM images")
326+ }
327+ var bufs []ports.DMABuf
328+ defer func() {
329+ closeBufs(bufs)
330+ if err != nil {
331+ o.dropImages(r)
332+ }
333+ }()
334+ for range o.fbs {
335+ img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable)
336+ if cerr != nil {
337+ return cerr
338+ }
339+ o.images = append(o.images, img)
340+ b, derr := img.dmabuf()
341+ if derr != nil {
342+ return derr
343+ }
344+ bufs = append(bufs, b)
345+ }
346+ if err = r.ImportTargets(bufs); err != nil {
231 return err347 return err
232 }348 }
233- o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier349+ return o.showBufs(r, bufs, allocGBM)
234- o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")350+}
235- return nil351+
352+// dropImages frees the output images: their framebuffers, the GBM buffers
353+// and the renderer targets built over them (they would keep the memory
354+// alive after the buffers are gone). It is a no-op when there are none.
355+func (o *Output) dropImages(r ports.Renderer) {
356+ if len(o.images) > 0 {
357+ _ = r.ImportTargets(nil)
358+ }
359+ o.freeImages()
236 }360 }
237 361
238-// freeImages removes the output images' framebuffers.362+// freeImages removes the output images' framebuffers and frees the images
363+// GBM allocated. Callers that still have a renderer use dropImages: this
364+// alone leaves the renderer's targets over the freed images (the renderer
365+// drops them when it closes, before Close frees the output's images).
239 func (o *Output) freeImages() {366 func (o *Output) freeImages() {
240 o.front.forget()367 o.front.forget()
241 o.forgetOverlayProbes()368 o.forgetOverlayProbes()
@@ -245,4 +372,8 @@ func (o *Output) freeImages() {
245 }372 }
246 o.fbs[i] = 0373 o.fbs[i] = 0
247 }374 }
375+ for _, img := range o.images {
376+ img.close()
377+ }
378+ o.images = nil
248 }379 }
M internal/adapters/drm/output.go +43 -13
@@ -197,10 +197,26 @@ 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+ // openAlloc opens the GBM image allocator, once per output (nil: GBM is
205+ // not used); alloc is it, allocErr why it failed. images are the GBM
206+ // images alive while they are shown.
207+ openAlloc func() (imageAllocator, error)
208+ alloc imageAllocator
209+ allocErr error
210+ images []imageBuffer
211+ // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so
212+ // commits wait for their fences on the CPU instead.
213+ 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
204220 // formats receives the direct scanout formats after each modeset;
205221 // sampled and device are what they are built from (Want).
206222 formats chan<- ports.OutputFormats
@@ -222,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf
222238 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
223239 log := card.log
224240 pipe := slices.Index(card.crtcs, crtc)
225- 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{}, 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}
226242 var err error
227243 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
228244 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -638,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p
638654 // (overlay) say. A frame that changes a plane's colour state is never async:
639655 // only FB_ID may change in an async commit.
640656 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 {
641668 o.observeSecurity()
642669 if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) {
643670 return errSecurityScene
@@ -672,9 +699,7 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
672699 }
673700 }
674701 o.contentProps(req)
675- if fence != nil {
676- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
677- }
702+ o.fenceProp(req, o.primary, fence)
678703 // Admission linearizes at this last defensive snapshot. An already
679704 // admitted native ioctl can complete, but precedes the black proof.
680705 o.observeSecurity()
@@ -691,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
691716 return errColorRefused
692717 }
693718 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
694- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)
719+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
695720 }
696721 // The same frame without the connector hint tells whether the hint
697722 // is the cause.
698723 if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) {
699724 o.log.Warn().Err(err).Msg("content type refused; disabled")
700725 o.contentProp = 0
701- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)
726+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
702727 }
703728 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
704729 // Retry without turning VRR on: if that passes, the driver
705730 // refuses VRR on this output. The overlay stays: the composed
706731 // image left its window out.
707- 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 {
708733 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
709734 o.vrrProp = 0
710735 return nil
@@ -767,9 +792,7 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState,
767792 if withContent {
768793 o.contentProps(req)
769794 }
770- if fence != nil {
771- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
772- }
795+ o.fenceProp(req, o.primary, fence)
773796 return o.k.commit(req, atomicTestOnly, 0) == nil
774797 }
775798
@@ -912,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF
912935 // from now on.
913936 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
914937 cfb.noAsync = true
915- err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
938+ err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
916939 }
917940 if errors.Is(err, errColorRefused) {
918941 // KMS takes the buffer without the pipeline: compose it; the
@@ -1022,6 +1045,10 @@ func (o *Output) Close() {
10221045 o.cursor.free(o.k)
10231046 }
10241047 o.freeImages()
1048+ if o.alloc != nil {
1049+ o.alloc.close()
1050+ o.alloc = nil
1051+ }
10251052 o.frame.endPending()
10261053 o.dropRead()
10271054 o.shown, o.queued = 0, 0
@@ -1498,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
14981525 ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval))
14991526 ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead))
15001527 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+ }
15011531 ev = ev.Int("redrawn_pixels", r.TakeRedrawn())
15021532 if o.traceFlips {
15031533 ev = ev.Int("late_fences", fs.lateFences)
@@ -197,10 +197,26 @@ 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+ // openAlloc opens the GBM image allocator, once per output (nil: GBM is
205+ // not used); alloc is it, allocErr why it failed. images are the GBM
206+ // images alive while they are shown.
207+ openAlloc func() (imageAllocator, error)
208+ alloc imageAllocator
209+ allocErr error
210+ images []imageBuffer
211+ // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so
212+ // commits wait for their fences on the CPU instead.
213+ 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
204 // formats receives the direct scanout formats after each modeset;220 // formats receives the direct scanout formats after each modeset;
205 // sampled and device are what they are built from (Want).221 // sampled and device are what they are built from (Want).
206 formats chan<- ports.OutputFormats222 formats chan<- ports.OutputFormats
@@ -222,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf
222 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) {
223 log := card.log239 log := card.log
224 pipe := slices.Index(card.crtcs, crtc)240 pipe := slices.Index(card.crtcs, crtc)
225- 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{}, 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}
226 var err error242 var err error
227 if o.saved, err = getCrtc(card.fd, crtc); err != nil {243 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
228 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")
@@ -638,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p
638 // (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:
639 // only FB_ID may change in an async commit.655 // only FB_ID may change in an async commit.
640 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 {
641 o.observeSecurity()668 o.observeSecurity()
642 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) {
643 return errSecurityScene670 return errSecurityScene
@@ -672,9 +699,7 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
672 }699 }
673 }700 }
674 o.contentProps(req)701 o.contentProps(req)
675- if fence != nil {702+ o.fenceProp(req, o.primary, fence)
676- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
677- }
678 // Admission linearizes at this last defensive snapshot. An already703 // Admission linearizes at this last defensive snapshot. An already
679 // admitted native ioctl can complete, but precedes the black proof.704 // admitted native ioctl can complete, but precedes the black proof.
680 o.observeSecurity()705 o.observeSecurity()
@@ -691,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
691 return errColorRefused716 return errColorRefused
692 }717 }
693 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {718 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
694- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)719+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
695 }720 }
696 // The same frame without the connector hint tells whether the hint721 // The same frame without the connector hint tells whether the hint
697 // is the cause.722 // is the cause.
698 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) {
699 o.log.Warn().Err(err).Msg("content type refused; disabled")724 o.log.Warn().Err(err).Msg("content type refused; disabled")
700 o.contentProp = 0725 o.contentProp = 0
701- return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc)726+ return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc)
702 }727 }
703 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {728 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
704 // Retry without turning VRR on: if that passes, the driver729 // Retry without turning VRR on: if that passes, the driver
705 // refuses VRR on this output. The overlay stays: the composed730 // refuses VRR on this output. The overlay stays: the composed
706 // image left its window out.731 // image left its window out.
707- 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 {
708 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")
709 o.vrrProp = 0734 o.vrrProp = 0
710 return nil735 return nil
@@ -767,9 +792,7 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState,
767 if withContent {792 if withContent {
768 o.contentProps(req)793 o.contentProps(req)
769 }794 }
770- if fence != nil {795+ o.fenceProp(req, o.primary, fence)
771- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
772- }
773 return o.k.commit(req, atomicTestOnly, 0) == nil796 return o.k.commit(req, atomicTestOnly, 0) == nil
774 }797 }
775 798
@@ -912,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF
912 // from now on.935 // from now on.
913 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")
914 cfb.noAsync = true937 cfb.noAsync = true
915- err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)938+ err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
916 }939 }
917 if errors.Is(err, errColorRefused) {940 if errors.Is(err, errColorRefused) {
918 // KMS takes the buffer without the pipeline: compose it; the941 // KMS takes the buffer without the pipeline: compose it; the
@@ -1022,6 +1045,10 @@ func (o *Output) Close() {
1022 o.cursor.free(o.k)1045 o.cursor.free(o.k)
1023 }1046 }
1024 o.freeImages()1047 o.freeImages()
1048+ if o.alloc != nil {
1049+ o.alloc.close()
1050+ o.alloc = nil
1051+ }
1025 o.frame.endPending()1052 o.frame.endPending()
1026 o.dropRead()1053 o.dropRead()
1027 o.shown, o.queued = 0, 01054 o.shown, o.queued = 0, 0
@@ -1498,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1498 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))
1499 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))
1500 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+ }
1501 ev = ev.Int("redrawn_pixels", r.TakeRedrawn())1531 ev = ev.Int("redrawn_pixels", r.TakeRedrawn())
1502 if o.traceFlips {1532 if o.traceFlips {
1503 ev = ev.Int("late_fences", fs.lateFences)1533 ev = ev.Int("late_fences", fs.lateFences)
M internal/adapters/drm/overlay.go +2 -4
@@ -227,10 +227,8 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
227227 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
228228 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
229229 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
230- if ov.acquire != nil {
231- // KMS waits for the client's GPU work before scanning it out.
232- req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
233- }
230+ // KMS waits for the client's GPU work before scanning it out.
231+ o.fenceProp(req, p, ov.acquire)
234232 }
235233
236234 // close releases the frame's duplicate of the acquire fence.
@@ -227,10 +227,8 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
227 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))227 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
228 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))228 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
229 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))229 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
230- if ov.acquire != nil {230+ // KMS waits for the client's GPU work before scanning it out.
231- // KMS waits for the client's GPU work before scanning it out.231+ o.fenceProp(req, p, ov.acquire)
232- req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
233- }
234 }232 }
235 233
236 // close releases the frame's duplicate of the acquire fence.234 // close releases the frame's duplicate of the acquire fence.
M internal/adapters/drm/protection_retry_test.go +1 -0
@@ -248,6 +248,7 @@ func TestProtectedShowImagesHDRKeepsHDRForKMSErrors(t *testing.T) {
248248 errs = []error{nil, errno} // HDR test commit, then the disable
249249 }
250250 o, k, _ := testOutput(t, errs...)
251+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
251252 o.cursor = nil
252253 o.protected = true
253254 o.hdr.cap = hdrCapability{Capable: true}
@@ -248,6 +248,7 @@ func TestProtectedShowImagesHDRKeepsHDRForKMSErrors(t *testing.T) {
248 errs = []error{nil, errno} // HDR test commit, then the disable248 errs = []error{nil, errno} // HDR test commit, then the disable
249 }249 }
250 o, k, _ := testOutput(t, errs...)250 o, k, _ := testOutput(t, errs...)
251+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
251 o.cursor = nil252 o.cursor = nil
252 o.protected = true253 o.protected = true
253 o.hdr.cap = hdrCapability{Capable: true}254 o.hdr.cap = hdrCapability{Capable: true}
M internal/adapters/drm/scanout.go +6 -0
@@ -231,6 +231,9 @@ func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]por
231231 mode, reason = planeColor(c.Color, c.DMABuf.Format, c.Opaque, o.hdr.on, o.primary.pipeline)
232232 }
233233 }
234+ if reason == "" && !o.clientFenceReady(c.Acquire) {
235+ reason = "fence_pending"
236+ }
234237 if reason == "" {
235238 fb, reason = o.scanoutFB(c.DMABuf, time.Now())
236239 if reason == "" {
@@ -287,6 +290,9 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S
287290 reason = "color_refused"
288291 }
289292 }
293+ if reason == "" && !o.clientFenceReady(c.Acquire) {
294+ reason = "fence_pending"
295+ }
290296 if reason == "" {
291297 var fb uint32
292298 if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {
@@ -231,6 +231,9 @@ func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]por
231 mode, reason = planeColor(c.Color, c.DMABuf.Format, c.Opaque, o.hdr.on, o.primary.pipeline)231 mode, reason = planeColor(c.Color, c.DMABuf.Format, c.Opaque, o.hdr.on, o.primary.pipeline)
232 }232 }
233 }233 }
234+ if reason == "" && !o.clientFenceReady(c.Acquire) {
235+ reason = "fence_pending"
236+ }
234 if reason == "" {237 if reason == "" {
235 fb, reason = o.scanoutFB(c.DMABuf, time.Now())238 fb, reason = o.scanoutFB(c.DMABuf, time.Now())
236 if reason == "" {239 if reason == "" {
@@ -287,6 +290,9 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S
287 reason = "color_refused"290 reason = "color_refused"
288 }291 }
289 }292 }
293+ if reason == "" && !o.clientFenceReady(c.Acquire) {
294+ reason = "fence_pending"
295+ }
290 if reason == "" {296 if reason == "" {
291 var fb uint32297 var fb uint32
292 if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {298 if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {
M internal/adapters/drm/security_recovery_test.go +1 -0
@@ -16,6 +16,7 @@ import (
1616 // signals. Recovery revokes earlier proof before a fresh blocking black proof.
1717 func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) {
1818 o, k, commits, mu := testOutputMu(t)
19+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
1920 o.cursor, o.tearing = nil, false
2021 o.frame.stuckAfter = 40 * time.Millisecond
2122 o.flipped = make(chan flipEvent) // the event is intentionally lost
@@ -16,6 +16,7 @@ import (
16 // signals. Recovery revokes earlier proof before a fresh blocking black proof.16 // signals. Recovery revokes earlier proof before a fresh blocking black proof.
17 func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) {17 func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) {
18 o, k, commits, mu := testOutputMu(t)18 o, k, commits, mu := testOutputMu(t)
19+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
19 o.cursor, o.tearing = nil, false20 o.cursor, o.tearing = nil, false
20 o.frame.stuckAfter = 40 * time.Millisecond21 o.frame.stuckAfter = 40 * time.Millisecond
21 o.flipped = make(chan flipEvent) // the event is intentionally lost22 o.flipped = make(chan flipEvent) // the event is intentionally lost
M internal/adapters/drm/security_run_test.go +1 -0
@@ -19,6 +19,7 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
1919
2020 func securityRunWakePendingResumeAndOff(t *testing.T) {
2121 o, k, commits, mu := testOutputMu(t)
22+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
2223 o.cursor, o.tearing = nil, false
2324 var state atomic.Uint64
2425 securityGate(t, o, &state)
@@ -19,6 +19,7 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
19 19
20 func securityRunWakePendingResumeAndOff(t *testing.T) {20 func securityRunWakePendingResumeAndOff(t *testing.T) {
21 o, k, commits, mu := testOutputMu(t)21 o, k, commits, mu := testOutputMu(t)
22+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
22 o.cursor, o.tearing = nil, false23 o.cursor, o.tearing = nil, false
23 var state atomic.Uint6424 var state atomic.Uint64
24 securityGate(t, o, &state)25 securityGate(t, o, &state)
M internal/adapters/drm/security_startup_test.go +1 -0
@@ -15,6 +15,7 @@ import (
1515
1616 func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) {
1717 o, k, commits, mu := testOutputMu(t)
18+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
1819 o.cursor = nil
1920 var state atomic.Uint64
2021 state.Store(3)
@@ -15,6 +15,7 @@ import (
15 15
16 func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) {16 func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) {
17 o, k, commits, mu := testOutputMu(t)17 o, k, commits, mu := testOutputMu(t)
18+ o.fbs = [2]uint32{} // the first setup of a run: no image yet
18 o.cursor = nil19 o.cursor = nil
19 var state atomic.Uint6420 var state atomic.Uint64
20 state.Store(3)21 state.Store(3)
M internal/adapters/drm/vrr.go +3 -2
@@ -24,10 +24,11 @@ func (o *Output) probeAsync(r ports.Renderer) {
2424 req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back]))
2525 if done != nil {
2626 defer done.Close()
27- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd()))
27+ o.fenceProp(req, o.primary, done)
2828 }
2929 err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0)
30- o.asyncFence = err == nil && done != nil
30+ // With CPU waits no fence is in a commit, async or not.
31+ o.asyncFence = err == nil && (done != nil || o.cpuFences)
3132 o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe")
3233 }
3334
@@ -24,10 +24,11 @@ func (o *Output) probeAsync(r ports.Renderer) {
24 req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back]))24 req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back]))
25 if done != nil {25 if done != nil {
26 defer done.Close()26 defer done.Close()
27- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd()))27+ o.fenceProp(req, o.primary, done)
28 }28 }
29 err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0)29 err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0)
30- o.asyncFence = err == nil && done != nil30+ // With CPU waits no fence is in a commit, async or not.
31+ o.asyncFence = err == nil && (done != nil || o.cpuFences)
31 o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe")32 o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe")
32 }33 }
33 34
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 +208 -8
@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/bnema/neferwl/internal/ports"
1212 vk "github.com/bnema/purego-vulkan/vulkan"
13+ "golang.org/x/sys/unix"
1314 )
1415
1516 // Render targets (ADR 014). A DRM output scans out images the renderer
@@ -99,6 +100,35 @@ func (r *Renderer) ExportTargets(n int, modifiers []uint64, singlePlane bool) ([
99100 return out, nil
100101 }
101102
103+// ImportTargets makes the images the display allocated (GBM dmabufs, one
104+// memory object for all planes) the render targets, replacing the previous
105+// ones; no buffers only drops them. The caller keeps its files: the
106+// renderer imports duplicates. Each buffer must have the renderer's size and
107+// target format and a modifier from TargetModifiers. A refusal (the driver
108+// rejecting the explicit layout) returns an error and leaves no targets.
109+func (r *Renderer) ImportTargets(bufs []ports.DMABuf) error {
110+ r.dropTargets()
111+ if len(bufs) == 0 {
112+ return nil
113+ }
114+ if r.hdrNits > 0 && r.hdr.pipeline == 0 {
115+ return fmt.Errorf("HDR transform unavailable: %w", r.hdrError)
116+ }
117+ if !r.dd.HasGetMemoryFdPropertiesKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() || len(r.dmabuf.Formats) == 0 {
118+ return errors.New("device cannot import dmabufs")
119+ }
120+ for i := range bufs {
121+ t, err := r.importTarget(&bufs[i])
122+ if err != nil {
123+ r.dropTargets()
124+ return err
125+ }
126+ r.targets = append(r.targets, t)
127+ }
128+ r.current = 0
129+ return nil
130+}
131+
102132 // SetVirtualOutput marks the output as having no display (headless): an
103133 // empty display modifier list then accepts every exportable HDR modifier,
104134 // as it does for SDR. DRM never sets it. Call before ExportTargets.
@@ -124,11 +154,19 @@ func (r *Renderer) target() *target {
124154 // except for a virtual output (no display) or a test readback. singlePlane
125155 // drops the modifiers with more than one memory plane.
126156 func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 {
127- var out []uint64
128157 available := r.renderMods
129158 if r.hdrNits > 0 {
130159 available = r.hdrMods
131160 }
161+ return r.intersectModifiers(available, display, singlePlane)
162+}
163+
164+// intersectModifiers keeps the available modifiers the display lists (an
165+// empty display list accepts all of them, for SDR and for HDR only on a
166+// virtual output or a test readback), without the multi-plane ones when
167+// singlePlane.
168+func (r *Renderer) intersectModifiers(available, display []uint64, singlePlane bool) []uint64 {
169+ var out []uint64
132170 for _, m := range available {
133171 if singlePlane && r.modPlanes[m] > 1 {
134172 continue
@@ -140,6 +178,27 @@ func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64
140178 return out
141179 }
142180
181+// targetFormat is the Vulkan format and fourcc of the targets: XRGB8888, or
182+// XRGB2101010 in HDR.
183+func (r *Renderer) targetFormat() (vk.Format, uint32) {
184+ if r.hdrNits > 0 {
185+ return vk.FormatA2r10g10b10UnormPack32, fourccXR30
186+ }
187+ return vk.FormatB8g8r8a8Unorm, fourccXRGB
188+}
189+
190+// TargetModifiers lists the modifiers an image allocated by the display
191+// (GBM) can have to be imported as a target: those the device renders into
192+// and imports for the target format, that the display lists (empty: any),
193+// without multi-plane ones when singlePlane. Call after SetHDR.
194+func (r *Renderer) TargetModifiers(display []uint64, singlePlane bool) []uint64 {
195+ if r.physical == 0 || r.dd == nil || !r.dd.HasGetMemoryFdPropertiesKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() {
196+ return nil
197+ }
198+ format, _ := r.targetFormat()
199+ return r.intersectModifiers(r.probeImportModifiers(r.physical, format), display, singlePlane)
200+}
201+
143202 // probeRenderModifiers lists exportable SDR and HDR targets.
144203 func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
145204 r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm)
@@ -152,6 +211,34 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
152211 // records their plane counts in modPlanes. HDR targets need transfer-src only when
153212 // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).
154213 func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
214+ return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit)
215+}
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+
224+// probeImportModifiers lists the modifiers of format a target can be
225+// imported with: probeModifiers with the importable bit, for images the
226+// display allocator made. The answer is probed once per key: it does not
227+// change while the device lives.
228+func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
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
239+}
240+
241+func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {
155242 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
156243 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
157244 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
@@ -169,7 +256,7 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
169256 }
170257 // SDR targets are composed and read back; HDR targets are color
171258 // attachments, and transfer sources only with hdrReadback.
172- result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.exportable(physical, format, m) })
259+ result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.externalTarget(physical, format, m, feature) })
173260 if r.modPlanes == nil {
174261 r.modPlanes = make(map[uint64]uint32)
175262 }
@@ -216,7 +303,10 @@ func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFea
216303 return result, planes
217304 }
218305
219-func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
306+// externalTarget asks whether a target image of format and modifier with
307+// the target usage supports the external memory feature (exportable: the
308+// renderer allocates it; importable: the display allocator does).
309+func (r *Renderer) externalTarget(physical vk.PhysicalDevice, format vk.Format, modifier uint64, feature vk.ExternalMemoryFeatureFlags) bool {
220310 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
221311 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
222312 info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)}
@@ -225,7 +315,7 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modi
225315 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
226316 return false
227317 }
228- return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
318+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&feature != 0
229319 }
230320
231321 // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
@@ -248,10 +338,7 @@ func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
248338 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
249339 d := r.dd
250340 t := &target{exported: true, layout: vk.ImageLayoutUndefined}
251- format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB)
252- if r.hdrNits > 0 {
253- format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30
254- }
341+ format, fourcc := r.targetFormat()
255342 ok := false
256343 defer func() {
257344 if !ok {
@@ -354,3 +441,116 @@ func (r *Renderer) dropTargets() {
354441 // New targets hold no HDR frame until the next Render.
355442 r.targets, r.current, r.lastHDRTarget = nil, 0, -1
356443 }
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+
453+// importTarget makes a target of an image allocated outside the renderer.
454+// The target is shared with the display like an exported one: frames
455+// acquire it from and release it to the foreign queue family.
456+func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) {
457+ d := r.dd
458+ format, fourcc := r.targetFormat()
459+ n := len(b.Planes)
460+ want, known := r.modPlanes[b.Modifier]
461+ switch {
462+ case b.Format != fourcc || b.Width != r.width || b.Height != r.height:
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)
468+ case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):
469+ return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier)
470+ }
471+ if err := sameDMABuf(b.Planes); err != nil {
472+ return nil, err
473+ }
474+ fd, err := dupFile(b.Planes[0].File)
475+ if err != nil {
476+ return nil, err
477+ }
478+ // Vulkan owns the fd it imports on success: hand it its own duplicate.
479+ // Closed here on every path but the successful import.
480+ defer func() {
481+ if fd >= 0 {
482+ _ = unix.Close(fd)
483+ }
484+ }()
485+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
486+ ok := false
487+ defer func() {
488+ if !ok {
489+ r.freeTarget(t)
490+ }
491+ }()
492+ var layouts [maxModifierPlanes]vk.SubresourceLayout
493+ for i, p := range b.Planes {
494+ layouts[i] = vk.SubresourceLayout{Offset: vk.DeviceSize(p.Offset), RowPitch: vk.DeviceSize(p.Stride)}
495+ }
496+ explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: uint32(n), PlaneLayouts: &layouts[0]}
497+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
498+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback), SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
499+ if err := checked("vkCreateImage(imported target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
500+ return nil, err
501+ }
502+ fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
503+ if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
504+ return nil, err
505+ }
506+ var req vk.MemoryRequirements
507+ d.GetImageMemoryRequirements(r.device, t.image, &req)
508+ bits := req.MemoryTypeBits & fdProps.MemoryTypeBits
509+ kind, err := r.findMemoryType(bits, vk.MemoryPropertyDeviceLocalBit)
510+ if err != nil {
511+ if kind, err = r.findMemoryType(bits, 0); err != nil {
512+ return nil, err
513+ }
514+ }
515+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
516+ imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(fd)}
517+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
518+ if err := checked("vkAllocateMemory(imported target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
519+ return nil, err
520+ }
521+ fd = -1 // Vulkan owns it now.
522+ if err := checked("vkBindImageMemory(imported target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
523+ return nil, err
524+ }
525+ if t.view, err = r.imageView(t.image, format); err != nil {
526+ return nil, err
527+ }
528+ r.log.Debug().Uint64("modifier", b.Modifier).Int("planes", n).Msg("render target imported")
529+ ok = true
530+ return t, nil
531+}
532+
533+// sameDMABuf fails unless every plane file is the same dmabuf (same device
534+// and inode): the target is imported from the first plane's memory only, so
535+// planes in other buffers would read the wrong memory.
536+func sameDMABuf(planes []ports.DMABufPlane) error {
537+ var first unix.Stat_t
538+ for i, p := range planes {
539+ fd, err := dupFile(p.File)
540+ if err != nil {
541+ return fmt.Errorf("plane %d: %w", i, err)
542+ }
543+ var st unix.Stat_t
544+ err = unix.Fstat(fd, &st)
545+ _ = unix.Close(fd)
546+ if err != nil {
547+ return fmt.Errorf("stat plane %d: %w", i, err)
548+ }
549+ if i == 0 {
550+ first = st
551+ } else if st.Dev != first.Dev || st.Ino != first.Ino {
552+ return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i)
553+ }
554+ }
555+ return nil
556+}
@@ -10,6 +10,7 @@ import (
10 10
11 "github.com/bnema/neferwl/internal/ports"11 "github.com/bnema/neferwl/internal/ports"
12 vk "github.com/bnema/purego-vulkan/vulkan"12 vk "github.com/bnema/purego-vulkan/vulkan"
13+ "golang.org/x/sys/unix"
13 )14 )
14 15
15 // Render targets (ADR 014). A DRM output scans out images the renderer16 // Render targets (ADR 014). A DRM output scans out images the renderer
@@ -99,6 +100,35 @@ func (r *Renderer) ExportTargets(n int, modifiers []uint64, singlePlane bool) ([
99 return out, nil100 return out, nil
100 }101 }
101 102
103+// ImportTargets makes the images the display allocated (GBM dmabufs, one
104+// memory object for all planes) the render targets, replacing the previous
105+// ones; no buffers only drops them. The caller keeps its files: the
106+// renderer imports duplicates. Each buffer must have the renderer's size and
107+// target format and a modifier from TargetModifiers. A refusal (the driver
108+// rejecting the explicit layout) returns an error and leaves no targets.
109+func (r *Renderer) ImportTargets(bufs []ports.DMABuf) error {
110+ r.dropTargets()
111+ if len(bufs) == 0 {
112+ return nil
113+ }
114+ if r.hdrNits > 0 && r.hdr.pipeline == 0 {
115+ return fmt.Errorf("HDR transform unavailable: %w", r.hdrError)
116+ }
117+ if !r.dd.HasGetMemoryFdPropertiesKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() || len(r.dmabuf.Formats) == 0 {
118+ return errors.New("device cannot import dmabufs")
119+ }
120+ for i := range bufs {
121+ t, err := r.importTarget(&bufs[i])
122+ if err != nil {
123+ r.dropTargets()
124+ return err
125+ }
126+ r.targets = append(r.targets, t)
127+ }
128+ r.current = 0
129+ return nil
130+}
131+
102 // SetVirtualOutput marks the output as having no display (headless): an132 // SetVirtualOutput marks the output as having no display (headless): an
103 // empty display modifier list then accepts every exportable HDR modifier,133 // empty display modifier list then accepts every exportable HDR modifier,
104 // as it does for SDR. DRM never sets it. Call before ExportTargets.134 // as it does for SDR. DRM never sets it. Call before ExportTargets.
@@ -124,11 +154,19 @@ func (r *Renderer) target() *target {
124 // except for a virtual output (no display) or a test readback. singlePlane154 // except for a virtual output (no display) or a test readback. singlePlane
125 // drops the modifiers with more than one memory plane.155 // drops the modifiers with more than one memory plane.
126 func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 {156 func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 {
127- var out []uint64
128 available := r.renderMods157 available := r.renderMods
129 if r.hdrNits > 0 {158 if r.hdrNits > 0 {
130 available = r.hdrMods159 available = r.hdrMods
131 }160 }
161+ return r.intersectModifiers(available, display, singlePlane)
162+}
163+
164+// intersectModifiers keeps the available modifiers the display lists (an
165+// empty display list accepts all of them, for SDR and for HDR only on a
166+// virtual output or a test readback), without the multi-plane ones when
167+// singlePlane.
168+func (r *Renderer) intersectModifiers(available, display []uint64, singlePlane bool) []uint64 {
169+ var out []uint64
132 for _, m := range available {170 for _, m := range available {
133 if singlePlane && r.modPlanes[m] > 1 {171 if singlePlane && r.modPlanes[m] > 1 {
134 continue172 continue
@@ -140,6 +178,27 @@ func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64
140 return out178 return out
141 }179 }
142 180
181+// targetFormat is the Vulkan format and fourcc of the targets: XRGB8888, or
182+// XRGB2101010 in HDR.
183+func (r *Renderer) targetFormat() (vk.Format, uint32) {
184+ if r.hdrNits > 0 {
185+ return vk.FormatA2r10g10b10UnormPack32, fourccXR30
186+ }
187+ return vk.FormatB8g8r8a8Unorm, fourccXRGB
188+}
189+
190+// TargetModifiers lists the modifiers an image allocated by the display
191+// (GBM) can have to be imported as a target: those the device renders into
192+// and imports for the target format, that the display lists (empty: any),
193+// without multi-plane ones when singlePlane. Call after SetHDR.
194+func (r *Renderer) TargetModifiers(display []uint64, singlePlane bool) []uint64 {
195+ if r.physical == 0 || r.dd == nil || !r.dd.HasGetMemoryFdPropertiesKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() {
196+ return nil
197+ }
198+ format, _ := r.targetFormat()
199+ return r.intersectModifiers(r.probeImportModifiers(r.physical, format), display, singlePlane)
200+}
201+
143 // probeRenderModifiers lists exportable SDR and HDR targets.202 // probeRenderModifiers lists exportable SDR and HDR targets.
144 func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {203 func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
145 r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm)204 r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm)
@@ -152,6 +211,34 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
152 // records their plane counts in modPlanes. HDR targets need transfer-src only when211 // records their plane counts in modPlanes. HDR targets need transfer-src only when
153 // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).212 // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).
154 func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {213 func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
214+ return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit)
215+}
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+
224+// probeImportModifiers lists the modifiers of format a target can be
225+// imported with: probeModifiers with the importable bit, for images the
226+// display allocator made. The answer is probed once per key: it does not
227+// change while the device lives.
228+func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
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
239+}
240+
241+func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {
155 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}242 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
156 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}243 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
157 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)244 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
@@ -169,7 +256,7 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
169 }256 }
170 // SDR targets are composed and read back; HDR targets are color257 // SDR targets are composed and read back; HDR targets are color
171 // attachments, and transfer sources only with hdrReadback.258 // attachments, and transfer sources only with hdrReadback.
172- result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.exportable(physical, format, m) })259+ result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.externalTarget(physical, format, m, feature) })
173 if r.modPlanes == nil {260 if r.modPlanes == nil {
174 r.modPlanes = make(map[uint64]uint32)261 r.modPlanes = make(map[uint64]uint32)
175 }262 }
@@ -216,7 +303,10 @@ func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFea
216 return result, planes303 return result, planes
217 }304 }
218 305
219-func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {306+// externalTarget asks whether a target image of format and modifier with
307+// the target usage supports the external memory feature (exportable: the
308+// renderer allocates it; importable: the display allocator does).
309+func (r *Renderer) externalTarget(physical vk.PhysicalDevice, format vk.Format, modifier uint64, feature vk.ExternalMemoryFeatureFlags) bool {
220 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}310 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
221 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}311 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
222 info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)}312 info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)}
@@ -225,7 +315,7 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modi
225 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {315 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
226 return false316 return false
227 }317 }
228- return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0318+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&feature != 0
229 }319 }
230 320
231 // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the321 // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
@@ -248,10 +338,7 @@ func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
248 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {338 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
249 d := r.dd339 d := r.dd
250 t := &target{exported: true, layout: vk.ImageLayoutUndefined}340 t := &target{exported: true, layout: vk.ImageLayoutUndefined}
251- format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB)341+ format, fourcc := r.targetFormat()
252- if r.hdrNits > 0 {
253- format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30
254- }
255 ok := false342 ok := false
256 defer func() {343 defer func() {
257 if !ok {344 if !ok {
@@ -354,3 +441,116 @@ func (r *Renderer) dropTargets() {
354 // New targets hold no HDR frame until the next Render.441 // New targets hold no HDR frame until the next Render.
355 r.targets, r.current, r.lastHDRTarget = nil, 0, -1442 r.targets, r.current, r.lastHDRTarget = nil, 0, -1
356 }443 }
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+
453+// importTarget makes a target of an image allocated outside the renderer.
454+// The target is shared with the display like an exported one: frames
455+// acquire it from and release it to the foreign queue family.
456+func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) {
457+ d := r.dd
458+ format, fourcc := r.targetFormat()
459+ n := len(b.Planes)
460+ want, known := r.modPlanes[b.Modifier]
461+ switch {
462+ case b.Format != fourcc || b.Width != r.width || b.Height != r.height:
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)
468+ case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):
469+ return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier)
470+ }
471+ if err := sameDMABuf(b.Planes); err != nil {
472+ return nil, err
473+ }
474+ fd, err := dupFile(b.Planes[0].File)
475+ if err != nil {
476+ return nil, err
477+ }
478+ // Vulkan owns the fd it imports on success: hand it its own duplicate.
479+ // Closed here on every path but the successful import.
480+ defer func() {
481+ if fd >= 0 {
482+ _ = unix.Close(fd)
483+ }
484+ }()
485+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
486+ ok := false
487+ defer func() {
488+ if !ok {
489+ r.freeTarget(t)
490+ }
491+ }()
492+ var layouts [maxModifierPlanes]vk.SubresourceLayout
493+ for i, p := range b.Planes {
494+ layouts[i] = vk.SubresourceLayout{Offset: vk.DeviceSize(p.Offset), RowPitch: vk.DeviceSize(p.Stride)}
495+ }
496+ explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: uint32(n), PlaneLayouts: &layouts[0]}
497+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
498+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback), SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
499+ if err := checked("vkCreateImage(imported target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
500+ return nil, err
501+ }
502+ fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
503+ if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
504+ return nil, err
505+ }
506+ var req vk.MemoryRequirements
507+ d.GetImageMemoryRequirements(r.device, t.image, &req)
508+ bits := req.MemoryTypeBits & fdProps.MemoryTypeBits
509+ kind, err := r.findMemoryType(bits, vk.MemoryPropertyDeviceLocalBit)
510+ if err != nil {
511+ if kind, err = r.findMemoryType(bits, 0); err != nil {
512+ return nil, err
513+ }
514+ }
515+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
516+ imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(fd)}
517+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
518+ if err := checked("vkAllocateMemory(imported target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
519+ return nil, err
520+ }
521+ fd = -1 // Vulkan owns it now.
522+ if err := checked("vkBindImageMemory(imported target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
523+ return nil, err
524+ }
525+ if t.view, err = r.imageView(t.image, format); err != nil {
526+ return nil, err
527+ }
528+ r.log.Debug().Uint64("modifier", b.Modifier).Int("planes", n).Msg("render target imported")
529+ ok = true
530+ return t, nil
531+}
532+
533+// sameDMABuf fails unless every plane file is the same dmabuf (same device
534+// and inode): the target is imported from the first plane's memory only, so
535+// planes in other buffers would read the wrong memory.
536+func sameDMABuf(planes []ports.DMABufPlane) error {
537+ var first unix.Stat_t
538+ for i, p := range planes {
539+ fd, err := dupFile(p.File)
540+ if err != nil {
541+ return fmt.Errorf("plane %d: %w", i, err)
542+ }
543+ var st unix.Stat_t
544+ err = unix.Fstat(fd, &st)
545+ _ = unix.Close(fd)
546+ if err != nil {
547+ return fmt.Errorf("stat plane %d: %w", i, err)
548+ }
549+ if i == 0 {
550+ first = st
551+ } else if st.Dev != first.Dev || st.Ino != first.Ino {
552+ return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i)
553+ }
554+ }
555+ return nil
556+}
M internal/adapters/vulkan/targets_test.go +172 -0
@@ -1,8 +1,10 @@
11 package vulkan
22
33 import (
4+ "errors"
45 "image/color"
56 "math"
7+ "os"
68 "slices"
79 "testing"
810
@@ -367,3 +369,173 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
367369 }
368370 }
369371 }
372+
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) {
413+ r, err := New(64, 32)
414+ if err != nil {
415+ t.Skipf("Vulkan unavailable: %v", err)
416+ }
417+ defer r.Close()
418+ mods := r.TargetModifiers(nil, false)
419+ if len(mods) == 0 {
420+ t.Skip("no importable target modifier")
421+ }
422+ if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
423+ t.Errorf("modifiers outside the display list: %#x", got)
424+ }
425+ if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] {
426+ t.Errorf("display list %#x gave %#x", mods[:1], got)
427+ }
428+ for _, m := range r.TargetModifiers(nil, true) {
429+ if r.modPlanes[m] > 1 {
430+ t.Errorf("single-plane list holds multi-plane modifier %#x", m)
431+ }
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+ }
541+}
@@ -1,8 +1,10 @@
1 package vulkan1 package vulkan
2 2
3 import (3 import (
4+ "errors"
4 "image/color"5 "image/color"
5 "math"6 "math"
7+ "os"
6 "slices"8 "slices"
7 "testing"9 "testing"
8 10
@@ -367,3 +369,173 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
367 }369 }
368 }370 }
369 }371 }
372+
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) {
413+ r, err := New(64, 32)
414+ if err != nil {
415+ t.Skipf("Vulkan unavailable: %v", err)
416+ }
417+ defer r.Close()
418+ mods := r.TargetModifiers(nil, false)
419+ if len(mods) == 0 {
420+ t.Skip("no importable target modifier")
421+ }
422+ if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
423+ t.Errorf("modifiers outside the display list: %#x", got)
424+ }
425+ if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] {
426+ t.Errorf("display list %#x gave %#x", mods[:1], got)
427+ }
428+ for _, m := range r.TargetModifiers(nil, true) {
429+ if r.modPlanes[m] > 1 {
430+ t.Errorf("single-plane list holds multi-plane modifier %#x", m)
431+ }
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+ }
541+}
A internal/app/gbm_targets_test.go +151 -0
@@ -0,0 +1,151 @@
1+package app
2+
3+import (
4+ "image/color"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "github.com/bnema/neferwl/internal/adapters/drm/gbm"
10+ "github.com/bnema/neferwl/internal/adapters/vulkan"
11+ "github.com/bnema/neferwl/internal/ports"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// fourccXRGB8888 is DRM_FORMAT_XRGB8888.
16+const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
17+
18+// renderNodeFor opens the render node of the GPU the renderer runs on.
19+func renderNodeFor(t *testing.T, dev uint64) *os.File {
20+ t.Helper()
21+ nodes, _ := filepath.Glob("/dev/dri/renderD*")
22+ for _, n := range nodes {
23+ var st unix.Stat_t
24+ if unix.Stat(n, &st) != nil || st.Rdev != dev {
25+ continue
26+ }
27+ f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
28+ if err != nil {
29+ t.Skipf("cannot open %s: %v", n, err)
30+ }
31+ return f
32+ }
33+ t.Skipf("no render node for device %#x", dev)
34+ return nil
35+}
36+
37+// Images allocated by GBM are imported as the renderer's targets (the path
38+// the DRM output takes) and frames drawn into them read back. Skips without
39+// a GPU, libgbm or an openable render node.
40+func TestGBMImagesAreRenderTargets(t *testing.T) {
41+ const w, h = 128, 64
42+ r, err := vulkan.New(w, h)
43+ if err != nil {
44+ t.Skipf("Vulkan unavailable: %v", err)
45+ }
46+ defer r.Close()
47+ sup := r.DMABuf()
48+ if sup.Device == 0 || len(sup.Formats) == 0 {
49+ t.Skip("device does not import dmabufs")
50+ }
51+ mods := r.TargetModifiers(nil, false)
52+ if len(mods) == 0 {
53+ t.Skip("no importable target modifier")
54+ }
55+ t.Logf("renderer target modifiers: %#x", mods)
56+ node := renderNodeFor(t, sup.Device)
57+ defer node.Close()
58+ dev, err := gbm.Open(int(node.Fd()))
59+ if err != nil {
60+ t.Skipf("GBM unavailable: %v", err)
61+ }
62+ defer dev.Close()
63+
64+ // A render node cannot always allocate scanout buffers.
65+ var bufs [2]*gbm.Buffer
66+ for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
67+ var cerr error
68+ for i := range bufs {
69+ if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil {
70+ break
71+ }
72+ }
73+ if cerr == nil {
74+ t.Logf("GBM usage %#x", usage)
75+ break
76+ }
77+ t.Logf("GBM usage %#x: %v", usage, cerr)
78+ for i := range bufs {
79+ bufs[i].Close()
80+ bufs[i] = nil
81+ }
82+ }
83+ if bufs[0] == nil || bufs[1] == nil {
84+ t.Skip("GBM allocated no image with the renderer's modifiers")
85+ }
86+ defer bufs[0].Close()
87+ defer bufs[1].Close()
88+
89+ var dmas []ports.DMABuf
90+ for _, b := range bufs {
91+ d, err := b.DMABuf()
92+ if err != nil {
93+ t.Fatal(err)
94+ }
95+ dmas = append(dmas, d)
96+ }
97+ defer func() {
98+ for _, d := range dmas {
99+ for _, p := range d.Planes {
100+ p.File.Close()
101+ }
102+ }
103+ }()
104+ t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride)
105+ if err := r.ImportTargets(dmas); err != nil {
106+ t.Fatalf("ImportTargets: %v", err)
107+ }
108+ t.Log("Vulkan import of the GBM images succeeded")
109+
110+ // Target 0: red background with a blue window; target 1: green.
111+ scenes := []ports.Scene{
112+ {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}},
113+ {Background: "#00ff00"},
114+ }
115+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 1, Height: 1, LogicalW: 64, LogicalH: 32, Opaque: true, Solid: &ports.SolidColor{B: 1, A: 1}}}
116+ type px struct {
117+ x, y int
118+ want color.RGBA
119+ }
120+ checks := [][]px{
121+ {{4, 4, color.RGBA{255, 0, 0, 255}}, {w - 4, h - 4, color.RGBA{255, 0, 0, 255}}, {64, 32, color.RGBA{0, 0, 255, 255}}, {36, 20, color.RGBA{0, 0, 255, 255}}},
122+ {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}},
123+ }
124+ // Twice around, so each image is drawn into again after the other one.
125+ for round := range 2 {
126+ for i := range scenes {
127+ r.UseTarget(i)
128+ s := scenes[i]
129+ s.Seq = uint64(round*2 + i + 1)
130+ done, err := r.Render(s, contents)
131+ if err != nil {
132+ t.Fatalf("render %d: %v", i, err)
133+ }
134+ if done != nil {
135+ done.Close()
136+ }
137+ img := r.Pixels()
138+ if img == nil {
139+ t.Fatalf("target %d: no pixels", i)
140+ }
141+ for _, c := range checks[i] {
142+ if got := img.At(c.x, c.y); got != c.want {
143+ t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want)
144+ }
145+ }
146+ }
147+ }
148+ if err := r.ImportTargets(nil); err != nil {
149+ t.Fatal(err)
150+ }
151+}
@@ -0,0 +1,151 @@
1+package app
2+
3+import (
4+ "image/color"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "github.com/bnema/neferwl/internal/adapters/drm/gbm"
10+ "github.com/bnema/neferwl/internal/adapters/vulkan"
11+ "github.com/bnema/neferwl/internal/ports"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// fourccXRGB8888 is DRM_FORMAT_XRGB8888.
16+const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
17+
18+// renderNodeFor opens the render node of the GPU the renderer runs on.
19+func renderNodeFor(t *testing.T, dev uint64) *os.File {
20+ t.Helper()
21+ nodes, _ := filepath.Glob("/dev/dri/renderD*")
22+ for _, n := range nodes {
23+ var st unix.Stat_t
24+ if unix.Stat(n, &st) != nil || st.Rdev != dev {
25+ continue
26+ }
27+ f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
28+ if err != nil {
29+ t.Skipf("cannot open %s: %v", n, err)
30+ }
31+ return f
32+ }
33+ t.Skipf("no render node for device %#x", dev)
34+ return nil
35+}
36+
37+// Images allocated by GBM are imported as the renderer's targets (the path
38+// the DRM output takes) and frames drawn into them read back. Skips without
39+// a GPU, libgbm or an openable render node.
40+func TestGBMImagesAreRenderTargets(t *testing.T) {
41+ const w, h = 128, 64
42+ r, err := vulkan.New(w, h)
43+ if err != nil {
44+ t.Skipf("Vulkan unavailable: %v", err)
45+ }
46+ defer r.Close()
47+ sup := r.DMABuf()
48+ if sup.Device == 0 || len(sup.Formats) == 0 {
49+ t.Skip("device does not import dmabufs")
50+ }
51+ mods := r.TargetModifiers(nil, false)
52+ if len(mods) == 0 {
53+ t.Skip("no importable target modifier")
54+ }
55+ t.Logf("renderer target modifiers: %#x", mods)
56+ node := renderNodeFor(t, sup.Device)
57+ defer node.Close()
58+ dev, err := gbm.Open(int(node.Fd()))
59+ if err != nil {
60+ t.Skipf("GBM unavailable: %v", err)
61+ }
62+ defer dev.Close()
63+
64+ // A render node cannot always allocate scanout buffers.
65+ var bufs [2]*gbm.Buffer
66+ for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
67+ var cerr error
68+ for i := range bufs {
69+ if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil {
70+ break
71+ }
72+ }
73+ if cerr == nil {
74+ t.Logf("GBM usage %#x", usage)
75+ break
76+ }
77+ t.Logf("GBM usage %#x: %v", usage, cerr)
78+ for i := range bufs {
79+ bufs[i].Close()
80+ bufs[i] = nil
81+ }
82+ }
83+ if bufs[0] == nil || bufs[1] == nil {
84+ t.Skip("GBM allocated no image with the renderer's modifiers")
85+ }
86+ defer bufs[0].Close()
87+ defer bufs[1].Close()
88+
89+ var dmas []ports.DMABuf
90+ for _, b := range bufs {
91+ d, err := b.DMABuf()
92+ if err != nil {
93+ t.Fatal(err)
94+ }
95+ dmas = append(dmas, d)
96+ }
97+ defer func() {
98+ for _, d := range dmas {
99+ for _, p := range d.Planes {
100+ p.File.Close()
101+ }
102+ }
103+ }()
104+ t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride)
105+ if err := r.ImportTargets(dmas); err != nil {
106+ t.Fatalf("ImportTargets: %v", err)
107+ }
108+ t.Log("Vulkan import of the GBM images succeeded")
109+
110+ // Target 0: red background with a blue window; target 1: green.
111+ scenes := []ports.Scene{
112+ {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}},
113+ {Background: "#00ff00"},
114+ }
115+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 1, Height: 1, LogicalW: 64, LogicalH: 32, Opaque: true, Solid: &ports.SolidColor{B: 1, A: 1}}}
116+ type px struct {
117+ x, y int
118+ want color.RGBA
119+ }
120+ checks := [][]px{
121+ {{4, 4, color.RGBA{255, 0, 0, 255}}, {w - 4, h - 4, color.RGBA{255, 0, 0, 255}}, {64, 32, color.RGBA{0, 0, 255, 255}}, {36, 20, color.RGBA{0, 0, 255, 255}}},
122+ {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}},
123+ }
124+ // Twice around, so each image is drawn into again after the other one.
125+ for round := range 2 {
126+ for i := range scenes {
127+ r.UseTarget(i)
128+ s := scenes[i]
129+ s.Seq = uint64(round*2 + i + 1)
130+ done, err := r.Render(s, contents)
131+ if err != nil {
132+ t.Fatalf("render %d: %v", i, err)
133+ }
134+ if done != nil {
135+ done.Close()
136+ }
137+ img := r.Pixels()
138+ if img == nil {
139+ t.Fatalf("target %d: no pixels", i)
140+ }
141+ for _, c := range checks[i] {
142+ if got := img.At(c.x, c.y); got != c.want {
143+ t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want)
144+ }
145+ }
146+ }
147+ }
148+ if err := r.ImportTargets(nil); err != nil {
149+ t.Fatal(err)
150+ }
151+}
M internal/mocks/ports/devices_mock.go +110 -0
@@ -504,6 +504,57 @@ func (_c *MockRenderer_HDRPixels_Call) RunAndReturn(run func() *image.RGBA64) *M
504504 return _c
505505 }
506506
507+// ImportTargets provides a mock function for the type MockRenderer
508+func (_mock *MockRenderer) ImportTargets(bufs []ports.DMABuf) error {
509+ ret := _mock.Called(bufs)
510+
511+ if len(ret) == 0 {
512+ panic("no return value specified for ImportTargets")
513+ }
514+
515+ var r0 error
516+ if returnFunc, ok := ret.Get(0).(func([]ports.DMABuf) error); ok {
517+ r0 = returnFunc(bufs)
518+ } else {
519+ r0 = ret.Error(0)
520+ }
521+ return r0
522+}
523+
524+// MockRenderer_ImportTargets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportTargets'
525+type MockRenderer_ImportTargets_Call struct {
526+ *mock.Call
527+}
528+
529+// ImportTargets is a helper method to define mock.On call
530+// - bufs []ports.DMABuf
531+func (_e *MockRenderer_Expecter) ImportTargets(bufs any) *MockRenderer_ImportTargets_Call {
532+ return &MockRenderer_ImportTargets_Call{Call: _e.mock.On("ImportTargets", bufs)}
533+}
534+
535+func (_c *MockRenderer_ImportTargets_Call) Run(run func(bufs []ports.DMABuf)) *MockRenderer_ImportTargets_Call {
536+ _c.Call.Run(func(args mock.Arguments) {
537+ var arg0 []ports.DMABuf
538+ if args[0] != nil {
539+ arg0 = args[0].([]ports.DMABuf)
540+ }
541+ run(
542+ arg0,
543+ )
544+ })
545+ return _c
546+}
547+
548+func (_c *MockRenderer_ImportTargets_Call) Return(err error) *MockRenderer_ImportTargets_Call {
549+ _c.Call.Return(err)
550+ return _c
551+}
552+
553+func (_c *MockRenderer_ImportTargets_Call) RunAndReturn(run func(bufs []ports.DMABuf) error) *MockRenderer_ImportTargets_Call {
554+ _c.Call.Return(run)
555+ return _c
556+}
557+
507558 // Pixels provides a mock function for the type MockRenderer
508559 func (_mock *MockRenderer) Pixels() *image.RGBA {
509560 ret := _mock.Called()
@@ -702,6 +753,65 @@ func (_c *MockRenderer_TakeRedrawn_Call) RunAndReturn(run func() int) *MockRende
702753 return _c
703754 }
704755
756+// TargetModifiers provides a mock function for the type MockRenderer
757+func (_mock *MockRenderer) TargetModifiers(display []uint64, singlePlane bool) []uint64 {
758+ ret := _mock.Called(display, singlePlane)
759+
760+ if len(ret) == 0 {
761+ panic("no return value specified for TargetModifiers")
762+ }
763+
764+ var r0 []uint64
765+ if returnFunc, ok := ret.Get(0).(func([]uint64, bool) []uint64); ok {
766+ r0 = returnFunc(display, singlePlane)
767+ } else {
768+ if ret.Get(0) != nil {
769+ r0 = ret.Get(0).([]uint64)
770+ }
771+ }
772+ return r0
773+}
774+
775+// MockRenderer_TargetModifiers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TargetModifiers'
776+type MockRenderer_TargetModifiers_Call struct {
777+ *mock.Call
778+}
779+
780+// TargetModifiers is a helper method to define mock.On call
781+// - display []uint64
782+// - singlePlane bool
783+func (_e *MockRenderer_Expecter) TargetModifiers(display any, singlePlane any) *MockRenderer_TargetModifiers_Call {
784+ return &MockRenderer_TargetModifiers_Call{Call: _e.mock.On("TargetModifiers", display, singlePlane)}
785+}
786+
787+func (_c *MockRenderer_TargetModifiers_Call) Run(run func(display []uint64, singlePlane bool)) *MockRenderer_TargetModifiers_Call {
788+ _c.Call.Run(func(args mock.Arguments) {
789+ var arg0 []uint64
790+ if args[0] != nil {
791+ arg0 = args[0].([]uint64)
792+ }
793+ var arg1 bool
794+ if args[1] != nil {
795+ arg1 = args[1].(bool)
796+ }
797+ run(
798+ arg0,
799+ arg1,
800+ )
801+ })
802+ return _c
803+}
804+
805+func (_c *MockRenderer_TargetModifiers_Call) Return(uint64s []uint64) *MockRenderer_TargetModifiers_Call {
806+ _c.Call.Return(uint64s)
807+ return _c
808+}
809+
810+func (_c *MockRenderer_TargetModifiers_Call) RunAndReturn(run func(display []uint64, singlePlane bool) []uint64) *MockRenderer_TargetModifiers_Call {
811+ _c.Call.Return(run)
812+ return _c
813+}
814+
705815 // Trim provides a mock function for the type MockRenderer
706816 func (_mock *MockRenderer) Trim(now time.Time) error {
707817 ret := _mock.Called(now)
@@ -504,6 +504,57 @@ func (_c *MockRenderer_HDRPixels_Call) RunAndReturn(run func() *image.RGBA64) *M
504 return _c504 return _c
505 }505 }
506 506
507+// ImportTargets provides a mock function for the type MockRenderer
508+func (_mock *MockRenderer) ImportTargets(bufs []ports.DMABuf) error {
509+ ret := _mock.Called(bufs)
510+
511+ if len(ret) == 0 {
512+ panic("no return value specified for ImportTargets")
513+ }
514+
515+ var r0 error
516+ if returnFunc, ok := ret.Get(0).(func([]ports.DMABuf) error); ok {
517+ r0 = returnFunc(bufs)
518+ } else {
519+ r0 = ret.Error(0)
520+ }
521+ return r0
522+}
523+
524+// MockRenderer_ImportTargets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportTargets'
525+type MockRenderer_ImportTargets_Call struct {
526+ *mock.Call
527+}
528+
529+// ImportTargets is a helper method to define mock.On call
530+// - bufs []ports.DMABuf
531+func (_e *MockRenderer_Expecter) ImportTargets(bufs any) *MockRenderer_ImportTargets_Call {
532+ return &MockRenderer_ImportTargets_Call{Call: _e.mock.On("ImportTargets", bufs)}
533+}
534+
535+func (_c *MockRenderer_ImportTargets_Call) Run(run func(bufs []ports.DMABuf)) *MockRenderer_ImportTargets_Call {
536+ _c.Call.Run(func(args mock.Arguments) {
537+ var arg0 []ports.DMABuf
538+ if args[0] != nil {
539+ arg0 = args[0].([]ports.DMABuf)
540+ }
541+ run(
542+ arg0,
543+ )
544+ })
545+ return _c
546+}
547+
548+func (_c *MockRenderer_ImportTargets_Call) Return(err error) *MockRenderer_ImportTargets_Call {
549+ _c.Call.Return(err)
550+ return _c
551+}
552+
553+func (_c *MockRenderer_ImportTargets_Call) RunAndReturn(run func(bufs []ports.DMABuf) error) *MockRenderer_ImportTargets_Call {
554+ _c.Call.Return(run)
555+ return _c
556+}
557+
507 // Pixels provides a mock function for the type MockRenderer558 // Pixels provides a mock function for the type MockRenderer
508 func (_mock *MockRenderer) Pixels() *image.RGBA {559 func (_mock *MockRenderer) Pixels() *image.RGBA {
509 ret := _mock.Called()560 ret := _mock.Called()
@@ -702,6 +753,65 @@ func (_c *MockRenderer_TakeRedrawn_Call) RunAndReturn(run func() int) *MockRende
702 return _c753 return _c
703 }754 }
704 755
756+// TargetModifiers provides a mock function for the type MockRenderer
757+func (_mock *MockRenderer) TargetModifiers(display []uint64, singlePlane bool) []uint64 {
758+ ret := _mock.Called(display, singlePlane)
759+
760+ if len(ret) == 0 {
761+ panic("no return value specified for TargetModifiers")
762+ }
763+
764+ var r0 []uint64
765+ if returnFunc, ok := ret.Get(0).(func([]uint64, bool) []uint64); ok {
766+ r0 = returnFunc(display, singlePlane)
767+ } else {
768+ if ret.Get(0) != nil {
769+ r0 = ret.Get(0).([]uint64)
770+ }
771+ }
772+ return r0
773+}
774+
775+// MockRenderer_TargetModifiers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TargetModifiers'
776+type MockRenderer_TargetModifiers_Call struct {
777+ *mock.Call
778+}
779+
780+// TargetModifiers is a helper method to define mock.On call
781+// - display []uint64
782+// - singlePlane bool
783+func (_e *MockRenderer_Expecter) TargetModifiers(display any, singlePlane any) *MockRenderer_TargetModifiers_Call {
784+ return &MockRenderer_TargetModifiers_Call{Call: _e.mock.On("TargetModifiers", display, singlePlane)}
785+}
786+
787+func (_c *MockRenderer_TargetModifiers_Call) Run(run func(display []uint64, singlePlane bool)) *MockRenderer_TargetModifiers_Call {
788+ _c.Call.Run(func(args mock.Arguments) {
789+ var arg0 []uint64
790+ if args[0] != nil {
791+ arg0 = args[0].([]uint64)
792+ }
793+ var arg1 bool
794+ if args[1] != nil {
795+ arg1 = args[1].(bool)
796+ }
797+ run(
798+ arg0,
799+ arg1,
800+ )
801+ })
802+ return _c
803+}
804+
805+func (_c *MockRenderer_TargetModifiers_Call) Return(uint64s []uint64) *MockRenderer_TargetModifiers_Call {
806+ _c.Call.Return(uint64s)
807+ return _c
808+}
809+
810+func (_c *MockRenderer_TargetModifiers_Call) RunAndReturn(run func(display []uint64, singlePlane bool) []uint64) *MockRenderer_TargetModifiers_Call {
811+ _c.Call.Return(run)
812+ return _c
813+}
814+
705 // Trim provides a mock function for the type MockRenderer815 // Trim provides a mock function for the type MockRenderer
706 func (_mock *MockRenderer) Trim(now time.Time) error {816 func (_mock *MockRenderer) Trim(now time.Time) error {
707 ret := _mock.Called(now)817 ret := _mock.Called(now)
M internal/ports/devices.go +20 -3
@@ -36,11 +36,12 @@ type CaptureFrame interface {
3636 }
3737
3838 // Renderer draws scenes for an output. One goroutine owns it. Frames go
39-// to its own image until ExportTargets gives it scanout images; then each
40-// Render draws into the target chosen by UseTarget (ADR 014: no CPU pass).
39+// to its own image until ExportTargets or ImportTargets gives it scanout
40+// images; then each Render draws into the target chosen by UseTarget
41+// (ADR 014: no CPU pass).
4142 type Renderer interface {
4243 // SetHDR selects the HDR10 output transform (0 disables it).
43- // The DRM output calls this before ExportTargets.
44+ // The DRM output calls this before ExportTargets or ImportTargets.
4445 SetHDR(sdrNits float64)
4546 // Render draws the scene. done is a sync file signalled when the GPU
4647 // finished the frame (nil: already finished); the caller closes it.
@@ -53,6 +54,22 @@ type Renderer interface {
5354 // restricts the choice to one-plane modifiers.
5455 // n = 0 drops the targets.
5556 ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]DMABuf, error)
57+ // TargetModifiers lists the modifiers an image allocated outside the
58+ // renderer (by the display's allocator) can have to be imported as a
59+ // target: those the device renders into and imports for the XRGB8888
60+ // (SDR) or XRGB2101010 (HDR) format, that the display lists (empty: any),
61+ // without multi-plane ones when singlePlane. Empty when the device
62+ // cannot import dmabufs.
63+ TargetModifiers(display []uint64, singlePlane bool) []uint64
64+ // ImportTargets makes images allocated outside the renderer its
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.
72+ ImportTargets(bufs []DMABuf) error
5673 // UseTarget selects the exported image the next Render draws into.
5774 UseTarget(i int)
5875 // CursorBuffers allocates two linear ARGB8888 images of size×size the
@@ -36,11 +36,12 @@ type CaptureFrame interface {
36 }36 }
37 37
38 // Renderer draws scenes for an output. One goroutine owns it. Frames go38 // Renderer draws scenes for an output. One goroutine owns it. Frames go
39-// to its own image until ExportTargets gives it scanout images; then each39+// to its own image until ExportTargets or ImportTargets gives it scanout
40-// Render draws into the target chosen by UseTarget (ADR 014: no CPU pass).40+// images; then each Render draws into the target chosen by UseTarget
41+// (ADR 014: no CPU pass).
41 type Renderer interface {42 type Renderer interface {
42 // SetHDR selects the HDR10 output transform (0 disables it).43 // SetHDR selects the HDR10 output transform (0 disables it).
43- // The DRM output calls this before ExportTargets.44+ // The DRM output calls this before ExportTargets or ImportTargets.
44 SetHDR(sdrNits float64)45 SetHDR(sdrNits float64)
45 // 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
46 // finished the frame (nil: already finished); the caller closes it.47 // finished the frame (nil: already finished); the caller closes it.
@@ -53,6 +54,22 @@ type Renderer interface {
53 // restricts the choice to one-plane modifiers.54 // restricts the choice to one-plane modifiers.
54 // n = 0 drops the targets.55 // n = 0 drops the targets.
55 ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]DMABuf, error)56 ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]DMABuf, error)
57+ // TargetModifiers lists the modifiers an image allocated outside the
58+ // renderer (by the display's allocator) can have to be imported as a
59+ // target: those the device renders into and imports for the XRGB8888
60+ // (SDR) or XRGB2101010 (HDR) format, that the display lists (empty: any),
61+ // without multi-plane ones when singlePlane. Empty when the device
62+ // cannot import dmabufs.
63+ TargetModifiers(display []uint64, singlePlane bool) []uint64
64+ // ImportTargets makes images allocated outside the renderer its
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.
72+ ImportTargets(bufs []DMABuf) error
56 // UseTarget selects the exported image the next Render draws into.73 // UseTarget selects the exported image the next Render draws into.
57 UseTarget(i int)74 UseTarget(i int)
58 // CursorBuffers allocates two linear ARGB8888 images of size×size the75 // CursorBuffers allocates two linear ARGB8888 images of size×size the

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