detached 53269c8

fix(drm): allocate output images with GBM and import them into Vulkan

Scanout of Vulkan-exported images corrupts on nvidia-drm (bands, displaced
fragments) although KMS accepts the framebuffer. Output images are now
allocated with GBM (SCANOUT|RENDERING) from plane IN_FORMATS intersected
with the renderer's importable target modifiers, then imported into Vulkan
with explicit modifier layouts. Vulkan export stays the fallback per image
kind.

On nvidia-drm older than 560.35.3, IN_FENCE_FD is not set: the render
fence is waited on the CPU and pending client fences compose instead of
direct scanout or overlay.
commit
53269c84573ace9f0e9546a3a5834b9f62cdba66
author
brice · · 2026-10-21 19:45 UTC
parent
c64260b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
28 files · +2093 -52
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`), the allocator of the display driver, with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. NVIDIA shows images allocated by Vulkan corrupted, which is why every driver uses GBM.
32+- **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`).
33+- **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one.
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`), the allocator of the display driver, with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. NVIDIA shows images allocated by Vulkan corrupted, which is why every driver uses GBM.
32+- **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`).
33+- **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one.
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 +60 -0
@@ -0,0 +1,60 @@
1+package drm
2+
3+import (
4+ "github.com/bnema/neferwl/internal/adapters/drm/gbm"
5+ "github.com/bnema/neferwl/internal/ports"
6+ "github.com/bnema/zerowrap"
7+)
8+
9+// imageAllocator makes the output images with the display driver's own
10+// allocator (GBM), which knows the layouts the driver scans out correctly:
11+// images allocated by the renderer show corrupted on NVIDIA. One per
12+// output, used by its goroutine only.
13+type imageAllocator interface {
14+ // create allocates an image whose modifier is one of modifiers.
15+ create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error)
16+ close()
17+}
18+
19+// imageBuffer is one allocated image, kept alive while it is shown.
20+type imageBuffer interface {
21+ // dmabuf returns the image with fresh descriptors the caller closes.
22+ dmabuf() (ports.DMABuf, error)
23+ close()
24+}
25+
26+// gbmAllocator allocates scanout-capable, renderable images with libgbm.
27+type gbmAllocator struct {
28+ dev *gbm.Device
29+ log zerowrap.Logger
30+ // flaglessLogged: the missing usage flags were logged once.
31+ flaglessLogged bool
32+}
33+
34+// openGBM opens a GBM allocator on a duplicate of the card fd.
35+func openGBM(fd int, log zerowrap.Logger) (imageAllocator, error) {
36+ dev, err := gbm.Open(fd)
37+ if err != nil {
38+ return nil, err
39+ }
40+ return &gbmAllocator{dev: dev, log: log}, nil
41+}
42+
43+func (a *gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
44+ b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)
45+ if err != nil {
46+ return nil, err
47+ }
48+ if b.Flagless && !a.flaglessLogged {
49+ a.flaglessLogged = true
50+ a.log.Info().Msg("libgbm lacks gbm_bo_create_with_modifiers2: images are allocated without the scanout and rendering flags")
51+ }
52+ return gbmImage{b}, nil
53+}
54+
55+func (a *gbmAllocator) close() { a.dev.Close() }
56+
57+type gbmImage struct{ b *gbm.Buffer }
58+
59+func (i gbmImage) dmabuf() (ports.DMABuf, error) { return i.b.DMABuf() }
60+func (i gbmImage) close() { i.b.Close() }
@@ -0,0 +1,60 @@
1+package drm
2+
3+import (
4+ "github.com/bnema/neferwl/internal/adapters/drm/gbm"
5+ "github.com/bnema/neferwl/internal/ports"
6+ "github.com/bnema/zerowrap"
7+)
8+
9+// imageAllocator makes the output images with the display driver's own
10+// allocator (GBM), which knows the layouts the driver scans out correctly:
11+// images allocated by the renderer show corrupted on NVIDIA. One per
12+// output, used by its goroutine only.
13+type imageAllocator interface {
14+ // create allocates an image whose modifier is one of modifiers.
15+ create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error)
16+ close()
17+}
18+
19+// imageBuffer is one allocated image, kept alive while it is shown.
20+type imageBuffer interface {
21+ // dmabuf returns the image with fresh descriptors the caller closes.
22+ dmabuf() (ports.DMABuf, error)
23+ close()
24+}
25+
26+// gbmAllocator allocates scanout-capable, renderable images with libgbm.
27+type gbmAllocator struct {
28+ dev *gbm.Device
29+ log zerowrap.Logger
30+ // flaglessLogged: the missing usage flags were logged once.
31+ flaglessLogged bool
32+}
33+
34+// openGBM opens a GBM allocator on a duplicate of the card fd.
35+func openGBM(fd int, log zerowrap.Logger) (imageAllocator, error) {
36+ dev, err := gbm.Open(fd)
37+ if err != nil {
38+ return nil, err
39+ }
40+ return &gbmAllocator{dev: dev, log: log}, nil
41+}
42+
43+func (a *gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) {
44+ b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering)
45+ if err != nil {
46+ return nil, err
47+ }
48+ if b.Flagless && !a.flaglessLogged {
49+ a.flaglessLogged = true
50+ a.log.Info().Msg("libgbm lacks gbm_bo_create_with_modifiers2: images are allocated without the scanout and rendering flags")
51+ }
52+ return gbmImage{b}, nil
53+}
54+
55+func (a *gbmAllocator) close() { a.dev.Close() }
56+
57+type gbmImage struct{ b *gbm.Buffer }
58+
59+func (i gbmImage) dmabuf() (ports.DMABuf, error) { return i.b.DMABuf() }
60+func (i gbmImage) close() { i.b.Close() }
A internal/adapters/drm/alloc_test.go +278 -0
@@ -0,0 +1,278 @@
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+// gbmImages makes an allocator that hands out two images; each image
26+// closes exactly once.
27+func gbmImages(t *testing.T, planes int) (imageAllocator, *int) {
28+ a := newMockimageAllocator(t)
29+ closed := new(int)
30+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) {
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+ a.EXPECT().close().Return().Maybe()
37+ return a, 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 := gbmImages(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.imageAlloc != allocGBM || o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 {
66+ t.Fatalf("alloc %q kind %d modifier %#x images %d closed %d", o.imageAlloc, o.kind, o.imageMod, len(o.images), *closed)
67+ }
68+ o.freeImages()
69+ if *closed != 2 || o.images != nil {
70+ t.Fatalf("closed %d images, %d left", *closed, len(o.images))
71+ }
72+}
73+
74+// The plane's modifiers reach the renderer with the single-plane rule, and
75+// a GBM failure falls back to the renderer's own images of the same kind.
76+func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
77+ for name, tc := range map[string]struct {
78+ setup func(t *testing.T, o *Output, r *portsmocks.MockRenderer)
79+ }{
80+ "gbm unavailable": {func(t *testing.T, o *Output, r *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, 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, 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, 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+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()
103+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()
104+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
105+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
106+ }},
107+ "import refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
108+ a, closed := gbmImages(t, 1)
109+ _ = closed
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+ }},
114+ "framebuffer refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
115+ a, _ := gbmImages(t, 1)
116+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
117+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
118+ r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()
119+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
120+ o.k.(*mockkms).EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
121+ }},
122+ } {
123+ t.Run(name, func(t *testing.T) {
124+ o, k, _ := testOutput(t)
125+ o.cursor = nil
126+ r := gbmRenderer(t)
127+ tc.setup(t, o, r)
128+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
129+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
130+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
131+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
132+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
133+ if err := o.showImages(r, imagesDriver, nil); err != nil {
134+ t.Fatal(err)
135+ }
136+ if o.imageAlloc != allocVulkan || o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 {
137+ t.Fatalf("alloc %q kind %d images %d fbs %v", o.imageAlloc, o.kind, len(o.images), o.fbs)
138+ }
139+ })
140+ }
141+}
142+
143+// A failed GBM open is remembered: the next kind and the next setup do not
144+// open the library again.
145+func TestGBMOpenFailureIsRemembered(t *testing.T) {
146+ o, k, _ := testOutput(t, unix.EINVAL)
147+ o.cursor = nil
148+ opens := 0
149+ o.openAlloc = func() (imageAllocator, error) { opens++; return nil, errors.New("no libgbm") }
150+ r := gbmRenderer(t)
151+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(2), gbmBuf(2)}, nil).Once()
152+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
153+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
154+ k.EXPECT().rmFB(mock.Anything).Return(nil)
155+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
156+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
157+ if err := o.showImages(r, imagesDriver, nil); err != nil {
158+ t.Fatal(err)
159+ }
160+ if opens != 1 || o.kind != imagesSinglePlane {
161+ t.Fatalf("%d opens, kind %d", opens, o.kind)
162+ }
163+}
164+
165+// A modeset test that refuses GBM images tries the renderer's images of the
166+// same kind before the next kind.
167+func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
168+ o, k, commits := testOutput(t, unix.EINVAL)
169+ o.cursor = nil
170+ a, closed := gbmImages(t, 1)
171+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
172+ r := gbmRenderer(t)
173+ r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
174+ r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 })).Return(nil).Once()
175+ // The refused GBM images leave no renderer target behind.
176+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
177+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
178+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
179+ k.EXPECT().rmFB(mock.Anything).Return(nil)
180+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
181+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
182+ if err := o.showImages(r, imagesDriver, nil); err != nil {
183+ t.Fatal(err)
184+ }
185+ if o.imageAlloc != allocVulkan || o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 {
186+ t.Fatalf("alloc %q kind %d closed %d images %d commits %d", o.imageAlloc, o.kind, *closed, len(o.images), len(*commits))
187+ }
188+}
189+
190+// Closing the output frees the GBM images and the allocator.
191+func TestOutputFreeImagesClosesGBMImages(t *testing.T) {
192+ o, k, _ := testOutput(t)
193+ img := newMockimageBuffer(t)
194+ img.EXPECT().close().Return().Twice()
195+ o.images = []imageBuffer{img, img}
196+ o.fbs = [2]uint32{70, 71}
197+ k.EXPECT().rmFB(mock.Anything).Return(nil).Twice()
198+ o.freeImages()
199+ if o.images != nil || o.fbs != [2]uint32{} {
200+ t.Fatalf("images %v fbs %v", o.images, o.fbs)
201+ }
202+}
203+
204+// A setup that finds images left over frees them first, with the renderer's
205+// targets over GBM ones, and a refused modeset of GBM images (no fallback
206+// left) drops the targets with the buffers.
207+func TestSetupStartsFromCleanState(t *testing.T) {
208+ o, k, _ := testOutput(t)
209+ o.cursor = nil
210+ img := newMockimageBuffer(t)
211+ img.EXPECT().close().Return().Once()
212+ o.images = []imageBuffer{img}
213+ o.fbs = [2]uint32{70, 71}
214+ k.EXPECT().rmFB(uint32(70)).Return(nil).Once()
215+ k.EXPECT().rmFB(uint32(71)).Return(nil).Once()
216+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(80, nil)
217+ r := gbmRenderer(t)
218+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
219+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
220+ if err := o.exportImages(r, nil, false); err != nil {
221+ t.Fatal(err)
222+ }
223+ if o.images != nil || o.fbs != [2]uint32{80, 80} {
224+ t.Fatalf("images %v fbs %v", o.images, o.fbs)
225+ }
226+}
227+
228+// Without CPU fence waits every fence is ready; with them only a signalled
229+// client fence is (a pipe stands in: readable means signalled).
230+func TestClientFenceReady(t *testing.T) {
231+ r, w, err := os.Pipe()
232+ if err != nil {
233+ t.Fatal(err)
234+ }
235+ defer r.Close()
236+ defer w.Close()
237+ o, _, _ := testOutput(t)
238+ if !o.clientFenceReady(r) || !o.clientFenceReady(nil) {
239+ t.Fatal("fence not ready without CPU waits")
240+ }
241+ o.cpuFences = true
242+ if !o.clientFenceReady(nil) {
243+ t.Fatal("no fence must be ready")
244+ }
245+ if o.clientFenceReady(r) {
246+ t.Fatal("pending client fence ready")
247+ }
248+ _, _ = w.Write([]byte{1})
249+ if !o.clientFenceReady(r) {
250+ t.Fatal("signalled client fence not ready")
251+ }
252+}
253+
254+// With CPU waits a client buffer whose fence is pending is composed, not
255+// scanned out and not put on the overlay: neither plane waits for it. Once
256+// the fence signalled the same frame scans out.
257+func TestPendingClientFenceComposes(t *testing.T) {
258+ r, w, err := os.Pipe()
259+ if err != nil {
260+ t.Fatal(err)
261+ }
262+ defer r.Close()
263+ defer w.Close()
264+ o, k, _ := testOutput(t)
265+ k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
266+ o.cpuFences = true
267+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
268+ s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}}
269+ c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, Acquire: r, DMABuf: &ports.DMABuf{ID: 5, Format: fourccXRGB, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{File: r}}}}
270+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
271+ if fb, _, _ := o.scanoutFrame(s, contents); fb != 0 || o.reason != "fence_pending" {
272+ t.Fatalf("pending fence: fb %d reason %q", fb, o.reason)
273+ }
274+ _, _ = w.Write([]byte{1})
275+ if fb, _, _ := o.scanoutFrame(s, contents); fb == 0 {
276+ t.Fatalf("signalled fence: reason %q", o.reason)
277+ }
278+}
@@ -0,0 +1,278 @@
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+// gbmImages makes an allocator that hands out two images; each image
26+// closes exactly once.
27+func gbmImages(t *testing.T, planes int) (imageAllocator, *int) {
28+ a := newMockimageAllocator(t)
29+ closed := new(int)
30+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) {
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+ a.EXPECT().close().Return().Maybe()
37+ return a, 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 := gbmImages(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.imageAlloc != allocGBM || o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 {
66+ t.Fatalf("alloc %q kind %d modifier %#x images %d closed %d", o.imageAlloc, o.kind, o.imageMod, len(o.images), *closed)
67+ }
68+ o.freeImages()
69+ if *closed != 2 || o.images != nil {
70+ t.Fatalf("closed %d images, %d left", *closed, len(o.images))
71+ }
72+}
73+
74+// The plane's modifiers reach the renderer with the single-plane rule, and
75+// a GBM failure falls back to the renderer's own images of the same kind.
76+func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) {
77+ for name, tc := range map[string]struct {
78+ setup func(t *testing.T, o *Output, r *portsmocks.MockRenderer)
79+ }{
80+ "gbm unavailable": {func(t *testing.T, o *Output, r *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, 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, 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, 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+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once()
103+ a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once()
104+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
105+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
106+ }},
107+ "import refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
108+ a, closed := gbmImages(t, 1)
109+ _ = closed
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+ }},
114+ "framebuffer refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) {
115+ a, _ := gbmImages(t, 1)
116+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
117+ r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once()
118+ r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once()
119+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
120+ o.k.(*mockkms).EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once()
121+ }},
122+ } {
123+ t.Run(name, func(t *testing.T) {
124+ o, k, _ := testOutput(t)
125+ o.cursor = nil
126+ r := gbmRenderer(t)
127+ tc.setup(t, o, r)
128+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
129+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
130+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
131+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
132+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
133+ if err := o.showImages(r, imagesDriver, nil); err != nil {
134+ t.Fatal(err)
135+ }
136+ if o.imageAlloc != allocVulkan || o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 {
137+ t.Fatalf("alloc %q kind %d images %d fbs %v", o.imageAlloc, o.kind, len(o.images), o.fbs)
138+ }
139+ })
140+ }
141+}
142+
143+// A failed GBM open is remembered: the next kind and the next setup do not
144+// open the library again.
145+func TestGBMOpenFailureIsRemembered(t *testing.T) {
146+ o, k, _ := testOutput(t, unix.EINVAL)
147+ o.cursor = nil
148+ opens := 0
149+ o.openAlloc = func() (imageAllocator, error) { opens++; return nil, errors.New("no libgbm") }
150+ r := gbmRenderer(t)
151+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(2), gbmBuf(2)}, nil).Once()
152+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
153+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
154+ k.EXPECT().rmFB(mock.Anything).Return(nil)
155+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
156+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
157+ if err := o.showImages(r, imagesDriver, nil); err != nil {
158+ t.Fatal(err)
159+ }
160+ if opens != 1 || o.kind != imagesSinglePlane {
161+ t.Fatalf("%d opens, kind %d", opens, o.kind)
162+ }
163+}
164+
165+// A modeset test that refuses GBM images tries the renderer's images of the
166+// same kind before the next kind.
167+func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) {
168+ o, k, commits := testOutput(t, unix.EINVAL)
169+ o.cursor = nil
170+ a, closed := gbmImages(t, 1)
171+ o.openAlloc = func() (imageAllocator, error) { return a, nil }
172+ r := gbmRenderer(t)
173+ r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once()
174+ r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 })).Return(nil).Once()
175+ // The refused GBM images leave no renderer target behind.
176+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
177+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
178+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
179+ k.EXPECT().rmFB(mock.Anything).Return(nil)
180+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
181+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
182+ if err := o.showImages(r, imagesDriver, nil); err != nil {
183+ t.Fatal(err)
184+ }
185+ if o.imageAlloc != allocVulkan || o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 {
186+ t.Fatalf("alloc %q kind %d closed %d images %d commits %d", o.imageAlloc, o.kind, *closed, len(o.images), len(*commits))
187+ }
188+}
189+
190+// Closing the output frees the GBM images and the allocator.
191+func TestOutputFreeImagesClosesGBMImages(t *testing.T) {
192+ o, k, _ := testOutput(t)
193+ img := newMockimageBuffer(t)
194+ img.EXPECT().close().Return().Twice()
195+ o.images = []imageBuffer{img, img}
196+ o.fbs = [2]uint32{70, 71}
197+ k.EXPECT().rmFB(mock.Anything).Return(nil).Twice()
198+ o.freeImages()
199+ if o.images != nil || o.fbs != [2]uint32{} {
200+ t.Fatalf("images %v fbs %v", o.images, o.fbs)
201+ }
202+}
203+
204+// A setup that finds images left over frees them first, with the renderer's
205+// targets over GBM ones, and a refused modeset of GBM images (no fallback
206+// left) drops the targets with the buffers.
207+func TestSetupStartsFromCleanState(t *testing.T) {
208+ o, k, _ := testOutput(t)
209+ o.cursor = nil
210+ img := newMockimageBuffer(t)
211+ img.EXPECT().close().Return().Once()
212+ o.images = []imageBuffer{img}
213+ o.fbs = [2]uint32{70, 71}
214+ k.EXPECT().rmFB(uint32(70)).Return(nil).Once()
215+ k.EXPECT().rmFB(uint32(71)).Return(nil).Once()
216+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(80, nil)
217+ r := gbmRenderer(t)
218+ r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once()
219+ r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once()
220+ if err := o.exportImages(r, nil, false); err != nil {
221+ t.Fatal(err)
222+ }
223+ if o.images != nil || o.fbs != [2]uint32{80, 80} {
224+ t.Fatalf("images %v fbs %v", o.images, o.fbs)
225+ }
226+}
227+
228+// Without CPU fence waits every fence is ready; with them only a signalled
229+// client fence is (a pipe stands in: readable means signalled).
230+func TestClientFenceReady(t *testing.T) {
231+ r, w, err := os.Pipe()
232+ if err != nil {
233+ t.Fatal(err)
234+ }
235+ defer r.Close()
236+ defer w.Close()
237+ o, _, _ := testOutput(t)
238+ if !o.clientFenceReady(r) || !o.clientFenceReady(nil) {
239+ t.Fatal("fence not ready without CPU waits")
240+ }
241+ o.cpuFences = true
242+ if !o.clientFenceReady(nil) {
243+ t.Fatal("no fence must be ready")
244+ }
245+ if o.clientFenceReady(r) {
246+ t.Fatal("pending client fence ready")
247+ }
248+ _, _ = w.Write([]byte{1})
249+ if !o.clientFenceReady(r) {
250+ t.Fatal("signalled client fence not ready")
251+ }
252+}
253+
254+// With CPU waits a client buffer whose fence is pending is composed, not
255+// scanned out and not put on the overlay: neither plane waits for it. Once
256+// the fence signalled the same frame scans out.
257+func TestPendingClientFenceComposes(t *testing.T) {
258+ r, w, err := os.Pipe()
259+ if err != nil {
260+ t.Fatal(err)
261+ }
262+ defer r.Close()
263+ defer w.Close()
264+ o, k, _ := testOutput(t)
265+ k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe()
266+ o.cpuFences = true
267+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
268+ s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}}
269+ c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, Acquire: r, DMABuf: &ports.DMABuf{ID: 5, Format: fourccXRGB, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{File: r}}}}
270+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
271+ if fb, _, _ := o.scanoutFrame(s, contents); fb != 0 || o.reason != "fence_pending" {
272+ t.Fatalf("pending fence: fb %d reason %q", fb, o.reason)
273+ }
274+ _, _ = w.Write([]byte{1})
275+ if fb, _, _ := o.scanoutFrame(s, contents); fb == 0 {
276+ t.Fatalf("signalled fence: reason %q", o.reason)
277+ }
278+}
M internal/adapters/drm/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 +170 -0
@@ -0,0 +1,170 @@
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+)
16+
17+// Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
18+// the fence and shows a frame the GPU still draws
19+// (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the
20+// output waits for each fence on the CPU and commits without it.
21+
22+const (
23+ driverNVIDIA = "nvidia-drm"
24+ // nvidiaDRMVersionFile holds the loaded module's version.
25+ nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version"
26+ // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version).
27+ ioctlVersion = 0xC0406400
28+)
29+
30+// nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD.
31+var nvidiaFenceFixed = [3]int{560, 35, 3}
32+
33+// drmVersion is struct drm_version.
34+type drmVersion struct {
35+ major, minor, patch int32
36+ nameLen uint64
37+ name unsafe.Pointer
38+ dateLen uint64
39+ date unsafe.Pointer
40+ descLen uint64
41+ desc unsafe.Pointer
42+}
43+
44+// driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION).
45+func driverName(fd int) (string, error) {
46+ var buf [64]byte
47+ v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])}
48+ if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil {
49+ return "", err
50+ }
51+ return string(buf[:min(int(v.nameLen), len(buf))]), nil
52+}
53+
54+// parseVersion reads a dotted version such as "560.35.03" (missing parts
55+// are 0).
56+func parseVersion(s string) ([3]int, error) {
57+ var v [3]int
58+ parts := strings.Split(strings.TrimSpace(s), ".")
59+ if len(parts) == 0 || len(parts) > 3 || parts[0] == "" {
60+ return v, fmt.Errorf("version %q", s)
61+ }
62+ for i, p := range parts {
63+ // Digits only: Atoi would also take a sign.
64+ n, err := strconv.Atoi(p)
65+ if err != nil || p[0] < '0' || p[0] > '9' {
66+ return v, fmt.Errorf("version %q", s)
67+ }
68+ v[i] = 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 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 a CPU wait on a fence of our own render.
127+const fenceWaitLimit = time.Second
128+
129+// clientFenceReady reports whether a client's acquire fence lets its buffer
130+// be shown by a plane this frame. Where fences are waited for on the CPU, a
131+// client fence that has not signalled must not stall the output: the buffer
132+// is composed instead (the renderer waits for it on the GPU). A nil or
133+// unpollable fence counts as signalled, as in signalled.
134+func (o *Output) clientFenceReady(acquire *os.File) bool {
135+ if !o.cpuFences || acquire == nil {
136+ return true
137+ }
138+ // Wayland may close its file at any time: poll a duplicate.
139+ d := dupFence(acquire)
140+ if d == nil {
141+ return true
142+ }
143+ defer d.Close()
144+ return signalled([]*os.File{d})
145+}
146+
147+// waitFences blocks until our own render fences signalled when the driver
148+// cannot wait for them in the commit, for at most fenceWaitLimit each, or
149+// until the output stops. Client fences are not waited for here: planes take
150+// only buffers whose fence signalled (clientFenceReady). A failed wait is not
151+// fatal: the frame may then show early, as it would on any such driver.
152+func (o *Output) waitFences(fences ...*os.File) {
153+ if !o.cpuFences {
154+ return
155+ }
156+ ctx := o.runContext
157+ if ctx == nil {
158+ ctx = context.Background()
159+ }
160+ ctx, cancel := context.WithTimeout(ctx, fenceWaitLimit)
161+ defer cancel()
162+ for _, f := range fences {
163+ if f == nil {
164+ continue
165+ }
166+ if err := syncfile.Wait(ctx, f); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
167+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
168+ }
169+ }
170+}
@@ -0,0 +1,170 @@
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+)
16+
17+// Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for
18+// the fence and shows a frame the GPU still draws
19+// (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the
20+// output waits for each fence on the CPU and commits without it.
21+
22+const (
23+ driverNVIDIA = "nvidia-drm"
24+ // nvidiaDRMVersionFile holds the loaded module's version.
25+ nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version"
26+ // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version).
27+ ioctlVersion = 0xC0406400
28+)
29+
30+// nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD.
31+var nvidiaFenceFixed = [3]int{560, 35, 3}
32+
33+// drmVersion is struct drm_version.
34+type drmVersion struct {
35+ major, minor, patch int32
36+ nameLen uint64
37+ name unsafe.Pointer
38+ dateLen uint64
39+ date unsafe.Pointer
40+ descLen uint64
41+ desc unsafe.Pointer
42+}
43+
44+// driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION).
45+func driverName(fd int) (string, error) {
46+ var buf [64]byte
47+ v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])}
48+ if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil {
49+ return "", err
50+ }
51+ return string(buf[:min(int(v.nameLen), len(buf))]), nil
52+}
53+
54+// parseVersion reads a dotted version such as "560.35.03" (missing parts
55+// are 0).
56+func parseVersion(s string) ([3]int, error) {
57+ var v [3]int
58+ parts := strings.Split(strings.TrimSpace(s), ".")
59+ if len(parts) == 0 || len(parts) > 3 || parts[0] == "" {
60+ return v, fmt.Errorf("version %q", s)
61+ }
62+ for i, p := range parts {
63+ // Digits only: Atoi would also take a sign.
64+ n, err := strconv.Atoi(p)
65+ if err != nil || p[0] < '0' || p[0] > '9' {
66+ return v, fmt.Errorf("version %q", s)
67+ }
68+ v[i] = 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 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 a CPU wait on a fence of our own render.
127+const fenceWaitLimit = time.Second
128+
129+// clientFenceReady reports whether a client's acquire fence lets its buffer
130+// be shown by a plane this frame. Where fences are waited for on the CPU, a
131+// client fence that has not signalled must not stall the output: the buffer
132+// is composed instead (the renderer waits for it on the GPU). A nil or
133+// unpollable fence counts as signalled, as in signalled.
134+func (o *Output) clientFenceReady(acquire *os.File) bool {
135+ if !o.cpuFences || acquire == nil {
136+ return true
137+ }
138+ // Wayland may close its file at any time: poll a duplicate.
139+ d := dupFence(acquire)
140+ if d == nil {
141+ return true
142+ }
143+ defer d.Close()
144+ return signalled([]*os.File{d})
145+}
146+
147+// waitFences blocks until our own render fences signalled when the driver
148+// cannot wait for them in the commit, for at most fenceWaitLimit each, or
149+// until the output stops. Client fences are not waited for here: planes take
150+// only buffers whose fence signalled (clientFenceReady). A failed wait is not
151+// fatal: the frame may then show early, as it would on any such driver.
152+func (o *Output) waitFences(fences ...*os.File) {
153+ if !o.cpuFences {
154+ return
155+ }
156+ ctx := o.runContext
157+ if ctx == nil {
158+ ctx = context.Background()
159+ }
160+ ctx, cancel := context.WithTimeout(ctx, fenceWaitLimit)
161+ defer cancel()
162+ for _, f := range fences {
163+ if f == nil {
164+ continue
165+ }
166+ if err := syncfile.Wait(ctx, f); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
167+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence")
168+ }
169+ }
170+}
A internal/adapters/drm/fencegate_test.go +122 -0
@@ -0,0 +1,122 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/zerowrap"
10+)
11+
12+func TestParseVersion(t *testing.T) {
13+ for _, tc := range []struct {
14+ in string
15+ want [3]int
16+ ok bool
17+ }{
18+ {"560.35.03\n", [3]int{560, 35, 3}, true},
19+ {"615.78.08", [3]int{615, 78, 8}, true},
20+ {"550.120", [3]int{550, 120, 0}, true},
21+ {"570", [3]int{570, 0, 0}, true},
22+ {"", [3]int{}, false},
23+ {"abc", [3]int{}, false},
24+ {"560.x.3", [3]int{}, false},
25+ {"1.2.3.4", [3]int{}, false},
26+ {"-1.2", [3]int{}, false},
27+ {"+560.35.3", [3]int{}, false},
28+ {"560.+35.3", [3]int{}, false},
29+ {"560..3", [3]int{}, false},
30+ } {
31+ got, err := parseVersion(tc.in)
32+ if (err == nil) != tc.ok || tc.ok && got != tc.want {
33+ t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err)
34+ }
35+ }
36+}
37+
38+func TestVersionLess(t *testing.T) {
39+ for _, tc := range []struct {
40+ a, b [3]int
41+ want bool
42+ }{
43+ {[3]int{560, 35, 2}, [3]int{560, 35, 3}, true},
44+ {[3]int{560, 35, 3}, [3]int{560, 35, 3}, false},
45+ {[3]int{560, 35, 5}, [3]int{560, 35, 3}, false},
46+ {[3]int{550, 120, 0}, [3]int{560, 35, 3}, true},
47+ {[3]int{560, 36, 0}, [3]int{560, 35, 3}, false},
48+ {[3]int{615, 78, 8}, [3]int{560, 35, 3}, false},
49+ {[3]int{470, 99, 99}, [3]int{560, 0, 0}, true},
50+ } {
51+ if got := versionLess(tc.a, tc.b); got != tc.want {
52+ t.Errorf("versionLess(%v, %v) = %v", tc.a, tc.b, got)
53+ }
54+ }
55+}
56+
57+func TestBrokenInFence(t *testing.T) {
58+ for _, tc := range []struct {
59+ name, driver, version string
60+ err error
61+ want bool
62+ }{
63+ {"amd", "amdgpu", "", nil, false},
64+ {"intel", "i915", "garbage", nil, false},
65+ {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
66+ {"just before", driverNVIDIA, "560.35.2", nil, true},
67+ {"fixed", driverNVIDIA, "560.35.03\n", nil, false},
68+ {"newer", driverNVIDIA, "615.78.08", nil, false},
69+ {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
70+ {"unparsable", driverNVIDIA, "n/a", nil, true},
71+ } {
72+ if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want {
73+ t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want)
74+ }
75+ }
76+}
77+
78+// With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
79+func TestFenceProp(t *testing.T) {
80+ f, w, err := os.Pipe()
81+ if err != nil {
82+ t.Fatal(err)
83+ }
84+ defer f.Close()
85+ defer w.Close()
86+ for _, cpu := range []bool{false, true} {
87+ o, _, _ := testOutput(t)
88+ o.cpuFences = cpu
89+ req := &atomicReq{}
90+ o.fenceProp(req, o.primary, f)
91+ o.fenceProp(req, o.primary, nil)
92+ v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD"))
93+ if ok == cpu || ok && v != uint64(f.Fd()) {
94+ t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok)
95+ }
96+ }
97+}
98+
99+// The CPU wait returns once the fence signals (a pipe stands in: readable
100+// means signalled) and gives up after fenceWaitLimit.
101+func TestWaitFences(t *testing.T) {
102+ o, _, _ := testOutput(t)
103+ o.cpuFences = true
104+ o.log = zerowrap.Default()
105+ r, w, err := os.Pipe()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ defer r.Close()
110+ go func() {
111+ time.Sleep(20 * time.Millisecond)
112+ _, _ = w.Write([]byte{1})
113+ }()
114+ start := time.Now()
115+ o.waitFences(nil, r)
116+ if d := time.Since(start); d < 15*time.Millisecond || d > fenceWaitLimit/2 {
117+ t.Fatalf("waited %v for the fence", d)
118+ }
119+ w.Close()
120+ o.cpuFences = false
121+ o.waitFences(r) // not waited for: returns at once
122+}
@@ -0,0 +1,122 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/zerowrap"
10+)
11+
12+func TestParseVersion(t *testing.T) {
13+ for _, tc := range []struct {
14+ in string
15+ want [3]int
16+ ok bool
17+ }{
18+ {"560.35.03\n", [3]int{560, 35, 3}, true},
19+ {"615.78.08", [3]int{615, 78, 8}, true},
20+ {"550.120", [3]int{550, 120, 0}, true},
21+ {"570", [3]int{570, 0, 0}, true},
22+ {"", [3]int{}, false},
23+ {"abc", [3]int{}, false},
24+ {"560.x.3", [3]int{}, false},
25+ {"1.2.3.4", [3]int{}, false},
26+ {"-1.2", [3]int{}, false},
27+ {"+560.35.3", [3]int{}, false},
28+ {"560.+35.3", [3]int{}, false},
29+ {"560..3", [3]int{}, false},
30+ } {
31+ got, err := parseVersion(tc.in)
32+ if (err == nil) != tc.ok || tc.ok && got != tc.want {
33+ t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err)
34+ }
35+ }
36+}
37+
38+func TestVersionLess(t *testing.T) {
39+ for _, tc := range []struct {
40+ a, b [3]int
41+ want bool
42+ }{
43+ {[3]int{560, 35, 2}, [3]int{560, 35, 3}, true},
44+ {[3]int{560, 35, 3}, [3]int{560, 35, 3}, false},
45+ {[3]int{560, 35, 5}, [3]int{560, 35, 3}, false},
46+ {[3]int{550, 120, 0}, [3]int{560, 35, 3}, true},
47+ {[3]int{560, 36, 0}, [3]int{560, 35, 3}, false},
48+ {[3]int{615, 78, 8}, [3]int{560, 35, 3}, false},
49+ {[3]int{470, 99, 99}, [3]int{560, 0, 0}, true},
50+ } {
51+ if got := versionLess(tc.a, tc.b); got != tc.want {
52+ t.Errorf("versionLess(%v, %v) = %v", tc.a, tc.b, got)
53+ }
54+ }
55+}
56+
57+func TestBrokenInFence(t *testing.T) {
58+ for _, tc := range []struct {
59+ name, driver, version string
60+ err error
61+ want bool
62+ }{
63+ {"amd", "amdgpu", "", nil, false},
64+ {"intel", "i915", "garbage", nil, false},
65+ {"old nvidia", driverNVIDIA, "555.58.02", nil, true},
66+ {"just before", driverNVIDIA, "560.35.2", nil, true},
67+ {"fixed", driverNVIDIA, "560.35.03\n", nil, false},
68+ {"newer", driverNVIDIA, "615.78.08", nil, false},
69+ {"unreadable", driverNVIDIA, "", errors.New("no such file"), true},
70+ {"unparsable", driverNVIDIA, "n/a", nil, true},
71+ } {
72+ if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want {
73+ t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want)
74+ }
75+ }
76+}
77+
78+// With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does.
79+func TestFenceProp(t *testing.T) {
80+ f, w, err := os.Pipe()
81+ if err != nil {
82+ t.Fatal(err)
83+ }
84+ defer f.Close()
85+ defer w.Close()
86+ for _, cpu := range []bool{false, true} {
87+ o, _, _ := testOutput(t)
88+ o.cpuFences = cpu
89+ req := &atomicReq{}
90+ o.fenceProp(req, o.primary, f)
91+ o.fenceProp(req, o.primary, nil)
92+ v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD"))
93+ if ok == cpu || ok && v != uint64(f.Fd()) {
94+ t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok)
95+ }
96+ }
97+}
98+
99+// The CPU wait returns once the fence signals (a pipe stands in: readable
100+// means signalled) and gives up after fenceWaitLimit.
101+func TestWaitFences(t *testing.T) {
102+ o, _, _ := testOutput(t)
103+ o.cpuFences = true
104+ o.log = zerowrap.Default()
105+ r, w, err := os.Pipe()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ defer r.Close()
110+ go func() {
111+ time.Sleep(20 * time.Millisecond)
112+ _, _ = w.Write([]byte{1})
113+ }()
114+ start := time.Now()
115+ o.waitFences(nil, r)
116+ if d := time.Since(start); d < 15*time.Millisecond || d > fenceWaitLimit/2 {
117+ t.Fatalf("waited %v for the fence", d)
118+ }
119+ w.Close()
120+ o.cpuFences = false
121+ o.waitFences(r) // not waited for: returns at once
122+}
A internal/adapters/drm/gbm/gbm.go +312 -0
@@ -0,0 +1,312 @@
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+// ErrUnavailable marks a missing or unusable libgbm.
22+var ErrUnavailable = errors.New("gbm unavailable")
23+
24+// Usage is a set of GBM_BO_USE_* flags.
25+type Usage uint32
26+
27+const (
28+ // UseScanout (GBM_BO_USE_SCANOUT): the buffer can be shown by KMS.
29+ UseScanout Usage = 1 << 0
30+ // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.
31+ UseRendering Usage = 1 << 2
32+ // UseLinear (GBM_BO_USE_LINEAR): the layout is linear.
33+ UseLinear Usage = 1 << 4
34+)
35+
36+const (
37+ // modLinear is DRM_FORMAT_MOD_LINEAR.
38+ modLinear = 0
39+ // modInvalid is DRM_FORMAT_MOD_INVALID: an implicit layout.
40+ modInvalid = 1<<56 - 1
41+ // maxPlanes is the DRM limit of planes in a framebuffer.
42+ maxPlanes = 4
43+)
44+
45+var (
46+ loadOnce sync.Once
47+ loadErr error
48+
49+ createDevice func(fd int32) uintptr
50+ destroyDevice func(dev uintptr)
51+ createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr
52+ createWithModifiers func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32) uintptr
53+ createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr
54+ boModifier func(bo uintptr) uint64
55+ boPlaneCount func(bo uintptr) int32
56+ boFDForPlane func(bo uintptr, plane int32) int32
57+ boFD func(bo uintptr) int32
58+ boStrideForPlane func(bo uintptr, plane int32) uint32
59+ boOffset func(bo uintptr, plane int32) uint32
60+ boDestroy func(bo uintptr)
61+)
62+
63+// load opens libgbm once. Without it (or with a lacking one) the allocator
64+// is unavailable and callers fall back to another way to make images.
65+func load() error {
66+ loadOnce.Do(func() {
67+ lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
68+ if err != nil {
69+ loadErr = fmt.Errorf("%w: open libgbm.so.1: %v", ErrUnavailable, err)
70+ return
71+ }
72+ defer func() {
73+ if r := recover(); r != nil {
74+ loadErr = fmt.Errorf("%w: libgbm: %v", ErrUnavailable, r)
75+ }
76+ }()
77+ reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }
78+ have := func(name string) bool { p, err := purego.Dlsym(lib, name); return err == nil && p != 0 }
79+ reg(&createDevice, "gbm_create_device")
80+ reg(&destroyDevice, "gbm_device_destroy")
81+ reg(&createPlain, "gbm_bo_create")
82+ reg(&boModifier, "gbm_bo_get_modifier")
83+ reg(&boPlaneCount, "gbm_bo_get_plane_count")
84+ if have("gbm_bo_get_fd_for_plane") {
85+ reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
86+ } else {
87+ // Older libgbm: one fd for the whole object.
88+ reg(&boFD, "gbm_bo_get_fd")
89+ }
90+ reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")
91+ reg(&boOffset, "gbm_bo_get_offset")
92+ reg(&boDestroy, "gbm_bo_destroy")
93+ if have("gbm_bo_create_with_modifiers2") {
94+ reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
95+ }
96+ if have("gbm_bo_create_with_modifiers") {
97+ reg(&createWithModifiers, "gbm_bo_create_with_modifiers")
98+ }
99+ if createWithModifiers2 == nil && createWithModifiers == nil {
100+ loadErr = fmt.Errorf("%w: libgbm lacks gbm_bo_create_with_modifiers", ErrUnavailable)
101+ }
102+ })
103+ return loadErr
104+}
105+
106+// Device is a GBM device on a duplicate of a DRM device fd. Close its
107+// buffers before the device.
108+type Device struct {
109+ dev uintptr
110+ fd int
111+}
112+
113+// Open creates a GBM device on a duplicate of fd, which stays the caller's.
114+func Open(fd int) (*Device, error) {
115+ if err := load(); err != nil {
116+ return nil, err
117+ }
118+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
119+ if err != nil {
120+ return nil, fmt.Errorf("duplicate drm fd: %w", err)
121+ }
122+ dev := createDevice(int32(dup))
123+ if dev == 0 {
124+ _ = unix.Close(dup)
125+ return nil, errors.New("gbm_create_device failed")
126+ }
127+ return &Device{dev: dev, fd: dup}, nil
128+}
129+
130+// Close destroys the device and its fd.
131+func (d *Device) Close() {
132+ if d == nil || d.dev == 0 {
133+ return
134+ }
135+ destroyDevice(d.dev)
136+ _ = unix.Close(d.fd)
137+ d.dev = 0
138+}
139+
140+// Buffer is an allocated image. It owns one fd per plane and the GBM
141+// object, kept alive for as long as the image is used.
142+type Buffer struct {
143+ bo uintptr
144+ Width, Height int
145+ Format uint32
146+ Modifier uint64
147+ // Flagless: the buffer came from gbm_bo_create_with_modifiers, which
148+ // takes no usage flags (libgbm before 21.1): scanout was not asked for.
149+ Flagless bool
150+ planes []plane
151+}
152+
153+type plane struct {
154+ fd int
155+ offset, stride uint32
156+}
157+
158+// Create allocates an image whose modifier is one of modifiers (a
159+// non-empty list). A list holding only linear falls back to a plain
160+// linear allocation when GBM refuses the modifier call, and accepts an
161+// implicit layout (DRM_FORMAT_MOD_INVALID) as linear on either path. The
162+// result must be one memory object, because the renderer imports one
163+// dmabuf for all planes.
164+func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {
165+ if d == nil || d.dev == 0 {
166+ return nil, errors.New("gbm device closed")
167+ }
168+ if width <= 0 || height <= 0 || len(modifiers) == 0 {
169+ return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))
170+ }
171+ var bo uintptr
172+ flagless := false
173+ switch {
174+ case createWithModifiers2 != nil:
175+ bo = createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
176+ case createWithModifiers != nil:
177+ // This entry point cannot ask for scanout.
178+ flagless = true
179+ bo = createWithModifiers(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)))
180+ }
181+ runtime.KeepAlive(modifiers)
182+ onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear
183+ if bo == 0 && onlyLinear {
184+ bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|UseLinear))
185+ flagless = false
186+ }
187+ if bo == 0 {
188+ return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)
189+ }
190+ b, err := readBuffer(bo, width, height, format, acceptedModifiers(modifiers))
191+ if err != nil {
192+ boDestroy(bo)
193+ return nil, err
194+ }
195+ if onlyLinear && b.Modifier == modInvalid {
196+ b.Modifier = modLinear
197+ }
198+ b.Flagless = flagless
199+ return b, nil
200+}
201+
202+// acceptedModifiers is what an allocation may report for a request: the
203+// request itself, plus an implicit layout when only linear was asked for.
204+func acceptedModifiers(requested []uint64) []uint64 {
205+ if len(requested) == 1 && requested[0] == modLinear {
206+ return []uint64{modLinear, modInvalid}
207+ }
208+ return requested
209+}
210+
211+// readBuffer reads the layout of bo and validates it against the request.
212+// It does not destroy bo on error.
213+func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {
214+ b := &Buffer{bo: bo, Width: width, Height: height, Format: format, Modifier: boModifier(bo)}
215+ if err := checkModifier(modifiers, b.Modifier); err != nil {
216+ return nil, err
217+ }
218+ n := int(boPlaneCount(bo))
219+ if n < 1 || n > maxPlanes {
220+ return nil, fmt.Errorf("gbm: buffer has %d planes", n)
221+ }
222+ for i := range n {
223+ var fd int32
224+ if boFDForPlane != nil {
225+ fd = boFDForPlane(bo, int32(i))
226+ } else {
227+ fd = boFD(bo)
228+ }
229+ if fd < 0 {
230+ b.closeFDs()
231+ return nil, fmt.Errorf("gbm: no fd for plane %d", i)
232+ }
233+ b.planes = append(b.planes, plane{fd: int(fd), offset: boOffset(bo, int32(i)), stride: boStrideForPlane(bo, int32(i))})
234+ }
235+ fds := make([]int, len(b.planes))
236+ for i, p := range b.planes {
237+ fds[i] = p.fd
238+ }
239+ if err := checkOneObject(fds); err != nil {
240+ b.closeFDs()
241+ return nil, err
242+ }
243+ b.bo = bo
244+ return b, nil
245+}
246+
247+// checkModifier fails when the allocator returned a modifier that was not
248+// asked for (including DRM_FORMAT_MOD_INVALID, an implicit layout the
249+// renderer cannot import).
250+func checkModifier(requested []uint64, got uint64) error {
251+ if !slices.Contains(requested, got) {
252+ return fmt.Errorf("gbm: modifier %#x not in the requested list %#x", got, requested)
253+ }
254+ return nil
255+}
256+
257+// checkOneObject fails when the plane fds are not all the same dmabuf:
258+// disjoint planes cannot be imported as one memory object.
259+func checkOneObject(fds []int) error {
260+ var first unix.Stat_t
261+ for i, fd := range fds {
262+ var st unix.Stat_t
263+ if err := unix.Fstat(fd, &st); err != nil {
264+ return fmt.Errorf("gbm: stat plane %d: %w", i, err)
265+ }
266+ if i == 0 {
267+ first = st
268+ continue
269+ }
270+ if st.Dev != first.Dev || st.Ino != first.Ino {
271+ return fmt.Errorf("gbm: plane %d is a different dmabuf than plane 0", i)
272+ }
273+ }
274+ return nil
275+}
276+
277+func (b *Buffer) closeFDs() {
278+ for _, p := range b.planes {
279+ _ = unix.Close(p.fd)
280+ }
281+ b.planes = nil
282+}
283+
284+// DMABuf returns the image as a dmabuf with fresh descriptors (the caller
285+// closes them); the buffer keeps its own.
286+func (b *Buffer) DMABuf() (ports.DMABuf, error) {
287+ if b == nil || b.bo == 0 {
288+ return ports.DMABuf{}, errors.New("gbm buffer closed")
289+ }
290+ out := ports.DMABuf{Width: b.Width, Height: b.Height, Format: b.Format, Modifier: b.Modifier}
291+ for _, p := range b.planes {
292+ fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)
293+ if err != nil {
294+ for _, q := range out.Planes {
295+ _ = q.File.Close()
296+ }
297+ return ports.DMABuf{}, fmt.Errorf("duplicate gbm plane fd: %w", err)
298+ }
299+ out.Planes = append(out.Planes, ports.DMABufPlane{File: os.NewFile(uintptr(fd), "neferwl-gbm"), Offset: p.offset, Stride: p.stride})
300+ }
301+ return out, nil
302+}
303+
304+// Close frees the buffer and its descriptors.
305+func (b *Buffer) Close() {
306+ if b == nil || b.bo == 0 {
307+ return
308+ }
309+ b.closeFDs()
310+ boDestroy(b.bo)
311+ b.bo = 0
312+}
@@ -0,0 +1,312 @@
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+// ErrUnavailable marks a missing or unusable libgbm.
22+var ErrUnavailable = errors.New("gbm unavailable")
23+
24+// Usage is a set of GBM_BO_USE_* flags.
25+type Usage uint32
26+
27+const (
28+ // UseScanout (GBM_BO_USE_SCANOUT): the buffer can be shown by KMS.
29+ UseScanout Usage = 1 << 0
30+ // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it.
31+ UseRendering Usage = 1 << 2
32+ // UseLinear (GBM_BO_USE_LINEAR): the layout is linear.
33+ UseLinear Usage = 1 << 4
34+)
35+
36+const (
37+ // modLinear is DRM_FORMAT_MOD_LINEAR.
38+ modLinear = 0
39+ // modInvalid is DRM_FORMAT_MOD_INVALID: an implicit layout.
40+ modInvalid = 1<<56 - 1
41+ // maxPlanes is the DRM limit of planes in a framebuffer.
42+ maxPlanes = 4
43+)
44+
45+var (
46+ loadOnce sync.Once
47+ loadErr error
48+
49+ createDevice func(fd int32) uintptr
50+ destroyDevice func(dev uintptr)
51+ createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr
52+ createWithModifiers func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32) uintptr
53+ createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr
54+ boModifier func(bo uintptr) uint64
55+ boPlaneCount func(bo uintptr) int32
56+ boFDForPlane func(bo uintptr, plane int32) int32
57+ boFD func(bo uintptr) int32
58+ boStrideForPlane func(bo uintptr, plane int32) uint32
59+ boOffset func(bo uintptr, plane int32) uint32
60+ boDestroy func(bo uintptr)
61+)
62+
63+// load opens libgbm once. Without it (or with a lacking one) the allocator
64+// is unavailable and callers fall back to another way to make images.
65+func load() error {
66+ loadOnce.Do(func() {
67+ lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
68+ if err != nil {
69+ loadErr = fmt.Errorf("%w: open libgbm.so.1: %v", ErrUnavailable, err)
70+ return
71+ }
72+ defer func() {
73+ if r := recover(); r != nil {
74+ loadErr = fmt.Errorf("%w: libgbm: %v", ErrUnavailable, r)
75+ }
76+ }()
77+ reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) }
78+ have := func(name string) bool { p, err := purego.Dlsym(lib, name); return err == nil && p != 0 }
79+ reg(&createDevice, "gbm_create_device")
80+ reg(&destroyDevice, "gbm_device_destroy")
81+ reg(&createPlain, "gbm_bo_create")
82+ reg(&boModifier, "gbm_bo_get_modifier")
83+ reg(&boPlaneCount, "gbm_bo_get_plane_count")
84+ if have("gbm_bo_get_fd_for_plane") {
85+ reg(&boFDForPlane, "gbm_bo_get_fd_for_plane")
86+ } else {
87+ // Older libgbm: one fd for the whole object.
88+ reg(&boFD, "gbm_bo_get_fd")
89+ }
90+ reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane")
91+ reg(&boOffset, "gbm_bo_get_offset")
92+ reg(&boDestroy, "gbm_bo_destroy")
93+ if have("gbm_bo_create_with_modifiers2") {
94+ reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2")
95+ }
96+ if have("gbm_bo_create_with_modifiers") {
97+ reg(&createWithModifiers, "gbm_bo_create_with_modifiers")
98+ }
99+ if createWithModifiers2 == nil && createWithModifiers == nil {
100+ loadErr = fmt.Errorf("%w: libgbm lacks gbm_bo_create_with_modifiers", ErrUnavailable)
101+ }
102+ })
103+ return loadErr
104+}
105+
106+// Device is a GBM device on a duplicate of a DRM device fd. Close its
107+// buffers before the device.
108+type Device struct {
109+ dev uintptr
110+ fd int
111+}
112+
113+// Open creates a GBM device on a duplicate of fd, which stays the caller's.
114+func Open(fd int) (*Device, error) {
115+ if err := load(); err != nil {
116+ return nil, err
117+ }
118+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
119+ if err != nil {
120+ return nil, fmt.Errorf("duplicate drm fd: %w", err)
121+ }
122+ dev := createDevice(int32(dup))
123+ if dev == 0 {
124+ _ = unix.Close(dup)
125+ return nil, errors.New("gbm_create_device failed")
126+ }
127+ return &Device{dev: dev, fd: dup}, nil
128+}
129+
130+// Close destroys the device and its fd.
131+func (d *Device) Close() {
132+ if d == nil || d.dev == 0 {
133+ return
134+ }
135+ destroyDevice(d.dev)
136+ _ = unix.Close(d.fd)
137+ d.dev = 0
138+}
139+
140+// Buffer is an allocated image. It owns one fd per plane and the GBM
141+// object, kept alive for as long as the image is used.
142+type Buffer struct {
143+ bo uintptr
144+ Width, Height int
145+ Format uint32
146+ Modifier uint64
147+ // Flagless: the buffer came from gbm_bo_create_with_modifiers, which
148+ // takes no usage flags (libgbm before 21.1): scanout was not asked for.
149+ Flagless bool
150+ planes []plane
151+}
152+
153+type plane struct {
154+ fd int
155+ offset, stride uint32
156+}
157+
158+// Create allocates an image whose modifier is one of modifiers (a
159+// non-empty list). A list holding only linear falls back to a plain
160+// linear allocation when GBM refuses the modifier call, and accepts an
161+// implicit layout (DRM_FORMAT_MOD_INVALID) as linear on either path. The
162+// result must be one memory object, because the renderer imports one
163+// dmabuf for all planes.
164+func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) {
165+ if d == nil || d.dev == 0 {
166+ return nil, errors.New("gbm device closed")
167+ }
168+ if width <= 0 || height <= 0 || len(modifiers) == 0 {
169+ return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers))
170+ }
171+ var bo uintptr
172+ flagless := false
173+ switch {
174+ case createWithModifiers2 != nil:
175+ bo = createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage))
176+ case createWithModifiers != nil:
177+ // This entry point cannot ask for scanout.
178+ flagless = true
179+ bo = createWithModifiers(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)))
180+ }
181+ runtime.KeepAlive(modifiers)
182+ onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear
183+ if bo == 0 && onlyLinear {
184+ bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|UseLinear))
185+ flagless = false
186+ }
187+ if bo == 0 {
188+ return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers)
189+ }
190+ b, err := readBuffer(bo, width, height, format, acceptedModifiers(modifiers))
191+ if err != nil {
192+ boDestroy(bo)
193+ return nil, err
194+ }
195+ if onlyLinear && b.Modifier == modInvalid {
196+ b.Modifier = modLinear
197+ }
198+ b.Flagless = flagless
199+ return b, nil
200+}
201+
202+// acceptedModifiers is what an allocation may report for a request: the
203+// request itself, plus an implicit layout when only linear was asked for.
204+func acceptedModifiers(requested []uint64) []uint64 {
205+ if len(requested) == 1 && requested[0] == modLinear {
206+ return []uint64{modLinear, modInvalid}
207+ }
208+ return requested
209+}
210+
211+// readBuffer reads the layout of bo and validates it against the request.
212+// It does not destroy bo on error.
213+func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) {
214+ b := &Buffer{bo: bo, Width: width, Height: height, Format: format, Modifier: boModifier(bo)}
215+ if err := checkModifier(modifiers, b.Modifier); err != nil {
216+ return nil, err
217+ }
218+ n := int(boPlaneCount(bo))
219+ if n < 1 || n > maxPlanes {
220+ return nil, fmt.Errorf("gbm: buffer has %d planes", n)
221+ }
222+ for i := range n {
223+ var fd int32
224+ if boFDForPlane != nil {
225+ fd = boFDForPlane(bo, int32(i))
226+ } else {
227+ fd = boFD(bo)
228+ }
229+ if fd < 0 {
230+ b.closeFDs()
231+ return nil, fmt.Errorf("gbm: no fd for plane %d", i)
232+ }
233+ b.planes = append(b.planes, plane{fd: int(fd), offset: boOffset(bo, int32(i)), stride: boStrideForPlane(bo, int32(i))})
234+ }
235+ fds := make([]int, len(b.planes))
236+ for i, p := range b.planes {
237+ fds[i] = p.fd
238+ }
239+ if err := checkOneObject(fds); err != nil {
240+ b.closeFDs()
241+ return nil, err
242+ }
243+ b.bo = bo
244+ return b, nil
245+}
246+
247+// checkModifier fails when the allocator returned a modifier that was not
248+// asked for (including DRM_FORMAT_MOD_INVALID, an implicit layout the
249+// renderer cannot import).
250+func checkModifier(requested []uint64, got uint64) error {
251+ if !slices.Contains(requested, got) {
252+ return fmt.Errorf("gbm: modifier %#x not in the requested list %#x", got, requested)
253+ }
254+ return nil
255+}
256+
257+// checkOneObject fails when the plane fds are not all the same dmabuf:
258+// disjoint planes cannot be imported as one memory object.
259+func checkOneObject(fds []int) error {
260+ var first unix.Stat_t
261+ for i, fd := range fds {
262+ var st unix.Stat_t
263+ if err := unix.Fstat(fd, &st); err != nil {
264+ return fmt.Errorf("gbm: stat plane %d: %w", i, err)
265+ }
266+ if i == 0 {
267+ first = st
268+ continue
269+ }
270+ if st.Dev != first.Dev || st.Ino != first.Ino {
271+ return fmt.Errorf("gbm: plane %d is a different dmabuf than plane 0", i)
272+ }
273+ }
274+ return nil
275+}
276+
277+func (b *Buffer) closeFDs() {
278+ for _, p := range b.planes {
279+ _ = unix.Close(p.fd)
280+ }
281+ b.planes = nil
282+}
283+
284+// DMABuf returns the image as a dmabuf with fresh descriptors (the caller
285+// closes them); the buffer keeps its own.
286+func (b *Buffer) DMABuf() (ports.DMABuf, error) {
287+ if b == nil || b.bo == 0 {
288+ return ports.DMABuf{}, errors.New("gbm buffer closed")
289+ }
290+ out := ports.DMABuf{Width: b.Width, Height: b.Height, Format: b.Format, Modifier: b.Modifier}
291+ for _, p := range b.planes {
292+ fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0)
293+ if err != nil {
294+ for _, q := range out.Planes {
295+ _ = q.File.Close()
296+ }
297+ return ports.DMABuf{}, fmt.Errorf("duplicate gbm plane fd: %w", err)
298+ }
299+ out.Planes = append(out.Planes, ports.DMABufPlane{File: os.NewFile(uintptr(fd), "neferwl-gbm"), Offset: p.offset, Stride: p.stride})
300+ }
301+ return out, nil
302+}
303+
304+// Close frees the buffer and its descriptors.
305+func (b *Buffer) Close() {
306+ if b == nil || b.bo == 0 {
307+ return
308+ }
309+ b.closeFDs()
310+ boDestroy(b.bo)
311+ b.bo = 0
312+}
A internal/adapters/drm/gbm/gbm_test.go +95 -0
@@ -0,0 +1,95 @@
1+package gbm
2+
3+import (
4+ "os"
5+ "slices"
6+ "strings"
7+ "testing"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// An allocator answer outside the requested list is refused, including an
13+// implicit layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import.
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+ {"invalid", []uint64{tiled, 0}, modInvalid, false},
27+ {"empty list", nil, 0, false},
28+ } {
29+ if err := checkModifier(tc.requested, tc.got); (err == nil) != tc.ok {
30+ t.Errorf("%s: %v", tc.name, err)
31+ }
32+ }
33+}
34+
35+// Planes must be one dmabuf: the renderer imports a single memory object.
36+// Duplicates of one descriptor are the same inode; two files are not.
37+func TestCheckOneObject(t *testing.T) {
38+ f, err := os.CreateTemp(t.TempDir(), "a")
39+ if err != nil {
40+ t.Fatal(err)
41+ }
42+ defer f.Close()
43+ g, err := os.CreateTemp(t.TempDir(), "b")
44+ if err != nil {
45+ t.Fatal(err)
46+ }
47+ defer g.Close()
48+ dup, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
49+ if err != nil {
50+ t.Fatal(err)
51+ }
52+ defer unix.Close(dup)
53+ fd := int(f.Fd())
54+ if err := checkOneObject([]int{fd}); err != nil {
55+ t.Errorf("one plane: %v", err)
56+ }
57+ if err := checkOneObject([]int{fd, dup, fd}); err != nil {
58+ t.Errorf("shared planes: %v", err)
59+ }
60+ err = checkOneObject([]int{fd, int(g.Fd())})
61+ if err == nil || !strings.Contains(err.Error(), "plane 1") {
62+ t.Errorf("disjoint planes: %v", err)
63+ }
64+ if err := checkOneObject([]int{fd, -1}); err == nil {
65+ t.Error("bad descriptor accepted")
66+ }
67+}
68+
69+// A closed device and bad requests are refused without touching libgbm.
70+func TestCreateRejectsBadRequests(t *testing.T) {
71+ if _, err := (*Device)(nil).Create(8, 8, 0, []uint64{0}, UseRendering); err == nil {
72+ t.Error("nil device accepted")
73+ }
74+ d := &Device{dev: 1}
75+ for _, tc := range []struct {
76+ w, h int
77+ mods []uint64
78+ }{{0, 8, []uint64{0}}, {8, 0, []uint64{0}}, {8, 8, nil}} {
79+ if _, err := d.Create(tc.w, tc.h, 0, tc.mods, UseRendering); err == nil {
80+ t.Errorf("%dx%d with %d modifiers accepted", tc.w, tc.h, len(tc.mods))
81+ }
82+ }
83+}
84+
85+// Only a linear-only request accepts an implicit layout, as linear.
86+func TestAcceptedModifiers(t *testing.T) {
87+ if got := acceptedModifiers([]uint64{0}); checkModifier(got, modInvalid) != nil || checkModifier(got, 0) != nil || checkModifier(got, 5) == nil {
88+ t.Errorf("linear-only request accepts %#x", got)
89+ }
90+ for _, req := range [][]uint64{{5}, {0, 5}, {modInvalid}} {
91+ if got := acceptedModifiers(req); checkModifier(got, modInvalid) == nil && !slices.Contains(req, modInvalid) {
92+ t.Errorf("request %#x accepts an implicit layout", req)
93+ }
94+ }
95+}
@@ -0,0 +1,95 @@
1+package gbm
2+
3+import (
4+ "os"
5+ "slices"
6+ "strings"
7+ "testing"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// An allocator answer outside the requested list is refused, including an
13+// implicit layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import.
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+ {"invalid", []uint64{tiled, 0}, modInvalid, false},
27+ {"empty list", nil, 0, false},
28+ } {
29+ if err := checkModifier(tc.requested, tc.got); (err == nil) != tc.ok {
30+ t.Errorf("%s: %v", tc.name, err)
31+ }
32+ }
33+}
34+
35+// Planes must be one dmabuf: the renderer imports a single memory object.
36+// Duplicates of one descriptor are the same inode; two files are not.
37+func TestCheckOneObject(t *testing.T) {
38+ f, err := os.CreateTemp(t.TempDir(), "a")
39+ if err != nil {
40+ t.Fatal(err)
41+ }
42+ defer f.Close()
43+ g, err := os.CreateTemp(t.TempDir(), "b")
44+ if err != nil {
45+ t.Fatal(err)
46+ }
47+ defer g.Close()
48+ dup, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
49+ if err != nil {
50+ t.Fatal(err)
51+ }
52+ defer unix.Close(dup)
53+ fd := int(f.Fd())
54+ if err := checkOneObject([]int{fd}); err != nil {
55+ t.Errorf("one plane: %v", err)
56+ }
57+ if err := checkOneObject([]int{fd, dup, fd}); err != nil {
58+ t.Errorf("shared planes: %v", err)
59+ }
60+ err = checkOneObject([]int{fd, int(g.Fd())})
61+ if err == nil || !strings.Contains(err.Error(), "plane 1") {
62+ t.Errorf("disjoint planes: %v", err)
63+ }
64+ if err := checkOneObject([]int{fd, -1}); err == nil {
65+ t.Error("bad descriptor accepted")
66+ }
67+}
68+
69+// A closed device and bad requests are refused without touching libgbm.
70+func TestCreateRejectsBadRequests(t *testing.T) {
71+ if _, err := (*Device)(nil).Create(8, 8, 0, []uint64{0}, UseRendering); err == nil {
72+ t.Error("nil device accepted")
73+ }
74+ d := &Device{dev: 1}
75+ for _, tc := range []struct {
76+ w, h int
77+ mods []uint64
78+ }{{0, 8, []uint64{0}}, {8, 0, []uint64{0}}, {8, 8, nil}} {
79+ if _, err := d.Create(tc.w, tc.h, 0, tc.mods, UseRendering); err == nil {
80+ t.Errorf("%dx%d with %d modifiers accepted", tc.w, tc.h, len(tc.mods))
81+ }
82+ }
83+}
84+
85+// Only a linear-only request accepts an implicit layout, as linear.
86+func TestAcceptedModifiers(t *testing.T) {
87+ if got := acceptedModifiers([]uint64{0}); checkModifier(got, modInvalid) != nil || checkModifier(got, 0) != nil || checkModifier(got, 5) == nil {
88+ t.Errorf("linear-only request accepts %#x", got)
89+ }
90+ for _, req := range [][]uint64{{5}, {0, 5}, {modInvalid}} {
91+ if got := acceptedModifiers(req); checkModifier(got, modInvalid) == nil && !slices.Contains(req, modInvalid) {
92+ t.Errorf("request %#x accepts an implicit layout", req)
93+ }
94+ }
95+}
M internal/adapters/drm/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 +167 -27
@@ -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,30 @@ 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 o.imageAlloc == allocGBM {
116+ // The renderer's own images of this kind may still pass.
117+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
118+ o.dropImages(r)
119+ kind, cause, skipGBM = got, err, true
120+ continue
121+ }
122+ if got == imagesLinear {
123+ o.dropImages(r)
102124 return err
103125 }
104126 if o.imageMod == 0 {
105127 // The refused images were already linear: the later kinds
106128 // would export the same ones again.
107- o.freeImages()
129+ o.dropImages(r)
108130 return err
109131 }
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
132+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocVulkan).Msg("modeset refused the output images")
133+ o.dropImages(r)
134+ kind, cause, skipGBM = got+1, err, false
113135 if kind == imagesSinglePlane && o.planes == 1 {
114136 // The refused images already had one plane: single-plane
115137 // images would be the same ones.
@@ -119,11 +141,15 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
119141 }
120142
121143 // 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) {
144+// as dmabufs: allocated with GBM and imported by the renderer, else
145+// allocated and exported by the renderer, for each kind in turn. skipGBM
146+// leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a
147+// GPU that cannot export a scanout-capable image cannot drive the output.
148+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
125149 err := cause
126- for ; kind <= imagesLinear; kind++ {
150+ for first := true; kind <= imagesLinear; kind++ {
151+ noGBM := first && skipGBM
152+ first = false
127153 if o.runContext != nil && o.runContext.Err() != nil {
128154 return kind, o.runContext.Err()
129155 }
@@ -159,10 +185,17 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
159185 // attempt was the single-plane one.
160186 continue
161187 }
188+ if !noGBM && o.openAlloc != nil {
189+ gerr := o.gbmImages(r, mods, kind == imagesSinglePlane)
190+ if gerr == nil {
191+ return kind, nil
192+ }
193+ o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation")
194+ }
162195 if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {
163196 return kind, nil
164197 }
165- o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
198+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export")
166199 }
167200 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
168201 }
@@ -189,6 +222,7 @@ func (o *Output) primaryModifiers(format uint32) []uint64 {
189222
190223 // exportImages makes the renderer's exported targets the output images.
191224 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
225+ o.dropLive(r)
192226 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
193227 if err != nil {
194228 return err
@@ -207,12 +241,26 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
207241 p.File.Close()
208242 }
209243 }
210- // GPU memory starts undefined (old VRAM contents): clear both images
211- // before the modeset shows one.
244+ if err == nil {
245+ err = o.clearImages(r)
246+ }
247+ if err != nil {
248+ o.dropImages(r)
249+ _, _ = r.ExportTargets(0, nil, false)
250+ return err
251+ }
252+ o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
253+ o.imageAlloc = allocVulkan
254+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocVulkan).Msg("zero-copy output")
255+ return nil
256+}
257+
258+// clearImages draws black into both output images and waits for the GPU.
259+// GPU memory starts undefined (old VRAM contents): both are cleared before
260+// the modeset shows one.
261+func (o *Output) clearImages(r ports.Renderer) error {
262+ var err error
212263 for i := range o.fbs {
213- if err != nil {
214- break
215- }
216264 r.UseTarget(i)
217265 var done *os.File
218266 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
@@ -224,18 +272,105 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
224272 err = errors.Join(err, syncfile.Wait(waitCtx, done))
225273 done.Close()
226274 }
275+ if err != nil {
276+ return err
277+ }
227278 }
228- if err != nil {
229- o.freeImages()
230- _, _ = r.ExportTargets(0, nil, false)
279+ return nil
280+}
281+
282+// gbmImages allocates the output images with GBM, makes them the
283+// renderer's targets and framebuffers, and clears them. The modifiers are
284+// those the plane lists (mods, as for exportImages) that the renderer can
285+// render into and import. On failure nothing is left allocated.
286+func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
287+ o.dropLive(r)
288+ if o.alloc == nil {
289+ if o.allocErr != nil {
290+ return o.allocErr
291+ }
292+ if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil {
293+ o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images")
294+ return o.allocErr
295+ }
296+ }
297+ usable := r.TargetModifiers(mods, singlePlane)
298+ if len(usable) == 0 {
299+ return errors.New("no modifier both the plane and the renderer accept for GBM images")
300+ }
301+ var bufs []ports.DMABuf
302+ imported := false
303+ defer func() {
304+ for _, b := range bufs {
305+ for _, p := range b.Planes {
306+ p.File.Close()
307+ }
308+ }
309+ if err != nil {
310+ // A failed import leaves no renderer targets; a later failure
311+ // leaves ones over the images about to be freed.
312+ if imported {
313+ o.dropImages(r)
314+ } else {
315+ o.freeImages()
316+ }
317+ }
318+ }()
319+ for range o.fbs {
320+ img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable)
321+ if cerr != nil {
322+ return cerr
323+ }
324+ o.images = append(o.images, img)
325+ b, derr := img.dmabuf()
326+ if derr != nil {
327+ return derr
328+ }
329+ bufs = append(bufs, b)
330+ }
331+ if err = r.ImportTargets(bufs); err != nil {
332+ return err
333+ }
334+ imported = true
335+ for i := range bufs {
336+ if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
337+ return fmt.Errorf("output image has %d planes", n)
338+ }
339+ if o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()); err != nil {
340+ return err
341+ }
342+ }
343+ if err = o.clearImages(r); err != nil {
231344 return err
232345 }
233346 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")
347+ o.imageAlloc = allocGBM
348+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocGBM).Msg("zero-copy output")
235349 return nil
236350 }
237351
238-// freeImages removes the output images' framebuffers.
352+// dropLive makes sure no image is left from an earlier setup: setup starts
353+// from nothing, so what is still there is freed first.
354+func (o *Output) dropLive(r ports.Renderer) {
355+ if o.fbs != [2]uint32{} || len(o.images) > 0 {
356+ o.dropImages(r)
357+ }
358+}
359+
360+// dropImages frees the output images with the renderer targets built over
361+// the GBM ones: they would keep their memory (VRAM) alive after the
362+// buffers are gone.
363+func (o *Output) dropImages(r ports.Renderer) {
364+ if len(o.images) > 0 {
365+ _ = r.ImportTargets(nil)
366+ }
367+ o.freeImages()
368+}
369+
370+// freeImages removes the output images' framebuffers and frees the images
371+// GBM allocated. Callers that still have a renderer use dropImages: this
372+// alone leaves the renderer's targets over the freed images (the renderer
373+// drops them when it closes, before Close frees the output's images).
239374 func (o *Output) freeImages() {
240375 o.front.forget()
241376 o.forgetOverlayProbes()
@@ -245,4 +380,9 @@ func (o *Output) freeImages() {
245380 }
246381 o.fbs[i] = 0
247382 }
383+ for _, img := range o.images {
384+ img.close()
385+ }
386+ o.images = nil
387+ o.imageAlloc = ""
248388 }
@@ -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,30 @@ 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 o.imageAlloc == allocGBM {
116+ // The renderer's own images of this kind may still pass.
117+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
118+ o.dropImages(r)
119+ kind, cause, skipGBM = got, err, true
120+ continue
121+ }
122+ if got == imagesLinear {
123+ o.dropImages(r)
102 return err124 return err
103 }125 }
104 if o.imageMod == 0 {126 if o.imageMod == 0 {
105 // The refused images were already linear: the later kinds127 // The refused images were already linear: the later kinds
106 // would export the same ones again.128 // would export the same ones again.
107- o.freeImages()129+ o.dropImages(r)
108 return err130 return err
109 }131 }
110- o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")132+ 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()133+ o.dropImages(r)
112- kind, cause = got+1, err134+ kind, cause, skipGBM = got+1, err, false
113 if kind == imagesSinglePlane && o.planes == 1 {135 if kind == imagesSinglePlane && o.planes == 1 {
114 // The refused images already had one plane: single-plane136 // The refused images already had one plane: single-plane
115 // images would be the same ones.137 // images would be the same ones.
@@ -119,11 +141,15 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
119 }141 }
120 142
121 // setupImages gives the output the two renderer images it flips between,143 // setupImages gives the output the two renderer images it flips between,
122-// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that144+// as dmabufs: allocated with GBM and imported by the renderer, else
123-// cannot export a scanout-capable image cannot drive the output.145+// 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) {146+// leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a
147+// GPU that cannot export a scanout-capable image cannot drive the output.
148+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
125 err := cause149 err := cause
126- for ; kind <= imagesLinear; kind++ {150+ for first := true; kind <= imagesLinear; kind++ {
151+ noGBM := first && skipGBM
152+ first = false
127 if o.runContext != nil && o.runContext.Err() != nil {153 if o.runContext != nil && o.runContext.Err() != nil {
128 return kind, o.runContext.Err()154 return kind, o.runContext.Err()
129 }155 }
@@ -159,10 +185,17 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
159 // attempt was the single-plane one.185 // attempt was the single-plane one.
160 continue186 continue
161 }187 }
188+ if !noGBM && o.openAlloc != nil {
189+ gerr := o.gbmImages(r, mods, kind == imagesSinglePlane)
190+ if gerr == nil {
191+ return kind, nil
192+ }
193+ o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation")
194+ }
162 if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {195 if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {
163 return kind, nil196 return kind, nil
164 }197 }
165- o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")198+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export")
166 }199 }
167 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)200 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
168 }201 }
@@ -189,6 +222,7 @@ func (o *Output) primaryModifiers(format uint32) []uint64 {
189 222
190 // exportImages makes the renderer's exported targets the output images.223 // exportImages makes the renderer's exported targets the output images.
191 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {224 func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
225+ o.dropLive(r)
192 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)226 bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
193 if err != nil {227 if err != nil {
194 return err228 return err
@@ -207,12 +241,26 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
207 p.File.Close()241 p.File.Close()
208 }242 }
209 }243 }
210- // GPU memory starts undefined (old VRAM contents): clear both images244+ if err == nil {
211- // before the modeset shows one.245+ err = o.clearImages(r)
246+ }
247+ if err != nil {
248+ o.dropImages(r)
249+ _, _ = r.ExportTargets(0, nil, false)
250+ return err
251+ }
252+ o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
253+ o.imageAlloc = allocVulkan
254+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocVulkan).Msg("zero-copy output")
255+ return nil
256+}
257+
258+// clearImages draws black into both output images and waits for the GPU.
259+// GPU memory starts undefined (old VRAM contents): both are cleared before
260+// the modeset shows one.
261+func (o *Output) clearImages(r ports.Renderer) error {
262+ var err error
212 for i := range o.fbs {263 for i := range o.fbs {
213- if err != nil {
214- break
215- }
216 r.UseTarget(i)264 r.UseTarget(i)
217 var done *os.File265 var done *os.File
218 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {266 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
@@ -224,18 +272,105 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
224 err = errors.Join(err, syncfile.Wait(waitCtx, done))272 err = errors.Join(err, syncfile.Wait(waitCtx, done))
225 done.Close()273 done.Close()
226 }274 }
275+ if err != nil {
276+ return err
277+ }
227 }278 }
228- if err != nil {279+ return nil
229- o.freeImages()280+}
230- _, _ = r.ExportTargets(0, nil, false)281+
282+// gbmImages allocates the output images with GBM, makes them the
283+// renderer's targets and framebuffers, and clears them. The modifiers are
284+// those the plane lists (mods, as for exportImages) that the renderer can
285+// render into and import. On failure nothing is left allocated.
286+func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
287+ o.dropLive(r)
288+ if o.alloc == nil {
289+ if o.allocErr != nil {
290+ return o.allocErr
291+ }
292+ if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil {
293+ o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images")
294+ return o.allocErr
295+ }
296+ }
297+ usable := r.TargetModifiers(mods, singlePlane)
298+ if len(usable) == 0 {
299+ return errors.New("no modifier both the plane and the renderer accept for GBM images")
300+ }
301+ var bufs []ports.DMABuf
302+ imported := false
303+ defer func() {
304+ for _, b := range bufs {
305+ for _, p := range b.Planes {
306+ p.File.Close()
307+ }
308+ }
309+ if err != nil {
310+ // A failed import leaves no renderer targets; a later failure
311+ // leaves ones over the images about to be freed.
312+ if imported {
313+ o.dropImages(r)
314+ } else {
315+ o.freeImages()
316+ }
317+ }
318+ }()
319+ for range o.fbs {
320+ img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable)
321+ if cerr != nil {
322+ return cerr
323+ }
324+ o.images = append(o.images, img)
325+ b, derr := img.dmabuf()
326+ if derr != nil {
327+ return derr
328+ }
329+ bufs = append(bufs, b)
330+ }
331+ if err = r.ImportTargets(bufs); err != nil {
332+ return err
333+ }
334+ imported = true
335+ for i := range bufs {
336+ if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
337+ return fmt.Errorf("output image has %d planes", n)
338+ }
339+ if o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()); err != nil {
340+ return err
341+ }
342+ }
343+ if err = o.clearImages(r); err != nil {
231 return err344 return err
232 }345 }
233 o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier346 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")347+ o.imageAlloc = allocGBM
348+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocGBM).Msg("zero-copy output")
235 return nil349 return nil
236 }350 }
237 351
238-// freeImages removes the output images' framebuffers.352+// dropLive makes sure no image is left from an earlier setup: setup starts
353+// from nothing, so what is still there is freed first.
354+func (o *Output) dropLive(r ports.Renderer) {
355+ if o.fbs != [2]uint32{} || len(o.images) > 0 {
356+ o.dropImages(r)
357+ }
358+}
359+
360+// dropImages frees the output images with the renderer targets built over
361+// the GBM ones: they would keep their memory (VRAM) alive after the
362+// buffers are gone.
363+func (o *Output) dropImages(r ports.Renderer) {
364+ if len(o.images) > 0 {
365+ _ = r.ImportTargets(nil)
366+ }
367+ o.freeImages()
368+}
369+
370+// freeImages removes the output images' framebuffers and frees the images
371+// GBM allocated. Callers that still have a renderer use dropImages: this
372+// alone leaves the renderer's targets over the freed images (the renderer
373+// drops them when it closes, before Close frees the output's images).
239 func (o *Output) freeImages() {374 func (o *Output) freeImages() {
240 o.front.forget()375 o.front.forget()
241 o.forgetOverlayProbes()376 o.forgetOverlayProbes()
@@ -245,4 +380,9 @@ func (o *Output) freeImages() {
245 }380 }
246 o.fbs[i] = 0381 o.fbs[i] = 0
247 }382 }
383+ for _, img := range o.images {
384+ img.close()
385+ }
386+ o.images = nil
387+ o.imageAlloc = ""
248 }388 }
M internal/adapters/drm/output.go +22 -7
@@ -201,6 +201,18 @@ type Output struct {
201201 planes int
202202 // imageMod is the modifier of the exported images (0: linear).
203203 imageMod uint64
204+ // imageAlloc is who allocated the images (allocGBM, allocVulkan).
205+ imageAlloc string
206+ // openAlloc opens the GBM image allocator, once per output (nil: GBM is
207+ // not used); alloc is it, allocErr why it failed. images are the GBM
208+ // images alive while they are shown.
209+ openAlloc func() (imageAllocator, error)
210+ alloc imageAllocator
211+ allocErr error
212+ images []imageBuffer
213+ // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so
214+ // commits wait for their fences on the CPU instead.
215+ cpuFences bool
204216 // formats receives the direct scanout formats after each modeset;
205217 // sampled and device are what they are built from (Want).
206218 formats chan<- ports.OutputFormats
@@ -222,7 +234,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf
222234 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
223235 log := card.log
224236 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}
237+ o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd, card.log) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}
226238 var err error
227239 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
228240 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -649,6 +661,9 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
649661 if o.protected {
650662 async, vrr, ov, pc = false, false, overlayWin{}, colorUse{}
651663 }
664+ // A client fence (direct scanout, overlay) signalled already
665+ // (clientFenceReady): this waits only for our own render.
666+ o.waitFences(fence)
652667 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) {
653668 async = false
654669 }
@@ -672,9 +687,7 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
672687 }
673688 }
674689 o.contentProps(req)
675- if fence != nil {
676- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
677- }
690+ o.fenceProp(req, o.primary, fence)
678691 // Admission linearizes at this last defensive snapshot. An already
679692 // admitted native ioctl can complete, but precedes the black proof.
680693 o.observeSecurity()
@@ -767,9 +780,7 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState,
767780 if withContent {
768781 o.contentProps(req)
769782 }
770- if fence != nil {
771- req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
772- }
783+ o.fenceProp(req, o.primary, fence)
773784 return o.k.commit(req, atomicTestOnly, 0) == nil
774785 }
775786
@@ -1022,6 +1033,10 @@ func (o *Output) Close() {
10221033 o.cursor.free(o.k)
10231034 }
10241035 o.freeImages()
1036+ if o.alloc != nil {
1037+ o.alloc.close()
1038+ o.alloc = nil
1039+ }
10251040 o.frame.endPending()
10261041 o.dropRead()
10271042 o.shown, o.queued = 0, 0
@@ -201,6 +201,18 @@ type Output struct {
201 planes int201 planes int
202 // imageMod is the modifier of the exported images (0: linear).202 // imageMod is the modifier of the exported images (0: linear).
203 imageMod uint64203 imageMod uint64
204+ // imageAlloc is who allocated the images (allocGBM, allocVulkan).
205+ imageAlloc string
206+ // openAlloc opens the GBM image allocator, once per output (nil: GBM is
207+ // not used); alloc is it, allocErr why it failed. images are the GBM
208+ // images alive while they are shown.
209+ openAlloc func() (imageAllocator, error)
210+ alloc imageAllocator
211+ allocErr error
212+ images []imageBuffer
213+ // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so
214+ // commits wait for their fences on the CPU instead.
215+ cpuFences bool
204 // formats receives the direct scanout formats after each modeset;216 // formats receives the direct scanout formats after each modeset;
205 // sampled and device are what they are built from (Want).217 // sampled and device are what they are built from (Want).
206 formats chan<- ports.OutputFormats218 formats chan<- ports.OutputFormats
@@ -222,7 +234,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) {234 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
223 log := card.log235 log := card.log
224 pipe := slices.Index(card.crtcs, crtc)236 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}237+ o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd, card.log) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}
226 var err error238 var err error
227 if o.saved, err = getCrtc(card.fd, crtc); err != nil {239 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")240 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -649,6 +661,9 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
649 if o.protected {661 if o.protected {
650 async, vrr, ov, pc = false, false, overlayWin{}, colorUse{}662 async, vrr, ov, pc = false, false, overlayWin{}, colorUse{}
651 }663 }
664+ // A client fence (direct scanout, overlay) signalled already
665+ // (clientFenceReady): this waits only for our own render.
666+ o.waitFences(fence)
652 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) {667 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) {
653 async = false668 async = false
654 }669 }
@@ -672,9 +687,7 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool,
672 }687 }
673 }688 }
674 o.contentProps(req)689 o.contentProps(req)
675- if fence != nil {690+ 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 already691 // Admission linearizes at this last defensive snapshot. An already
679 // admitted native ioctl can complete, but precedes the black proof.692 // admitted native ioctl can complete, but precedes the black proof.
680 o.observeSecurity()693 o.observeSecurity()
@@ -767,9 +780,7 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState,
767 if withContent {780 if withContent {
768 o.contentProps(req)781 o.contentProps(req)
769 }782 }
770- if fence != nil {783+ 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) == nil784 return o.k.commit(req, atomicTestOnly, 0) == nil
774 }785 }
775 786
@@ -1022,6 +1033,10 @@ func (o *Output) Close() {
1022 o.cursor.free(o.k)1033 o.cursor.free(o.k)
1023 }1034 }
1024 o.freeImages()1035 o.freeImages()
1036+ if o.alloc != nil {
1037+ o.alloc.close()
1038+ o.alloc = nil
1039+ }
1025 o.frame.endPending()1040 o.frame.endPending()
1026 o.dropRead()1041 o.dropRead()
1027 o.shown, o.queued = 0, 01042 o.shown, o.queued = 0, 0
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/targets.go +182 -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,17 @@ 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+// probeImportModifiers lists the modifiers of format a target can be
218+// imported with: probeModifiers with the importable bit, for images the
219+// display allocator made.
220+func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
221+ return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit)
222+}
223+
224+func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {
155225 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
156226 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
157227 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
@@ -169,7 +239,7 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
169239 }
170240 // SDR targets are composed and read back; HDR targets are color
171241 // 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) })
242+ result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.externalTarget(physical, format, m, feature) })
173243 if r.modPlanes == nil {
174244 r.modPlanes = make(map[uint64]uint32)
175245 }
@@ -216,7 +286,10 @@ func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFea
216286 return result, planes
217287 }
218288
219-func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
289+// externalTarget asks whether a target image of format and modifier with
290+// the target usage supports the external memory feature (exportable: the
291+// renderer allocates it; importable: the display allocator does).
292+func (r *Renderer) externalTarget(physical vk.PhysicalDevice, format vk.Format, modifier uint64, feature vk.ExternalMemoryFeatureFlags) bool {
220293 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
221294 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
222295 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 +298,7 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modi
225298 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
226299 return false
227300 }
228- return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
301+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&feature != 0
229302 }
230303
231304 // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
@@ -248,10 +321,7 @@ func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
248321 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
249322 d := r.dd
250323 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- }
324+ format, fourcc := r.targetFormat()
255325 ok := false
256326 defer func() {
257327 if !ok {
@@ -354,3 +424,107 @@ func (r *Renderer) dropTargets() {
354424 // New targets hold no HDR frame until the next Render.
355425 r.targets, r.current, r.lastHDRTarget = nil, 0, -1
356426 }
427+
428+// importTarget makes a target of an image allocated outside the renderer.
429+// The target is shared with the display like an exported one: frames
430+// acquire it from and release it to the foreign queue family.
431+func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) {
432+ d := r.dd
433+ format, fourcc := r.targetFormat()
434+ n := len(b.Planes)
435+ switch {
436+ case b.Format != fourcc || b.Width != r.width || b.Height != r.height:
437+ return nil, fmt.Errorf("image %dx%d format %#x does not match target %dx%d format %#x", b.Width, b.Height, b.Format, r.width, r.height, fourcc)
438+ case n < 1 || n > maxModifierPlanes:
439+ return nil, fmt.Errorf("image has %d planes", n)
440+ case r.modPlanes[b.Modifier] != uint32(n):
441+ return nil, fmt.Errorf("modifier %#x: image has %d planes, device expects %d", b.Modifier, n, r.modPlanes[b.Modifier])
442+ case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):
443+ return nil, fmt.Errorf("modifier %#x cannot be imported as a target", b.Modifier)
444+ }
445+ if err := sameDMABuf(b.Planes); err != nil {
446+ return nil, err
447+ }
448+ fd, err := dupFile(b.Planes[0].File)
449+ if err != nil {
450+ return nil, err
451+ }
452+ // Vulkan owns the fd it imports on success: hand it its own duplicate.
453+ // Closed here on every path but the successful import.
454+ defer func() {
455+ if fd >= 0 {
456+ _ = unix.Close(fd)
457+ }
458+ }()
459+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
460+ ok := false
461+ defer func() {
462+ if !ok {
463+ r.freeTarget(t)
464+ }
465+ }()
466+ var layouts [maxModifierPlanes]vk.SubresourceLayout
467+ for i, p := range b.Planes {
468+ layouts[i] = vk.SubresourceLayout{Offset: vk.DeviceSize(p.Offset), RowPitch: vk.DeviceSize(p.Stride)}
469+ }
470+ explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: uint32(n), PlaneLayouts: &layouts[0]}
471+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
472+ 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}
473+ if err := checked("vkCreateImage(imported target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
474+ return nil, err
475+ }
476+ fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
477+ if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
478+ return nil, err
479+ }
480+ var req vk.MemoryRequirements
481+ d.GetImageMemoryRequirements(r.device, t.image, &req)
482+ bits := req.MemoryTypeBits & fdProps.MemoryTypeBits
483+ kind, err := r.findMemoryType(bits, vk.MemoryPropertyDeviceLocalBit)
484+ if err != nil {
485+ if kind, err = r.findMemoryType(bits, 0); err != nil {
486+ return nil, err
487+ }
488+ }
489+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
490+ imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(fd)}
491+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
492+ if err := checked("vkAllocateMemory(imported target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
493+ return nil, err
494+ }
495+ fd = -1 // Vulkan owns it now.
496+ if err := checked("vkBindImageMemory(imported target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
497+ return nil, err
498+ }
499+ if t.view, err = r.imageView(t.image, format); err != nil {
500+ return nil, err
501+ }
502+ r.log.Debug().Uint64("modifier", b.Modifier).Int("planes", n).Msg("render target imported")
503+ ok = true
504+ return t, nil
505+}
506+
507+// sameDMABuf fails unless every plane file is the same dmabuf (same device
508+// and inode): the target is imported from the first plane's memory only, so
509+// planes in other buffers would read the wrong memory.
510+func sameDMABuf(planes []ports.DMABufPlane) error {
511+ var first unix.Stat_t
512+ for i, p := range planes {
513+ fd, err := dupFile(p.File)
514+ if err != nil {
515+ return fmt.Errorf("plane %d: %w", i, err)
516+ }
517+ var st unix.Stat_t
518+ err = unix.Fstat(fd, &st)
519+ _ = unix.Close(fd)
520+ if err != nil {
521+ return fmt.Errorf("stat plane %d: %w", i, err)
522+ }
523+ if i == 0 {
524+ first = st
525+ } else if st.Dev != first.Dev || st.Ino != first.Ino {
526+ return fmt.Errorf("plane %d is not in the same dmabuf as plane 0", i)
527+ }
528+ }
529+ return nil
530+}
@@ -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,17 @@ 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+// probeImportModifiers lists the modifiers of format a target can be
218+// imported with: probeModifiers with the importable bit, for images the
219+// display allocator made.
220+func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
221+ return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit)
222+}
223+
224+func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 {
155 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}225 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
156 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}226 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
157 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)227 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
@@ -169,7 +239,7 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format)
169 }239 }
170 // SDR targets are composed and read back; HDR targets are color240 // SDR targets are composed and read back; HDR targets are color
171 // attachments, and transfer sources only with hdrReadback.241 // 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) })242+ result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.externalTarget(physical, format, m, feature) })
173 if r.modPlanes == nil {243 if r.modPlanes == nil {
174 r.modPlanes = make(map[uint64]uint32)244 r.modPlanes = make(map[uint64]uint32)
175 }245 }
@@ -216,7 +286,10 @@ func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFea
216 return result, planes286 return result, planes
217 }287 }
218 288
219-func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {289+// externalTarget asks whether a target image of format and modifier with
290+// the target usage supports the external memory feature (exportable: the
291+// renderer allocates it; importable: the display allocator does).
292+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}293 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}294 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)}295 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 +298,7 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modi
225 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {298 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
226 return false299 return false
227 }300 }
228- return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0301+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&feature != 0
229 }302 }
230 303
231 // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the304 // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
@@ -248,10 +321,7 @@ func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
248 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {321 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
249 d := r.dd322 d := r.dd
250 t := &target{exported: true, layout: vk.ImageLayoutUndefined}323 t := &target{exported: true, layout: vk.ImageLayoutUndefined}
251- format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB)324+ format, fourcc := r.targetFormat()
252- if r.hdrNits > 0 {
253- format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30
254- }
255 ok := false325 ok := false
256 defer func() {326 defer func() {
257 if !ok {327 if !ok {
@@ -354,3 +424,107 @@ func (r *Renderer) dropTargets() {
354 // New targets hold no HDR frame until the next Render.424 // New targets hold no HDR frame until the next Render.
355 r.targets, r.current, r.lastHDRTarget = nil, 0, -1425 r.targets, r.current, r.lastHDRTarget = nil, 0, -1
356 }426 }
427+
428+// importTarget makes a target of an image allocated outside the renderer.
429+// The target is shared with the display like an exported one: frames
430+// acquire it from and release it to the foreign queue family.
431+func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) {
432+ d := r.dd
433+ format, fourcc := r.targetFormat()
434+ n := len(b.Planes)
435+ switch {
436+ case b.Format != fourcc || b.Width != r.width || b.Height != r.height:
437+ return nil, fmt.Errorf("image %dx%d format %#x does not match target %dx%d format %#x", b.Width, b.Height, b.Format, r.width, r.height, fourcc)
438+ case n < 1 || n > maxModifierPlanes:
439+ return nil, fmt.Errorf("image has %d planes", n)
440+ case r.modPlanes[b.Modifier] != uint32(n):
441+ return nil, fmt.Errorf("modifier %#x: image has %d planes, device expects %d", b.Modifier, n, r.modPlanes[b.Modifier])
442+ case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit):
443+ return nil, fmt.Errorf("modifier %#x cannot be imported as a target", b.Modifier)
444+ }
445+ if err := sameDMABuf(b.Planes); err != nil {
446+ return nil, err
447+ }
448+ fd, err := dupFile(b.Planes[0].File)
449+ if err != nil {
450+ return nil, err
451+ }
452+ // Vulkan owns the fd it imports on success: hand it its own duplicate.
453+ // Closed here on every path but the successful import.
454+ defer func() {
455+ if fd >= 0 {
456+ _ = unix.Close(fd)
457+ }
458+ }()
459+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
460+ ok := false
461+ defer func() {
462+ if !ok {
463+ r.freeTarget(t)
464+ }
465+ }()
466+ var layouts [maxModifierPlanes]vk.SubresourceLayout
467+ for i, p := range b.Planes {
468+ layouts[i] = vk.SubresourceLayout{Offset: vk.DeviceSize(p.Offset), RowPitch: vk.DeviceSize(p.Stride)}
469+ }
470+ explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: uint32(n), PlaneLayouts: &layouts[0]}
471+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
472+ 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}
473+ if err := checked("vkCreateImage(imported target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
474+ return nil, err
475+ }
476+ fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
477+ if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
478+ return nil, err
479+ }
480+ var req vk.MemoryRequirements
481+ d.GetImageMemoryRequirements(r.device, t.image, &req)
482+ bits := req.MemoryTypeBits & fdProps.MemoryTypeBits
483+ kind, err := r.findMemoryType(bits, vk.MemoryPropertyDeviceLocalBit)
484+ if err != nil {
485+ if kind, err = r.findMemoryType(bits, 0); err != nil {
486+ return nil, err
487+ }
488+ }
489+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
490+ imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(fd)}
491+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
492+ if err := checked("vkAllocateMemory(imported target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
493+ return nil, err
494+ }
495+ fd = -1 // Vulkan owns it now.
496+ if err := checked("vkBindImageMemory(imported target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
497+ return nil, err
498+ }
499+ if t.view, err = r.imageView(t.image, format); err != nil {
500+ return nil, err
501+ }
502+ r.log.Debug().Uint64("modifier", b.Modifier).Int("planes", n).Msg("render target imported")
503+ ok = true
504+ return t, nil
505+}
506+
507+// sameDMABuf fails unless every plane file is the same dmabuf (same device
508+// and inode): the target is imported from the first plane's memory only, so
509+// planes in other buffers would read the wrong memory.
510+func sameDMABuf(planes []ports.DMABufPlane) error {
511+ var first unix.Stat_t
512+ for i, p := range planes {
513+ fd, err := dupFile(p.File)
514+ if err != nil {
515+ return fmt.Errorf("plane %d: %w", i, err)
516+ }
517+ var st unix.Stat_t
518+ err = unix.Fstat(fd, &st)
519+ _ = unix.Close(fd)
520+ if err != nil {
521+ return fmt.Errorf("stat plane %d: %w", i, err)
522+ }
523+ if i == 0 {
524+ first = st
525+ } else if st.Dev != first.Dev || st.Ino != first.Ino {
526+ return fmt.Errorf("plane %d is not in the same dmabuf as plane 0", i)
527+ }
528+ }
529+ return nil
530+}
M internal/adapters/vulkan/targets_test.go +77 -0
@@ -3,6 +3,7 @@ package vulkan
33 import (
44 "image/color"
55 "math"
6+ "os"
67 "slices"
78 "testing"
89
@@ -367,3 +368,79 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
367368 }
368369 }
369370 }
371+
372+// ImportTargets refuses images that do not fit the renderer (size, format,
373+// plane count, modifier) before touching the driver, and a refusal leaves
374+// no targets: frames go back to the renderer's own image.
375+func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
376+ r, err := New(64, 32)
377+ if err != nil {
378+ t.Skipf("Vulkan unavailable: %v", err)
379+ }
380+ defer r.Close()
381+ mods := r.TargetModifiers(nil, false)
382+ if len(mods) == 0 {
383+ t.Skip("no importable target modifier")
384+ }
385+ pipe := func() *os.File {
386+ f, w, err := os.Pipe()
387+ if err != nil {
388+ t.Fatal(err)
389+ }
390+ w.Close()
391+ t.Cleanup(func() { f.Close() })
392+ return f
393+ }
394+ good := func() ports.DMABuf {
395+ return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: mods[0], Planes: make([]ports.DMABufPlane, r.modPlanes[mods[0]])}
396+ }
397+ for name, mutate := range map[string]func(*ports.DMABuf){
398+ "width": func(b *ports.DMABuf) { b.Width = 65 },
399+ "height": func(b *ports.DMABuf) { b.Height = 31 },
400+ "format": func(b *ports.DMABuf) { b.Format = fourccXR30 },
401+ "planes": func(b *ports.DMABuf) { b.Planes = nil },
402+ "modifier": func(b *ports.DMABuf) { b.Modifier = 0x1234567 },
403+ } {
404+ b := good()
405+ for i := range b.Planes {
406+ b.Planes[i].File = pipe()
407+ }
408+ mutate(&b)
409+ if err := r.ImportTargets([]ports.DMABuf{b}); err == nil {
410+ t.Errorf("%s: mismatched image imported", name)
411+ }
412+ if len(r.targets) != 0 {
413+ t.Errorf("%s: %d targets left", name, len(r.targets))
414+ }
415+ }
416+ // Planes in two different dmabufs: refused whatever the plane count.
417+ if n := r.modPlanes[mods[0]]; n == 1 {
418+ two := good()
419+ two.Planes = []ports.DMABufPlane{{File: pipe()}, {File: pipe()}}
420+ if err := r.ImportTargets([]ports.DMABuf{two}); err == nil {
421+ t.Error("two-plane image imported for a one-plane modifier")
422+ }
423+ }
424+ if err := sameDMABuf([]ports.DMABufPlane{{File: pipe()}, {File: pipe()}}); err == nil {
425+ t.Error("planes in different files accepted")
426+ }
427+ same := pipe()
428+ if err := sameDMABuf([]ports.DMABufPlane{{File: same}, {File: same}}); err != nil {
429+ t.Errorf("planes in one file refused: %v", err)
430+ }
431+ if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
432+ t.Errorf("dropping targets: %v, %d left", err, len(r.targets))
433+ }
434+ // A display list without the renderer's modifiers leaves nothing.
435+ if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
436+ t.Errorf("modifiers outside the display list: %#x", got)
437+ }
438+ if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] {
439+ t.Errorf("display list %#x gave %#x", mods[:1], got)
440+ }
441+ for _, m := range r.TargetModifiers(nil, true) {
442+ if r.modPlanes[m] > 1 {
443+ t.Errorf("single-plane list holds multi-plane modifier %#x", m)
444+ }
445+ }
446+}
@@ -3,6 +3,7 @@ package vulkan
3 import (3 import (
4 "image/color"4 "image/color"
5 "math"5 "math"
6+ "os"
6 "slices"7 "slices"
7 "testing"8 "testing"
8 9
@@ -367,3 +368,79 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
367 }368 }
368 }369 }
369 }370 }
371+
372+// ImportTargets refuses images that do not fit the renderer (size, format,
373+// plane count, modifier) before touching the driver, and a refusal leaves
374+// no targets: frames go back to the renderer's own image.
375+func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
376+ r, err := New(64, 32)
377+ if err != nil {
378+ t.Skipf("Vulkan unavailable: %v", err)
379+ }
380+ defer r.Close()
381+ mods := r.TargetModifiers(nil, false)
382+ if len(mods) == 0 {
383+ t.Skip("no importable target modifier")
384+ }
385+ pipe := func() *os.File {
386+ f, w, err := os.Pipe()
387+ if err != nil {
388+ t.Fatal(err)
389+ }
390+ w.Close()
391+ t.Cleanup(func() { f.Close() })
392+ return f
393+ }
394+ good := func() ports.DMABuf {
395+ return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: mods[0], Planes: make([]ports.DMABufPlane, r.modPlanes[mods[0]])}
396+ }
397+ for name, mutate := range map[string]func(*ports.DMABuf){
398+ "width": func(b *ports.DMABuf) { b.Width = 65 },
399+ "height": func(b *ports.DMABuf) { b.Height = 31 },
400+ "format": func(b *ports.DMABuf) { b.Format = fourccXR30 },
401+ "planes": func(b *ports.DMABuf) { b.Planes = nil },
402+ "modifier": func(b *ports.DMABuf) { b.Modifier = 0x1234567 },
403+ } {
404+ b := good()
405+ for i := range b.Planes {
406+ b.Planes[i].File = pipe()
407+ }
408+ mutate(&b)
409+ if err := r.ImportTargets([]ports.DMABuf{b}); err == nil {
410+ t.Errorf("%s: mismatched image imported", name)
411+ }
412+ if len(r.targets) != 0 {
413+ t.Errorf("%s: %d targets left", name, len(r.targets))
414+ }
415+ }
416+ // Planes in two different dmabufs: refused whatever the plane count.
417+ if n := r.modPlanes[mods[0]]; n == 1 {
418+ two := good()
419+ two.Planes = []ports.DMABufPlane{{File: pipe()}, {File: pipe()}}
420+ if err := r.ImportTargets([]ports.DMABuf{two}); err == nil {
421+ t.Error("two-plane image imported for a one-plane modifier")
422+ }
423+ }
424+ if err := sameDMABuf([]ports.DMABufPlane{{File: pipe()}, {File: pipe()}}); err == nil {
425+ t.Error("planes in different files accepted")
426+ }
427+ same := pipe()
428+ if err := sameDMABuf([]ports.DMABufPlane{{File: same}, {File: same}}); err != nil {
429+ t.Errorf("planes in one file refused: %v", err)
430+ }
431+ if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
432+ t.Errorf("dropping targets: %v, %d left", err, len(r.targets))
433+ }
434+ // A display list without the renderer's modifiers leaves nothing.
435+ if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
436+ t.Errorf("modifiers outside the display list: %#x", got)
437+ }
438+ if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] {
439+ t.Errorf("display list %#x gave %#x", mods[:1], got)
440+ }
441+ for _, m := range r.TargetModifiers(nil, true) {
442+ if r.modPlanes[m] > 1 {
443+ t.Errorf("single-plane list holds multi-plane modifier %#x", m)
444+ }
445+ }
446+}
A internal/app/gbm_targets_test.go +148 -0
@@ -0,0 +1,148 @@
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+// renderNodeFor opens the render node of the GPU the renderer runs on.
16+func renderNodeFor(t *testing.T, dev uint64) *os.File {
17+ t.Helper()
18+ nodes, _ := filepath.Glob("/dev/dri/renderD*")
19+ for _, n := range nodes {
20+ var st unix.Stat_t
21+ if unix.Stat(n, &st) != nil || st.Rdev != dev {
22+ continue
23+ }
24+ f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
25+ if err != nil {
26+ t.Skipf("cannot open %s: %v", n, err)
27+ }
28+ return f
29+ }
30+ t.Skipf("no render node for device %#x", dev)
31+ return nil
32+}
33+
34+// Images allocated by GBM are imported as the renderer's targets (the path
35+// the DRM output takes) and frames drawn into them read back. Skips without
36+// a GPU, libgbm or an openable render node.
37+func TestGBMImagesAreRenderTargets(t *testing.T) {
38+ const w, h = 128, 64
39+ r, err := vulkan.New(w, h)
40+ if err != nil {
41+ t.Skipf("Vulkan unavailable: %v", err)
42+ }
43+ defer r.Close()
44+ sup := r.DMABuf()
45+ if sup.Device == 0 || len(sup.Formats) == 0 {
46+ t.Skip("device does not import dmabufs")
47+ }
48+ mods := r.TargetModifiers(nil, false)
49+ if len(mods) == 0 {
50+ t.Skip("no importable target modifier")
51+ }
52+ t.Logf("renderer target modifiers: %#x", mods)
53+ node := renderNodeFor(t, sup.Device)
54+ defer node.Close()
55+ dev, err := gbm.Open(int(node.Fd()))
56+ if err != nil {
57+ t.Skipf("GBM unavailable: %v", err)
58+ }
59+ defer dev.Close()
60+
61+ // A render node cannot always allocate scanout buffers.
62+ var bufs [2]*gbm.Buffer
63+ for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
64+ var cerr error
65+ for i := range bufs {
66+ if bufs[i], cerr = dev.Create(w, h, 'X'|'R'<<8|'2'<<16|'4'<<24, mods, usage); cerr != nil {
67+ break
68+ }
69+ }
70+ if cerr == nil {
71+ t.Logf("GBM usage %#x", usage)
72+ break
73+ }
74+ t.Logf("GBM usage %#x: %v", usage, cerr)
75+ for i := range bufs {
76+ bufs[i].Close()
77+ bufs[i] = nil
78+ }
79+ }
80+ if bufs[0] == nil || bufs[1] == nil {
81+ t.Skip("GBM allocated no image with the renderer's modifiers")
82+ }
83+ defer bufs[0].Close()
84+ defer bufs[1].Close()
85+
86+ var dmas []ports.DMABuf
87+ for _, b := range bufs {
88+ d, err := b.DMABuf()
89+ if err != nil {
90+ t.Fatal(err)
91+ }
92+ dmas = append(dmas, d)
93+ }
94+ defer func() {
95+ for _, d := range dmas {
96+ for _, p := range d.Planes {
97+ p.File.Close()
98+ }
99+ }
100+ }()
101+ t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride)
102+ if err := r.ImportTargets(dmas); err != nil {
103+ t.Fatalf("ImportTargets: %v", err)
104+ }
105+ t.Log("Vulkan import of the GBM images succeeded")
106+
107+ // Target 0: red background with a blue window; target 1: green.
108+ scenes := []ports.Scene{
109+ {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}},
110+ {Background: "#00ff00"},
111+ }
112+ 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}}}
113+ type px struct {
114+ x, y int
115+ want color.RGBA
116+ }
117+ checks := [][]px{
118+ {{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}}},
119+ {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}},
120+ }
121+ // Twice around, so each image is drawn into again after the other one.
122+ for round := range 2 {
123+ for i := range scenes {
124+ r.UseTarget(i)
125+ s := scenes[i]
126+ s.Seq = uint64(round*2 + i + 1)
127+ done, err := r.Render(s, contents)
128+ if err != nil {
129+ t.Fatalf("render %d: %v", i, err)
130+ }
131+ if done != nil {
132+ done.Close()
133+ }
134+ img := r.Pixels()
135+ if img == nil {
136+ t.Fatalf("target %d: no pixels", i)
137+ }
138+ for _, c := range checks[i] {
139+ if got := img.At(c.x, c.y); got != c.want {
140+ t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want)
141+ }
142+ }
143+ }
144+ }
145+ if err := r.ImportTargets(nil); err != nil {
146+ t.Fatal(err)
147+ }
148+}
@@ -0,0 +1,148 @@
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+// renderNodeFor opens the render node of the GPU the renderer runs on.
16+func renderNodeFor(t *testing.T, dev uint64) *os.File {
17+ t.Helper()
18+ nodes, _ := filepath.Glob("/dev/dri/renderD*")
19+ for _, n := range nodes {
20+ var st unix.Stat_t
21+ if unix.Stat(n, &st) != nil || st.Rdev != dev {
22+ continue
23+ }
24+ f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
25+ if err != nil {
26+ t.Skipf("cannot open %s: %v", n, err)
27+ }
28+ return f
29+ }
30+ t.Skipf("no render node for device %#x", dev)
31+ return nil
32+}
33+
34+// Images allocated by GBM are imported as the renderer's targets (the path
35+// the DRM output takes) and frames drawn into them read back. Skips without
36+// a GPU, libgbm or an openable render node.
37+func TestGBMImagesAreRenderTargets(t *testing.T) {
38+ const w, h = 128, 64
39+ r, err := vulkan.New(w, h)
40+ if err != nil {
41+ t.Skipf("Vulkan unavailable: %v", err)
42+ }
43+ defer r.Close()
44+ sup := r.DMABuf()
45+ if sup.Device == 0 || len(sup.Formats) == 0 {
46+ t.Skip("device does not import dmabufs")
47+ }
48+ mods := r.TargetModifiers(nil, false)
49+ if len(mods) == 0 {
50+ t.Skip("no importable target modifier")
51+ }
52+ t.Logf("renderer target modifiers: %#x", mods)
53+ node := renderNodeFor(t, sup.Device)
54+ defer node.Close()
55+ dev, err := gbm.Open(int(node.Fd()))
56+ if err != nil {
57+ t.Skipf("GBM unavailable: %v", err)
58+ }
59+ defer dev.Close()
60+
61+ // A render node cannot always allocate scanout buffers.
62+ var bufs [2]*gbm.Buffer
63+ for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
64+ var cerr error
65+ for i := range bufs {
66+ if bufs[i], cerr = dev.Create(w, h, 'X'|'R'<<8|'2'<<16|'4'<<24, mods, usage); cerr != nil {
67+ break
68+ }
69+ }
70+ if cerr == nil {
71+ t.Logf("GBM usage %#x", usage)
72+ break
73+ }
74+ t.Logf("GBM usage %#x: %v", usage, cerr)
75+ for i := range bufs {
76+ bufs[i].Close()
77+ bufs[i] = nil
78+ }
79+ }
80+ if bufs[0] == nil || bufs[1] == nil {
81+ t.Skip("GBM allocated no image with the renderer's modifiers")
82+ }
83+ defer bufs[0].Close()
84+ defer bufs[1].Close()
85+
86+ var dmas []ports.DMABuf
87+ for _, b := range bufs {
88+ d, err := b.DMABuf()
89+ if err != nil {
90+ t.Fatal(err)
91+ }
92+ dmas = append(dmas, d)
93+ }
94+ defer func() {
95+ for _, d := range dmas {
96+ for _, p := range d.Planes {
97+ p.File.Close()
98+ }
99+ }
100+ }()
101+ t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride)
102+ if err := r.ImportTargets(dmas); err != nil {
103+ t.Fatalf("ImportTargets: %v", err)
104+ }
105+ t.Log("Vulkan import of the GBM images succeeded")
106+
107+ // Target 0: red background with a blue window; target 1: green.
108+ scenes := []ports.Scene{
109+ {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}},
110+ {Background: "#00ff00"},
111+ }
112+ 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}}}
113+ type px struct {
114+ x, y int
115+ want color.RGBA
116+ }
117+ checks := [][]px{
118+ {{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}}},
119+ {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}},
120+ }
121+ // Twice around, so each image is drawn into again after the other one.
122+ for round := range 2 {
123+ for i := range scenes {
124+ r.UseTarget(i)
125+ s := scenes[i]
126+ s.Seq = uint64(round*2 + i + 1)
127+ done, err := r.Render(s, contents)
128+ if err != nil {
129+ t.Fatalf("render %d: %v", i, err)
130+ }
131+ if done != nil {
132+ done.Close()
133+ }
134+ img := r.Pixels()
135+ if img == nil {
136+ t.Fatalf("target %d: no pixels", i)
137+ }
138+ for _, c := range checks[i] {
139+ if got := img.At(c.x, c.y); got != c.want {
140+ t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want)
141+ }
142+ }
143+ }
144+ }
145+ if err := r.ImportTargets(nil); err != nil {
146+ t.Fatal(err)
147+ }
148+}
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 +19 -2
@@ -36,8 +36,9 @@ 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).
4344 // The DRM output calls this before ExportTargets.
@@ -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 is
66+ // one memory object (every plane file is the same dmabuf, the planes
67+ // differing by offset; disjoint planes are refused) of the renderer's
68+ // size and target format, with a modifier from TargetModifiers. The
69+ // caller keeps its files and must
70+ // keep the memory alive while the targets are in use. An error leaves
71+ // the renderer without targets.
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,8 +36,9 @@ 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.
@@ -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 is
66+ // one memory object (every plane file is the same dmabuf, the planes
67+ // differing by offset; disjoint planes are refused) of the renderer's
68+ // size and target format, with a modifier from TargetModifiers. The
69+ // caller keeps its files and must
70+ // keep the memory alive while the targets are in use. An error leaves
71+ // the renderer without targets.
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 23

Tags 10

↑↓ moveenter openesc close