Merge branch 'fix/gbm-scanout-images' into main (!24)
fix(drm): allocate output images with GBM and import them into Vulkan
30 files · +2354 -69
M .mockery.yml +10 -0
| @@ -34,6 +34,16 @@ packages: | |||
| 34 | 34 | dir: internal/adapters/drm | |
| 35 | 35 | pkgname: drm | |
| 36 | 36 | 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 | 47 | github.com/bnema/neferwl/internal/adapters/seat: | |
| 38 | 48 | interfaces: | |
| 39 | 49 | disabler: | |
| @@ -34,6 +34,16 @@ packages: | |||||
| 34 | dir: internal/adapters/drm | 34 | dir: internal/adapters/drm | ||
| 35 | pkgname: drm | 35 | pkgname: drm | ||
| 36 | filename: kms_mock_test.go | 36 | 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 | |||
| 20 | 20 | ||
| 21 | 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 | 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 | 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 | 25 | - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate. | |
| 26 | 26 | ||
| 27 | + | ### Output images | |
| 28 | + | ||
| 29 | + | A DRM output flips between two images the renderer draws into, with no CPU copy. | |
| 30 | + | ||
| 31 | + | - **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. GBM is the display driver's own allocator, so the layouts it picks scan out correctly on every driver. | |
| 32 | + | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23). | |
| 33 | + | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable. | |
| 34 | + | ||
| 27 | 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 | 37 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | |
| @@ -20,10 +20,18 @@ About 35 channels connect them, all created in one place, so the full data flow | |||||
| 20 | 20 | ||||
| 21 | - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating. | 21 | - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating. | ||
| 22 | - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](../performance.md). | 22 | - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](../performance.md). | ||
| 23 | - | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary. | 23 | + | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan, libinput and GBM are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary. |
| 24 | - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing. | 24 | - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing. | ||
| 25 | - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate. | 25 | - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate. | ||
| 26 | 26 | ||||
| 27 | + | ### Output images | |||
| 28 | + | ||||
| 29 | + | A DRM output flips between two images the renderer draws into, with no CPU copy. | |||
| 30 | + | ||||
| 31 | + | - **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. GBM is the display driver's own allocator, so the layouts it picks scan out correctly on every driver. | |||
| 32 | + | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23). | |||
| 33 | + | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable. | |||
| 34 | + | ||||
| 27 | I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | 35 | I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | ||
| 28 | 36 | ||||
| 29 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | 37 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | ||
A internal/adapters/drm/alloc.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | |
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // imageAllocator makes the output images with the display driver's own | |
| 9 | + | // allocator (GBM), which knows the layouts the driver scans out correctly: | |
| 10 | + | // images allocated by the renderer show corrupted on NVIDIA. One per | |
| 11 | + | // output, used by its goroutine only. | |
| 12 | + | type imageAllocator interface { | |
| 13 | + | // create allocates an image whose modifier is one of modifiers. | |
| 14 | + | create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) | |
| 15 | + | close() | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // imageBuffer is one allocated image, kept alive while it is shown. | |
| 19 | + | type imageBuffer interface { | |
| 20 | + | // dmabuf returns the image with fresh descriptors the caller closes. | |
| 21 | + | dmabuf() (ports.DMABuf, error) | |
| 22 | + | close() | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // gbmAllocator allocates scanout-capable, renderable images with libgbm. | |
| 26 | + | type gbmAllocator struct{ dev *gbm.Device } | |
| 27 | + | ||
| 28 | + | // openGBM opens a GBM allocator on a duplicate of the card fd. | |
| 29 | + | func openGBM(fd int) (imageAllocator, error) { | |
| 30 | + | dev, err := gbm.Open(fd) | |
| 31 | + | if err != nil { | |
| 32 | + | return nil, err | |
| 33 | + | } | |
| 34 | + | return gbmAllocator{dev}, nil | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | |
| 38 | + | b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering) | |
| 39 | + | if err != nil { | |
| 40 | + | return nil, err | |
| 41 | + | } | |
| 42 | + | return gbmImage{b}, nil | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (a gbmAllocator) close() { a.dev.Close() } | |
| 46 | + | ||
| 47 | + | type gbmImage struct{ b *gbm.Buffer } | |
| 48 | + | ||
| 49 | + | func (i gbmImage) dmabuf() (ports.DMABuf, error) { return i.b.DMABuf() } | |
| 50 | + | func (i gbmImage) close() { i.b.Close() } | |
| @@ -0,0 +1,50 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | |||
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // imageAllocator makes the output images with the display driver's own | |||
| 9 | + | // allocator (GBM), which knows the layouts the driver scans out correctly: | |||
| 10 | + | // images allocated by the renderer show corrupted on NVIDIA. One per | |||
| 11 | + | // output, used by its goroutine only. | |||
| 12 | + | type imageAllocator interface { | |||
| 13 | + | // create allocates an image whose modifier is one of modifiers. | |||
| 14 | + | create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) | |||
| 15 | + | close() | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // imageBuffer is one allocated image, kept alive while it is shown. | |||
| 19 | + | type imageBuffer interface { | |||
| 20 | + | // dmabuf returns the image with fresh descriptors the caller closes. | |||
| 21 | + | dmabuf() (ports.DMABuf, error) | |||
| 22 | + | close() | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // gbmAllocator allocates scanout-capable, renderable images with libgbm. | |||
| 26 | + | type gbmAllocator struct{ dev *gbm.Device } | |||
| 27 | + | ||||
| 28 | + | // openGBM opens a GBM allocator on a duplicate of the card fd. | |||
| 29 | + | func openGBM(fd int) (imageAllocator, error) { | |||
| 30 | + | dev, err := gbm.Open(fd) | |||
| 31 | + | if err != nil { | |||
| 32 | + | return nil, err | |||
| 33 | + | } | |||
| 34 | + | return gbmAllocator{dev}, nil | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | |||
| 38 | + | b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering) | |||
| 39 | + | if err != nil { | |||
| 40 | + | return nil, err | |||
| 41 | + | } | |||
| 42 | + | return gbmImage{b}, nil | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (a gbmAllocator) close() { a.dev.Close() } | |||
| 46 | + | ||||
| 47 | + | type gbmImage struct{ b *gbm.Buffer } | |||
| 48 | + | ||||
| 49 | + | func (i gbmImage) dmabuf() (ports.DMABuf, error) { return i.b.DMABuf() } | |||
| 50 | + | func (i gbmImage) close() { i.b.Close() } | |||
A internal/adapters/drm/alloc_test.go +234 -0
| @@ -0,0 +1,234 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "os" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/stretchr/testify/mock" | |
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // gbmBuf is a dmabuf over a closed pipe, as an image buffer hands out. | |
| 15 | + | func gbmBuf(planes int) ports.DMABuf { | |
| 16 | + | b := ports.DMABuf{Modifier: tiledMod} | |
| 17 | + | for range planes { | |
| 18 | + | f, w, _ := os.Pipe() | |
| 19 | + | w.Close() | |
| 20 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: f}) | |
| 21 | + | } | |
| 22 | + | return b | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // twoGBMImages makes an allocator that hands out two images of the given | |
| 26 | + | // plane count; each image closes exactly once, counted in closed. | |
| 27 | + | func twoGBMImages(t *testing.T, planes int) (a imageAllocator, closed *int) { | |
| 28 | + | m := newMockimageAllocator(t) | |
| 29 | + | closed = new(int) | |
| 30 | + | m.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) { | |
| 31 | + | img := newMockimageBuffer(t) | |
| 32 | + | img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil }) | |
| 33 | + | img.EXPECT().close().Run(func() { *closed++ }).Once() | |
| 34 | + | return img, nil | |
| 35 | + | }).Twice() | |
| 36 | + | m.EXPECT().close().Return().Maybe() | |
| 37 | + | return m, closed | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | |
| 41 | + | r := portsmocks.NewMockRenderer(t) | |
| 42 | + | r.EXPECT().SetHDR(float64(0)).Return().Maybe() | |
| 43 | + | r.EXPECT().UseTarget(mock.Anything).Return().Maybe() | |
| 44 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe() | |
| 45 | + | return r | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // GBM images come first: the renderer imports them, the framebuffers are | |
| 49 | + | // made from them, and no image is exported. | |
| 50 | + | func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | |
| 51 | + | o, k, _ := testOutput(t) | |
| 52 | + | o.cursor = nil | |
| 53 | + | a, closed := twoGBMImages(t, 1) | |
| 54 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 55 | + | r := gbmRenderer(t) | |
| 56 | + | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | |
| 57 | + | r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 && b[0].Modifier == tiledMod })).Return(nil).Once() | |
| 58 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |
| 59 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 60 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 61 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 62 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 63 | + | t.Fatal(err) | |
| 64 | + | } | |
| 65 | + | if o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 { | |
| 66 | + | t.Fatalf("kind %d modifier %#x images %d closed %d", o.kind, o.imageMod, len(o.images), *closed) | |
| 67 | + | } | |
| 68 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 69 | + | o.dropImages(r) | |
| 70 | + | if *closed != 2 || o.images != nil || o.fbs != [2]uint32{} { | |
| 71 | + | t.Fatalf("closed %d images, %d left, fbs %v", *closed, len(o.images), o.fbs) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // A GBM failure at any step falls back to the renderer's own images of the same kind. | |
| 76 | + | func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | |
| 77 | + | for name, tc := range map[string]struct { | |
| 78 | + | setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) | |
| 79 | + | }{ | |
| 80 | + | "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) { | |
| 81 | + | o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") } | |
| 82 | + | }}, | |
| 83 | + | "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 84 | + | a := newMockimageAllocator(t) | |
| 85 | + | a.EXPECT().close().Return().Maybe() | |
| 86 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 87 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once() | |
| 88 | + | }}, | |
| 89 | + | "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 90 | + | a := newMockimageAllocator(t) | |
| 91 | + | a.EXPECT().close().Return().Maybe() | |
| 92 | + | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once() | |
| 93 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 94 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 95 | + | }}, | |
| 96 | + | "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 97 | + | a := newMockimageAllocator(t) | |
| 98 | + | a.EXPECT().close().Return().Maybe() | |
| 99 | + | img := newMockimageBuffer(t) | |
| 100 | + | img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once() | |
| 101 | + | img.EXPECT().close().Return().Once() // freed with the failed attempt | |
| 102 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 103 | + | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once() | |
| 104 | + | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once() | |
| 105 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 106 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 107 | + | }}, | |
| 108 | + | "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 109 | + | a, _ := twoGBMImages(t, 1) | |
| 110 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 111 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 112 | + | r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once() | |
| 113 | + | // The images made before the refusal go with a renderer drop. | |
| 114 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 115 | + | }}, | |
| 116 | + | "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 117 | + | a, _ := twoGBMImages(t, 1) | |
| 118 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 119 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 120 | + | r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once() | |
| 121 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 122 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once() | |
| 123 | + | }}, | |
| 124 | + | } { | |
| 125 | + | t.Run(name, func(t *testing.T) { | |
| 126 | + | o, k, _ := testOutput(t) | |
| 127 | + | o.cursor = nil | |
| 128 | + | r := gbmRenderer(t) | |
| 129 | + | tc.setup(t, o, k, r) | |
| 130 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |
| 131 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe() | |
| 132 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 133 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 134 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 135 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 136 | + | t.Fatal(err) | |
| 137 | + | } | |
| 138 | + | // The renderer's own images: no GBM image is alive. | |
| 139 | + | if o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 { | |
| 140 | + | t.Fatalf("kind %d images %d fbs %v", o.kind, len(o.images), o.fbs) | |
| 141 | + | } | |
| 142 | + | }) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // A failed GBM open is remembered: the next kind and the next setup do not | |
| 147 | + | // open the library again. | |
| 148 | + | func TestGBMOpenFailureIsRemembered(t *testing.T) { | |
| 149 | + | o, k, _ := testOutput(t, unix.EINVAL) | |
| 150 | + | o.cursor = nil | |
| 151 | + | opens := 0 | |
| 152 | + | o.openAlloc = func() (imageAllocator, error) { opens++; return nil, errors.New("no libgbm") } | |
| 153 | + | r := gbmRenderer(t) | |
| 154 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(2), gbmBuf(2)}, nil).Once() | |
| 155 | + | r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |
| 156 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |
| 157 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 158 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 159 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 160 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 161 | + | t.Fatal(err) | |
| 162 | + | } | |
| 163 | + | if opens != 1 || o.kind != imagesSinglePlane { | |
| 164 | + | t.Fatalf("%d opens, kind %d", opens, o.kind) | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // A modeset test that refuses GBM images tries the renderer's images of the | |
| 169 | + | // same kind before the next kind. | |
| 170 | + | func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | |
| 171 | + | o, k, commits := testOutput(t, unix.EINVAL) | |
| 172 | + | o.cursor = nil | |
| 173 | + | a, closed := twoGBMImages(t, 1) | |
| 174 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 175 | + | r := gbmRenderer(t) | |
| 176 | + | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | |
| 177 | + | r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 })).Return(nil).Once() | |
| 178 | + | // The refused GBM images leave no renderer target behind. | |
| 179 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 180 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |
| 181 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |
| 182 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 183 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 184 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 185 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 186 | + | t.Fatal(err) | |
| 187 | + | } | |
| 188 | + | if o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 { | |
| 189 | + | t.Fatalf("kind %d closed %d images %d commits %d", o.kind, *closed, len(o.images), len(*commits)) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // Closing the output frees the GBM images and closes the allocator once. | |
| 194 | + | func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) { | |
| 195 | + | o, k, _ := testOutput(t) | |
| 196 | + | o.cursor = nil | |
| 197 | + | img := newMockimageBuffer(t) | |
| 198 | + | img.EXPECT().close().Return().Twice() | |
| 199 | + | alloc := newMockimageAllocator(t) | |
| 200 | + | alloc.EXPECT().close().Return().Once() | |
| 201 | + | o.images = []imageBuffer{img, img} | |
| 202 | + | o.alloc = alloc | |
| 203 | + | o.fbs = [2]uint32{70, 71} | |
| 204 | + | k.EXPECT().rmFB(uint32(70)).Return(nil).Once() | |
| 205 | + | k.EXPECT().rmFB(uint32(71)).Return(nil).Once() | |
| 206 | + | o.Close() | |
| 207 | + | if o.images != nil || o.alloc != nil || o.fbs != [2]uint32{} { | |
| 208 | + | t.Fatalf("images %v alloc %v fbs %v", o.images, o.alloc, o.fbs) | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | // A setup that finds images left over frees them first, with the renderer's | |
| 213 | + | // targets over GBM ones: the renderer never keeps a target over a freed | |
| 214 | + | // image. | |
| 215 | + | func TestSetupStartsFromCleanState(t *testing.T) { | |
| 216 | + | o, k, _ := testOutput(t) | |
| 217 | + | o.cursor = nil | |
| 218 | + | img := newMockimageBuffer(t) | |
| 219 | + | img.EXPECT().close().Return().Once() | |
| 220 | + | o.images = []imageBuffer{img} | |
| 221 | + | o.fbs = [2]uint32{70, 71} | |
| 222 | + | k.EXPECT().rmFB(uint32(70)).Return(nil).Once() | |
| 223 | + | k.EXPECT().rmFB(uint32(71)).Return(nil).Once() | |
| 224 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(80, nil) | |
| 225 | + | r := gbmRenderer(t) | |
| 226 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 227 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |
| 228 | + | if err := o.exportImages(r, nil, false); err != nil { | |
| 229 | + | t.Fatal(err) | |
| 230 | + | } | |
| 231 | + | if o.images != nil || o.fbs != [2]uint32{80, 80} { | |
| 232 | + | t.Fatalf("images %v fbs %v", o.images, o.fbs) | |
| 233 | + | } | |
| 234 | + | } | |
| @@ -0,0 +1,234 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "os" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/stretchr/testify/mock" | |||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // gbmBuf is a dmabuf over a closed pipe, as an image buffer hands out. | |||
| 15 | + | func gbmBuf(planes int) ports.DMABuf { | |||
| 16 | + | b := ports.DMABuf{Modifier: tiledMod} | |||
| 17 | + | for range planes { | |||
| 18 | + | f, w, _ := os.Pipe() | |||
| 19 | + | w.Close() | |||
| 20 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: f}) | |||
| 21 | + | } | |||
| 22 | + | return b | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // twoGBMImages makes an allocator that hands out two images of the given | |||
| 26 | + | // plane count; each image closes exactly once, counted in closed. | |||
| 27 | + | func twoGBMImages(t *testing.T, planes int) (a imageAllocator, closed *int) { | |||
| 28 | + | m := newMockimageAllocator(t) | |||
| 29 | + | closed = new(int) | |||
| 30 | + | m.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) { | |||
| 31 | + | img := newMockimageBuffer(t) | |||
| 32 | + | img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil }) | |||
| 33 | + | img.EXPECT().close().Run(func() { *closed++ }).Once() | |||
| 34 | + | return img, nil | |||
| 35 | + | }).Twice() | |||
| 36 | + | m.EXPECT().close().Return().Maybe() | |||
| 37 | + | return m, closed | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | |||
| 41 | + | r := portsmocks.NewMockRenderer(t) | |||
| 42 | + | r.EXPECT().SetHDR(float64(0)).Return().Maybe() | |||
| 43 | + | r.EXPECT().UseTarget(mock.Anything).Return().Maybe() | |||
| 44 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe() | |||
| 45 | + | return r | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // GBM images come first: the renderer imports them, the framebuffers are | |||
| 49 | + | // made from them, and no image is exported. | |||
| 50 | + | func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | |||
| 51 | + | o, k, _ := testOutput(t) | |||
| 52 | + | o.cursor = nil | |||
| 53 | + | a, closed := twoGBMImages(t, 1) | |||
| 54 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 55 | + | r := gbmRenderer(t) | |||
| 56 | + | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | |||
| 57 | + | r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 && b[0].Modifier == tiledMod })).Return(nil).Once() | |||
| 58 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |||
| 59 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 60 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 61 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 62 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | if o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 { | |||
| 66 | + | t.Fatalf("kind %d modifier %#x images %d closed %d", o.kind, o.imageMod, len(o.images), *closed) | |||
| 67 | + | } | |||
| 68 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 69 | + | o.dropImages(r) | |||
| 70 | + | if *closed != 2 || o.images != nil || o.fbs != [2]uint32{} { | |||
| 71 | + | t.Fatalf("closed %d images, %d left, fbs %v", *closed, len(o.images), o.fbs) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // A GBM failure at any step falls back to the renderer's own images of the same kind. | |||
| 76 | + | func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | |||
| 77 | + | for name, tc := range map[string]struct { | |||
| 78 | + | setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) | |||
| 79 | + | }{ | |||
| 80 | + | "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) { | |||
| 81 | + | o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") } | |||
| 82 | + | }}, | |||
| 83 | + | "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |||
| 84 | + | a := newMockimageAllocator(t) | |||
| 85 | + | a.EXPECT().close().Return().Maybe() | |||
| 86 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 87 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once() | |||
| 88 | + | }}, | |||
| 89 | + | "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |||
| 90 | + | a := newMockimageAllocator(t) | |||
| 91 | + | a.EXPECT().close().Return().Maybe() | |||
| 92 | + | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once() | |||
| 93 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 94 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |||
| 95 | + | }}, | |||
| 96 | + | "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |||
| 97 | + | a := newMockimageAllocator(t) | |||
| 98 | + | a.EXPECT().close().Return().Maybe() | |||
| 99 | + | img := newMockimageBuffer(t) | |||
| 100 | + | img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once() | |||
| 101 | + | img.EXPECT().close().Return().Once() // freed with the failed attempt | |||
| 102 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 103 | + | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once() | |||
| 104 | + | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once() | |||
| 105 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 106 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |||
| 107 | + | }}, | |||
| 108 | + | "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |||
| 109 | + | a, _ := twoGBMImages(t, 1) | |||
| 110 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 111 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |||
| 112 | + | r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once() | |||
| 113 | + | // The images made before the refusal go with a renderer drop. | |||
| 114 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 115 | + | }}, | |||
| 116 | + | "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |||
| 117 | + | a, _ := twoGBMImages(t, 1) | |||
| 118 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 119 | + | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |||
| 120 | + | r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once() | |||
| 121 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 122 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once() | |||
| 123 | + | }}, | |||
| 124 | + | } { | |||
| 125 | + | t.Run(name, func(t *testing.T) { | |||
| 126 | + | o, k, _ := testOutput(t) | |||
| 127 | + | o.cursor = nil | |||
| 128 | + | r := gbmRenderer(t) | |||
| 129 | + | tc.setup(t, o, k, r) | |||
| 130 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |||
| 131 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe() | |||
| 132 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 133 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 134 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 135 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 136 | + | t.Fatal(err) | |||
| 137 | + | } | |||
| 138 | + | // The renderer's own images: no GBM image is alive. | |||
| 139 | + | if o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 { | |||
| 140 | + | t.Fatalf("kind %d images %d fbs %v", o.kind, len(o.images), o.fbs) | |||
| 141 | + | } | |||
| 142 | + | }) | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | // A failed GBM open is remembered: the next kind and the next setup do not | |||
| 147 | + | // open the library again. | |||
| 148 | + | func TestGBMOpenFailureIsRemembered(t *testing.T) { | |||
| 149 | + | o, k, _ := testOutput(t, unix.EINVAL) | |||
| 150 | + | o.cursor = nil | |||
| 151 | + | opens := 0 | |||
| 152 | + | o.openAlloc = func() (imageAllocator, error) { opens++; return nil, errors.New("no libgbm") } | |||
| 153 | + | r := gbmRenderer(t) | |||
| 154 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(2), gbmBuf(2)}, nil).Once() | |||
| 155 | + | r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |||
| 156 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |||
| 157 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 158 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 159 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 160 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 161 | + | t.Fatal(err) | |||
| 162 | + | } | |||
| 163 | + | if opens != 1 || o.kind != imagesSinglePlane { | |||
| 164 | + | t.Fatalf("%d opens, kind %d", opens, o.kind) | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // A modeset test that refuses GBM images tries the renderer's images of the | |||
| 169 | + | // same kind before the next kind. | |||
| 170 | + | func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | |||
| 171 | + | o, k, commits := testOutput(t, unix.EINVAL) | |||
| 172 | + | o.cursor = nil | |||
| 173 | + | a, closed := twoGBMImages(t, 1) | |||
| 174 | + | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |||
| 175 | + | r := gbmRenderer(t) | |||
| 176 | + | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | |||
| 177 | + | r.EXPECT().ImportTargets(mock.MatchedBy(func(b []ports.DMABuf) bool { return len(b) == 2 })).Return(nil).Once() | |||
| 178 | + | // The refused GBM images leave no renderer target behind. | |||
| 179 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 180 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |||
| 181 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil) | |||
| 182 | + | k.EXPECT().rmFB(mock.Anything).Return(nil) | |||
| 183 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 184 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 185 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 186 | + | t.Fatal(err) | |||
| 187 | + | } | |||
| 188 | + | if o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 { | |||
| 189 | + | t.Fatalf("kind %d closed %d images %d commits %d", o.kind, *closed, len(o.images), len(*commits)) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | // Closing the output frees the GBM images and closes the allocator once. | |||
| 194 | + | func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) { | |||
| 195 | + | o, k, _ := testOutput(t) | |||
| 196 | + | o.cursor = nil | |||
| 197 | + | img := newMockimageBuffer(t) | |||
| 198 | + | img.EXPECT().close().Return().Twice() | |||
| 199 | + | alloc := newMockimageAllocator(t) | |||
| 200 | + | alloc.EXPECT().close().Return().Once() | |||
| 201 | + | o.images = []imageBuffer{img, img} | |||
| 202 | + | o.alloc = alloc | |||
| 203 | + | o.fbs = [2]uint32{70, 71} | |||
| 204 | + | k.EXPECT().rmFB(uint32(70)).Return(nil).Once() | |||
| 205 | + | k.EXPECT().rmFB(uint32(71)).Return(nil).Once() | |||
| 206 | + | o.Close() | |||
| 207 | + | if o.images != nil || o.alloc != nil || o.fbs != [2]uint32{} { | |||
| 208 | + | t.Fatalf("images %v alloc %v fbs %v", o.images, o.alloc, o.fbs) | |||
| 209 | + | } | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | // A setup that finds images left over frees them first, with the renderer's | |||
| 213 | + | // targets over GBM ones: the renderer never keeps a target over a freed | |||
| 214 | + | // image. | |||
| 215 | + | func TestSetupStartsFromCleanState(t *testing.T) { | |||
| 216 | + | o, k, _ := testOutput(t) | |||
| 217 | + | o.cursor = nil | |||
| 218 | + | img := newMockimageBuffer(t) | |||
| 219 | + | img.EXPECT().close().Return().Once() | |||
| 220 | + | o.images = []imageBuffer{img} | |||
| 221 | + | o.fbs = [2]uint32{70, 71} | |||
| 222 | + | k.EXPECT().rmFB(uint32(70)).Return(nil).Once() | |||
| 223 | + | k.EXPECT().rmFB(uint32(71)).Return(nil).Once() | |||
| 224 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(80, nil) | |||
| 225 | + | r := gbmRenderer(t) | |||
| 226 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 227 | + | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |||
| 228 | + | if err := o.exportImages(r, nil, false); err != nil { | |||
| 229 | + | t.Fatal(err) | |||
| 230 | + | } | |||
| 231 | + | if o.images != nil || o.fbs != [2]uint32{80, 80} { | |||
| 232 | + | t.Fatalf("images %v fbs %v", o.images, o.fbs) | |||
| 233 | + | } | |||
| 234 | + | } | |||
M internal/adapters/drm/amdgpu_rules_test.go +1 -0
| @@ -407,6 +407,7 @@ func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T) | |||
| 407 | 407 | } | |
| 408 | 408 | return nil | |
| 409 | 409 | }) | |
| 410 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 410 | 411 | o.cursor = nil | |
| 411 | 412 | var state atomic.Uint64 | |
| 412 | 413 | state.Store(3) | |
| @@ -407,6 +407,7 @@ func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T) | |||||
| 407 | } | 407 | } | ||
| 408 | return nil | 408 | return nil | ||
| 409 | }) | 409 | }) | ||
| 410 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |||
| 410 | o.cursor = nil | 411 | o.cursor = nil | ||
| 411 | var state atomic.Uint64 | 412 | 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 { | |||
| 38 | 38 | taken map[uint32]bool | |
| 39 | 39 | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | |
| 40 | 40 | 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 | 44 | // serials numbers the commits of every output (event userData). | |
| 42 | 45 | serials atomic.Uint64 | |
| 43 | 46 | // 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 | 68 | flips[c] = make(chan flipEvent, 4) | |
| 66 | 69 | } | |
| 67 | 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)}, 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 | |
| 69 | 72 | } | |
| 70 | 73 | ||
| 71 | 74 | // Path is the device path, e.g. /dev/dri/card1. | |
| @@ -38,6 +38,9 @@ type Card struct { | |||||
| 38 | taken map[uint32]bool | 38 | taken map[uint32]bool | ||
| 39 | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | 39 | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | ||
| 40 | async bool | 40 | 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.Uint64 | 45 | serials atomic.Uint64 | ||
| 43 | // formats receives each output's direct scanout formats (nil: none | 46 | // 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)}, 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 |
| 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) { | |||
| 284 | 284 | // single-plane attempt KMS would refuse. | |
| 285 | 285 | func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) { | |
| 286 | 286 | o, k, commits := testOutput(t) | |
| 287 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 287 | 288 | o.cursor = nil | |
| 288 | 289 | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}} | |
| 289 | 290 | 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 = nil | 288 | o.cursor = nil | ||
| 288 | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}} | 289 | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}} | ||
| 289 | r := portsmocks.NewMockRenderer(t) | 290 | r := portsmocks.NewMockRenderer(t) | ||
A internal/adapters/drm/fencegate.go +214 -0
| @@ -0,0 +1,214 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "fmt" | |
| 7 | + | "os" | |
| 8 | + | "strconv" | |
| 9 | + | "strings" | |
| 10 | + | "time" | |
| 11 | + | "unsafe" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/neferwl/internal/adapters/syncfile" | |
| 14 | + | "github.com/bnema/zerowrap" | |
| 15 | + | "golang.org/x/sys/unix" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for | |
| 19 | + | // the fence and shows a frame the GPU still draws | |
| 20 | + | // (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the | |
| 21 | + | // output waits for each fence on the CPU and commits without it. | |
| 22 | + | ||
| 23 | + | const ( | |
| 24 | + | driverNVIDIA = "nvidia-drm" | |
| 25 | + | // nvidiaDRMVersionFile holds the loaded module's version. | |
| 26 | + | nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version" | |
| 27 | + | // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version). | |
| 28 | + | ioctlVersion = 0xC0406400 | |
| 29 | + | ) | |
| 30 | + | ||
| 31 | + | // nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD. | |
| 32 | + | var nvidiaFenceFixed = [3]int{560, 35, 3} | |
| 33 | + | ||
| 34 | + | // drmVersion is struct drm_version. | |
| 35 | + | type drmVersion struct { | |
| 36 | + | major, minor, patch int32 | |
| 37 | + | nameLen uint64 | |
| 38 | + | name unsafe.Pointer | |
| 39 | + | dateLen uint64 | |
| 40 | + | date unsafe.Pointer | |
| 41 | + | descLen uint64 | |
| 42 | + | desc unsafe.Pointer | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION). | |
| 46 | + | func driverName(fd int) (string, error) { | |
| 47 | + | var buf [64]byte | |
| 48 | + | v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])} | |
| 49 | + | if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil { | |
| 50 | + | return "", err | |
| 51 | + | } | |
| 52 | + | return string(buf[:min(int(v.nameLen), len(buf))]), nil | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // parseVersion reads a dotted version such as "560.35.03" (missing parts | |
| 56 | + | // are 0). Parts are plain digits: no sign. | |
| 57 | + | func parseVersion(s string) ([3]int, error) { | |
| 58 | + | var v [3]int | |
| 59 | + | parts := strings.Split(strings.TrimSpace(s), ".") | |
| 60 | + | if len(parts) > len(v) { | |
| 61 | + | return v, fmt.Errorf("version %q", s) | |
| 62 | + | } | |
| 63 | + | for i, p := range parts { | |
| 64 | + | n, err := strconv.ParseUint(p, 10, 31) | |
| 65 | + | if err != nil { | |
| 66 | + | return v, fmt.Errorf("version %q", s) | |
| 67 | + | } | |
| 68 | + | v[i] = int(n) | |
| 69 | + | } | |
| 70 | + | return v, nil | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // versionLess reports whether a < b. | |
| 74 | + | func versionLess(a, b [3]int) bool { | |
| 75 | + | for i := range a { | |
| 76 | + | if a[i] != b[i] { | |
| 77 | + | return a[i] < b[i] | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | return false | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // brokenInFence reports whether the driver may mishandle IN_FENCE_FD: | |
| 84 | + | // nvidia-drm before 560.35.3, or whose version is unknown (version is the | |
| 85 | + | // module version file's content, err its read error). | |
| 86 | + | func brokenInFence(driver, version string, err error) bool { | |
| 87 | + | if driver != driverNVIDIA { | |
| 88 | + | return false | |
| 89 | + | } | |
| 90 | + | if err != nil { | |
| 91 | + | return true | |
| 92 | + | } | |
| 93 | + | v, perr := parseVersion(version) | |
| 94 | + | return perr != nil || versionLess(v, nvidiaFenceFixed) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // fenceGate decides, once per card, whether commits carry no IN_FENCE_FD. | |
| 98 | + | func fenceGate(fd int, log zerowrap.Logger) bool { | |
| 99 | + | driver, err := driverName(fd) | |
| 100 | + | if err != nil { | |
| 101 | + | log.Debug().Err(err).Msg("drm driver name unavailable") | |
| 102 | + | return false | |
| 103 | + | } | |
| 104 | + | var version string | |
| 105 | + | var verr error | |
| 106 | + | if driver == driverNVIDIA { | |
| 107 | + | b, rerr := os.ReadFile(nvidiaDRMVersionFile) | |
| 108 | + | version, verr = string(b), rerr | |
| 109 | + | } | |
| 110 | + | if !brokenInFence(driver, version, verr) { | |
| 111 | + | return false | |
| 112 | + | } | |
| 113 | + | log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU") | |
| 114 | + | return true | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // fenceProp sets IN_FENCE_FD on plane p to the fence, unless there is none | |
| 118 | + | // or the fences are waited for on the CPU (waitFences). | |
| 119 | + | func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) { | |
| 120 | + | if fence == nil || o.cpuFences { | |
| 121 | + | return | |
| 122 | + | } | |
| 123 | + | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // fenceWaitLimit bounds the CPU wait of one commit, whatever its number of | |
| 127 | + | // fences. | |
| 128 | + | const fenceWaitLimit = time.Second | |
| 129 | + | ||
| 130 | + | // fenceWarnEvery is the least time between two warnings about a fence wait | |
| 131 | + | // that hit fenceWaitLimit. | |
| 132 | + | const fenceWarnEvery = 10 * time.Second | |
| 133 | + | ||
| 134 | + | // fencePoll polls one fence without blocking. fn is built once, so polling | |
| 135 | + | // allocates nothing; the output goroutine owns it. | |
| 136 | + | type fencePoll struct { | |
| 137 | + | fds [1]unix.PollFd | |
| 138 | + | ready bool | |
| 139 | + | fn func(fd uintptr) | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func (p *fencePoll) init() { | |
| 143 | + | p.fn = func(fd uintptr) { | |
| 144 | + | p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN} | |
| 145 | + | n, err := unix.Poll(p.fds[:], 0) | |
| 146 | + | p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0 | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // clientFenceReady reports whether a client's acquire fence lets its buffer | |
| 151 | + | // be shown by a plane this frame. Where fences are waited for on the CPU, a | |
| 152 | + | // client fence that has not signalled must not stall the output: the buffer | |
| 153 | + | // is composed instead (the renderer waits for it on the GPU). Without CPU | |
| 154 | + | // waits, and for no fence, it is ready; with them, a fence that cannot be | |
| 155 | + | // polled is not ready. | |
| 156 | + | func (o *Output) clientFenceReady(acquire *os.File) bool { | |
| 157 | + | if !o.cpuFences || acquire == nil { | |
| 158 | + | return true | |
| 159 | + | } | |
| 160 | + | if o.fencePoll.fn == nil { | |
| 161 | + | o.fencePoll.init() | |
| 162 | + | } | |
| 163 | + | raw, err := acquire.SyscallConn() | |
| 164 | + | if err != nil { | |
| 165 | + | return false | |
| 166 | + | } | |
| 167 | + | o.fencePoll.ready = false | |
| 168 | + | // Control keeps the descriptor open for the call: Wayland closing its | |
| 169 | + | // file meanwhile cannot hand us a reused number. | |
| 170 | + | if raw.Control(o.fencePoll.fn) != nil { | |
| 171 | + | return false | |
| 172 | + | } | |
| 173 | + | return o.fencePoll.ready | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // waitFences blocks until the fences signalled when the driver cannot wait | |
| 177 | + | // for them in the commit, for at most fenceWaitLimit in all, or until the | |
| 178 | + | // output stops. A client fence passed here (direct scanout) already | |
| 179 | + | // signalled: planes take only such buffers (clientFenceReady), so it | |
| 180 | + | // returns at once. A failed or timed out wait is not fatal: the frame may | |
| 181 | + | // then show early, as it would on any such driver. A timeout is counted in | |
| 182 | + | // the stats and warned about at most every fenceWarnEvery. | |
| 183 | + | func (o *Output) waitFences(fences ...*os.File) { | |
| 184 | + | if !o.cpuFences { | |
| 185 | + | return | |
| 186 | + | } | |
| 187 | + | ctx := o.runContext | |
| 188 | + | if ctx == nil { | |
| 189 | + | ctx = context.Background() | |
| 190 | + | } | |
| 191 | + | limit := o.fenceLimit | |
| 192 | + | if limit == 0 { | |
| 193 | + | limit = fenceWaitLimit | |
| 194 | + | } | |
| 195 | + | ctx, cancel := context.WithTimeout(ctx, limit) | |
| 196 | + | defer cancel() | |
| 197 | + | for _, f := range fences { | |
| 198 | + | if f == nil { | |
| 199 | + | continue | |
| 200 | + | } | |
| 201 | + | err := syncfile.Wait(ctx, f) | |
| 202 | + | switch { | |
| 203 | + | case err == nil || errors.Is(err, context.Canceled): | |
| 204 | + | case errors.Is(err, context.DeadlineExceeded): | |
| 205 | + | o.flipStats.fenceTimeouts++ | |
| 206 | + | if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery { | |
| 207 | + | o.fenceWarnAt = now | |
| 208 | + | o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway") | |
| 209 | + | } | |
| 210 | + | default: | |
| 211 | + | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence") | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | } | |
| @@ -0,0 +1,214 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "fmt" | |||
| 7 | + | "os" | |||
| 8 | + | "strconv" | |||
| 9 | + | "strings" | |||
| 10 | + | "time" | |||
| 11 | + | "unsafe" | |||
| 12 | + | ||||
| 13 | + | "github.com/bnema/neferwl/internal/adapters/syncfile" | |||
| 14 | + | "github.com/bnema/zerowrap" | |||
| 15 | + | "golang.org/x/sys/unix" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for | |||
| 19 | + | // the fence and shows a frame the GPU still draws | |||
| 20 | + | // (NVIDIA/open-gpu-kernel-modules#622, fixed in 560.35.3). On those the | |||
| 21 | + | // output waits for each fence on the CPU and commits without it. | |||
| 22 | + | ||||
| 23 | + | const ( | |||
| 24 | + | driverNVIDIA = "nvidia-drm" | |||
| 25 | + | // nvidiaDRMVersionFile holds the loaded module's version. | |||
| 26 | + | nvidiaDRMVersionFile = "/sys/module/nvidia_drm/version" | |||
| 27 | + | // ioctlVersion is DRM_IOWR('d', 0x00, struct drm_version). | |||
| 28 | + | ioctlVersion = 0xC0406400 | |||
| 29 | + | ) | |||
| 30 | + | ||||
| 31 | + | // nvidiaFenceFixed is the first nvidia-drm version that honours IN_FENCE_FD. | |||
| 32 | + | var nvidiaFenceFixed = [3]int{560, 35, 3} | |||
| 33 | + | ||||
| 34 | + | // drmVersion is struct drm_version. | |||
| 35 | + | type drmVersion struct { | |||
| 36 | + | major, minor, patch int32 | |||
| 37 | + | nameLen uint64 | |||
| 38 | + | name unsafe.Pointer | |||
| 39 | + | dateLen uint64 | |||
| 40 | + | date unsafe.Pointer | |||
| 41 | + | descLen uint64 | |||
| 42 | + | desc unsafe.Pointer | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // driverName is the kernel driver of the DRM device fd (DRM_IOCTL_VERSION). | |||
| 46 | + | func driverName(fd int) (string, error) { | |||
| 47 | + | var buf [64]byte | |||
| 48 | + | v := drmVersion{nameLen: uint64(len(buf)), name: unsafe.Pointer(&buf[0])} | |||
| 49 | + | if err := ioctl(fd, ioctlVersion, unsafe.Pointer(&v)); err != nil { | |||
| 50 | + | return "", err | |||
| 51 | + | } | |||
| 52 | + | return string(buf[:min(int(v.nameLen), len(buf))]), nil | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // parseVersion reads a dotted version such as "560.35.03" (missing parts | |||
| 56 | + | // are 0). Parts are plain digits: no sign. | |||
| 57 | + | func parseVersion(s string) ([3]int, error) { | |||
| 58 | + | var v [3]int | |||
| 59 | + | parts := strings.Split(strings.TrimSpace(s), ".") | |||
| 60 | + | if len(parts) > len(v) { | |||
| 61 | + | return v, fmt.Errorf("version %q", s) | |||
| 62 | + | } | |||
| 63 | + | for i, p := range parts { | |||
| 64 | + | n, err := strconv.ParseUint(p, 10, 31) | |||
| 65 | + | if err != nil { | |||
| 66 | + | return v, fmt.Errorf("version %q", s) | |||
| 67 | + | } | |||
| 68 | + | v[i] = int(n) | |||
| 69 | + | } | |||
| 70 | + | return v, nil | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // versionLess reports whether a < b. | |||
| 74 | + | func versionLess(a, b [3]int) bool { | |||
| 75 | + | for i := range a { | |||
| 76 | + | if a[i] != b[i] { | |||
| 77 | + | return a[i] < b[i] | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | return false | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // brokenInFence reports whether the driver may mishandle IN_FENCE_FD: | |||
| 84 | + | // nvidia-drm before 560.35.3, or whose version is unknown (version is the | |||
| 85 | + | // module version file's content, err its read error). | |||
| 86 | + | func brokenInFence(driver, version string, err error) bool { | |||
| 87 | + | if driver != driverNVIDIA { | |||
| 88 | + | return false | |||
| 89 | + | } | |||
| 90 | + | if err != nil { | |||
| 91 | + | return true | |||
| 92 | + | } | |||
| 93 | + | v, perr := parseVersion(version) | |||
| 94 | + | return perr != nil || versionLess(v, nvidiaFenceFixed) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // fenceGate decides, once per card, whether commits carry no IN_FENCE_FD. | |||
| 98 | + | func fenceGate(fd int, log zerowrap.Logger) bool { | |||
| 99 | + | driver, err := driverName(fd) | |||
| 100 | + | if err != nil { | |||
| 101 | + | log.Debug().Err(err).Msg("drm driver name unavailable") | |||
| 102 | + | return false | |||
| 103 | + | } | |||
| 104 | + | var version string | |||
| 105 | + | var verr error | |||
| 106 | + | if driver == driverNVIDIA { | |||
| 107 | + | b, rerr := os.ReadFile(nvidiaDRMVersionFile) | |||
| 108 | + | version, verr = string(b), rerr | |||
| 109 | + | } | |||
| 110 | + | if !brokenInFence(driver, version, verr) { | |||
| 111 | + | return false | |||
| 112 | + | } | |||
| 113 | + | log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU") | |||
| 114 | + | return true | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // fenceProp sets IN_FENCE_FD on plane p to the fence, unless there is none | |||
| 118 | + | // or the fences are waited for on the CPU (waitFences). | |||
| 119 | + | func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) { | |||
| 120 | + | if fence == nil || o.cpuFences { | |||
| 121 | + | return | |||
| 122 | + | } | |||
| 123 | + | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // fenceWaitLimit bounds the CPU wait of one commit, whatever its number of | |||
| 127 | + | // fences. | |||
| 128 | + | const fenceWaitLimit = time.Second | |||
| 129 | + | ||||
| 130 | + | // fenceWarnEvery is the least time between two warnings about a fence wait | |||
| 131 | + | // that hit fenceWaitLimit. | |||
| 132 | + | const fenceWarnEvery = 10 * time.Second | |||
| 133 | + | ||||
| 134 | + | // fencePoll polls one fence without blocking. fn is built once, so polling | |||
| 135 | + | // allocates nothing; the output goroutine owns it. | |||
| 136 | + | type fencePoll struct { | |||
| 137 | + | fds [1]unix.PollFd | |||
| 138 | + | ready bool | |||
| 139 | + | fn func(fd uintptr) | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (p *fencePoll) init() { | |||
| 143 | + | p.fn = func(fd uintptr) { | |||
| 144 | + | p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN} | |||
| 145 | + | n, err := unix.Poll(p.fds[:], 0) | |||
| 146 | + | p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0 | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // clientFenceReady reports whether a client's acquire fence lets its buffer | |||
| 151 | + | // be shown by a plane this frame. Where fences are waited for on the CPU, a | |||
| 152 | + | // client fence that has not signalled must not stall the output: the buffer | |||
| 153 | + | // is composed instead (the renderer waits for it on the GPU). Without CPU | |||
| 154 | + | // waits, and for no fence, it is ready; with them, a fence that cannot be | |||
| 155 | + | // polled is not ready. | |||
| 156 | + | func (o *Output) clientFenceReady(acquire *os.File) bool { | |||
| 157 | + | if !o.cpuFences || acquire == nil { | |||
| 158 | + | return true | |||
| 159 | + | } | |||
| 160 | + | if o.fencePoll.fn == nil { | |||
| 161 | + | o.fencePoll.init() | |||
| 162 | + | } | |||
| 163 | + | raw, err := acquire.SyscallConn() | |||
| 164 | + | if err != nil { | |||
| 165 | + | return false | |||
| 166 | + | } | |||
| 167 | + | o.fencePoll.ready = false | |||
| 168 | + | // Control keeps the descriptor open for the call: Wayland closing its | |||
| 169 | + | // file meanwhile cannot hand us a reused number. | |||
| 170 | + | if raw.Control(o.fencePoll.fn) != nil { | |||
| 171 | + | return false | |||
| 172 | + | } | |||
| 173 | + | return o.fencePoll.ready | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | // waitFences blocks until the fences signalled when the driver cannot wait | |||
| 177 | + | // for them in the commit, for at most fenceWaitLimit in all, or until the | |||
| 178 | + | // output stops. A client fence passed here (direct scanout) already | |||
| 179 | + | // signalled: planes take only such buffers (clientFenceReady), so it | |||
| 180 | + | // returns at once. A failed or timed out wait is not fatal: the frame may | |||
| 181 | + | // then show early, as it would on any such driver. A timeout is counted in | |||
| 182 | + | // the stats and warned about at most every fenceWarnEvery. | |||
| 183 | + | func (o *Output) waitFences(fences ...*os.File) { | |||
| 184 | + | if !o.cpuFences { | |||
| 185 | + | return | |||
| 186 | + | } | |||
| 187 | + | ctx := o.runContext | |||
| 188 | + | if ctx == nil { | |||
| 189 | + | ctx = context.Background() | |||
| 190 | + | } | |||
| 191 | + | limit := o.fenceLimit | |||
| 192 | + | if limit == 0 { | |||
| 193 | + | limit = fenceWaitLimit | |||
| 194 | + | } | |||
| 195 | + | ctx, cancel := context.WithTimeout(ctx, limit) | |||
| 196 | + | defer cancel() | |||
| 197 | + | for _, f := range fences { | |||
| 198 | + | if f == nil { | |||
| 199 | + | continue | |||
| 200 | + | } | |||
| 201 | + | err := syncfile.Wait(ctx, f) | |||
| 202 | + | switch { | |||
| 203 | + | case err == nil || errors.Is(err, context.Canceled): | |||
| 204 | + | case errors.Is(err, context.DeadlineExceeded): | |||
| 205 | + | o.flipStats.fenceTimeouts++ | |||
| 206 | + | if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery { | |||
| 207 | + | o.fenceWarnAt = now | |||
| 208 | + | o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway") | |||
| 209 | + | } | |||
| 210 | + | default: | |||
| 211 | + | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence") | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | } | |||
A internal/adapters/drm/fencegate_test.go +283 -0
| @@ -0,0 +1,283 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |
| 12 | + | "github.com/stretchr/testify/mock" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // pipeFence returns a pipe standing in for a sync file: its read end is | |
| 17 | + | // readable (signalled) once the write end got a byte. Both ends close with | |
| 18 | + | // the test. | |
| 19 | + | func pipeFence(t *testing.T) (fence, signal *os.File) { | |
| 20 | + | t.Helper() | |
| 21 | + | r, w, err := os.Pipe() | |
| 22 | + | if err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |
| 26 | + | return r, w | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func TestParseVersion(t *testing.T) { | |
| 30 | + | for _, tc := range []struct { | |
| 31 | + | in string | |
| 32 | + | want [3]int | |
| 33 | + | ok bool | |
| 34 | + | }{ | |
| 35 | + | {"560.35.03\n", [3]int{560, 35, 3}, true}, | |
| 36 | + | {"615.78.08", [3]int{615, 78, 8}, true}, | |
| 37 | + | {"550.120", [3]int{550, 120, 0}, true}, | |
| 38 | + | {"570", [3]int{570, 0, 0}, true}, | |
| 39 | + | {"", [3]int{}, false}, | |
| 40 | + | {"abc", [3]int{}, false}, | |
| 41 | + | {"560.x.3", [3]int{}, false}, | |
| 42 | + | {"1.2.3.4", [3]int{}, false}, | |
| 43 | + | {"-1.2", [3]int{}, false}, | |
| 44 | + | {"+560.35.3", [3]int{}, false}, | |
| 45 | + | {"560.+35.3", [3]int{}, false}, | |
| 46 | + | {"560.-5.3", [3]int{}, false}, | |
| 47 | + | {"560..3", [3]int{}, false}, | |
| 48 | + | {"99999999999.1.1", [3]int{}, false}, | |
| 49 | + | } { | |
| 50 | + | got, err := parseVersion(tc.in) | |
| 51 | + | if (err == nil) != tc.ok || tc.ok && got != tc.want { | |
| 52 | + | t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // The gate opens for nvidia-drm before 560.35.3 or of unknown version, and | |
| 58 | + | // for no other driver. | |
| 59 | + | func TestBrokenInFence(t *testing.T) { | |
| 60 | + | for _, tc := range []struct { | |
| 61 | + | name, driver, version string | |
| 62 | + | err error | |
| 63 | + | want bool | |
| 64 | + | }{ | |
| 65 | + | {"amd", "amdgpu", "", nil, false}, | |
| 66 | + | {"intel", "i915", "garbage", nil, false}, | |
| 67 | + | {"old nvidia", driverNVIDIA, "555.58.02", nil, true}, | |
| 68 | + | {"one patch before", driverNVIDIA, "560.35.2", nil, true}, | |
| 69 | + | {"minor before", driverNVIDIA, "560.34.99", nil, true}, | |
| 70 | + | {"fixed", driverNVIDIA, "560.35.03\n", nil, false}, | |
| 71 | + | {"patch after", driverNVIDIA, "560.35.5", nil, false}, | |
| 72 | + | {"minor after", driverNVIDIA, "560.36.0", nil, false}, | |
| 73 | + | {"major after", driverNVIDIA, "561.0.0", nil, false}, | |
| 74 | + | {"newer", driverNVIDIA, "615.78.08", nil, false}, | |
| 75 | + | {"unreadable", driverNVIDIA, "", errors.New("no such file"), true}, | |
| 76 | + | {"unparsable", driverNVIDIA, "n/a", nil, true}, | |
| 77 | + | } { | |
| 78 | + | if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want { | |
| 79 | + | t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node | |
| 85 | + | // answers it too: skips without one. | |
| 86 | + | func TestDriverName(t *testing.T) { | |
| 87 | + | nodes, _ := filepath.Glob("/dev/dri/renderD*") | |
| 88 | + | if len(nodes) == 0 { | |
| 89 | + | t.Skip("no /dev/dri/renderD* node") | |
| 90 | + | } | |
| 91 | + | for _, n := range nodes { | |
| 92 | + | f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0) | |
| 93 | + | if err != nil { | |
| 94 | + | continue | |
| 95 | + | } | |
| 96 | + | defer f.Close() | |
| 97 | + | name, err := driverName(int(f.Fd())) | |
| 98 | + | if err != nil || name == "" { | |
| 99 | + | t.Fatalf("driverName(%s) = %q, %v", n, name, err) | |
| 100 | + | } | |
| 101 | + | t.Logf("%s: %s", n, name) | |
| 102 | + | return | |
| 103 | + | } | |
| 104 | + | t.Skip("no render node can be opened") | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does. | |
| 108 | + | func TestFenceProp(t *testing.T) { | |
| 109 | + | f, _ := pipeFence(t) | |
| 110 | + | for _, cpu := range []bool{false, true} { | |
| 111 | + | o, _, _ := testOutput(t) | |
| 112 | + | o.cpuFences = cpu | |
| 113 | + | req := &atomicReq{} | |
| 114 | + | o.fenceProp(req, o.primary, f) | |
| 115 | + | o.fenceProp(req, o.primary, nil) | |
| 116 | + | v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD")) | |
| 117 | + | if ok == cpu || ok && v != uint64(f.Fd()) { | |
| 118 | + | t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // waitFences returns for a fence that signalled, for one that never will | |
| 124 | + | // once the output stops, and at once where fences go to the kernel. | |
| 125 | + | func TestWaitFences(t *testing.T) { | |
| 126 | + | returns := func(t *testing.T, o *Output, fences ...*os.File) { | |
| 127 | + | t.Helper() | |
| 128 | + | done := make(chan struct{}) | |
| 129 | + | go func() { o.waitFences(fences...); close(done) }() | |
| 130 | + | select { | |
| 131 | + | case <-done: | |
| 132 | + | case <-time.After(30 * time.Second): // failure guard only | |
| 133 | + | t.Fatal("waitFences did not return") | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | t.Run("signalled", func(t *testing.T) { | |
| 137 | + | o, _, _ := testOutput(t) | |
| 138 | + | o.cpuFences = true | |
| 139 | + | f, w := pipeFence(t) | |
| 140 | + | _, _ = w.Write([]byte{1}) | |
| 141 | + | returns(t, o, nil, f) | |
| 142 | + | }) | |
| 143 | + | t.Run("never signalled, output stopped", func(t *testing.T) { | |
| 144 | + | o, _, _ := testOutput(t) | |
| 145 | + | o.cpuFences = true | |
| 146 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 147 | + | cancel() | |
| 148 | + | o.runContext = ctx | |
| 149 | + | f, _ := pipeFence(t) | |
| 150 | + | returns(t, o, f) | |
| 151 | + | if o.flipStats.fenceTimeouts != 0 { | |
| 152 | + | t.Fatal("a stopped output counted a fence timeout") | |
| 153 | + | } | |
| 154 | + | }) | |
| 155 | + | t.Run("kernel waits", func(t *testing.T) { | |
| 156 | + | o, _, _ := testOutput(t) | |
| 157 | + | f, _ := pipeFence(t) | |
| 158 | + | returns(t, o, f) | |
| 159 | + | }) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // A fence wait that hit its limit counts in the stats, and warns at most | |
| 163 | + | // once per fenceWarnEvery. | |
| 164 | + | func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) { | |
| 165 | + | o, _, _ := testOutput(t) | |
| 166 | + | o.cpuFences = true | |
| 167 | + | o.fenceLimit = time.Millisecond | |
| 168 | + | f, _ := pipeFence(t) | |
| 169 | + | o.waitFences(f) | |
| 170 | + | if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() { | |
| 171 | + | t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt) | |
| 172 | + | } | |
| 173 | + | first := o.fenceWarnAt | |
| 174 | + | o.waitFences(f) | |
| 175 | + | if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) { | |
| 176 | + | t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // Without CPU fence waits every fence is ready; with them only a signalled | |
| 181 | + | // client fence is, and one that cannot be polled is not. | |
| 182 | + | func TestClientFenceReady(t *testing.T) { | |
| 183 | + | f, w := pipeFence(t) | |
| 184 | + | o, _, _ := testOutput(t) | |
| 185 | + | if !o.clientFenceReady(f) || !o.clientFenceReady(nil) { | |
| 186 | + | t.Fatal("fence not ready without CPU waits") | |
| 187 | + | } | |
| 188 | + | o.cpuFences = true | |
| 189 | + | if !o.clientFenceReady(nil) { | |
| 190 | + | t.Fatal("no fence must be ready") | |
| 191 | + | } | |
| 192 | + | if o.clientFenceReady(f) { | |
| 193 | + | t.Fatal("pending client fence ready") | |
| 194 | + | } | |
| 195 | + | _, _ = w.Write([]byte{1}) | |
| 196 | + | if !o.clientFenceReady(f) { | |
| 197 | + | t.Fatal("signalled client fence not ready") | |
| 198 | + | } | |
| 199 | + | closed, cw := pipeFence(t) | |
| 200 | + | closed.Close() | |
| 201 | + | cw.Close() | |
| 202 | + | if o.clientFenceReady(closed) { | |
| 203 | + | t.Fatal("closed fence ready") | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | // Polling a fence allocates nothing. | |
| 208 | + | func TestClientFenceReadyAllocatesNothing(t *testing.T) { | |
| 209 | + | f, _ := pipeFence(t) | |
| 210 | + | o, _, _ := testOutput(t) | |
| 211 | + | o.cpuFences = true | |
| 212 | + | o.clientFenceReady(f) // builds the poll closure | |
| 213 | + | if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 { | |
| 214 | + | t.Fatalf("%v allocations per poll", n) | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // With CPU waits a client buffer whose fence is pending is composed, on the | |
| 219 | + | // primary plane and on the overlay alike: neither plane waits for it. Once | |
| 220 | + | // the fence signalled the same frame uses the plane. | |
| 221 | + | func TestPendingClientFenceComposes(t *testing.T) { | |
| 222 | + | for _, tc := range []struct { | |
| 223 | + | name string | |
| 224 | + | reason func(o *Output) string | |
| 225 | + | // decide returns whether the plane took the buffer. | |
| 226 | + | decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool | |
| 227 | + | }{ | |
| 228 | + | {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |
| 229 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 230 | + | s, c := fullscreenScene() | |
| 231 | + | content := c[1] | |
| 232 | + | content.Acquire = fence | |
| 233 | + | fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content}) | |
| 234 | + | return fb != 0 | |
| 235 | + | }}, | |
| 236 | + | {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |
| 237 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 238 | + | s, c := overlayScene() | |
| 239 | + | content := c[2] | |
| 240 | + | content.Acquire = fence | |
| 241 | + | ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content}) | |
| 242 | + | ov.close() | |
| 243 | + | return ov.fb != 0 | |
| 244 | + | }}, | |
| 245 | + | } { | |
| 246 | + | t.Run(tc.name, func(t *testing.T) { | |
| 247 | + | o, k, _ := overlayOutput(t) | |
| 248 | + | k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe() | |
| 249 | + | o.cpuFences = true | |
| 250 | + | fence, w := pipeFence(t) | |
| 251 | + | if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" { | |
| 252 | + | t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o)) | |
| 253 | + | } | |
| 254 | + | _, _ = w.Write([]byte{1}) | |
| 255 | + | if !tc.decide(t, o, k, fence) { | |
| 256 | + | t.Fatalf("signalled fence: reason %q", tc.reason(o)) | |
| 257 | + | } | |
| 258 | + | }) | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // A commit that is retried (here: VRR refused, then the same frame without | |
| 263 | + | // it) waits for its fence once, not once per attempt. | |
| 264 | + | func TestCommitWaitsForItsFenceOnce(t *testing.T) { | |
| 265 | + | o, _, commits := testOutput(t, unix.EINVAL) | |
| 266 | + | o.cpuFences = true | |
| 267 | + | o.fenceLimit = time.Millisecond | |
| 268 | + | fence, _ := pipeFence(t) | |
| 269 | + | if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil { | |
| 270 | + | t.Fatal(err) | |
| 271 | + | } | |
| 272 | + | if len(*commits) < 2 { | |
| 273 | + | t.Fatalf("%d commits: no retry happened", len(*commits)) | |
| 274 | + | } | |
| 275 | + | if o.flipStats.fenceTimeouts != 1 { | |
| 276 | + | t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits)) | |
| 277 | + | } | |
| 278 | + | for i, c := range *commits { | |
| 279 | + | if _, ok := c.req.value(tPrimary, pFence); ok { | |
| 280 | + | t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i) | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | } | |
| @@ -0,0 +1,283 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 12 | + | "github.com/stretchr/testify/mock" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // pipeFence returns a pipe standing in for a sync file: its read end is | |||
| 17 | + | // readable (signalled) once the write end got a byte. Both ends close with | |||
| 18 | + | // the test. | |||
| 19 | + | func pipeFence(t *testing.T) (fence, signal *os.File) { | |||
| 20 | + | t.Helper() | |||
| 21 | + | r, w, err := os.Pipe() | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |||
| 26 | + | return r, w | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func TestParseVersion(t *testing.T) { | |||
| 30 | + | for _, tc := range []struct { | |||
| 31 | + | in string | |||
| 32 | + | want [3]int | |||
| 33 | + | ok bool | |||
| 34 | + | }{ | |||
| 35 | + | {"560.35.03\n", [3]int{560, 35, 3}, true}, | |||
| 36 | + | {"615.78.08", [3]int{615, 78, 8}, true}, | |||
| 37 | + | {"550.120", [3]int{550, 120, 0}, true}, | |||
| 38 | + | {"570", [3]int{570, 0, 0}, true}, | |||
| 39 | + | {"", [3]int{}, false}, | |||
| 40 | + | {"abc", [3]int{}, false}, | |||
| 41 | + | {"560.x.3", [3]int{}, false}, | |||
| 42 | + | {"1.2.3.4", [3]int{}, false}, | |||
| 43 | + | {"-1.2", [3]int{}, false}, | |||
| 44 | + | {"+560.35.3", [3]int{}, false}, | |||
| 45 | + | {"560.+35.3", [3]int{}, false}, | |||
| 46 | + | {"560.-5.3", [3]int{}, false}, | |||
| 47 | + | {"560..3", [3]int{}, false}, | |||
| 48 | + | {"99999999999.1.1", [3]int{}, false}, | |||
| 49 | + | } { | |||
| 50 | + | got, err := parseVersion(tc.in) | |||
| 51 | + | if (err == nil) != tc.ok || tc.ok && got != tc.want { | |||
| 52 | + | t.Errorf("parseVersion(%q) = %v, %v", tc.in, got, err) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // The gate opens for nvidia-drm before 560.35.3 or of unknown version, and | |||
| 58 | + | // for no other driver. | |||
| 59 | + | func TestBrokenInFence(t *testing.T) { | |||
| 60 | + | for _, tc := range []struct { | |||
| 61 | + | name, driver, version string | |||
| 62 | + | err error | |||
| 63 | + | want bool | |||
| 64 | + | }{ | |||
| 65 | + | {"amd", "amdgpu", "", nil, false}, | |||
| 66 | + | {"intel", "i915", "garbage", nil, false}, | |||
| 67 | + | {"old nvidia", driverNVIDIA, "555.58.02", nil, true}, | |||
| 68 | + | {"one patch before", driverNVIDIA, "560.35.2", nil, true}, | |||
| 69 | + | {"minor before", driverNVIDIA, "560.34.99", nil, true}, | |||
| 70 | + | {"fixed", driverNVIDIA, "560.35.03\n", nil, false}, | |||
| 71 | + | {"patch after", driverNVIDIA, "560.35.5", nil, false}, | |||
| 72 | + | {"minor after", driverNVIDIA, "560.36.0", nil, false}, | |||
| 73 | + | {"major after", driverNVIDIA, "561.0.0", nil, false}, | |||
| 74 | + | {"newer", driverNVIDIA, "615.78.08", nil, false}, | |||
| 75 | + | {"unreadable", driverNVIDIA, "", errors.New("no such file"), true}, | |||
| 76 | + | {"unparsable", driverNVIDIA, "n/a", nil, true}, | |||
| 77 | + | } { | |||
| 78 | + | if got := brokenInFence(tc.driver, tc.version, tc.err); got != tc.want { | |||
| 79 | + | t.Errorf("%s: brokenInFence = %v, want %v", tc.name, got, tc.want) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node | |||
| 85 | + | // answers it too: skips without one. | |||
| 86 | + | func TestDriverName(t *testing.T) { | |||
| 87 | + | nodes, _ := filepath.Glob("/dev/dri/renderD*") | |||
| 88 | + | if len(nodes) == 0 { | |||
| 89 | + | t.Skip("no /dev/dri/renderD* node") | |||
| 90 | + | } | |||
| 91 | + | for _, n := range nodes { | |||
| 92 | + | f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0) | |||
| 93 | + | if err != nil { | |||
| 94 | + | continue | |||
| 95 | + | } | |||
| 96 | + | defer f.Close() | |||
| 97 | + | name, err := driverName(int(f.Fd())) | |||
| 98 | + | if err != nil || name == "" { | |||
| 99 | + | t.Fatalf("driverName(%s) = %q, %v", n, name, err) | |||
| 100 | + | } | |||
| 101 | + | t.Logf("%s: %s", n, name) | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | t.Skip("no render node can be opened") | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does. | |||
| 108 | + | func TestFenceProp(t *testing.T) { | |||
| 109 | + | f, _ := pipeFence(t) | |||
| 110 | + | for _, cpu := range []bool{false, true} { | |||
| 111 | + | o, _, _ := testOutput(t) | |||
| 112 | + | o.cpuFences = cpu | |||
| 113 | + | req := &atomicReq{} | |||
| 114 | + | o.fenceProp(req, o.primary, f) | |||
| 115 | + | o.fenceProp(req, o.primary, nil) | |||
| 116 | + | v, ok := req.value(o.primary.id, o.primary.prop("IN_FENCE_FD")) | |||
| 117 | + | if ok == cpu || ok && v != uint64(f.Fd()) { | |||
| 118 | + | t.Errorf("cpuFences=%v: IN_FENCE_FD %d, set %v", cpu, v, ok) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // waitFences returns for a fence that signalled, for one that never will | |||
| 124 | + | // once the output stops, and at once where fences go to the kernel. | |||
| 125 | + | func TestWaitFences(t *testing.T) { | |||
| 126 | + | returns := func(t *testing.T, o *Output, fences ...*os.File) { | |||
| 127 | + | t.Helper() | |||
| 128 | + | done := make(chan struct{}) | |||
| 129 | + | go func() { o.waitFences(fences...); close(done) }() | |||
| 130 | + | select { | |||
| 131 | + | case <-done: | |||
| 132 | + | case <-time.After(30 * time.Second): // failure guard only | |||
| 133 | + | t.Fatal("waitFences did not return") | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | t.Run("signalled", func(t *testing.T) { | |||
| 137 | + | o, _, _ := testOutput(t) | |||
| 138 | + | o.cpuFences = true | |||
| 139 | + | f, w := pipeFence(t) | |||
| 140 | + | _, _ = w.Write([]byte{1}) | |||
| 141 | + | returns(t, o, nil, f) | |||
| 142 | + | }) | |||
| 143 | + | t.Run("never signalled, output stopped", func(t *testing.T) { | |||
| 144 | + | o, _, _ := testOutput(t) | |||
| 145 | + | o.cpuFences = true | |||
| 146 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 147 | + | cancel() | |||
| 148 | + | o.runContext = ctx | |||
| 149 | + | f, _ := pipeFence(t) | |||
| 150 | + | returns(t, o, f) | |||
| 151 | + | if o.flipStats.fenceTimeouts != 0 { | |||
| 152 | + | t.Fatal("a stopped output counted a fence timeout") | |||
| 153 | + | } | |||
| 154 | + | }) | |||
| 155 | + | t.Run("kernel waits", func(t *testing.T) { | |||
| 156 | + | o, _, _ := testOutput(t) | |||
| 157 | + | f, _ := pipeFence(t) | |||
| 158 | + | returns(t, o, f) | |||
| 159 | + | }) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // A fence wait that hit its limit counts in the stats, and warns at most | |||
| 163 | + | // once per fenceWarnEvery. | |||
| 164 | + | func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) { | |||
| 165 | + | o, _, _ := testOutput(t) | |||
| 166 | + | o.cpuFences = true | |||
| 167 | + | o.fenceLimit = time.Millisecond | |||
| 168 | + | f, _ := pipeFence(t) | |||
| 169 | + | o.waitFences(f) | |||
| 170 | + | if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() { | |||
| 171 | + | t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt) | |||
| 172 | + | } | |||
| 173 | + | first := o.fenceWarnAt | |||
| 174 | + | o.waitFences(f) | |||
| 175 | + | if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) { | |||
| 176 | + | t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // Without CPU fence waits every fence is ready; with them only a signalled | |||
| 181 | + | // client fence is, and one that cannot be polled is not. | |||
| 182 | + | func TestClientFenceReady(t *testing.T) { | |||
| 183 | + | f, w := pipeFence(t) | |||
| 184 | + | o, _, _ := testOutput(t) | |||
| 185 | + | if !o.clientFenceReady(f) || !o.clientFenceReady(nil) { | |||
| 186 | + | t.Fatal("fence not ready without CPU waits") | |||
| 187 | + | } | |||
| 188 | + | o.cpuFences = true | |||
| 189 | + | if !o.clientFenceReady(nil) { | |||
| 190 | + | t.Fatal("no fence must be ready") | |||
| 191 | + | } | |||
| 192 | + | if o.clientFenceReady(f) { | |||
| 193 | + | t.Fatal("pending client fence ready") | |||
| 194 | + | } | |||
| 195 | + | _, _ = w.Write([]byte{1}) | |||
| 196 | + | if !o.clientFenceReady(f) { | |||
| 197 | + | t.Fatal("signalled client fence not ready") | |||
| 198 | + | } | |||
| 199 | + | closed, cw := pipeFence(t) | |||
| 200 | + | closed.Close() | |||
| 201 | + | cw.Close() | |||
| 202 | + | if o.clientFenceReady(closed) { | |||
| 203 | + | t.Fatal("closed fence ready") | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | // Polling a fence allocates nothing. | |||
| 208 | + | func TestClientFenceReadyAllocatesNothing(t *testing.T) { | |||
| 209 | + | f, _ := pipeFence(t) | |||
| 210 | + | o, _, _ := testOutput(t) | |||
| 211 | + | o.cpuFences = true | |||
| 212 | + | o.clientFenceReady(f) // builds the poll closure | |||
| 213 | + | if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 { | |||
| 214 | + | t.Fatalf("%v allocations per poll", n) | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // With CPU waits a client buffer whose fence is pending is composed, on the | |||
| 219 | + | // primary plane and on the overlay alike: neither plane waits for it. Once | |||
| 220 | + | // the fence signalled the same frame uses the plane. | |||
| 221 | + | func TestPendingClientFenceComposes(t *testing.T) { | |||
| 222 | + | for _, tc := range []struct { | |||
| 223 | + | name string | |||
| 224 | + | reason func(o *Output) string | |||
| 225 | + | // decide returns whether the plane took the buffer. | |||
| 226 | + | decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool | |||
| 227 | + | }{ | |||
| 228 | + | {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |||
| 229 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 230 | + | s, c := fullscreenScene() | |||
| 231 | + | content := c[1] | |||
| 232 | + | content.Acquire = fence | |||
| 233 | + | fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content}) | |||
| 234 | + | return fb != 0 | |||
| 235 | + | }}, | |||
| 236 | + | {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |||
| 237 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 238 | + | s, c := overlayScene() | |||
| 239 | + | content := c[2] | |||
| 240 | + | content.Acquire = fence | |||
| 241 | + | ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content}) | |||
| 242 | + | ov.close() | |||
| 243 | + | return ov.fb != 0 | |||
| 244 | + | }}, | |||
| 245 | + | } { | |||
| 246 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 247 | + | o, k, _ := overlayOutput(t) | |||
| 248 | + | k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe() | |||
| 249 | + | o.cpuFences = true | |||
| 250 | + | fence, w := pipeFence(t) | |||
| 251 | + | if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" { | |||
| 252 | + | t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o)) | |||
| 253 | + | } | |||
| 254 | + | _, _ = w.Write([]byte{1}) | |||
| 255 | + | if !tc.decide(t, o, k, fence) { | |||
| 256 | + | t.Fatalf("signalled fence: reason %q", tc.reason(o)) | |||
| 257 | + | } | |||
| 258 | + | }) | |||
| 259 | + | } | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // A commit that is retried (here: VRR refused, then the same frame without | |||
| 263 | + | // it) waits for its fence once, not once per attempt. | |||
| 264 | + | func TestCommitWaitsForItsFenceOnce(t *testing.T) { | |||
| 265 | + | o, _, commits := testOutput(t, unix.EINVAL) | |||
| 266 | + | o.cpuFences = true | |||
| 267 | + | o.fenceLimit = time.Millisecond | |||
| 268 | + | fence, _ := pipeFence(t) | |||
| 269 | + | if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil { | |||
| 270 | + | t.Fatal(err) | |||
| 271 | + | } | |||
| 272 | + | if len(*commits) < 2 { | |||
| 273 | + | t.Fatalf("%d commits: no retry happened", len(*commits)) | |||
| 274 | + | } | |||
| 275 | + | if o.flipStats.fenceTimeouts != 1 { | |||
| 276 | + | t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits)) | |||
| 277 | + | } | |||
| 278 | + | for i, c := range *commits { | |||
| 279 | + | if _, ok := c.req.value(tPrimary, pFence); ok { | |||
| 280 | + | t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i) | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | } | |||
M internal/adapters/drm/flip_stats.go +3 -0
| @@ -30,6 +30,9 @@ type flipStats struct { | |||
| 30 | 30 | // stateCarried counts deferred state changes a frame carried instead. | |
| 31 | 31 | // Many state commits while a video plays point at a lost vblank each. | |
| 32 | 32 | stateCommits, stateCarried int | |
| 33 | + | // fenceTimeouts counts commits whose CPU fence wait hit its limit | |
| 34 | + | // (fencegate.go). | |
| 35 | + | fenceTimeouts int | |
| 33 | 36 | } | |
| 34 | 37 | ||
| 35 | 38 | // nominalPeriod is the time between two vblanks at the mode's refresh | |
| @@ -30,6 +30,9 @@ type flipStats struct { | |||||
| 30 | // stateCarried counts deferred state changes a frame carried instead. | 30 | // stateCarried counts deferred state changes a frame carried instead. | ||
| 31 | // Many state commits while a video plays point at a lost vblank each. | 31 | // Many state commits while a video plays point at a lost vblank each. | ||
| 32 | stateCommits, stateCarried int | 32 | stateCommits, stateCarried int | ||
| 33 | + | // fenceTimeouts counts commits whose CPU fence wait hit its limit | |||
| 34 | + | // (fencegate.go). | |||
| 35 | + | fenceTimeouts int | |||
| 33 | } | 36 | } | ||
| 34 | 37 | ||||
| 35 | // nominalPeriod is the time between two vblanks at the mode's refresh | 38 | // nominalPeriod is the time between two vblanks at the mode's refresh | ||
A internal/adapters/drm/gbm/gbm.go +276 -0
| @@ -0,0 +1,276 @@ | |||
| 1 | + | // Package gbm allocates dmabuf images with libgbm, loaded at runtime with | |
| 2 | + | // purego. Display drivers know best how to lay out a buffer they will scan | |
| 3 | + | // out (NVIDIA's block-linear layouts in particular), so the DRM output asks | |
| 4 | + | // GBM for its images and the renderer imports them. | |
| 5 | + | package gbm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "errors" | |
| 9 | + | "fmt" | |
| 10 | + | "os" | |
| 11 | + | "runtime" | |
| 12 | + | "slices" | |
| 13 | + | "sync" | |
| 14 | + | "unsafe" | |
| 15 | + | ||
| 16 | + | "github.com/bnema/neferwl/internal/ports" | |
| 17 | + | "github.com/bnema/purego" | |
| 18 | + | "golang.org/x/sys/unix" | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // Usage is a set of GBM_BO_USE_* flags. | |
| 22 | + | type Usage uint32 | |
| 23 | + | ||
| 24 | + | const ( | |
| 25 | + | // UseScanout (GBM_BO_USE_SCANOUT): the buffer can be shown by KMS. | |
| 26 | + | UseScanout Usage = 1 << 0 | |
| 27 | + | // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it. | |
| 28 | + | UseRendering Usage = 1 << 2 | |
| 29 | + | // useLinear (GBM_BO_USE_LINEAR): the layout is linear. | |
| 30 | + | useLinear Usage = 1 << 4 | |
| 31 | + | ) | |
| 32 | + | ||
| 33 | + | const ( | |
| 34 | + | // modLinear is DRM_FORMAT_MOD_LINEAR. | |
| 35 | + | modLinear = 0 | |
| 36 | + | // modInvalid is DRM_FORMAT_MOD_INVALID: an implicit layout. | |
| 37 | + | modInvalid = 1<<56 - 1 | |
| 38 | + | // maxPlanes is the DRM limit of planes in a framebuffer. | |
| 39 | + | maxPlanes = 4 | |
| 40 | + | ) | |
| 41 | + | ||
| 42 | + | var ( | |
| 43 | + | loadOnce sync.Once | |
| 44 | + | loadErr error | |
| 45 | + | ||
| 46 | + | createDevice func(fd int32) uintptr | |
| 47 | + | destroyDevice func(dev uintptr) | |
| 48 | + | createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr | |
| 49 | + | createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr | |
| 50 | + | boModifier func(bo uintptr) uint64 | |
| 51 | + | boPlaneCount func(bo uintptr) int32 | |
| 52 | + | boFDForPlane func(bo uintptr, plane int32) int32 | |
| 53 | + | boStrideForPlane func(bo uintptr, plane int32) uint32 | |
| 54 | + | boOffset func(bo uintptr, plane int32) uint32 | |
| 55 | + | boDestroy func(bo uintptr) | |
| 56 | + | ) | |
| 57 | + | ||
| 58 | + | // load opens libgbm once. Without it, or with one that lacks a function | |
| 59 | + | // below (gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane came with | |
| 60 | + | // Mesa 21.1), the allocator is unavailable and callers fall back to another | |
| 61 | + | // way to make images. | |
| 62 | + | func load() error { | |
| 63 | + | loadOnce.Do(func() { | |
| 64 | + | lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL) | |
| 65 | + | if err != nil { | |
| 66 | + | loadErr = fmt.Errorf("open libgbm.so.1: %w", err) | |
| 67 | + | return | |
| 68 | + | } | |
| 69 | + | defer func() { | |
| 70 | + | if r := recover(); r != nil { | |
| 71 | + | loadErr = fmt.Errorf("libgbm: %v", r) | |
| 72 | + | } | |
| 73 | + | }() | |
| 74 | + | reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) } | |
| 75 | + | reg(&createDevice, "gbm_create_device") | |
| 76 | + | reg(&destroyDevice, "gbm_device_destroy") | |
| 77 | + | reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2") | |
| 78 | + | reg(&createPlain, "gbm_bo_create") | |
| 79 | + | reg(&boModifier, "gbm_bo_get_modifier") | |
| 80 | + | reg(&boPlaneCount, "gbm_bo_get_plane_count") | |
| 81 | + | reg(&boFDForPlane, "gbm_bo_get_fd_for_plane") | |
| 82 | + | reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane") | |
| 83 | + | reg(&boOffset, "gbm_bo_get_offset") | |
| 84 | + | reg(&boDestroy, "gbm_bo_destroy") | |
| 85 | + | }) | |
| 86 | + | return loadErr | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Device is a GBM device on a duplicate of a DRM device fd. Close its | |
| 90 | + | // buffers before the device. | |
| 91 | + | type Device struct { | |
| 92 | + | dev uintptr | |
| 93 | + | fd int | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Open creates a GBM device on a duplicate of fd, which stays the caller's. | |
| 97 | + | func Open(fd int) (*Device, error) { | |
| 98 | + | if err := load(); err != nil { | |
| 99 | + | return nil, err | |
| 100 | + | } | |
| 101 | + | dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0) | |
| 102 | + | if err != nil { | |
| 103 | + | return nil, fmt.Errorf("duplicate drm fd: %w", err) | |
| 104 | + | } | |
| 105 | + | dev := createDevice(int32(dup)) | |
| 106 | + | if dev == 0 { | |
| 107 | + | _ = unix.Close(dup) | |
| 108 | + | return nil, errors.New("gbm_create_device failed") | |
| 109 | + | } | |
| 110 | + | return &Device{dev: dev, fd: dup}, nil | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // Close destroys the device and its fd. | |
| 114 | + | func (d *Device) Close() { | |
| 115 | + | if d == nil || d.dev == 0 { | |
| 116 | + | return | |
| 117 | + | } | |
| 118 | + | destroyDevice(d.dev) | |
| 119 | + | _ = unix.Close(d.fd) | |
| 120 | + | d.dev = 0 | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // Buffer is an allocated image. It owns one fd per plane and the GBM | |
| 124 | + | // object, kept alive for as long as the image is used. | |
| 125 | + | type Buffer struct { | |
| 126 | + | bo uintptr | |
| 127 | + | width, height int | |
| 128 | + | format uint32 | |
| 129 | + | modifier uint64 | |
| 130 | + | planes []plane | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | type plane struct { | |
| 134 | + | fd int | |
| 135 | + | offset, stride uint32 | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // Create allocates an image whose modifier is one of modifiers (a | |
| 139 | + | // non-empty list). A list holding only linear falls back to a plain | |
| 140 | + | // linear allocation when GBM refuses the modifier call, and accepts an | |
| 141 | + | // implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The | |
| 142 | + | // result must be one memory object, because the renderer imports one | |
| 143 | + | // dmabuf for all planes. | |
| 144 | + | func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) { | |
| 145 | + | if d == nil || d.dev == 0 { | |
| 146 | + | return nil, errors.New("gbm device closed") | |
| 147 | + | } | |
| 148 | + | if width <= 0 || height <= 0 || len(modifiers) == 0 { | |
| 149 | + | return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers)) | |
| 150 | + | } | |
| 151 | + | bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage)) | |
| 152 | + | runtime.KeepAlive(modifiers) | |
| 153 | + | onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear | |
| 154 | + | if bo == 0 && onlyLinear { | |
| 155 | + | bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear)) | |
| 156 | + | } | |
| 157 | + | if bo == 0 { | |
| 158 | + | return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers) | |
| 159 | + | } | |
| 160 | + | b, err := readBuffer(bo, width, height, format, acceptedModifiers(modifiers)) | |
| 161 | + | if err != nil { | |
| 162 | + | boDestroy(bo) | |
| 163 | + | return nil, err | |
| 164 | + | } | |
| 165 | + | if onlyLinear && b.modifier == modInvalid { | |
| 166 | + | b.modifier = modLinear | |
| 167 | + | } | |
| 168 | + | return b, nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // acceptedModifiers is what an allocation may report for a request: the | |
| 172 | + | // request itself, plus an implicit layout when only linear was asked for. | |
| 173 | + | func acceptedModifiers(requested []uint64) []uint64 { | |
| 174 | + | if len(requested) == 1 && requested[0] == modLinear { | |
| 175 | + | return []uint64{modLinear, modInvalid} | |
| 176 | + | } | |
| 177 | + | return requested | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // readBuffer reads the layout of bo and validates it against the request. | |
| 181 | + | // It does not destroy bo on error. | |
| 182 | + | func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) { | |
| 183 | + | b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)} | |
| 184 | + | if err := checkModifier(modifiers, b.modifier); err != nil { | |
| 185 | + | return nil, err | |
| 186 | + | } | |
| 187 | + | n := int(boPlaneCount(bo)) | |
| 188 | + | if n < 1 || n > maxPlanes { | |
| 189 | + | return nil, fmt.Errorf("gbm: buffer has %d planes", n) | |
| 190 | + | } | |
| 191 | + | for i := range n { | |
| 192 | + | fd := boFDForPlane(bo, int32(i)) | |
| 193 | + | if fd < 0 { | |
| 194 | + | b.closeFDs() | |
| 195 | + | return nil, fmt.Errorf("gbm: no fd for plane %d", i) | |
| 196 | + | } | |
| 197 | + | b.planes = append(b.planes, plane{fd: int(fd), offset: boOffset(bo, int32(i)), stride: boStrideForPlane(bo, int32(i))}) | |
| 198 | + | } | |
| 199 | + | fds := make([]int, len(b.planes)) | |
| 200 | + | for i, p := range b.planes { | |
| 201 | + | fds[i] = p.fd | |
| 202 | + | } | |
| 203 | + | if err := checkOneObject(fds); err != nil { | |
| 204 | + | b.closeFDs() | |
| 205 | + | return nil, err | |
| 206 | + | } | |
| 207 | + | b.bo = bo | |
| 208 | + | return b, nil | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // checkModifier fails when the allocator returned a modifier that was not | |
| 212 | + | // asked for (including DRM_FORMAT_MOD_INVALID, an implicit layout the | |
| 213 | + | // renderer cannot import). | |
| 214 | + | func checkModifier(requested []uint64, got uint64) error { | |
| 215 | + | if !slices.Contains(requested, got) { | |
| 216 | + | return fmt.Errorf("gbm: modifier %#x not in the requested list %#x", got, requested) | |
| 217 | + | } | |
| 218 | + | return nil | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // checkOneObject fails when the plane fds are not all the same dmabuf: | |
| 222 | + | // disjoint planes cannot be imported as one memory object. | |
| 223 | + | func checkOneObject(fds []int) error { | |
| 224 | + | var first unix.Stat_t | |
| 225 | + | for i, fd := range fds { | |
| 226 | + | var st unix.Stat_t | |
| 227 | + | if err := unix.Fstat(fd, &st); err != nil { | |
| 228 | + | return fmt.Errorf("gbm: stat plane %d: %w", i, err) | |
| 229 | + | } | |
| 230 | + | if i == 0 { | |
| 231 | + | first = st | |
| 232 | + | continue | |
| 233 | + | } | |
| 234 | + | if st.Dev != first.Dev || st.Ino != first.Ino { | |
| 235 | + | return fmt.Errorf("gbm: plane %d is a different dmabuf than plane 0", i) | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | return nil | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func (b *Buffer) closeFDs() { | |
| 242 | + | for _, p := range b.planes { | |
| 243 | + | _ = unix.Close(p.fd) | |
| 244 | + | } | |
| 245 | + | b.planes = nil | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | // DMABuf returns the image as a dmabuf with fresh descriptors (the caller | |
| 249 | + | // closes them); the buffer keeps its own. | |
| 250 | + | func (b *Buffer) DMABuf() (ports.DMABuf, error) { | |
| 251 | + | if b == nil || b.bo == 0 { | |
| 252 | + | return ports.DMABuf{}, errors.New("gbm buffer closed") | |
| 253 | + | } | |
| 254 | + | out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier} | |
| 255 | + | for _, p := range b.planes { | |
| 256 | + | fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0) | |
| 257 | + | if err != nil { | |
| 258 | + | for _, q := range out.Planes { | |
| 259 | + | _ = q.File.Close() | |
| 260 | + | } | |
| 261 | + | return ports.DMABuf{}, fmt.Errorf("duplicate gbm plane fd: %w", err) | |
| 262 | + | } | |
| 263 | + | out.Planes = append(out.Planes, ports.DMABufPlane{File: os.NewFile(uintptr(fd), "neferwl-gbm"), Offset: p.offset, Stride: p.stride}) | |
| 264 | + | } | |
| 265 | + | return out, nil | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // Close frees the buffer and its descriptors. | |
| 269 | + | func (b *Buffer) Close() { | |
| 270 | + | if b == nil || b.bo == 0 { | |
| 271 | + | return | |
| 272 | + | } | |
| 273 | + | b.closeFDs() | |
| 274 | + | boDestroy(b.bo) | |
| 275 | + | b.bo = 0 | |
| 276 | + | } | |
| @@ -0,0 +1,276 @@ | |||||
| 1 | + | // Package gbm allocates dmabuf images with libgbm, loaded at runtime with | |||
| 2 | + | // purego. Display drivers know best how to lay out a buffer they will scan | |||
| 3 | + | // out (NVIDIA's block-linear layouts in particular), so the DRM output asks | |||
| 4 | + | // GBM for its images and the renderer imports them. | |||
| 5 | + | package gbm | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "errors" | |||
| 9 | + | "fmt" | |||
| 10 | + | "os" | |||
| 11 | + | "runtime" | |||
| 12 | + | "slices" | |||
| 13 | + | "sync" | |||
| 14 | + | "unsafe" | |||
| 15 | + | ||||
| 16 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 17 | + | "github.com/bnema/purego" | |||
| 18 | + | "golang.org/x/sys/unix" | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // Usage is a set of GBM_BO_USE_* flags. | |||
| 22 | + | type Usage uint32 | |||
| 23 | + | ||||
| 24 | + | const ( | |||
| 25 | + | // UseScanout (GBM_BO_USE_SCANOUT): the buffer can be shown by KMS. | |||
| 26 | + | UseScanout Usage = 1 << 0 | |||
| 27 | + | // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it. | |||
| 28 | + | UseRendering Usage = 1 << 2 | |||
| 29 | + | // useLinear (GBM_BO_USE_LINEAR): the layout is linear. | |||
| 30 | + | useLinear Usage = 1 << 4 | |||
| 31 | + | ) | |||
| 32 | + | ||||
| 33 | + | const ( | |||
| 34 | + | // modLinear is DRM_FORMAT_MOD_LINEAR. | |||
| 35 | + | modLinear = 0 | |||
| 36 | + | // modInvalid is DRM_FORMAT_MOD_INVALID: an implicit layout. | |||
| 37 | + | modInvalid = 1<<56 - 1 | |||
| 38 | + | // maxPlanes is the DRM limit of planes in a framebuffer. | |||
| 39 | + | maxPlanes = 4 | |||
| 40 | + | ) | |||
| 41 | + | ||||
| 42 | + | var ( | |||
| 43 | + | loadOnce sync.Once | |||
| 44 | + | loadErr error | |||
| 45 | + | ||||
| 46 | + | createDevice func(fd int32) uintptr | |||
| 47 | + | destroyDevice func(dev uintptr) | |||
| 48 | + | createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr | |||
| 49 | + | createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr | |||
| 50 | + | boModifier func(bo uintptr) uint64 | |||
| 51 | + | boPlaneCount func(bo uintptr) int32 | |||
| 52 | + | boFDForPlane func(bo uintptr, plane int32) int32 | |||
| 53 | + | boStrideForPlane func(bo uintptr, plane int32) uint32 | |||
| 54 | + | boOffset func(bo uintptr, plane int32) uint32 | |||
| 55 | + | boDestroy func(bo uintptr) | |||
| 56 | + | ) | |||
| 57 | + | ||||
| 58 | + | // load opens libgbm once. Without it, or with one that lacks a function | |||
| 59 | + | // below (gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane came with | |||
| 60 | + | // Mesa 21.1), the allocator is unavailable and callers fall back to another | |||
| 61 | + | // way to make images. | |||
| 62 | + | func load() error { | |||
| 63 | + | loadOnce.Do(func() { | |||
| 64 | + | lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL) | |||
| 65 | + | if err != nil { | |||
| 66 | + | loadErr = fmt.Errorf("open libgbm.so.1: %w", err) | |||
| 67 | + | return | |||
| 68 | + | } | |||
| 69 | + | defer func() { | |||
| 70 | + | if r := recover(); r != nil { | |||
| 71 | + | loadErr = fmt.Errorf("libgbm: %v", r) | |||
| 72 | + | } | |||
| 73 | + | }() | |||
| 74 | + | reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) } | |||
| 75 | + | reg(&createDevice, "gbm_create_device") | |||
| 76 | + | reg(&destroyDevice, "gbm_device_destroy") | |||
| 77 | + | reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2") | |||
| 78 | + | reg(&createPlain, "gbm_bo_create") | |||
| 79 | + | reg(&boModifier, "gbm_bo_get_modifier") | |||
| 80 | + | reg(&boPlaneCount, "gbm_bo_get_plane_count") | |||
| 81 | + | reg(&boFDForPlane, "gbm_bo_get_fd_for_plane") | |||
| 82 | + | reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane") | |||
| 83 | + | reg(&boOffset, "gbm_bo_get_offset") | |||
| 84 | + | reg(&boDestroy, "gbm_bo_destroy") | |||
| 85 | + | }) | |||
| 86 | + | return loadErr | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // Device is a GBM device on a duplicate of a DRM device fd. Close its | |||
| 90 | + | // buffers before the device. | |||
| 91 | + | type Device struct { | |||
| 92 | + | dev uintptr | |||
| 93 | + | fd int | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // Open creates a GBM device on a duplicate of fd, which stays the caller's. | |||
| 97 | + | func Open(fd int) (*Device, error) { | |||
| 98 | + | if err := load(); err != nil { | |||
| 99 | + | return nil, err | |||
| 100 | + | } | |||
| 101 | + | dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0) | |||
| 102 | + | if err != nil { | |||
| 103 | + | return nil, fmt.Errorf("duplicate drm fd: %w", err) | |||
| 104 | + | } | |||
| 105 | + | dev := createDevice(int32(dup)) | |||
| 106 | + | if dev == 0 { | |||
| 107 | + | _ = unix.Close(dup) | |||
| 108 | + | return nil, errors.New("gbm_create_device failed") | |||
| 109 | + | } | |||
| 110 | + | return &Device{dev: dev, fd: dup}, nil | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // Close destroys the device and its fd. | |||
| 114 | + | func (d *Device) Close() { | |||
| 115 | + | if d == nil || d.dev == 0 { | |||
| 116 | + | return | |||
| 117 | + | } | |||
| 118 | + | destroyDevice(d.dev) | |||
| 119 | + | _ = unix.Close(d.fd) | |||
| 120 | + | d.dev = 0 | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // Buffer is an allocated image. It owns one fd per plane and the GBM | |||
| 124 | + | // object, kept alive for as long as the image is used. | |||
| 125 | + | type Buffer struct { | |||
| 126 | + | bo uintptr | |||
| 127 | + | width, height int | |||
| 128 | + | format uint32 | |||
| 129 | + | modifier uint64 | |||
| 130 | + | planes []plane | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | type plane struct { | |||
| 134 | + | fd int | |||
| 135 | + | offset, stride uint32 | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // Create allocates an image whose modifier is one of modifiers (a | |||
| 139 | + | // non-empty list). A list holding only linear falls back to a plain | |||
| 140 | + | // linear allocation when GBM refuses the modifier call, and accepts an | |||
| 141 | + | // implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The | |||
| 142 | + | // result must be one memory object, because the renderer imports one | |||
| 143 | + | // dmabuf for all planes. | |||
| 144 | + | func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) { | |||
| 145 | + | if d == nil || d.dev == 0 { | |||
| 146 | + | return nil, errors.New("gbm device closed") | |||
| 147 | + | } | |||
| 148 | + | if width <= 0 || height <= 0 || len(modifiers) == 0 { | |||
| 149 | + | return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers)) | |||
| 150 | + | } | |||
| 151 | + | bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage)) | |||
| 152 | + | runtime.KeepAlive(modifiers) | |||
| 153 | + | onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear | |||
| 154 | + | if bo == 0 && onlyLinear { | |||
| 155 | + | bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear)) | |||
| 156 | + | } | |||
| 157 | + | if bo == 0 { | |||
| 158 | + | return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers) | |||
| 159 | + | } | |||
| 160 | + | b, err := readBuffer(bo, width, height, format, acceptedModifiers(modifiers)) | |||
| 161 | + | if err != nil { | |||
| 162 | + | boDestroy(bo) | |||
| 163 | + | return nil, err | |||
| 164 | + | } | |||
| 165 | + | if onlyLinear && b.modifier == modInvalid { | |||
| 166 | + | b.modifier = modLinear | |||
| 167 | + | } | |||
| 168 | + | return b, nil | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | // acceptedModifiers is what an allocation may report for a request: the | |||
| 172 | + | // request itself, plus an implicit layout when only linear was asked for. | |||
| 173 | + | func acceptedModifiers(requested []uint64) []uint64 { | |||
| 174 | + | if len(requested) == 1 && requested[0] == modLinear { | |||
| 175 | + | return []uint64{modLinear, modInvalid} | |||
| 176 | + | } | |||
| 177 | + | return requested | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // readBuffer reads the layout of bo and validates it against the request. | |||
| 181 | + | // It does not destroy bo on error. | |||
| 182 | + | func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) { | |||
| 183 | + | b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)} | |||
| 184 | + | if err := checkModifier(modifiers, b.modifier); err != nil { | |||
| 185 | + | return nil, err | |||
| 186 | + | } | |||
| 187 | + | n := int(boPlaneCount(bo)) | |||
| 188 | + | if n < 1 || n > maxPlanes { | |||
| 189 | + | return nil, fmt.Errorf("gbm: buffer has %d planes", n) | |||
| 190 | + | } | |||
| 191 | + | for i := range n { | |||
| 192 | + | fd := boFDForPlane(bo, int32(i)) | |||
| 193 | + | if fd < 0 { | |||
| 194 | + | b.closeFDs() | |||
| 195 | + | return nil, fmt.Errorf("gbm: no fd for plane %d", i) | |||
| 196 | + | } | |||
| 197 | + | b.planes = append(b.planes, plane{fd: int(fd), offset: boOffset(bo, int32(i)), stride: boStrideForPlane(bo, int32(i))}) | |||
| 198 | + | } | |||
| 199 | + | fds := make([]int, len(b.planes)) | |||
| 200 | + | for i, p := range b.planes { | |||
| 201 | + | fds[i] = p.fd | |||
| 202 | + | } | |||
| 203 | + | if err := checkOneObject(fds); err != nil { | |||
| 204 | + | b.closeFDs() | |||
| 205 | + | return nil, err | |||
| 206 | + | } | |||
| 207 | + | b.bo = bo | |||
| 208 | + | return b, nil | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // checkModifier fails when the allocator returned a modifier that was not | |||
| 212 | + | // asked for (including DRM_FORMAT_MOD_INVALID, an implicit layout the | |||
| 213 | + | // renderer cannot import). | |||
| 214 | + | func checkModifier(requested []uint64, got uint64) error { | |||
| 215 | + | if !slices.Contains(requested, got) { | |||
| 216 | + | return fmt.Errorf("gbm: modifier %#x not in the requested list %#x", got, requested) | |||
| 217 | + | } | |||
| 218 | + | return nil | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // checkOneObject fails when the plane fds are not all the same dmabuf: | |||
| 222 | + | // disjoint planes cannot be imported as one memory object. | |||
| 223 | + | func checkOneObject(fds []int) error { | |||
| 224 | + | var first unix.Stat_t | |||
| 225 | + | for i, fd := range fds { | |||
| 226 | + | var st unix.Stat_t | |||
| 227 | + | if err := unix.Fstat(fd, &st); err != nil { | |||
| 228 | + | return fmt.Errorf("gbm: stat plane %d: %w", i, err) | |||
| 229 | + | } | |||
| 230 | + | if i == 0 { | |||
| 231 | + | first = st | |||
| 232 | + | continue | |||
| 233 | + | } | |||
| 234 | + | if st.Dev != first.Dev || st.Ino != first.Ino { | |||
| 235 | + | return fmt.Errorf("gbm: plane %d is a different dmabuf than plane 0", i) | |||
| 236 | + | } | |||
| 237 | + | } | |||
| 238 | + | return nil | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func (b *Buffer) closeFDs() { | |||
| 242 | + | for _, p := range b.planes { | |||
| 243 | + | _ = unix.Close(p.fd) | |||
| 244 | + | } | |||
| 245 | + | b.planes = nil | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | // DMABuf returns the image as a dmabuf with fresh descriptors (the caller | |||
| 249 | + | // closes them); the buffer keeps its own. | |||
| 250 | + | func (b *Buffer) DMABuf() (ports.DMABuf, error) { | |||
| 251 | + | if b == nil || b.bo == 0 { | |||
| 252 | + | return ports.DMABuf{}, errors.New("gbm buffer closed") | |||
| 253 | + | } | |||
| 254 | + | out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier} | |||
| 255 | + | for _, p := range b.planes { | |||
| 256 | + | fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0) | |||
| 257 | + | if err != nil { | |||
| 258 | + | for _, q := range out.Planes { | |||
| 259 | + | _ = q.File.Close() | |||
| 260 | + | } | |||
| 261 | + | return ports.DMABuf{}, fmt.Errorf("duplicate gbm plane fd: %w", err) | |||
| 262 | + | } | |||
| 263 | + | out.Planes = append(out.Planes, ports.DMABufPlane{File: os.NewFile(uintptr(fd), "neferwl-gbm"), Offset: p.offset, Stride: p.stride}) | |||
| 264 | + | } | |||
| 265 | + | return out, nil | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | // Close frees the buffer and its descriptors. | |||
| 269 | + | func (b *Buffer) Close() { | |||
| 270 | + | if b == nil || b.bo == 0 { | |||
| 271 | + | return | |||
| 272 | + | } | |||
| 273 | + | b.closeFDs() | |||
| 274 | + | boDestroy(b.bo) | |||
| 275 | + | b.bo = 0 | |||
| 276 | + | } | |||
A internal/adapters/drm/gbm/gbm_test.go +88 -0
| @@ -0,0 +1,88 @@ | |||
| 1 | + | package gbm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // An allocator answer outside the request is refused, including an implicit | |
| 12 | + | // layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import; only a | |
| 13 | + | // linear-only request takes it, as linear. | |
| 14 | + | func TestCheckModifier(t *testing.T) { | |
| 15 | + | const tiled = 0x300000000606014 | |
| 16 | + | for _, tc := range []struct { | |
| 17 | + | name string | |
| 18 | + | requested []uint64 | |
| 19 | + | got uint64 | |
| 20 | + | ok bool | |
| 21 | + | }{ | |
| 22 | + | {"listed", []uint64{tiled, 0}, tiled, true}, | |
| 23 | + | {"linear listed", []uint64{tiled, 0}, 0, true}, | |
| 24 | + | {"not listed", []uint64{tiled}, 0, false}, | |
| 25 | + | {"other vendor", []uint64{tiled}, 0x200000000401b03, false}, | |
| 26 | + | {"empty list", nil, 0, false}, | |
| 27 | + | {"linear only takes linear", []uint64{0}, 0, true}, | |
| 28 | + | {"linear only takes invalid", []uint64{0}, modInvalid, true}, | |
| 29 | + | {"linear only refuses others", []uint64{0}, tiled, false}, | |
| 30 | + | {"tiled refuses invalid", []uint64{tiled}, modInvalid, false}, | |
| 31 | + | {"tiled or linear refuses invalid", []uint64{tiled, 0}, modInvalid, false}, | |
| 32 | + | {"0 and 5 refuses invalid", []uint64{0, 5}, modInvalid, false}, | |
| 33 | + | } { | |
| 34 | + | if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok { | |
| 35 | + | t.Errorf("%s: %v", tc.name, err) | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Planes must be one dmabuf: the renderer imports a single memory object. | |
| 41 | + | // Duplicates of one descriptor are the same inode; two files are not. | |
| 42 | + | func TestCheckOneObject(t *testing.T) { | |
| 43 | + | f, err := os.CreateTemp(t.TempDir(), "a") | |
| 44 | + | if err != nil { | |
| 45 | + | t.Fatal(err) | |
| 46 | + | } | |
| 47 | + | defer f.Close() | |
| 48 | + | g, err := os.CreateTemp(t.TempDir(), "b") | |
| 49 | + | if err != nil { | |
| 50 | + | t.Fatal(err) | |
| 51 | + | } | |
| 52 | + | defer g.Close() | |
| 53 | + | dup, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0) | |
| 54 | + | if err != nil { | |
| 55 | + | t.Fatal(err) | |
| 56 | + | } | |
| 57 | + | defer unix.Close(dup) | |
| 58 | + | fd := int(f.Fd()) | |
| 59 | + | if err := checkOneObject([]int{fd}); err != nil { | |
| 60 | + | t.Errorf("one plane: %v", err) | |
| 61 | + | } | |
| 62 | + | if err := checkOneObject([]int{fd, dup, fd}); err != nil { | |
| 63 | + | t.Errorf("shared planes: %v", err) | |
| 64 | + | } | |
| 65 | + | err = checkOneObject([]int{fd, int(g.Fd())}) | |
| 66 | + | if err == nil || !strings.Contains(err.Error(), "plane 1") { | |
| 67 | + | t.Errorf("disjoint planes: %v", err) | |
| 68 | + | } | |
| 69 | + | if err := checkOneObject([]int{fd, -1}); err == nil { | |
| 70 | + | t.Error("bad descriptor accepted") | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // A closed device and bad requests are refused without touching libgbm. | |
| 75 | + | func TestCreateRejectsBadRequests(t *testing.T) { | |
| 76 | + | if _, err := (*Device)(nil).Create(8, 8, 0, []uint64{0}, UseRendering); err == nil { | |
| 77 | + | t.Error("nil device accepted") | |
| 78 | + | } | |
| 79 | + | d := &Device{dev: 1} | |
| 80 | + | for _, tc := range []struct { | |
| 81 | + | w, h int | |
| 82 | + | mods []uint64 | |
| 83 | + | }{{0, 8, []uint64{0}}, {8, 0, []uint64{0}}, {8, 8, nil}} { | |
| 84 | + | if _, err := d.Create(tc.w, tc.h, 0, tc.mods, UseRendering); err == nil { | |
| 85 | + | t.Errorf("%dx%d with %d modifiers accepted", tc.w, tc.h, len(tc.mods)) | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | } | |
| @@ -0,0 +1,88 @@ | |||||
| 1 | + | package gbm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // An allocator answer outside the request is refused, including an implicit | |||
| 12 | + | // layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import; only a | |||
| 13 | + | // linear-only request takes it, as linear. | |||
| 14 | + | func TestCheckModifier(t *testing.T) { | |||
| 15 | + | const tiled = 0x300000000606014 | |||
| 16 | + | for _, tc := range []struct { | |||
| 17 | + | name string | |||
| 18 | + | requested []uint64 | |||
| 19 | + | got uint64 | |||
| 20 | + | ok bool | |||
| 21 | + | }{ | |||
| 22 | + | {"listed", []uint64{tiled, 0}, tiled, true}, | |||
| 23 | + | {"linear listed", []uint64{tiled, 0}, 0, true}, | |||
| 24 | + | {"not listed", []uint64{tiled}, 0, false}, | |||
| 25 | + | {"other vendor", []uint64{tiled}, 0x200000000401b03, false}, | |||
| 26 | + | {"empty list", nil, 0, false}, | |||
| 27 | + | {"linear only takes linear", []uint64{0}, 0, true}, | |||
| 28 | + | {"linear only takes invalid", []uint64{0}, modInvalid, true}, | |||
| 29 | + | {"linear only refuses others", []uint64{0}, tiled, false}, | |||
| 30 | + | {"tiled refuses invalid", []uint64{tiled}, modInvalid, false}, | |||
| 31 | + | {"tiled or linear refuses invalid", []uint64{tiled, 0}, modInvalid, false}, | |||
| 32 | + | {"0 and 5 refuses invalid", []uint64{0, 5}, modInvalid, false}, | |||
| 33 | + | } { | |||
| 34 | + | if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok { | |||
| 35 | + | t.Errorf("%s: %v", tc.name, err) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // Planes must be one dmabuf: the renderer imports a single memory object. | |||
| 41 | + | // Duplicates of one descriptor are the same inode; two files are not. | |||
| 42 | + | func TestCheckOneObject(t *testing.T) { | |||
| 43 | + | f, err := os.CreateTemp(t.TempDir(), "a") | |||
| 44 | + | if err != nil { | |||
| 45 | + | t.Fatal(err) | |||
| 46 | + | } | |||
| 47 | + | defer f.Close() | |||
| 48 | + | g, err := os.CreateTemp(t.TempDir(), "b") | |||
| 49 | + | if err != nil { | |||
| 50 | + | t.Fatal(err) | |||
| 51 | + | } | |||
| 52 | + | defer g.Close() | |||
| 53 | + | dup, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0) | |||
| 54 | + | if err != nil { | |||
| 55 | + | t.Fatal(err) | |||
| 56 | + | } | |||
| 57 | + | defer unix.Close(dup) | |||
| 58 | + | fd := int(f.Fd()) | |||
| 59 | + | if err := checkOneObject([]int{fd}); err != nil { | |||
| 60 | + | t.Errorf("one plane: %v", err) | |||
| 61 | + | } | |||
| 62 | + | if err := checkOneObject([]int{fd, dup, fd}); err != nil { | |||
| 63 | + | t.Errorf("shared planes: %v", err) | |||
| 64 | + | } | |||
| 65 | + | err = checkOneObject([]int{fd, int(g.Fd())}) | |||
| 66 | + | if err == nil || !strings.Contains(err.Error(), "plane 1") { | |||
| 67 | + | t.Errorf("disjoint planes: %v", err) | |||
| 68 | + | } | |||
| 69 | + | if err := checkOneObject([]int{fd, -1}); err == nil { | |||
| 70 | + | t.Error("bad descriptor accepted") | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // A closed device and bad requests are refused without touching libgbm. | |||
| 75 | + | func TestCreateRejectsBadRequests(t *testing.T) { | |||
| 76 | + | if _, err := (*Device)(nil).Create(8, 8, 0, []uint64{0}, UseRendering); err == nil { | |||
| 77 | + | t.Error("nil device accepted") | |||
| 78 | + | } | |||
| 79 | + | d := &Device{dev: 1} | |||
| 80 | + | for _, tc := range []struct { | |||
| 81 | + | w, h int | |||
| 82 | + | mods []uint64 | |||
| 83 | + | }{{0, 8, []uint64{0}}, {8, 0, []uint64{0}}, {8, 8, nil}} { | |||
| 84 | + | if _, err := d.Create(tc.w, tc.h, 0, tc.mods, UseRendering); err == nil { | |||
| 85 | + | t.Errorf("%dx%d with %d modifiers accepted", tc.w, tc.h, len(tc.mods)) | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | } | |||
M internal/adapters/drm/hdr_test.go +1 -0
| @@ -148,6 +148,7 @@ func TestHDRSettingsRestartComparison(t *testing.T) { | |||
| 148 | 148 | ||
| 149 | 149 | func TestHDRSuccessfulModeset(t *testing.T) { | |
| 150 | 150 | o, k, commits := testOutput(t) | |
| 151 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 151 | 152 | o.cursor = nil | |
| 152 | 153 | o.hdr.cap = hdrCapability{Capable: true} | |
| 153 | 154 | 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 = nil | 152 | o.cursor = nil | ||
| 152 | o.hdr.cap = hdrCapability{Capable: true} | 153 | o.hdr.cap = hdrCapability{Capable: true} | ||
| 153 | o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness} | 154 | o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: ports.DefaultSDRBrightness} | ||
A internal/adapters/drm/image_allocator_mock_test.go +158 -0
| @@ -0,0 +1,158 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package drm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockimageAllocator creates a new instance of mockimageAllocator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func newMockimageAllocator(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockimageAllocator { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockimageAllocator{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { | |
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | + | helper.Helper() | |
| 27 | + | } | |
| 28 | + | mock.AssertExpectations(t) | |
| 29 | + | }) | |
| 30 | + | ||
| 31 | + | return mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // mockimageAllocator is an autogenerated mock type for the imageAllocator type | |
| 35 | + | type mockimageAllocator struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockimageAllocator_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockimageAllocator) EXPECT() *mockimageAllocator_Expecter { | |
| 44 | + | return &mockimageAllocator_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // close provides a mock function for the type mockimageAllocator | |
| 48 | + | func (_mock *mockimageAllocator) close() { | |
| 49 | + | _mock.Called() | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // mockimageAllocator_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close' | |
| 54 | + | type mockimageAllocator_close_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // close is a helper method to define mock.On call | |
| 59 | + | func (_e *mockimageAllocator_Expecter) close() *mockimageAllocator_close_Call { | |
| 60 | + | return &mockimageAllocator_close_Call{Call: _e.mock.On("close")} | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func (_c *mockimageAllocator_close_Call) Run(run func()) *mockimageAllocator_close_Call { | |
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 65 | + | run() | |
| 66 | + | }) | |
| 67 | + | return _c | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *mockimageAllocator_close_Call) Return() *mockimageAllocator_close_Call { | |
| 71 | + | _c.Call.Return() | |
| 72 | + | return _c | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (_c *mockimageAllocator_close_Call) RunAndReturn(run func()) *mockimageAllocator_close_Call { | |
| 76 | + | _c.Run(run) | |
| 77 | + | return _c | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // create provides a mock function for the type mockimageAllocator | |
| 81 | + | func (_mock *mockimageAllocator) create(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | |
| 82 | + | ret := _mock.Called(width, height, format, modifiers) | |
| 83 | + | ||
| 84 | + | if len(ret) == 0 { | |
| 85 | + | panic("no return value specified for create") | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | var r0 imageBuffer | |
| 89 | + | var r1 error | |
| 90 | + | if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) (imageBuffer, error)); ok { | |
| 91 | + | return returnFunc(width, height, format, modifiers) | |
| 92 | + | } | |
| 93 | + | if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) imageBuffer); ok { | |
| 94 | + | r0 = returnFunc(width, height, format, modifiers) | |
| 95 | + | } else { | |
| 96 | + | if ret.Get(0) != nil { | |
| 97 | + | r0 = ret.Get(0).(imageBuffer) | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | if returnFunc, ok := ret.Get(1).(func(int, int, uint32, []uint64) error); ok { | |
| 101 | + | r1 = returnFunc(width, height, format, modifiers) | |
| 102 | + | } else { | |
| 103 | + | r1 = ret.Error(1) | |
| 104 | + | } | |
| 105 | + | return r0, r1 | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // mockimageAllocator_create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'create' | |
| 109 | + | type mockimageAllocator_create_Call struct { | |
| 110 | + | *mock.Call | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // create is a helper method to define mock.On call | |
| 114 | + | // - width int | |
| 115 | + | // - height int | |
| 116 | + | // - format uint32 | |
| 117 | + | // - modifiers []uint64 | |
| 118 | + | func (_e *mockimageAllocator_Expecter) create(width any, height any, format any, modifiers any) *mockimageAllocator_create_Call { | |
| 119 | + | return &mockimageAllocator_create_Call{Call: _e.mock.On("create", width, height, format, modifiers)} | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | func (_c *mockimageAllocator_create_Call) Run(run func(width int, height int, format uint32, modifiers []uint64)) *mockimageAllocator_create_Call { | |
| 123 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 124 | + | var arg0 int | |
| 125 | + | if args[0] != nil { | |
| 126 | + | arg0 = args[0].(int) | |
| 127 | + | } | |
| 128 | + | var arg1 int | |
| 129 | + | if args[1] != nil { | |
| 130 | + | arg1 = args[1].(int) | |
| 131 | + | } | |
| 132 | + | var arg2 uint32 | |
| 133 | + | if args[2] != nil { | |
| 134 | + | arg2 = args[2].(uint32) | |
| 135 | + | } | |
| 136 | + | var arg3 []uint64 | |
| 137 | + | if args[3] != nil { | |
| 138 | + | arg3 = args[3].([]uint64) | |
| 139 | + | } | |
| 140 | + | run( | |
| 141 | + | arg0, | |
| 142 | + | arg1, | |
| 143 | + | arg2, | |
| 144 | + | arg3, | |
| 145 | + | ) | |
| 146 | + | }) | |
| 147 | + | return _c | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func (_c *mockimageAllocator_create_Call) Return(imageBufferMoqParam imageBuffer, err error) *mockimageAllocator_create_Call { | |
| 151 | + | _c.Call.Return(imageBufferMoqParam, err) | |
| 152 | + | return _c | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func (_c *mockimageAllocator_create_Call) RunAndReturn(run func(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error)) *mockimageAllocator_create_Call { | |
| 156 | + | _c.Call.Return(run) | |
| 157 | + | return _c | |
| 158 | + | } | |
| @@ -0,0 +1,158 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package drm | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // newMockimageAllocator creates a new instance of mockimageAllocator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 12 | + | // The first argument is typically a *testing.T value. | |||
| 13 | + | func newMockimageAllocator(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *mockimageAllocator { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &mockimageAllocator{} | |||
| 22 | + | mock.Mock.Test(t) | |||
| 23 | + | ||||
| 24 | + | t.Cleanup(func() { | |||
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 26 | + | helper.Helper() | |||
| 27 | + | } | |||
| 28 | + | mock.AssertExpectations(t) | |||
| 29 | + | }) | |||
| 30 | + | ||||
| 31 | + | return mock | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // mockimageAllocator is an autogenerated mock type for the imageAllocator type | |||
| 35 | + | type mockimageAllocator struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type mockimageAllocator_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *mockimageAllocator) EXPECT() *mockimageAllocator_Expecter { | |||
| 44 | + | return &mockimageAllocator_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // close provides a mock function for the type mockimageAllocator | |||
| 48 | + | func (_mock *mockimageAllocator) close() { | |||
| 49 | + | _mock.Called() | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // mockimageAllocator_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close' | |||
| 54 | + | type mockimageAllocator_close_Call struct { | |||
| 55 | + | *mock.Call | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // close is a helper method to define mock.On call | |||
| 59 | + | func (_e *mockimageAllocator_Expecter) close() *mockimageAllocator_close_Call { | |||
| 60 | + | return &mockimageAllocator_close_Call{Call: _e.mock.On("close")} | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func (_c *mockimageAllocator_close_Call) Run(run func()) *mockimageAllocator_close_Call { | |||
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 65 | + | run() | |||
| 66 | + | }) | |||
| 67 | + | return _c | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (_c *mockimageAllocator_close_Call) Return() *mockimageAllocator_close_Call { | |||
| 71 | + | _c.Call.Return() | |||
| 72 | + | return _c | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (_c *mockimageAllocator_close_Call) RunAndReturn(run func()) *mockimageAllocator_close_Call { | |||
| 76 | + | _c.Run(run) | |||
| 77 | + | return _c | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // create provides a mock function for the type mockimageAllocator | |||
| 81 | + | func (_mock *mockimageAllocator) create(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | |||
| 82 | + | ret := _mock.Called(width, height, format, modifiers) | |||
| 83 | + | ||||
| 84 | + | if len(ret) == 0 { | |||
| 85 | + | panic("no return value specified for create") | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | var r0 imageBuffer | |||
| 89 | + | var r1 error | |||
| 90 | + | if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) (imageBuffer, error)); ok { | |||
| 91 | + | return returnFunc(width, height, format, modifiers) | |||
| 92 | + | } | |||
| 93 | + | if returnFunc, ok := ret.Get(0).(func(int, int, uint32, []uint64) imageBuffer); ok { | |||
| 94 | + | r0 = returnFunc(width, height, format, modifiers) | |||
| 95 | + | } else { | |||
| 96 | + | if ret.Get(0) != nil { | |||
| 97 | + | r0 = ret.Get(0).(imageBuffer) | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | if returnFunc, ok := ret.Get(1).(func(int, int, uint32, []uint64) error); ok { | |||
| 101 | + | r1 = returnFunc(width, height, format, modifiers) | |||
| 102 | + | } else { | |||
| 103 | + | r1 = ret.Error(1) | |||
| 104 | + | } | |||
| 105 | + | return r0, r1 | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // mockimageAllocator_create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'create' | |||
| 109 | + | type mockimageAllocator_create_Call struct { | |||
| 110 | + | *mock.Call | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // create is a helper method to define mock.On call | |||
| 114 | + | // - width int | |||
| 115 | + | // - height int | |||
| 116 | + | // - format uint32 | |||
| 117 | + | // - modifiers []uint64 | |||
| 118 | + | func (_e *mockimageAllocator_Expecter) create(width any, height any, format any, modifiers any) *mockimageAllocator_create_Call { | |||
| 119 | + | return &mockimageAllocator_create_Call{Call: _e.mock.On("create", width, height, format, modifiers)} | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | func (_c *mockimageAllocator_create_Call) Run(run func(width int, height int, format uint32, modifiers []uint64)) *mockimageAllocator_create_Call { | |||
| 123 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 124 | + | var arg0 int | |||
| 125 | + | if args[0] != nil { | |||
| 126 | + | arg0 = args[0].(int) | |||
| 127 | + | } | |||
| 128 | + | var arg1 int | |||
| 129 | + | if args[1] != nil { | |||
| 130 | + | arg1 = args[1].(int) | |||
| 131 | + | } | |||
| 132 | + | var arg2 uint32 | |||
| 133 | + | if args[2] != nil { | |||
| 134 | + | arg2 = args[2].(uint32) | |||
| 135 | + | } | |||
| 136 | + | var arg3 []uint64 | |||
| 137 | + | if args[3] != nil { | |||
| 138 | + | arg3 = args[3].([]uint64) | |||
| 139 | + | } | |||
| 140 | + | run( | |||
| 141 | + | arg0, | |||
| 142 | + | arg1, | |||
| 143 | + | arg2, | |||
| 144 | + | arg3, | |||
| 145 | + | ) | |||
| 146 | + | }) | |||
| 147 | + | return _c | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | func (_c *mockimageAllocator_create_Call) Return(imageBufferMoqParam imageBuffer, err error) *mockimageAllocator_create_Call { | |||
| 151 | + | _c.Call.Return(imageBufferMoqParam, err) | |||
| 152 | + | return _c | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | func (_c *mockimageAllocator_create_Call) RunAndReturn(run func(width int, height int, format uint32, modifiers []uint64) (imageBuffer, error)) *mockimageAllocator_create_Call { | |||
| 156 | + | _c.Call.Return(run) | |||
| 157 | + | return _c | |||
| 158 | + | } | |||
A internal/adapters/drm/image_buffer_mock_test.go +132 -0
| @@ -0,0 +1,132 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package drm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // newMockimageBuffer creates a new instance of mockimageBuffer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 13 | + | // The first argument is typically a *testing.T value. | |
| 14 | + | func newMockimageBuffer(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *mockimageBuffer { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &mockimageBuffer{} | |
| 23 | + | mock.Mock.Test(t) | |
| 24 | + | ||
| 25 | + | t.Cleanup(func() { | |
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 27 | + | helper.Helper() | |
| 28 | + | } | |
| 29 | + | mock.AssertExpectations(t) | |
| 30 | + | }) | |
| 31 | + | ||
| 32 | + | return mock | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // mockimageBuffer is an autogenerated mock type for the imageBuffer type | |
| 36 | + | type mockimageBuffer struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type mockimageBuffer_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *mockimageBuffer) EXPECT() *mockimageBuffer_Expecter { | |
| 45 | + | return &mockimageBuffer_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // close provides a mock function for the type mockimageBuffer | |
| 49 | + | func (_mock *mockimageBuffer) close() { | |
| 50 | + | _mock.Called() | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // mockimageBuffer_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close' | |
| 55 | + | type mockimageBuffer_close_Call struct { | |
| 56 | + | *mock.Call | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // close is a helper method to define mock.On call | |
| 60 | + | func (_e *mockimageBuffer_Expecter) close() *mockimageBuffer_close_Call { | |
| 61 | + | return &mockimageBuffer_close_Call{Call: _e.mock.On("close")} | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func (_c *mockimageBuffer_close_Call) Run(run func()) *mockimageBuffer_close_Call { | |
| 65 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 66 | + | run() | |
| 67 | + | }) | |
| 68 | + | return _c | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (_c *mockimageBuffer_close_Call) Return() *mockimageBuffer_close_Call { | |
| 72 | + | _c.Call.Return() | |
| 73 | + | return _c | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func (_c *mockimageBuffer_close_Call) RunAndReturn(run func()) *mockimageBuffer_close_Call { | |
| 77 | + | _c.Run(run) | |
| 78 | + | return _c | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // dmabuf provides a mock function for the type mockimageBuffer | |
| 82 | + | func (_mock *mockimageBuffer) dmabuf() (ports.DMABuf, error) { | |
| 83 | + | ret := _mock.Called() | |
| 84 | + | ||
| 85 | + | if len(ret) == 0 { | |
| 86 | + | panic("no return value specified for dmabuf") | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | var r0 ports.DMABuf | |
| 90 | + | var r1 error | |
| 91 | + | if returnFunc, ok := ret.Get(0).(func() (ports.DMABuf, error)); ok { | |
| 92 | + | return returnFunc() | |
| 93 | + | } | |
| 94 | + | if returnFunc, ok := ret.Get(0).(func() ports.DMABuf); ok { | |
| 95 | + | r0 = returnFunc() | |
| 96 | + | } else { | |
| 97 | + | r0 = ret.Get(0).(ports.DMABuf) | |
| 98 | + | } | |
| 99 | + | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| 100 | + | r1 = returnFunc() | |
| 101 | + | } else { | |
| 102 | + | r1 = ret.Error(1) | |
| 103 | + | } | |
| 104 | + | return r0, r1 | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // mockimageBuffer_dmabuf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'dmabuf' | |
| 108 | + | type mockimageBuffer_dmabuf_Call struct { | |
| 109 | + | *mock.Call | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // dmabuf is a helper method to define mock.On call | |
| 113 | + | func (_e *mockimageBuffer_Expecter) dmabuf() *mockimageBuffer_dmabuf_Call { | |
| 114 | + | return &mockimageBuffer_dmabuf_Call{Call: _e.mock.On("dmabuf")} | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func (_c *mockimageBuffer_dmabuf_Call) Run(run func()) *mockimageBuffer_dmabuf_Call { | |
| 118 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 119 | + | run() | |
| 120 | + | }) | |
| 121 | + | return _c | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | func (_c *mockimageBuffer_dmabuf_Call) Return(dMABuf ports.DMABuf, err error) *mockimageBuffer_dmabuf_Call { | |
| 125 | + | _c.Call.Return(dMABuf, err) | |
| 126 | + | return _c | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func (_c *mockimageBuffer_dmabuf_Call) RunAndReturn(run func() (ports.DMABuf, error)) *mockimageBuffer_dmabuf_Call { | |
| 130 | + | _c.Call.Return(run) | |
| 131 | + | return _c | |
| 132 | + | } | |
| @@ -0,0 +1,132 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package drm | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | mock "github.com/stretchr/testify/mock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // newMockimageBuffer creates a new instance of mockimageBuffer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 13 | + | // The first argument is typically a *testing.T value. | |||
| 14 | + | func newMockimageBuffer(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *mockimageBuffer { | |||
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 19 | + | helper.Helper() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mock := &mockimageBuffer{} | |||
| 23 | + | mock.Mock.Test(t) | |||
| 24 | + | ||||
| 25 | + | t.Cleanup(func() { | |||
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 27 | + | helper.Helper() | |||
| 28 | + | } | |||
| 29 | + | mock.AssertExpectations(t) | |||
| 30 | + | }) | |||
| 31 | + | ||||
| 32 | + | return mock | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // mockimageBuffer is an autogenerated mock type for the imageBuffer type | |||
| 36 | + | type mockimageBuffer struct { | |||
| 37 | + | mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | type mockimageBuffer_Expecter struct { | |||
| 41 | + | mock *mock.Mock | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (_m *mockimageBuffer) EXPECT() *mockimageBuffer_Expecter { | |||
| 45 | + | return &mockimageBuffer_Expecter{mock: &_m.Mock} | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // close provides a mock function for the type mockimageBuffer | |||
| 49 | + | func (_mock *mockimageBuffer) close() { | |||
| 50 | + | _mock.Called() | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // mockimageBuffer_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close' | |||
| 55 | + | type mockimageBuffer_close_Call struct { | |||
| 56 | + | *mock.Call | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // close is a helper method to define mock.On call | |||
| 60 | + | func (_e *mockimageBuffer_Expecter) close() *mockimageBuffer_close_Call { | |||
| 61 | + | return &mockimageBuffer_close_Call{Call: _e.mock.On("close")} | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func (_c *mockimageBuffer_close_Call) Run(run func()) *mockimageBuffer_close_Call { | |||
| 65 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 66 | + | run() | |||
| 67 | + | }) | |||
| 68 | + | return _c | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func (_c *mockimageBuffer_close_Call) Return() *mockimageBuffer_close_Call { | |||
| 72 | + | _c.Call.Return() | |||
| 73 | + | return _c | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func (_c *mockimageBuffer_close_Call) RunAndReturn(run func()) *mockimageBuffer_close_Call { | |||
| 77 | + | _c.Run(run) | |||
| 78 | + | return _c | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // dmabuf provides a mock function for the type mockimageBuffer | |||
| 82 | + | func (_mock *mockimageBuffer) dmabuf() (ports.DMABuf, error) { | |||
| 83 | + | ret := _mock.Called() | |||
| 84 | + | ||||
| 85 | + | if len(ret) == 0 { | |||
| 86 | + | panic("no return value specified for dmabuf") | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | var r0 ports.DMABuf | |||
| 90 | + | var r1 error | |||
| 91 | + | if returnFunc, ok := ret.Get(0).(func() (ports.DMABuf, error)); ok { | |||
| 92 | + | return returnFunc() | |||
| 93 | + | } | |||
| 94 | + | if returnFunc, ok := ret.Get(0).(func() ports.DMABuf); ok { | |||
| 95 | + | r0 = returnFunc() | |||
| 96 | + | } else { | |||
| 97 | + | r0 = ret.Get(0).(ports.DMABuf) | |||
| 98 | + | } | |||
| 99 | + | if returnFunc, ok := ret.Get(1).(func() error); ok { | |||
| 100 | + | r1 = returnFunc() | |||
| 101 | + | } else { | |||
| 102 | + | r1 = ret.Error(1) | |||
| 103 | + | } | |||
| 104 | + | return r0, r1 | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // mockimageBuffer_dmabuf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'dmabuf' | |||
| 108 | + | type mockimageBuffer_dmabuf_Call struct { | |||
| 109 | + | *mock.Call | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // dmabuf is a helper method to define mock.On call | |||
| 113 | + | func (_e *mockimageBuffer_Expecter) dmabuf() *mockimageBuffer_dmabuf_Call { | |||
| 114 | + | return &mockimageBuffer_dmabuf_Call{Call: _e.mock.On("dmabuf")} | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (_c *mockimageBuffer_dmabuf_Call) Run(run func()) *mockimageBuffer_dmabuf_Call { | |||
| 118 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 119 | + | run() | |||
| 120 | + | }) | |||
| 121 | + | return _c | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | func (_c *mockimageBuffer_dmabuf_Call) Return(dMABuf ports.DMABuf, err error) *mockimageBuffer_dmabuf_Call { | |||
| 125 | + | _c.Call.Return(dMABuf, err) | |||
| 126 | + | return _c | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func (_c *mockimageBuffer_dmabuf_Call) RunAndReturn(run func() (ports.DMABuf, error)) *mockimageBuffer_dmabuf_Call { | |||
| 130 | + | _c.Call.Return(run) | |||
| 131 | + | return _c | |||
| 132 | + | } | |||
M internal/adapters/drm/images.go +168 -37
| @@ -13,13 +13,21 @@ import ( | |||
| 13 | 13 | ||
| 14 | 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 | 19 | type imageKind int | |
| 18 | 20 | ||
| 19 | 21 | 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" | |
| 23 | 31 | ) | |
| 24 | 32 | ||
| 25 | 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 | 67 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR") | |
| 60 | 68 | o.hdr.failed = true | |
| 61 | 69 | o.hdr.on = false | |
| 62 | - | o.freeImages() | |
| 70 | + | o.dropImages(r) | |
| 63 | 71 | r.SetHDR(0) | |
| 64 | 72 | _, _ = r.ExportTargets(0, nil, false) | |
| 65 | 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 | 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 | 90 | for { | |
| 80 | - | got, err := o.setupImages(r, kind, cause) | |
| 91 | + | got, err := o.setupImages(r, kind, cause, skipGBM) | |
| 81 | 92 | if err != nil { | |
| 82 | 93 | return err | |
| 83 | 94 | } | |
| @@ -97,19 +108,31 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||
| 97 | 108 | // are fine; retry the protected modeset later. | |
| 98 | 109 | return protectedCommitError{err} | |
| 99 | 110 | } | |
| 100 | - | if !refused(err) || got == imagesLinear { | |
| 101 | - | o.freeImages() | |
| 111 | + | if !refused(err) { | |
| 112 | + | o.dropImages(r) | |
| 113 | + | return err | |
| 114 | + | } | |
| 115 | + | if len(o.images) > 0 { | |
| 116 | + | // GBM images (the renderer imported them). | |
| 117 | + | // The renderer's own images of this kind may still pass. | |
| 118 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images") | |
| 119 | + | o.dropImages(r) | |
| 120 | + | kind, cause, skipGBM = got, err, true | |
| 121 | + | continue | |
| 122 | + | } | |
| 123 | + | if got == imagesLinear { | |
| 124 | + | o.dropImages(r) | |
| 102 | 125 | return err | |
| 103 | 126 | } | |
| 104 | 127 | if o.imageMod == 0 { | |
| 105 | 128 | // The refused images were already linear: the later kinds | |
| 106 | 129 | // would export the same ones again. | |
| 107 | - | o.freeImages() | |
| 130 | + | o.dropImages(r) | |
| 108 | 131 | return err | |
| 109 | 132 | } | |
| 110 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | |
| 111 | - | o.freeImages() | |
| 112 | - | kind, cause = got+1, err | |
| 133 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocVulkan).Msg("modeset refused the output images") | |
| 134 | + | o.dropImages(r) | |
| 135 | + | kind, cause, skipGBM = got+1, err, false | |
| 113 | 136 | if kind == imagesSinglePlane && o.planes == 1 { | |
| 114 | 137 | // The refused images already had one plane: single-plane | |
| 115 | 138 | // images would be the same ones. | |
| @@ -119,11 +142,15 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||
| 119 | 142 | } | |
| 120 | 143 | ||
| 121 | 144 | // setupImages gives the output the two renderer images it flips between, | |
| 122 | - | // exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that | |
| 123 | - | // cannot export a scanout-capable image cannot drive the output. | |
| 124 | - | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | |
| 145 | + | // as dmabufs: allocated with GBM and imported by the renderer, else | |
| 146 | + | // allocated and exported by the renderer, for each kind in turn. skipGBM | |
| 147 | + | // leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a | |
| 148 | + | // GPU that cannot export a scanout-capable image cannot drive the output. | |
| 149 | + | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) { | |
| 125 | 150 | err := cause | |
| 151 | + | start := kind | |
| 126 | 152 | for ; kind <= imagesLinear; kind++ { | |
| 153 | + | noGBM := skipGBM && kind == start | |
| 127 | 154 | if o.runContext != nil && o.runContext.Err() != nil { | |
| 128 | 155 | return kind, o.runContext.Err() | |
| 129 | 156 | } | |
| @@ -159,10 +186,20 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima | |||
| 159 | 186 | // attempt was the single-plane one. | |
| 160 | 187 | continue | |
| 161 | 188 | } | |
| 189 | + | if !noGBM && o.openAlloc != nil { | |
| 190 | + | gerr := o.gbmImages(r, mods, kind == imagesSinglePlane) | |
| 191 | + | if gerr == nil { | |
| 192 | + | return kind, nil | |
| 193 | + | } | |
| 194 | + | o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation") | |
| 195 | + | } | |
| 196 | + | // Deprecated fallback (neferwl#23): renderer-exported images, kept | |
| 197 | + | // while GBM is validated on few setups. Remove once no report shows | |
| 198 | + | // "alloc":"vulkan" for a few releases. | |
| 162 | 199 | if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil { | |
| 163 | 200 | return kind, nil | |
| 164 | 201 | } | |
| 165 | - | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export") | |
| 202 | + | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export") | |
| 166 | 203 | } | |
| 167 | 204 | return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err) | |
| 168 | 205 | } | |
| @@ -187,32 +224,69 @@ func (o *Output) primaryModifiers(format uint32) []uint64 { | |||
| 187 | 224 | return mods | |
| 188 | 225 | } | |
| 189 | 226 | ||
| 227 | + | // closeBufs closes every plane file of bufs. The callers own them: the | |
| 228 | + | // renderer and the framebuffers keep their own references to the memory. | |
| 229 | + | func closeBufs(bufs []ports.DMABuf) { | |
| 230 | + | for _, b := range bufs { | |
| 231 | + | for _, p := range b.Planes { | |
| 232 | + | _ = p.File.Close() | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 190 | 237 | // exportImages makes the renderer's exported targets the output images. | |
| 238 | + | // | |
| 239 | + | // Deprecated fallback for gbmImages, to remove with neferwl#23. Exported | |
| 240 | + | // Vulkan images corrupt scanout on nvidia-drm. | |
| 191 | 241 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | |
| 242 | + | o.dropImages(r) | |
| 192 | 243 | bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane) | |
| 193 | 244 | if err != nil { | |
| 194 | 245 | return err | |
| 195 | 246 | } | |
| 247 | + | defer closeBufs(bufs) | |
| 248 | + | if err = o.showBufs(r, bufs, allocVulkan); err != nil { | |
| 249 | + | o.dropImages(r) | |
| 250 | + | _, _ = r.ExportTargets(0, nil, false) | |
| 251 | + | return err | |
| 252 | + | } | |
| 253 | + | return nil | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | // showBufs makes framebuffers of the output images bufs (already the | |
| 257 | + | // renderer's targets), clears them and records their layout. The caller | |
| 258 | + | // closes the plane files and drops the images on error. | |
| 259 | + | func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error { | |
| 196 | 260 | o.planes, o.imageMod = 0, 0 | |
| 197 | 261 | for i := range bufs { | |
| 198 | - | if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes { | |
| 199 | - | err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes))) | |
| 200 | - | } | |
| 201 | - | if err == nil { | |
| 202 | - | o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()) | |
| 262 | + | if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes { | |
| 263 | + | return fmt.Errorf("output image has %d planes", n) | |
| 203 | 264 | } | |
| 204 | - | // Every plane owns its file; the framebuffer keeps its own | |
| 205 | - | // reference to the buffer. | |
| 206 | - | for _, p := range bufs[i].Planes { | |
| 207 | - | p.File.Close() | |
| 265 | + | fb, err := o.k.addFB(&bufs[i], o.imageFormat()) | |
| 266 | + | if err != nil { | |
| 267 | + | return err | |
| 208 | 268 | } | |
| 269 | + | o.fbs[i] = fb | |
| 270 | + | } | |
| 271 | + | if err := o.clearImages(r); err != nil { | |
| 272 | + | return err | |
| 209 | 273 | } | |
| 210 | - | // GPU memory starts undefined (old VRAM contents): clear both images | |
| 211 | - | // before the modeset shows one. | |
| 274 | + | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | |
| 275 | + | ev := o.log.Info() | |
| 276 | + | if alloc == allocVulkan { | |
| 277 | + | // The deprecated fallback is in use: make it visible in reports. | |
| 278 | + | ev = o.log.Warn() | |
| 279 | + | } | |
| 280 | + | ev.Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output") | |
| 281 | + | return nil | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | // clearImages draws black into both output images and waits for the GPU. | |
| 285 | + | // GPU memory starts undefined (old VRAM contents): both are cleared before | |
| 286 | + | // the modeset shows one. | |
| 287 | + | func (o *Output) clearImages(r ports.Renderer) error { | |
| 288 | + | var err error | |
| 212 | 289 | for i := range o.fbs { | |
| 213 | - | if err != nil { | |
| 214 | - | break | |
| 215 | - | } | |
| 216 | 290 | r.UseTarget(i) | |
| 217 | 291 | var done *os.File | |
| 218 | 292 | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | |
| @@ -224,18 +298,71 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) | |||
| 224 | 298 | err = errors.Join(err, syncfile.Wait(waitCtx, done)) | |
| 225 | 299 | done.Close() | |
| 226 | 300 | } | |
| 301 | + | if err != nil { | |
| 302 | + | return err | |
| 303 | + | } | |
| 227 | 304 | } | |
| 228 | - | if err != nil { | |
| 229 | - | o.freeImages() | |
| 230 | - | _, _ = r.ExportTargets(0, nil, false) | |
| 305 | + | return nil | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // gbmImages allocates the output images with GBM, makes them the | |
| 309 | + | // renderer's targets and framebuffers, and clears them. The modifiers are | |
| 310 | + | // those the plane lists (mods, as for exportImages) that the renderer can | |
| 311 | + | // render into and import. On failure nothing is left allocated. | |
| 312 | + | func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) { | |
| 313 | + | o.dropImages(r) | |
| 314 | + | if o.alloc == nil { | |
| 315 | + | if o.allocErr != nil { | |
| 316 | + | return o.allocErr | |
| 317 | + | } | |
| 318 | + | if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil { | |
| 319 | + | o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images") | |
| 320 | + | return o.allocErr | |
| 321 | + | } | |
| 322 | + | } | |
| 323 | + | usable := r.TargetModifiers(mods, singlePlane) | |
| 324 | + | if len(usable) == 0 { | |
| 325 | + | return errors.New("no modifier both the plane and the renderer accept for GBM images") | |
| 326 | + | } | |
| 327 | + | var bufs []ports.DMABuf | |
| 328 | + | defer func() { | |
| 329 | + | closeBufs(bufs) | |
| 330 | + | if err != nil { | |
| 331 | + | o.dropImages(r) | |
| 332 | + | } | |
| 333 | + | }() | |
| 334 | + | for range o.fbs { | |
| 335 | + | img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable) | |
| 336 | + | if cerr != nil { | |
| 337 | + | return cerr | |
| 338 | + | } | |
| 339 | + | o.images = append(o.images, img) | |
| 340 | + | b, derr := img.dmabuf() | |
| 341 | + | if derr != nil { | |
| 342 | + | return derr | |
| 343 | + | } | |
| 344 | + | bufs = append(bufs, b) | |
| 345 | + | } | |
| 346 | + | if err = r.ImportTargets(bufs); err != nil { | |
| 231 | 347 | return err | |
| 232 | 348 | } | |
| 233 | - | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | |
| 234 | - | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output") | |
| 235 | - | return nil | |
| 349 | + | return o.showBufs(r, bufs, allocGBM) | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // dropImages frees the output images: their framebuffers, the GBM buffers | |
| 353 | + | // and the renderer targets built over them (they would keep the memory | |
| 354 | + | // alive after the buffers are gone). It is a no-op when there are none. | |
| 355 | + | func (o *Output) dropImages(r ports.Renderer) { | |
| 356 | + | if len(o.images) > 0 { | |
| 357 | + | _ = r.ImportTargets(nil) | |
| 358 | + | } | |
| 359 | + | o.freeImages() | |
| 236 | 360 | } | |
| 237 | 361 | ||
| 238 | - | // freeImages removes the output images' framebuffers. | |
| 362 | + | // freeImages removes the output images' framebuffers and frees the images | |
| 363 | + | // GBM allocated. Callers that still have a renderer use dropImages: this | |
| 364 | + | // alone leaves the renderer's targets over the freed images (the renderer | |
| 365 | + | // drops them when it closes, before Close frees the output's images). | |
| 239 | 366 | func (o *Output) freeImages() { | |
| 240 | 367 | o.front.forget() | |
| 241 | 368 | o.forgetOverlayProbes() | |
| @@ -245,4 +372,8 @@ func (o *Output) freeImages() { | |||
| 245 | 372 | } | |
| 246 | 373 | o.fbs[i] = 0 | |
| 247 | 374 | } | |
| 375 | + | for _, img := range o.images { | |
| 376 | + | img.close() | |
| 377 | + | } | |
| 378 | + | o.images = nil | |
| 248 | 379 | } | |
| @@ -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 int | 19 | 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 plane | 23 | + | imagesSinglePlane // driver modifier with one memory plane |
| 22 | - | imagesLinear // exported, linear | 24 | + | 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 = true | 68 | o.hdr.failed = true | ||
| 61 | o.hdr.on = false | 69 | 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 err | 93 | return err | ||
| 83 | } | 94 | } | ||
| @@ -97,19 +108,31 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||||
| 97 | // are fine; retry the protected modeset later. | 108 | // are fine; retry the protected modeset later. | ||
| 98 | return protectedCommitError{err} | 109 | return protectedCommitError{err} | ||
| 99 | } | 110 | } | ||
| 100 | - | if !refused(err) || got == imagesLinear { | 111 | + | if !refused(err) { |
| 101 | - | o.freeImages() | 112 | + | o.dropImages(r) |
| 113 | + | return err | |||
| 114 | + | } | |||
| 115 | + | if len(o.images) > 0 { | |||
| 116 | + | // GBM images (the renderer imported them). | |||
| 117 | + | // The renderer's own images of this kind may still pass. | |||
| 118 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images") | |||
| 119 | + | o.dropImages(r) | |||
| 120 | + | kind, cause, skipGBM = got, err, true | |||
| 121 | + | continue | |||
| 122 | + | } | |||
| 123 | + | if got == imagesLinear { | |||
| 124 | + | o.dropImages(r) | |||
| 102 | return err | 125 | return err | ||
| 103 | } | 126 | } | ||
| 104 | if o.imageMod == 0 { | 127 | if o.imageMod == 0 { | ||
| 105 | // The refused images were already linear: the later kinds | 128 | // The refused images were already linear: the later kinds | ||
| 106 | // would export the same ones again. | 129 | // would export the same ones again. | ||
| 107 | - | o.freeImages() | 130 | + | o.dropImages(r) |
| 108 | return err | 131 | return err | ||
| 109 | } | 132 | } | ||
| 110 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | 133 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocVulkan).Msg("modeset refused the output images") |
| 111 | - | o.freeImages() | 134 | + | o.dropImages(r) |
| 112 | - | kind, cause = got+1, err | 135 | + | kind, cause, skipGBM = got+1, err, false |
| 113 | if kind == imagesSinglePlane && o.planes == 1 { | 136 | if kind == imagesSinglePlane && o.planes == 1 { | ||
| 114 | // The refused images already had one plane: single-plane | 137 | // The refused images already had one plane: single-plane | ||
| 115 | // images would be the same ones. | 138 | // images would be the same ones. | ||
| @@ -119,11 +142,15 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||||
| 119 | } | 142 | } | ||
| 120 | 143 | ||||
| 121 | // setupImages gives the output the two renderer images it flips between, | 144 | // setupImages gives the output the two renderer images it flips between, | ||
| 122 | - | // exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that | 145 | + | // as dmabufs: allocated with GBM and imported by the renderer, else |
| 123 | - | // cannot export a scanout-capable image cannot drive the output. | 146 | + | // allocated and exported by the renderer, for each kind in turn. skipGBM |
| 124 | - | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | 147 | + | // leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a |
| 148 | + | // GPU that cannot export a scanout-capable image cannot drive the output. | |||
| 149 | + | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) { | |||
| 125 | err := cause | 150 | err := cause | ||
| 151 | + | start := kind | |||
| 126 | for ; kind <= imagesLinear; kind++ { | 152 | for ; kind <= imagesLinear; kind++ { | ||
| 153 | + | noGBM := skipGBM && kind == start | |||
| 127 | if o.runContext != nil && o.runContext.Err() != nil { | 154 | if o.runContext != nil && o.runContext.Err() != nil { | ||
| 128 | return kind, o.runContext.Err() | 155 | return kind, o.runContext.Err() | ||
| 129 | } | 156 | } | ||
| @@ -159,10 +186,20 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima | |||||
| 159 | // attempt was the single-plane one. | 186 | // attempt was the single-plane one. | ||
| 160 | continue | 187 | continue | ||
| 161 | } | 188 | } | ||
| 189 | + | if !noGBM && o.openAlloc != nil { | |||
| 190 | + | gerr := o.gbmImages(r, mods, kind == imagesSinglePlane) | |||
| 191 | + | if gerr == nil { | |||
| 192 | + | return kind, nil | |||
| 193 | + | } | |||
| 194 | + | o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation") | |||
| 195 | + | } | |||
| 196 | + | // Deprecated fallback (neferwl#23): renderer-exported images, kept | |||
| 197 | + | // while GBM is validated on few setups. Remove once no report shows | |||
| 198 | + | // "alloc":"vulkan" for a few releases. | |||
| 162 | if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil { | 199 | if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil { | ||
| 163 | return kind, nil | 200 | return kind, nil | ||
| 164 | } | 201 | } | ||
| 165 | - | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export") | 202 | + | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export") |
| 166 | } | 203 | } | ||
| 167 | return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err) | 204 | return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err) | ||
| 168 | } | 205 | } | ||
| @@ -187,32 +224,69 @@ func (o *Output) primaryModifiers(format uint32) []uint64 { | |||||
| 187 | return mods | 224 | return mods | ||
| 188 | } | 225 | } | ||
| 189 | 226 | ||||
| 227 | + | // closeBufs closes every plane file of bufs. The callers own them: the | |||
| 228 | + | // renderer and the framebuffers keep their own references to the memory. | |||
| 229 | + | func closeBufs(bufs []ports.DMABuf) { | |||
| 230 | + | for _, b := range bufs { | |||
| 231 | + | for _, p := range b.Planes { | |||
| 232 | + | _ = p.File.Close() | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | } | |||
| 236 | + | ||||
| 190 | // exportImages makes the renderer's exported targets the output images. | 237 | // exportImages makes the renderer's exported targets the output images. | ||
| 238 | + | // | |||
| 239 | + | // Deprecated fallback for gbmImages, to remove with neferwl#23. Exported | |||
| 240 | + | // Vulkan images corrupt scanout on nvidia-drm. | |||
| 191 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | 241 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | ||
| 242 | + | o.dropImages(r) | |||
| 192 | bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane) | 243 | bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane) | ||
| 193 | if err != nil { | 244 | if err != nil { | ||
| 194 | return err | 245 | return err | ||
| 195 | } | 246 | } | ||
| 247 | + | defer closeBufs(bufs) | |||
| 248 | + | if err = o.showBufs(r, bufs, allocVulkan); err != nil { | |||
| 249 | + | o.dropImages(r) | |||
| 250 | + | _, _ = r.ExportTargets(0, nil, false) | |||
| 251 | + | return err | |||
| 252 | + | } | |||
| 253 | + | return nil | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | // showBufs makes framebuffers of the output images bufs (already the | |||
| 257 | + | // renderer's targets), clears them and records their layout. The caller | |||
| 258 | + | // closes the plane files and drops the images on error. | |||
| 259 | + | func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error { | |||
| 196 | o.planes, o.imageMod = 0, 0 | 260 | o.planes, o.imageMod = 0, 0 | ||
| 197 | for i := range bufs { | 261 | for i := range bufs { | ||
| 198 | - | if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes { | 262 | + | if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes { |
| 199 | - | err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes))) | 263 | + | return fmt.Errorf("output image has %d planes", n) |
| 200 | - | } | |||
| 201 | - | if err == nil { | |||
| 202 | - | o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()) | |||
| 203 | } | 264 | } | ||
| 204 | - | // Every plane owns its file; the framebuffer keeps its own | 265 | + | fb, err := o.k.addFB(&bufs[i], o.imageFormat()) |
| 205 | - | // reference to the buffer. | 266 | + | if err != nil { |
| 206 | - | for _, p := range bufs[i].Planes { | 267 | + | return err |
| 207 | - | p.File.Close() | |||
| 208 | } | 268 | } | ||
| 269 | + | o.fbs[i] = fb | |||
| 270 | + | } | |||
| 271 | + | if err := o.clearImages(r); err != nil { | |||
| 272 | + | return err | |||
| 209 | } | 273 | } | ||
| 210 | - | // GPU memory starts undefined (old VRAM contents): clear both images | 274 | + | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier |
| 211 | - | // before the modeset shows one. | 275 | + | ev := o.log.Info() |
| 276 | + | if alloc == allocVulkan { | |||
| 277 | + | // The deprecated fallback is in use: make it visible in reports. | |||
| 278 | + | ev = o.log.Warn() | |||
| 279 | + | } | |||
| 280 | + | ev.Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output") | |||
| 281 | + | return nil | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | // clearImages draws black into both output images and waits for the GPU. | |||
| 285 | + | // GPU memory starts undefined (old VRAM contents): both are cleared before | |||
| 286 | + | // the modeset shows one. | |||
| 287 | + | func (o *Output) clearImages(r ports.Renderer) error { | |||
| 288 | + | var err error | |||
| 212 | for i := range o.fbs { | 289 | for i := range o.fbs { | ||
| 213 | - | if err != nil { | |||
| 214 | - | break | |||
| 215 | - | } | |||
| 216 | r.UseTarget(i) | 290 | r.UseTarget(i) | ||
| 217 | var done *os.File | 291 | var done *os.File | ||
| 218 | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | 292 | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | ||
| @@ -224,18 +298,71 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) | |||||
| 224 | err = errors.Join(err, syncfile.Wait(waitCtx, done)) | 298 | err = errors.Join(err, syncfile.Wait(waitCtx, done)) | ||
| 225 | done.Close() | 299 | done.Close() | ||
| 226 | } | 300 | } | ||
| 301 | + | if err != nil { | |||
| 302 | + | return err | |||
| 303 | + | } | |||
| 227 | } | 304 | } | ||
| 228 | - | if err != nil { | 305 | + | return nil |
| 229 | - | o.freeImages() | 306 | + | } |
| 230 | - | _, _ = r.ExportTargets(0, nil, false) | 307 | + | |
| 308 | + | // gbmImages allocates the output images with GBM, makes them the | |||
| 309 | + | // renderer's targets and framebuffers, and clears them. The modifiers are | |||
| 310 | + | // those the plane lists (mods, as for exportImages) that the renderer can | |||
| 311 | + | // render into and import. On failure nothing is left allocated. | |||
| 312 | + | func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) { | |||
| 313 | + | o.dropImages(r) | |||
| 314 | + | if o.alloc == nil { | |||
| 315 | + | if o.allocErr != nil { | |||
| 316 | + | return o.allocErr | |||
| 317 | + | } | |||
| 318 | + | if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil { | |||
| 319 | + | o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images") | |||
| 320 | + | return o.allocErr | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | usable := r.TargetModifiers(mods, singlePlane) | |||
| 324 | + | if len(usable) == 0 { | |||
| 325 | + | return errors.New("no modifier both the plane and the renderer accept for GBM images") | |||
| 326 | + | } | |||
| 327 | + | var bufs []ports.DMABuf | |||
| 328 | + | defer func() { | |||
| 329 | + | closeBufs(bufs) | |||
| 330 | + | if err != nil { | |||
| 331 | + | o.dropImages(r) | |||
| 332 | + | } | |||
| 333 | + | }() | |||
| 334 | + | for range o.fbs { | |||
| 335 | + | img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable) | |||
| 336 | + | if cerr != nil { | |||
| 337 | + | return cerr | |||
| 338 | + | } | |||
| 339 | + | o.images = append(o.images, img) | |||
| 340 | + | b, derr := img.dmabuf() | |||
| 341 | + | if derr != nil { | |||
| 342 | + | return derr | |||
| 343 | + | } | |||
| 344 | + | bufs = append(bufs, b) | |||
| 345 | + | } | |||
| 346 | + | if err = r.ImportTargets(bufs); err != nil { | |||
| 231 | return err | 347 | return err | ||
| 232 | } | 348 | } | ||
| 233 | - | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | 349 | + | return o.showBufs(r, bufs, allocGBM) |
| 234 | - | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output") | 350 | + | } |
| 235 | - | return nil | 351 | + | |
| 352 | + | // dropImages frees the output images: their framebuffers, the GBM buffers | |||
| 353 | + | // and the renderer targets built over them (they would keep the memory | |||
| 354 | + | // alive after the buffers are gone). It is a no-op when there are none. | |||
| 355 | + | func (o *Output) dropImages(r ports.Renderer) { | |||
| 356 | + | if len(o.images) > 0 { | |||
| 357 | + | _ = r.ImportTargets(nil) | |||
| 358 | + | } | |||
| 359 | + | o.freeImages() | |||
| 236 | } | 360 | } | ||
| 237 | 361 | ||||
| 238 | - | // freeImages removes the output images' framebuffers. | 362 | + | // freeImages removes the output images' framebuffers and frees the images |
| 363 | + | // GBM allocated. Callers that still have a renderer use dropImages: this | |||
| 364 | + | // alone leaves the renderer's targets over the freed images (the renderer | |||
| 365 | + | // drops them when it closes, before Close frees the output's images). | |||
| 239 | func (o *Output) freeImages() { | 366 | func (o *Output) freeImages() { | ||
| 240 | o.front.forget() | 367 | o.front.forget() | ||
| 241 | o.forgetOverlayProbes() | 368 | o.forgetOverlayProbes() | ||
| @@ -245,4 +372,8 @@ func (o *Output) freeImages() { | |||||
| 245 | } | 372 | } | ||
| 246 | o.fbs[i] = 0 | 373 | o.fbs[i] = 0 | ||
| 247 | } | 374 | } | ||
| 375 | + | for _, img := range o.images { | |||
| 376 | + | img.close() | |||
| 377 | + | } | |||
| 378 | + | o.images = nil | |||
| 248 | } | 379 | } | ||
M internal/adapters/drm/output.go +43 -13
| @@ -197,10 +197,26 @@ type Output struct { | |||
| 197 | 197 | protectNotBefore time.Time | |
| 198 | 198 | // kind is how the images were made. | |
| 199 | 199 | kind imageKind | |
| 200 | - | // planes is the memory plane count of the last exported images. | |
| 200 | + | // planes is the memory plane count of the output images. | |
| 201 | 201 | planes int | |
| 202 | - | // imageMod is the modifier of the exported images (0: linear). | |
| 202 | + | // imageMod is the modifier of the output images (0: linear). | |
| 203 | 203 | imageMod uint64 | |
| 204 | + | // openAlloc opens the GBM image allocator, once per output (nil: GBM is | |
| 205 | + | // not used); alloc is it, allocErr why it failed. images are the GBM | |
| 206 | + | // images alive while they are shown. | |
| 207 | + | openAlloc func() (imageAllocator, error) | |
| 208 | + | alloc imageAllocator | |
| 209 | + | allocErr error | |
| 210 | + | images []imageBuffer | |
| 211 | + | // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so | |
| 212 | + | // commits wait for their fences on the CPU instead. | |
| 213 | + | cpuFences bool | |
| 214 | + | // fencePoll polls client fences; fenceWarnAt is when a fence wait | |
| 215 | + | // timeout was last warned about. | |
| 216 | + | // fenceLimit overrides fenceWaitLimit (zero: no override; tests). | |
| 217 | + | fencePoll fencePoll | |
| 218 | + | fenceWarnAt time.Time | |
| 219 | + | fenceLimit time.Duration | |
| 204 | 220 | // formats receives the direct scanout formats after each modeset; | |
| 205 | 221 | // sampled and device are what they are built from (Want). | |
| 206 | 222 | formats chan<- ports.OutputFormats | |
| @@ -222,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf | |||
| 222 | 238 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 223 | 239 | log := card.log | |
| 224 | 240 | pipe := slices.Index(card.crtcs, crtc) | |
| 225 | - | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} | |
| 241 | + | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} | |
| 226 | 242 | var err error | |
| 227 | 243 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 228 | 244 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| @@ -638,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 638 | 654 | // (overlay) say. A frame that changes a plane's colour state is never async: | |
| 639 | 655 | // only FB_ID may change in an async commit. | |
| 640 | 656 | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | |
| 657 | + | // Where fences are waited for on the CPU, once per commit and before | |
| 658 | + | // any state is read: the retries below go through commitRect. The | |
| 659 | + | // fence is our own render's, or a client's that already signalled | |
| 660 | + | // (clientFenceReady). | |
| 661 | + | o.waitFences(fence) | |
| 662 | + | return o.commitRect(fb, fence, async, vrr, f, ov, rect, pc) | |
| 663 | + | } | |
| 664 | + | ||
| 665 | + | // commitRect is commitWithRect once the fence was waited for; its retries | |
| 666 | + | // call it again, without waiting again. | |
| 667 | + | func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | |
| 641 | 668 | o.observeSecurity() | |
| 642 | 669 | if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) { | |
| 643 | 670 | return errSecurityScene | |
| @@ -672,9 +699,7 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||
| 672 | 699 | } | |
| 673 | 700 | } | |
| 674 | 701 | o.contentProps(req) | |
| 675 | - | if fence != nil { | |
| 676 | - | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 677 | - | } | |
| 702 | + | o.fenceProp(req, o.primary, fence) | |
| 678 | 703 | // Admission linearizes at this last defensive snapshot. An already | |
| 679 | 704 | // admitted native ioctl can complete, but precedes the black proof. | |
| 680 | 705 | o.observeSecurity() | |
| @@ -691,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||
| 691 | 716 | return errColorRefused | |
| 692 | 717 | } | |
| 693 | 718 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 694 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 719 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 695 | 720 | } | |
| 696 | 721 | // The same frame without the connector hint tells whether the hint | |
| 697 | 722 | // is the cause. | |
| 698 | 723 | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) { | |
| 699 | 724 | o.log.Warn().Err(err).Msg("content type refused; disabled") | |
| 700 | 725 | o.contentProp = 0 | |
| 701 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 726 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 702 | 727 | } | |
| 703 | 728 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |
| 704 | 729 | // Retry without turning VRR on: if that passes, the driver | |
| 705 | 730 | // refuses VRR on this output. The overlay stays: the composed | |
| 706 | 731 | // image left its window out. | |
| 707 | - | if o.commitWithRect(fb, fence, false, false, f, ov, rect, pc) == nil { | |
| 732 | + | if o.commitRect(fb, fence, false, false, f, ov, rect, pc) == nil { | |
| 708 | 733 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | |
| 709 | 734 | o.vrrProp = 0 | |
| 710 | 735 | return nil | |
| @@ -767,9 +792,7 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState, | |||
| 767 | 792 | if withContent { | |
| 768 | 793 | o.contentProps(req) | |
| 769 | 794 | } | |
| 770 | - | if fence != nil { | |
| 771 | - | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 772 | - | } | |
| 795 | + | o.fenceProp(req, o.primary, fence) | |
| 773 | 796 | return o.k.commit(req, atomicTestOnly, 0) == nil | |
| 774 | 797 | } | |
| 775 | 798 | ||
| @@ -912,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF | |||
| 912 | 935 | // from now on. | |
| 913 | 936 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | |
| 914 | 937 | cfb.noAsync = true | |
| 915 | - | err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) | |
| 938 | + | err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) | |
| 916 | 939 | } | |
| 917 | 940 | if errors.Is(err, errColorRefused) { | |
| 918 | 941 | // KMS takes the buffer without the pipeline: compose it; the | |
| @@ -1022,6 +1045,10 @@ func (o *Output) Close() { | |||
| 1022 | 1045 | o.cursor.free(o.k) | |
| 1023 | 1046 | } | |
| 1024 | 1047 | o.freeImages() | |
| 1048 | + | if o.alloc != nil { | |
| 1049 | + | o.alloc.close() | |
| 1050 | + | o.alloc = nil | |
| 1051 | + | } | |
| 1025 | 1052 | o.frame.endPending() | |
| 1026 | 1053 | o.dropRead() | |
| 1027 | 1054 | o.shown, o.queued = 0, 0 | |
| @@ -1498,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1498 | 1525 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | |
| 1499 | 1526 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | |
| 1500 | 1527 | ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried) | |
| 1528 | + | if fs.fenceTimeouts > 0 { | |
| 1529 | + | ev = ev.Int("fence_timeouts", fs.fenceTimeouts) | |
| 1530 | + | } | |
| 1501 | 1531 | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | |
| 1502 | 1532 | if o.traceFlips { | |
| 1503 | 1533 | ev = ev.Int("late_fences", fs.lateFences) | |
| @@ -197,10 +197,26 @@ type Output struct { | |||||
| 197 | protectNotBefore time.Time | 197 | protectNotBefore time.Time | ||
| 198 | // kind is how the images were made. | 198 | // kind is how the images were made. | ||
| 199 | kind imageKind | 199 | kind imageKind | ||
| 200 | - | // planes is the memory plane count of the last exported images. | 200 | + | // planes is the memory plane count of the output images. |
| 201 | planes int | 201 | planes int | ||
| 202 | - | // imageMod is the modifier of the exported images (0: linear). | 202 | + | // imageMod is the modifier of the output images (0: linear). |
| 203 | imageMod uint64 | 203 | imageMod uint64 | ||
| 204 | + | // openAlloc opens the GBM image allocator, once per output (nil: GBM is | |||
| 205 | + | // not used); alloc is it, allocErr why it failed. images are the GBM | |||
| 206 | + | // images alive while they are shown. | |||
| 207 | + | openAlloc func() (imageAllocator, error) | |||
| 208 | + | alloc imageAllocator | |||
| 209 | + | allocErr error | |||
| 210 | + | images []imageBuffer | |||
| 211 | + | // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so | |||
| 212 | + | // commits wait for their fences on the CPU instead. | |||
| 213 | + | cpuFences bool | |||
| 214 | + | // fencePoll polls client fences; fenceWarnAt is when a fence wait | |||
| 215 | + | // timeout was last warned about. | |||
| 216 | + | // fenceLimit overrides fenceWaitLimit (zero: no override; tests). | |||
| 217 | + | fencePoll fencePoll | |||
| 218 | + | fenceWarnAt time.Time | |||
| 219 | + | fenceLimit time.Duration | |||
| 204 | // formats receives the direct scanout formats after each modeset; | 220 | // formats receives the direct scanout formats after each modeset; | ||
| 205 | // sampled and device are what they are built from (Want). | 221 | // sampled and device are what they are built from (Want). | ||
| 206 | formats chan<- ports.OutputFormats | 222 | formats chan<- ports.OutputFormats | ||
| @@ -222,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf | |||||
| 222 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 238 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 223 | log := card.log | 239 | log := card.log | ||
| 224 | pipe := slices.Index(card.crtcs, crtc) | 240 | pipe := slices.Index(card.crtcs, crtc) | ||
| 225 | - | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} | 241 | + | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} |
| 226 | var err error | 242 | var err error | ||
| 227 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 243 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 228 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 244 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| @@ -638,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 638 | // (overlay) say. A frame that changes a plane's colour state is never async: | 654 | // (overlay) say. A frame that changes a plane's colour state is never async: | ||
| 639 | // only FB_ID may change in an async commit. | 655 | // only FB_ID may change in an async commit. | ||
| 640 | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | 656 | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | ||
| 657 | + | // Where fences are waited for on the CPU, once per commit and before | |||
| 658 | + | // any state is read: the retries below go through commitRect. The | |||
| 659 | + | // fence is our own render's, or a client's that already signalled | |||
| 660 | + | // (clientFenceReady). | |||
| 661 | + | o.waitFences(fence) | |||
| 662 | + | return o.commitRect(fb, fence, async, vrr, f, ov, rect, pc) | |||
| 663 | + | } | |||
| 664 | + | ||||
| 665 | + | // commitRect is commitWithRect once the fence was waited for; its retries | |||
| 666 | + | // call it again, without waiting again. | |||
| 667 | + | func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | |||
| 641 | o.observeSecurity() | 668 | o.observeSecurity() | ||
| 642 | if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) { | 669 | if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) { | ||
| 643 | return errSecurityScene | 670 | return errSecurityScene | ||
| @@ -672,9 +699,7 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||||
| 672 | } | 699 | } | ||
| 673 | } | 700 | } | ||
| 674 | o.contentProps(req) | 701 | o.contentProps(req) | ||
| 675 | - | if fence != nil { | 702 | + | o.fenceProp(req, o.primary, fence) |
| 676 | - | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |||
| 677 | - | } | |||
| 678 | // Admission linearizes at this last defensive snapshot. An already | 703 | // Admission linearizes at this last defensive snapshot. An already | ||
| 679 | // admitted native ioctl can complete, but precedes the black proof. | 704 | // admitted native ioctl can complete, but precedes the black proof. | ||
| 680 | o.observeSecurity() | 705 | o.observeSecurity() | ||
| @@ -691,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||||
| 691 | return errColorRefused | 716 | return errColorRefused | ||
| 692 | } | 717 | } | ||
| 693 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | 718 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | ||
| 694 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | 719 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) |
| 695 | } | 720 | } | ||
| 696 | // The same frame without the connector hint tells whether the hint | 721 | // The same frame without the connector hint tells whether the hint | ||
| 697 | // is the cause. | 722 | // is the cause. | ||
| 698 | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) { | 723 | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) { | ||
| 699 | o.log.Warn().Err(err).Msg("content type refused; disabled") | 724 | o.log.Warn().Err(err).Msg("content type refused; disabled") | ||
| 700 | o.contentProp = 0 | 725 | o.contentProp = 0 | ||
| 701 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | 726 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) |
| 702 | } | 727 | } | ||
| 703 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | 728 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | ||
| 704 | // Retry without turning VRR on: if that passes, the driver | 729 | // Retry without turning VRR on: if that passes, the driver | ||
| 705 | // refuses VRR on this output. The overlay stays: the composed | 730 | // refuses VRR on this output. The overlay stays: the composed | ||
| 706 | // image left its window out. | 731 | // image left its window out. | ||
| 707 | - | if o.commitWithRect(fb, fence, false, false, f, ov, rect, pc) == nil { | 732 | + | if o.commitRect(fb, fence, false, false, f, ov, rect, pc) == nil { |
| 708 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | 733 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | ||
| 709 | o.vrrProp = 0 | 734 | o.vrrProp = 0 | ||
| 710 | return nil | 735 | return nil | ||
| @@ -767,9 +792,7 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState, | |||||
| 767 | if withContent { | 792 | if withContent { | ||
| 768 | o.contentProps(req) | 793 | o.contentProps(req) | ||
| 769 | } | 794 | } | ||
| 770 | - | if fence != nil { | 795 | + | o.fenceProp(req, o.primary, fence) |
| 771 | - | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |||
| 772 | - | } | |||
| 773 | return o.k.commit(req, atomicTestOnly, 0) == nil | 796 | return o.k.commit(req, atomicTestOnly, 0) == nil | ||
| 774 | } | 797 | } | ||
| 775 | 798 | ||||
| @@ -912,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF | |||||
| 912 | // from now on. | 935 | // from now on. | ||
| 913 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 936 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 914 | cfb.noAsync = true | 937 | cfb.noAsync = true | ||
| 915 | - | err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) | 938 | + | err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) |
| 916 | } | 939 | } | ||
| 917 | if errors.Is(err, errColorRefused) { | 940 | if errors.Is(err, errColorRefused) { | ||
| 918 | // KMS takes the buffer without the pipeline: compose it; the | 941 | // KMS takes the buffer without the pipeline: compose it; the | ||
| @@ -1022,6 +1045,10 @@ func (o *Output) Close() { | |||||
| 1022 | o.cursor.free(o.k) | 1045 | o.cursor.free(o.k) | ||
| 1023 | } | 1046 | } | ||
| 1024 | o.freeImages() | 1047 | o.freeImages() | ||
| 1048 | + | if o.alloc != nil { | |||
| 1049 | + | o.alloc.close() | |||
| 1050 | + | o.alloc = nil | |||
| 1051 | + | } | |||
| 1025 | o.frame.endPending() | 1052 | o.frame.endPending() | ||
| 1026 | o.dropRead() | 1053 | o.dropRead() | ||
| 1027 | o.shown, o.queued = 0, 0 | 1054 | o.shown, o.queued = 0, 0 | ||
| @@ -1498,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1498 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | 1525 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | ||
| 1499 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | 1526 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | ||
| 1500 | ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried) | 1527 | ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried) | ||
| 1528 | + | if fs.fenceTimeouts > 0 { | |||
| 1529 | + | ev = ev.Int("fence_timeouts", fs.fenceTimeouts) | |||
| 1530 | + | } | |||
| 1501 | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | 1531 | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | ||
| 1502 | if o.traceFlips { | 1532 | if o.traceFlips { | ||
| 1503 | ev = ev.Int("late_fences", fs.lateFences) | 1533 | ev = ev.Int("late_fences", fs.lateFences) | ||
M internal/adapters/drm/overlay.go +2 -4
| @@ -227,10 +227,8 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) { | |||
| 227 | 227 | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | |
| 228 | 228 | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | |
| 229 | 229 | 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) | |
| 234 | 232 | } | |
| 235 | 233 | ||
| 236 | 234 | // 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) { | |||
| 248 | 248 | errs = []error{nil, errno} // HDR test commit, then the disable | |
| 249 | 249 | } | |
| 250 | 250 | o, k, _ := testOutput(t, errs...) | |
| 251 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 251 | 252 | o.cursor = nil | |
| 252 | 253 | o.protected = true | |
| 253 | 254 | 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 disable | 248 | 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 = nil | 252 | o.cursor = nil | ||
| 252 | o.protected = true | 253 | 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 | |||
| 231 | 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 | 237 | if reason == "" { | |
| 235 | 238 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | |
| 236 | 239 | if reason == "" { | |
| @@ -287,6 +290,9 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||
| 287 | 290 | reason = "color_refused" | |
| 288 | 291 | } | |
| 289 | 292 | } | |
| 293 | + | if reason == "" && !o.clientFenceReady(c.Acquire) { | |
| 294 | + | reason = "fence_pending" | |
| 295 | + | } | |
| 290 | 296 | if reason == "" { | |
| 291 | 297 | var fb uint32 | |
| 292 | 298 | 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 uint32 | 297 | 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 ( | |||
| 16 | 16 | // signals. Recovery revokes earlier proof before a fresh blocking black proof. | |
| 17 | 17 | func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) { | |
| 18 | 18 | o, k, commits, mu := testOutputMu(t) | |
| 19 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 19 | 20 | o.cursor, o.tearing = nil, false | |
| 20 | 21 | o.frame.stuckAfter = 40 * time.Millisecond | |
| 21 | 22 | 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, false | 20 | o.cursor, o.tearing = nil, false | ||
| 20 | o.frame.stuckAfter = 40 * time.Millisecond | 21 | o.frame.stuckAfter = 40 * time.Millisecond | ||
| 21 | o.flipped = make(chan flipEvent) // the event is intentionally lost | 22 | 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) { | |||
| 19 | 19 | ||
| 20 | 20 | func securityRunWakePendingResumeAndOff(t *testing.T) { | |
| 21 | 21 | o, k, commits, mu := testOutputMu(t) | |
| 22 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 22 | 23 | o.cursor, o.tearing = nil, false | |
| 23 | 24 | var state atomic.Uint64 | |
| 24 | 25 | 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, false | 23 | o.cursor, o.tearing = nil, false | ||
| 23 | var state atomic.Uint64 | 24 | 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 ( | |||
| 15 | 15 | ||
| 16 | 16 | func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) { | |
| 17 | 17 | o, k, commits, mu := testOutputMu(t) | |
| 18 | + | o.fbs = [2]uint32{} // the first setup of a run: no image yet | |
| 18 | 19 | o.cursor = nil | |
| 19 | 20 | var state atomic.Uint64 | |
| 20 | 21 | 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 = nil | 19 | o.cursor = nil | ||
| 19 | var state atomic.Uint64 | 20 | 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) { | |||
| 24 | 24 | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back])) | |
| 25 | 25 | if done != nil { | |
| 26 | 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 | 29 | 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) | |
| 31 | 32 | o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe") | |
| 32 | 33 | } | |
| 33 | 34 | ||
| @@ -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 != nil | 30 | + | // With CPU waits no fence is in a commit, async or not. |
| 31 | + | o.asyncFence = err == nil && (done != nil || o.cpuFences) | |||
| 31 | o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe") | 32 | o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe") | ||
| 32 | } | 33 | } | ||
| 33 | 34 | ||||
M internal/adapters/vulkan/renderer.go +3 -0
| @@ -34,6 +34,9 @@ type Renderer struct { | |||
| 34 | 34 | current int | |
| 35 | 35 | renderMods []uint64 | |
| 36 | 36 | hdrMods []uint64 | |
| 37 | + | // importMods caches the modifiers a target of a format can be imported | |
| 38 | + | // with (probeImportModifiers), by what the answer depends on. | |
| 39 | + | importMods map[importKey][]uint64 | |
| 37 | 40 | hdrNits float64 | |
| 38 | 41 | hdr hdrPass | |
| 39 | 42 | hdrError error | |
| @@ -34,6 +34,9 @@ type Renderer struct { | |||||
| 34 | current int | 34 | current int | ||
| 35 | renderMods []uint64 | 35 | renderMods []uint64 | ||
| 36 | hdrMods []uint64 | 36 | hdrMods []uint64 | ||
| 37 | + | // importMods caches the modifiers a target of a format can be imported | |||
| 38 | + | // with (probeImportModifiers), by what the answer depends on. | |||
| 39 | + | importMods map[importKey][]uint64 | |||
| 37 | hdrNits float64 | 40 | hdrNits float64 | ||
| 38 | hdr hdrPass | 41 | hdr hdrPass | ||
| 39 | hdrError error | 42 | hdrError error | ||
M internal/adapters/vulkan/targets.go +208 -8
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/neferwl/internal/ports" | |
| 12 | 12 | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | 16 | // 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 | 100 | 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 | 132 | // SetVirtualOutput marks the output as having no display (headless): an | |
| 103 | 133 | // empty display modifier list then accepts every exportable HDR modifier, | |
| 104 | 134 | // as it does for SDR. DRM never sets it. Call before ExportTargets. | |
| @@ -124,11 +154,19 @@ func (r *Renderer) target() *target { | |||
| 124 | 154 | // except for a virtual output (no display) or a test readback. singlePlane | |
| 125 | 155 | // drops the modifiers with more than one memory plane. | |
| 126 | 156 | func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 { | |
| 127 | - | var out []uint64 | |
| 128 | 157 | available := r.renderMods | |
| 129 | 158 | if r.hdrNits > 0 { | |
| 130 | 159 | 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 | 170 | for _, m := range available { | |
| 133 | 171 | if singlePlane && r.modPlanes[m] > 1 { | |
| 134 | 172 | continue | |
| @@ -140,6 +178,27 @@ func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 | |||
| 140 | 178 | 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 | 202 | // probeRenderModifiers lists exportable SDR and HDR targets. | |
| 144 | 203 | func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |
| 145 | 204 | r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm) | |
| @@ -152,6 +211,34 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||
| 152 | 211 | // records their plane counts in modPlanes. HDR targets need transfer-src only when | |
| 153 | 212 | // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM). | |
| 154 | 213 | func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | |
| 214 | + | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit) | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // importKey is what the importable modifiers of a target depend on: its | |
| 218 | + | // format and whether test readback adds the transfer-source requirement. | |
| 219 | + | type importKey struct { | |
| 220 | + | format vk.Format | |
| 221 | + | readback bool | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | // probeImportModifiers lists the modifiers of format a target can be | |
| 225 | + | // imported with: probeModifiers with the importable bit, for images the | |
| 226 | + | // display allocator made. The answer is probed once per key: it does not | |
| 227 | + | // change while the device lives. | |
| 228 | + | func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | |
| 229 | + | key := importKey{format, r.hdrReadback} | |
| 230 | + | if mods, ok := r.importMods[key]; ok { | |
| 231 | + | return mods | |
| 232 | + | } | |
| 233 | + | mods := r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit) | |
| 234 | + | if r.importMods == nil { | |
| 235 | + | r.importMods = make(map[importKey][]uint64) | |
| 236 | + | } | |
| 237 | + | r.importMods[key] = mods | |
| 238 | + | return mods | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 { | |
| 155 | 242 | list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT} | |
| 156 | 243 | fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)} | |
| 157 | 244 | r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp) | |
| @@ -169,7 +256,7 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) | |||
| 169 | 256 | } | |
| 170 | 257 | // SDR targets are composed and read back; HDR targets are color | |
| 171 | 258 | // attachments, and transfer sources only with hdrReadback. | |
| 172 | - | result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.exportable(physical, format, m) }) | |
| 259 | + | result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.externalTarget(physical, format, m, feature) }) | |
| 173 | 260 | if r.modPlanes == nil { | |
| 174 | 261 | r.modPlanes = make(map[uint64]uint32) | |
| 175 | 262 | } | |
| @@ -216,7 +303,10 @@ func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFea | |||
| 216 | 303 | return result, planes | |
| 217 | 304 | } | |
| 218 | 305 | ||
| 219 | - | func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool { | |
| 306 | + | // externalTarget asks whether a target image of format and modifier with | |
| 307 | + | // the target usage supports the external memory feature (exportable: the | |
| 308 | + | // renderer allocates it; importable: the display allocator does). | |
| 309 | + | func (r *Renderer) externalTarget(physical vk.PhysicalDevice, format vk.Format, modifier uint64, feature vk.ExternalMemoryFeatureFlags) bool { | |
| 220 | 310 | mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive} | |
| 221 | 311 | ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |
| 222 | 312 | info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)} | |
| @@ -225,7 +315,7 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modi | |||
| 225 | 315 | if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success { | |
| 226 | 316 | return false | |
| 227 | 317 | } | |
| 228 | - | return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0 | |
| 318 | + | return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&feature != 0 | |
| 229 | 319 | } | |
| 230 | 320 | ||
| 231 | 321 | // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the | |
| @@ -248,10 +338,7 @@ func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags { | |||
| 248 | 338 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | |
| 249 | 339 | d := r.dd | |
| 250 | 340 | t := &target{exported: true, layout: vk.ImageLayoutUndefined} | |
| 251 | - | format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB) | |
| 252 | - | if r.hdrNits > 0 { | |
| 253 | - | format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30 | |
| 254 | - | } | |
| 341 | + | format, fourcc := r.targetFormat() | |
| 255 | 342 | ok := false | |
| 256 | 343 | defer func() { | |
| 257 | 344 | if !ok { | |
| @@ -354,3 +441,116 @@ func (r *Renderer) dropTargets() { | |||
| 354 | 441 | // New targets hold no HDR frame until the next Render. | |
| 355 | 442 | r.targets, r.current, r.lastHDRTarget = nil, 0, -1 | |
| 356 | 443 | } | |
| 444 | + | ||
| 445 | + | // What ImportTargets refuses an image for, before asking the driver. | |
| 446 | + | var ( | |
| 447 | + | errImageGeometry = errors.New("image size or format does not match the target") | |
| 448 | + | errImageModifier = errors.New("image modifier is not an importable target modifier") | |
| 449 | + | errImagePlanes = errors.New("image plane count does not match its modifier") | |
| 450 | + | errImageDMABuf = errors.New("image planes are not in one dmabuf") | |
| 451 | + | ) | |
| 452 | + | ||
| 453 | + | // importTarget makes a target of an image allocated outside the renderer. | |
| 454 | + | // The target is shared with the display like an exported one: frames | |
| 455 | + | // acquire it from and release it to the foreign queue family. | |
| 456 | + | func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) { | |
| 457 | + | d := r.dd | |
| 458 | + | format, fourcc := r.targetFormat() | |
| 459 | + | n := len(b.Planes) | |
| 460 | + | want, known := r.modPlanes[b.Modifier] | |
| 461 | + | switch { | |
| 462 | + | case b.Format != fourcc || b.Width != r.width || b.Height != r.height: | |
| 463 | + | return nil, fmt.Errorf("%w: image %dx%d format %#x, target %dx%d format %#x", errImageGeometry, b.Width, b.Height, b.Format, r.width, r.height, fourcc) | |
| 464 | + | case !known: | |
| 465 | + | return nil, fmt.Errorf("%w: %#x is not a target modifier of the device", errImageModifier, b.Modifier) | |
| 466 | + | case n < 1 || n > maxModifierPlanes || want != uint32(n): | |
| 467 | + | return nil, fmt.Errorf("%w: modifier %#x: image has %d planes, device expects %d", errImagePlanes, b.Modifier, n, want) | |
| 468 | + | case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit): | |
| 469 | + | return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier) | |
| 470 | + | } | |
| 471 | + | if err := sameDMABuf(b.Planes); err != nil { | |
| 472 | + | return nil, err | |
| 473 | + | } | |
| 474 | + | fd, err := dupFile(b.Planes[0].File) | |
| 475 | + | if err != nil { | |
| 476 | + | return nil, err | |
| 477 | + | } | |
| 478 | + | // Vulkan owns the fd it imports on success: hand it its own duplicate. | |
| 479 | + | // Closed here on every path but the successful import. | |
| 480 | + | defer func() { | |
| 481 | + | if fd >= 0 { | |
| 482 | + | _ = unix.Close(fd) | |
| 483 | + | } | |
| 484 | + | }() | |
| 485 | + | t := &target{exported: true, layout: vk.ImageLayoutUndefined} | |
| 486 | + | ok := false | |
| 487 | + | defer func() { | |
| 488 | + | if !ok { | |
| 489 | + | r.freeTarget(t) | |
| 490 | + | } | |
| 491 | + | }() | |
| 492 | + | var layouts [maxModifierPlanes]vk.SubresourceLayout | |
| 493 | + | for i, p := range b.Planes { | |
| 494 | + | layouts[i] = vk.SubresourceLayout{Offset: vk.DeviceSize(p.Offset), RowPitch: vk.DeviceSize(p.Stride)} | |
| 495 | + | } | |
| 496 | + | explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: uint32(n), PlaneLayouts: &layouts[0]} | |
| 497 | + | external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |
| 498 | + | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback), SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | |
| 499 | + | if err := checked("vkCreateImage(imported target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil { | |
| 500 | + | return nil, err | |
| 501 | + | } | |
| 502 | + | fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR} | |
| 503 | + | if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil { | |
| 504 | + | return nil, err | |
| 505 | + | } | |
| 506 | + | var req vk.MemoryRequirements | |
| 507 | + | d.GetImageMemoryRequirements(r.device, t.image, &req) | |
| 508 | + | bits := req.MemoryTypeBits & fdProps.MemoryTypeBits | |
| 509 | + | kind, err := r.findMemoryType(bits, vk.MemoryPropertyDeviceLocalBit) | |
| 510 | + | if err != nil { | |
| 511 | + | if kind, err = r.findMemoryType(bits, 0); err != nil { | |
| 512 | + | return nil, err | |
| 513 | + | } | |
| 514 | + | } | |
| 515 | + | dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image} | |
| 516 | + | imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(fd)} | |
| 517 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind} | |
| 518 | + | if err := checked("vkAllocateMemory(imported target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil { | |
| 519 | + | return nil, err | |
| 520 | + | } | |
| 521 | + | fd = -1 // Vulkan owns it now. | |
| 522 | + | if err := checked("vkBindImageMemory(imported target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil { | |
| 523 | + | return nil, err | |
| 524 | + | } | |
| 525 | + | if t.view, err = r.imageView(t.image, format); err != nil { | |
| 526 | + | return nil, err | |
| 527 | + | } | |
| 528 | + | r.log.Debug().Uint64("modifier", b.Modifier).Int("planes", n).Msg("render target imported") | |
| 529 | + | ok = true | |
| 530 | + | return t, nil | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | // sameDMABuf fails unless every plane file is the same dmabuf (same device | |
| 534 | + | // and inode): the target is imported from the first plane's memory only, so | |
| 535 | + | // planes in other buffers would read the wrong memory. | |
| 536 | + | func sameDMABuf(planes []ports.DMABufPlane) error { | |
| 537 | + | var first unix.Stat_t | |
| 538 | + | for i, p := range planes { | |
| 539 | + | fd, err := dupFile(p.File) | |
| 540 | + | if err != nil { | |
| 541 | + | return fmt.Errorf("plane %d: %w", i, err) | |
| 542 | + | } | |
| 543 | + | var st unix.Stat_t | |
| 544 | + | err = unix.Fstat(fd, &st) | |
| 545 | + | _ = unix.Close(fd) | |
| 546 | + | if err != nil { | |
| 547 | + | return fmt.Errorf("stat plane %d: %w", i, err) | |
| 548 | + | } | |
| 549 | + | if i == 0 { | |
| 550 | + | first = st | |
| 551 | + | } else if st.Dev != first.Dev || st.Ino != first.Ino { | |
| 552 | + | return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i) | |
| 553 | + | } | |
| 554 | + | } | |
| 555 | + | return nil | |
| 556 | + | } | |
| @@ -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 renderer | 16 | // 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, nil | 100 | 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): an | 132 | // 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. singlePlane | 154 | // 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.renderMods | 157 | available := r.renderMods | ||
| 129 | if r.hdrNits > 0 { | 158 | if r.hdrNits > 0 { | ||
| 130 | available = r.hdrMods | 159 | 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 | continue | 172 | continue | ||
| @@ -140,6 +178,27 @@ func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 | |||||
| 140 | return out | 178 | return out | ||
| 141 | } | 179 | } | ||
| 142 | 180 | ||||
| 181 | + | // targetFormat is the Vulkan format and fourcc of the targets: XRGB8888, or | |||
| 182 | + | // XRGB2101010 in HDR. | |||
| 183 | + | func (r *Renderer) targetFormat() (vk.Format, uint32) { | |||
| 184 | + | if r.hdrNits > 0 { | |||
| 185 | + | return vk.FormatA2r10g10b10UnormPack32, fourccXR30 | |||
| 186 | + | } | |||
| 187 | + | return vk.FormatB8g8r8a8Unorm, fourccXRGB | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // TargetModifiers lists the modifiers an image allocated by the display | |||
| 191 | + | // (GBM) can have to be imported as a target: those the device renders into | |||
| 192 | + | // and imports for the target format, that the display lists (empty: any), | |||
| 193 | + | // without multi-plane ones when singlePlane. Call after SetHDR. | |||
| 194 | + | func (r *Renderer) TargetModifiers(display []uint64, singlePlane bool) []uint64 { | |||
| 195 | + | if r.physical == 0 || r.dd == nil || !r.dd.HasGetMemoryFdPropertiesKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() { | |||
| 196 | + | return nil | |||
| 197 | + | } | |||
| 198 | + | format, _ := r.targetFormat() | |||
| 199 | + | return r.intersectModifiers(r.probeImportModifiers(r.physical, format), display, singlePlane) | |||
| 200 | + | } | |||
| 201 | + | ||||
| 143 | // probeRenderModifiers lists exportable SDR and HDR targets. | 202 | // probeRenderModifiers lists exportable SDR and HDR targets. | ||
| 144 | func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | 203 | func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | ||
| 145 | r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm) | 204 | r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm) | ||
| @@ -152,6 +211,34 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||||
| 152 | // records their plane counts in modPlanes. HDR targets need transfer-src only when | 211 | // records their plane counts in modPlanes. HDR targets need transfer-src only when | ||
| 153 | // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM). | 212 | // hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM). | ||
| 154 | func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | 213 | func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | ||
| 214 | + | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit) | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // importKey is what the importable modifiers of a target depend on: its | |||
| 218 | + | // format and whether test readback adds the transfer-source requirement. | |||
| 219 | + | type importKey struct { | |||
| 220 | + | format vk.Format | |||
| 221 | + | readback bool | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | // probeImportModifiers lists the modifiers of format a target can be | |||
| 225 | + | // imported with: probeModifiers with the importable bit, for images the | |||
| 226 | + | // display allocator made. The answer is probed once per key: it does not | |||
| 227 | + | // change while the device lives. | |||
| 228 | + | func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | |||
| 229 | + | key := importKey{format, r.hdrReadback} | |||
| 230 | + | if mods, ok := r.importMods[key]; ok { | |||
| 231 | + | return mods | |||
| 232 | + | } | |||
| 233 | + | mods := r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit) | |||
| 234 | + | if r.importMods == nil { | |||
| 235 | + | r.importMods = make(map[importKey][]uint64) | |||
| 236 | + | } | |||
| 237 | + | r.importMods[key] = mods | |||
| 238 | + | return mods | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 { | |||
| 155 | list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT} | 242 | list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT} | ||
| 156 | fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)} | 243 | fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)} | ||
| 157 | r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp) | 244 | r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp) | ||
| @@ -169,7 +256,7 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) | |||||
| 169 | } | 256 | } | ||
| 170 | // SDR targets are composed and read back; HDR targets are color | 257 | // SDR targets are composed and read back; HDR targets are color | ||
| 171 | // attachments, and transfer sources only with hdrReadback. | 258 | // attachments, and transfer sources only with hdrReadback. | ||
| 172 | - | result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.exportable(physical, format, m) }) | 259 | + | result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.externalTarget(physical, format, m, feature) }) |
| 173 | if r.modPlanes == nil { | 260 | if r.modPlanes == nil { | ||
| 174 | r.modPlanes = make(map[uint64]uint32) | 261 | r.modPlanes = make(map[uint64]uint32) | ||
| 175 | } | 262 | } | ||
| @@ -216,7 +303,10 @@ func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFea | |||||
| 216 | return result, planes | 303 | return result, planes | ||
| 217 | } | 304 | } | ||
| 218 | 305 | ||||
| 219 | - | func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool { | 306 | + | // externalTarget asks whether a target image of format and modifier with |
| 307 | + | // the target usage supports the external memory feature (exportable: the | |||
| 308 | + | // renderer allocates it; importable: the display allocator does). | |||
| 309 | + | func (r *Renderer) externalTarget(physical vk.PhysicalDevice, format vk.Format, modifier uint64, feature vk.ExternalMemoryFeatureFlags) bool { | |||
| 220 | mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive} | 310 | mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive} | ||
| 221 | ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | 311 | ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | ||
| 222 | info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)} | 312 | info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)} | ||
| @@ -225,7 +315,7 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modi | |||||
| 225 | if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success { | 315 | if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success { | ||
| 226 | return false | 316 | return false | ||
| 227 | } | 317 | } | ||
| 228 | - | return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0 | 318 | + | return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&feature != 0 |
| 229 | } | 319 | } | ||
| 230 | 320 | ||||
| 231 | // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the | 321 | // VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the | ||
| @@ -248,10 +338,7 @@ func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags { | |||||
| 248 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | 338 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | ||
| 249 | d := r.dd | 339 | d := r.dd | ||
| 250 | t := &target{exported: true, layout: vk.ImageLayoutUndefined} | 340 | t := &target{exported: true, layout: vk.ImageLayoutUndefined} | ||
| 251 | - | format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB) | 341 | + | format, fourcc := r.targetFormat() |
| 252 | - | if r.hdrNits > 0 { | |||
| 253 | - | format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30 | |||
| 254 | - | } | |||
| 255 | ok := false | 342 | ok := false | ||
| 256 | defer func() { | 343 | defer func() { | ||
| 257 | if !ok { | 344 | if !ok { | ||
| @@ -354,3 +441,116 @@ func (r *Renderer) dropTargets() { | |||||
| 354 | // New targets hold no HDR frame until the next Render. | 441 | // New targets hold no HDR frame until the next Render. | ||
| 355 | r.targets, r.current, r.lastHDRTarget = nil, 0, -1 | 442 | r.targets, r.current, r.lastHDRTarget = nil, 0, -1 | ||
| 356 | } | 443 | } | ||
| 444 | + | ||||
| 445 | + | // What ImportTargets refuses an image for, before asking the driver. | |||
| 446 | + | var ( | |||
| 447 | + | errImageGeometry = errors.New("image size or format does not match the target") | |||
| 448 | + | errImageModifier = errors.New("image modifier is not an importable target modifier") | |||
| 449 | + | errImagePlanes = errors.New("image plane count does not match its modifier") | |||
| 450 | + | errImageDMABuf = errors.New("image planes are not in one dmabuf") | |||
| 451 | + | ) | |||
| 452 | + | ||||
| 453 | + | // importTarget makes a target of an image allocated outside the renderer. | |||
| 454 | + | // The target is shared with the display like an exported one: frames | |||
| 455 | + | // acquire it from and release it to the foreign queue family. | |||
| 456 | + | func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) { | |||
| 457 | + | d := r.dd | |||
| 458 | + | format, fourcc := r.targetFormat() | |||
| 459 | + | n := len(b.Planes) | |||
| 460 | + | want, known := r.modPlanes[b.Modifier] | |||
| 461 | + | switch { | |||
| 462 | + | case b.Format != fourcc || b.Width != r.width || b.Height != r.height: | |||
| 463 | + | return nil, fmt.Errorf("%w: image %dx%d format %#x, target %dx%d format %#x", errImageGeometry, b.Width, b.Height, b.Format, r.width, r.height, fourcc) | |||
| 464 | + | case !known: | |||
| 465 | + | return nil, fmt.Errorf("%w: %#x is not a target modifier of the device", errImageModifier, b.Modifier) | |||
| 466 | + | case n < 1 || n > maxModifierPlanes || want != uint32(n): | |||
| 467 | + | return nil, fmt.Errorf("%w: modifier %#x: image has %d planes, device expects %d", errImagePlanes, b.Modifier, n, want) | |||
| 468 | + | case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit): | |||
| 469 | + | return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier) | |||
| 470 | + | } | |||
| 471 | + | if err := sameDMABuf(b.Planes); err != nil { | |||
| 472 | + | return nil, err | |||
| 473 | + | } | |||
| 474 | + | fd, err := dupFile(b.Planes[0].File) | |||
| 475 | + | if err != nil { | |||
| 476 | + | return nil, err | |||
| 477 | + | } | |||
| 478 | + | // Vulkan owns the fd it imports on success: hand it its own duplicate. | |||
| 479 | + | // Closed here on every path but the successful import. | |||
| 480 | + | defer func() { | |||
| 481 | + | if fd >= 0 { | |||
| 482 | + | _ = unix.Close(fd) | |||
| 483 | + | } | |||
| 484 | + | }() | |||
| 485 | + | t := &target{exported: true, layout: vk.ImageLayoutUndefined} | |||
| 486 | + | ok := false | |||
| 487 | + | defer func() { | |||
| 488 | + | if !ok { | |||
| 489 | + | r.freeTarget(t) | |||
| 490 | + | } | |||
| 491 | + | }() | |||
| 492 | + | var layouts [maxModifierPlanes]vk.SubresourceLayout | |||
| 493 | + | for i, p := range b.Planes { | |||
| 494 | + | layouts[i] = vk.SubresourceLayout{Offset: vk.DeviceSize(p.Offset), RowPitch: vk.DeviceSize(p.Stride)} | |||
| 495 | + | } | |||
| 496 | + | explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: uint32(n), PlaneLayouts: &layouts[0]} | |||
| 497 | + | external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |||
| 498 | + | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback), SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | |||
| 499 | + | if err := checked("vkCreateImage(imported target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil { | |||
| 500 | + | return nil, err | |||
| 501 | + | } | |||
| 502 | + | fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR} | |||
| 503 | + | if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil { | |||
| 504 | + | return nil, err | |||
| 505 | + | } | |||
| 506 | + | var req vk.MemoryRequirements | |||
| 507 | + | d.GetImageMemoryRequirements(r.device, t.image, &req) | |||
| 508 | + | bits := req.MemoryTypeBits & fdProps.MemoryTypeBits | |||
| 509 | + | kind, err := r.findMemoryType(bits, vk.MemoryPropertyDeviceLocalBit) | |||
| 510 | + | if err != nil { | |||
| 511 | + | if kind, err = r.findMemoryType(bits, 0); err != nil { | |||
| 512 | + | return nil, err | |||
| 513 | + | } | |||
| 514 | + | } | |||
| 515 | + | dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image} | |||
| 516 | + | imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(fd)} | |||
| 517 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 518 | + | if err := checked("vkAllocateMemory(imported target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil { | |||
| 519 | + | return nil, err | |||
| 520 | + | } | |||
| 521 | + | fd = -1 // Vulkan owns it now. | |||
| 522 | + | if err := checked("vkBindImageMemory(imported target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil { | |||
| 523 | + | return nil, err | |||
| 524 | + | } | |||
| 525 | + | if t.view, err = r.imageView(t.image, format); err != nil { | |||
| 526 | + | return nil, err | |||
| 527 | + | } | |||
| 528 | + | r.log.Debug().Uint64("modifier", b.Modifier).Int("planes", n).Msg("render target imported") | |||
| 529 | + | ok = true | |||
| 530 | + | return t, nil | |||
| 531 | + | } | |||
| 532 | + | ||||
| 533 | + | // sameDMABuf fails unless every plane file is the same dmabuf (same device | |||
| 534 | + | // and inode): the target is imported from the first plane's memory only, so | |||
| 535 | + | // planes in other buffers would read the wrong memory. | |||
| 536 | + | func sameDMABuf(planes []ports.DMABufPlane) error { | |||
| 537 | + | var first unix.Stat_t | |||
| 538 | + | for i, p := range planes { | |||
| 539 | + | fd, err := dupFile(p.File) | |||
| 540 | + | if err != nil { | |||
| 541 | + | return fmt.Errorf("plane %d: %w", i, err) | |||
| 542 | + | } | |||
| 543 | + | var st unix.Stat_t | |||
| 544 | + | err = unix.Fstat(fd, &st) | |||
| 545 | + | _ = unix.Close(fd) | |||
| 546 | + | if err != nil { | |||
| 547 | + | return fmt.Errorf("stat plane %d: %w", i, err) | |||
| 548 | + | } | |||
| 549 | + | if i == 0 { | |||
| 550 | + | first = st | |||
| 551 | + | } else if st.Dev != first.Dev || st.Ino != first.Ino { | |||
| 552 | + | return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i) | |||
| 553 | + | } | |||
| 554 | + | } | |||
| 555 | + | return nil | |||
| 556 | + | } | |||
M internal/adapters/vulkan/targets_test.go +172 -0
| @@ -1,8 +1,10 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "errors" | |
| 4 | 5 | "image/color" | |
| 5 | 6 | "math" | |
| 7 | + | "os" | |
| 6 | 8 | "slices" | |
| 7 | 9 | "testing" | |
| 8 | 10 | ||
| @@ -367,3 +369,173 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) { | |||
| 367 | 369 | } | |
| 368 | 370 | } | |
| 369 | 371 | } | |
| 372 | + | ||
| 373 | + | // pipeFile is a file for plane tests: a distinct inode per call. | |
| 374 | + | func pipeFile(t *testing.T) *os.File { | |
| 375 | + | t.Helper() | |
| 376 | + | f, w, err := os.Pipe() | |
| 377 | + | if err != nil { | |
| 378 | + | t.Fatal(err) | |
| 379 | + | } | |
| 380 | + | w.Close() | |
| 381 | + | t.Cleanup(func() { f.Close() }) | |
| 382 | + | return f | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | // Planes must be in one dmabuf: the same file or duplicates of it are, two | |
| 386 | + | // files are not. No device needed. | |
| 387 | + | func TestSameDMABuf(t *testing.T) { | |
| 388 | + | one, other := pipeFile(t), pipeFile(t) | |
| 389 | + | for _, tc := range []struct { | |
| 390 | + | name string | |
| 391 | + | planes []ports.DMABufPlane | |
| 392 | + | ok bool | |
| 393 | + | }{ | |
| 394 | + | {"one plane", []ports.DMABufPlane{{File: one}}, true}, | |
| 395 | + | {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true}, | |
| 396 | + | {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false}, | |
| 397 | + | {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false}, | |
| 398 | + | {"no file", []ports.DMABufPlane{{File: one}, {}}, false}, | |
| 399 | + | } { | |
| 400 | + | err := sameDMABuf(tc.planes) | |
| 401 | + | if (err == nil) != tc.ok { | |
| 402 | + | t.Errorf("%s: %v", tc.name, err) | |
| 403 | + | } | |
| 404 | + | if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) { | |
| 405 | + | t.Errorf("%s: %v is not errImageDMABuf", tc.name, err) | |
| 406 | + | } | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | // TargetModifiers keeps the modifiers the display lists and drops | |
| 411 | + | // multi-plane ones for singlePlane. | |
| 412 | + | func TestTargetModifiersFiltering(t *testing.T) { | |
| 413 | + | r, err := New(64, 32) | |
| 414 | + | if err != nil { | |
| 415 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 416 | + | } | |
| 417 | + | defer r.Close() | |
| 418 | + | mods := r.TargetModifiers(nil, false) | |
| 419 | + | if len(mods) == 0 { | |
| 420 | + | t.Skip("no importable target modifier") | |
| 421 | + | } | |
| 422 | + | if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 { | |
| 423 | + | t.Errorf("modifiers outside the display list: %#x", got) | |
| 424 | + | } | |
| 425 | + | if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] { | |
| 426 | + | t.Errorf("display list %#x gave %#x", mods[:1], got) | |
| 427 | + | } | |
| 428 | + | for _, m := range r.TargetModifiers(nil, true) { | |
| 429 | + | if r.modPlanes[m] > 1 { | |
| 430 | + | t.Errorf("single-plane list holds multi-plane modifier %#x", m) | |
| 431 | + | } | |
| 432 | + | } | |
| 433 | + | // The answer is probed once and kept. | |
| 434 | + | if len(r.importMods) == 0 { | |
| 435 | + | t.Error("import modifiers not cached") | |
| 436 | + | } | |
| 437 | + | again := r.TargetModifiers(nil, false) | |
| 438 | + | if !slices.Equal(mods, again) { | |
| 439 | + | t.Errorf("cached list differs: %#x vs %#x", mods, again) | |
| 440 | + | } | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | // ImportTargets refuses an image that does not fit the renderer before | |
| 444 | + | // asking the driver, says why, and leaves no targets: frames go back to the | |
| 445 | + | // renderer's own image. | |
| 446 | + | func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |
| 447 | + | r, err := New(64, 32) | |
| 448 | + | if err != nil { | |
| 449 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 450 | + | } | |
| 451 | + | defer r.Close() | |
| 452 | + | var single uint64 | |
| 453 | + | found := false | |
| 454 | + | for _, m := range r.TargetModifiers(nil, true) { | |
| 455 | + | single, found = m, true | |
| 456 | + | break | |
| 457 | + | } | |
| 458 | + | if !found { | |
| 459 | + | t.Skip("no importable one-plane target modifier") | |
| 460 | + | } | |
| 461 | + | good := func() ports.DMABuf { | |
| 462 | + | return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}} | |
| 463 | + | } | |
| 464 | + | for _, tc := range []struct { | |
| 465 | + | name string | |
| 466 | + | mutate func(*ports.DMABuf) | |
| 467 | + | want error | |
| 468 | + | }{ | |
| 469 | + | {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry}, | |
| 470 | + | {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry}, | |
| 471 | + | {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry}, | |
| 472 | + | {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier}, | |
| 473 | + | {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes}, | |
| 474 | + | {"too many planes", func(b *ports.DMABuf) { | |
| 475 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |
| 476 | + | }, errImagePlanes}, | |
| 477 | + | } { | |
| 478 | + | t.Run(tc.name, func(t *testing.T) { | |
| 479 | + | b := good() | |
| 480 | + | tc.mutate(&b) | |
| 481 | + | err := r.ImportTargets([]ports.DMABuf{b}) | |
| 482 | + | if !errors.Is(err, tc.want) { | |
| 483 | + | t.Fatalf("error %v, want %v", err, tc.want) | |
| 484 | + | } | |
| 485 | + | if len(r.targets) != 0 { | |
| 486 | + | t.Fatalf("%d targets left", len(r.targets)) | |
| 487 | + | } | |
| 488 | + | }) | |
| 489 | + | } | |
| 490 | + | t.Run("second image refused drops the first", func(t *testing.T) { | |
| 491 | + | // A real dmabuf for the first image: the renderer's own export. | |
| 492 | + | exported, err := r.ExportTargets(1, []uint64{single}, true) | |
| 493 | + | if err != nil { | |
| 494 | + | t.Skipf("no exportable target to reuse: %v", err) | |
| 495 | + | } | |
| 496 | + | defer closeBufs(exported) | |
| 497 | + | bad := good() | |
| 498 | + | bad.Width = 1 | |
| 499 | + | err = r.ImportTargets([]ports.DMABuf{exported[0], bad}) | |
| 500 | + | if !errors.Is(err, errImageGeometry) { | |
| 501 | + | t.Fatalf("error %v, want the second image's geometry refusal", err) | |
| 502 | + | } | |
| 503 | + | if len(r.targets) != 0 { | |
| 504 | + | t.Fatalf("%d targets left", len(r.targets)) | |
| 505 | + | } | |
| 506 | + | }) | |
| 507 | + | t.Run("dropping", func(t *testing.T) { | |
| 508 | + | if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 { | |
| 509 | + | t.Fatalf("%v, %d targets", err, len(r.targets)) | |
| 510 | + | } | |
| 511 | + | }) | |
| 512 | + | t.Run("planes in different dmabufs", func(t *testing.T) { | |
| 513 | + | multi := uint64(0) | |
| 514 | + | for _, m := range r.TargetModifiers(nil, false) { | |
| 515 | + | if r.modPlanes[m] > 1 { | |
| 516 | + | multi = m | |
| 517 | + | break | |
| 518 | + | } | |
| 519 | + | } | |
| 520 | + | if multi == 0 { | |
| 521 | + | t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf") | |
| 522 | + | } | |
| 523 | + | b := good() | |
| 524 | + | b.Modifier = multi | |
| 525 | + | b.Planes = nil | |
| 526 | + | for range r.modPlanes[multi] { | |
| 527 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |
| 528 | + | } | |
| 529 | + | if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) { | |
| 530 | + | t.Fatalf("error %v", err) | |
| 531 | + | } | |
| 532 | + | }) | |
| 533 | + | } | |
| 534 | + | ||
| 535 | + | func closeBufs(bufs []ports.DMABuf) { | |
| 536 | + | for _, b := range bufs { | |
| 537 | + | for _, p := range b.Planes { | |
| 538 | + | p.File.Close() | |
| 539 | + | } | |
| 540 | + | } | |
| 541 | + | } | |
| @@ -1,8 +1,10 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "errors" | |||
| 4 | "image/color" | 5 | "image/color" | ||
| 5 | "math" | 6 | "math" | ||
| 7 | + | "os" | |||
| 6 | "slices" | 8 | "slices" | ||
| 7 | "testing" | 9 | "testing" | ||
| 8 | 10 | ||||
| @@ -367,3 +369,173 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) { | |||||
| 367 | } | 369 | } | ||
| 368 | } | 370 | } | ||
| 369 | } | 371 | } | ||
| 372 | + | ||||
| 373 | + | // pipeFile is a file for plane tests: a distinct inode per call. | |||
| 374 | + | func pipeFile(t *testing.T) *os.File { | |||
| 375 | + | t.Helper() | |||
| 376 | + | f, w, err := os.Pipe() | |||
| 377 | + | if err != nil { | |||
| 378 | + | t.Fatal(err) | |||
| 379 | + | } | |||
| 380 | + | w.Close() | |||
| 381 | + | t.Cleanup(func() { f.Close() }) | |||
| 382 | + | return f | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | // Planes must be in one dmabuf: the same file or duplicates of it are, two | |||
| 386 | + | // files are not. No device needed. | |||
| 387 | + | func TestSameDMABuf(t *testing.T) { | |||
| 388 | + | one, other := pipeFile(t), pipeFile(t) | |||
| 389 | + | for _, tc := range []struct { | |||
| 390 | + | name string | |||
| 391 | + | planes []ports.DMABufPlane | |||
| 392 | + | ok bool | |||
| 393 | + | }{ | |||
| 394 | + | {"one plane", []ports.DMABufPlane{{File: one}}, true}, | |||
| 395 | + | {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true}, | |||
| 396 | + | {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false}, | |||
| 397 | + | {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false}, | |||
| 398 | + | {"no file", []ports.DMABufPlane{{File: one}, {}}, false}, | |||
| 399 | + | } { | |||
| 400 | + | err := sameDMABuf(tc.planes) | |||
| 401 | + | if (err == nil) != tc.ok { | |||
| 402 | + | t.Errorf("%s: %v", tc.name, err) | |||
| 403 | + | } | |||
| 404 | + | if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) { | |||
| 405 | + | t.Errorf("%s: %v is not errImageDMABuf", tc.name, err) | |||
| 406 | + | } | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | // TargetModifiers keeps the modifiers the display lists and drops | |||
| 411 | + | // multi-plane ones for singlePlane. | |||
| 412 | + | func TestTargetModifiersFiltering(t *testing.T) { | |||
| 413 | + | r, err := New(64, 32) | |||
| 414 | + | if err != nil { | |||
| 415 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 416 | + | } | |||
| 417 | + | defer r.Close() | |||
| 418 | + | mods := r.TargetModifiers(nil, false) | |||
| 419 | + | if len(mods) == 0 { | |||
| 420 | + | t.Skip("no importable target modifier") | |||
| 421 | + | } | |||
| 422 | + | if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 { | |||
| 423 | + | t.Errorf("modifiers outside the display list: %#x", got) | |||
| 424 | + | } | |||
| 425 | + | if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] { | |||
| 426 | + | t.Errorf("display list %#x gave %#x", mods[:1], got) | |||
| 427 | + | } | |||
| 428 | + | for _, m := range r.TargetModifiers(nil, true) { | |||
| 429 | + | if r.modPlanes[m] > 1 { | |||
| 430 | + | t.Errorf("single-plane list holds multi-plane modifier %#x", m) | |||
| 431 | + | } | |||
| 432 | + | } | |||
| 433 | + | // The answer is probed once and kept. | |||
| 434 | + | if len(r.importMods) == 0 { | |||
| 435 | + | t.Error("import modifiers not cached") | |||
| 436 | + | } | |||
| 437 | + | again := r.TargetModifiers(nil, false) | |||
| 438 | + | if !slices.Equal(mods, again) { | |||
| 439 | + | t.Errorf("cached list differs: %#x vs %#x", mods, again) | |||
| 440 | + | } | |||
| 441 | + | } | |||
| 442 | + | ||||
| 443 | + | // ImportTargets refuses an image that does not fit the renderer before | |||
| 444 | + | // asking the driver, says why, and leaves no targets: frames go back to the | |||
| 445 | + | // renderer's own image. | |||
| 446 | + | func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |||
| 447 | + | r, err := New(64, 32) | |||
| 448 | + | if err != nil { | |||
| 449 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 450 | + | } | |||
| 451 | + | defer r.Close() | |||
| 452 | + | var single uint64 | |||
| 453 | + | found := false | |||
| 454 | + | for _, m := range r.TargetModifiers(nil, true) { | |||
| 455 | + | single, found = m, true | |||
| 456 | + | break | |||
| 457 | + | } | |||
| 458 | + | if !found { | |||
| 459 | + | t.Skip("no importable one-plane target modifier") | |||
| 460 | + | } | |||
| 461 | + | good := func() ports.DMABuf { | |||
| 462 | + | return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}} | |||
| 463 | + | } | |||
| 464 | + | for _, tc := range []struct { | |||
| 465 | + | name string | |||
| 466 | + | mutate func(*ports.DMABuf) | |||
| 467 | + | want error | |||
| 468 | + | }{ | |||
| 469 | + | {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry}, | |||
| 470 | + | {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry}, | |||
| 471 | + | {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry}, | |||
| 472 | + | {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier}, | |||
| 473 | + | {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes}, | |||
| 474 | + | {"too many planes", func(b *ports.DMABuf) { | |||
| 475 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |||
| 476 | + | }, errImagePlanes}, | |||
| 477 | + | } { | |||
| 478 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 479 | + | b := good() | |||
| 480 | + | tc.mutate(&b) | |||
| 481 | + | err := r.ImportTargets([]ports.DMABuf{b}) | |||
| 482 | + | if !errors.Is(err, tc.want) { | |||
| 483 | + | t.Fatalf("error %v, want %v", err, tc.want) | |||
| 484 | + | } | |||
| 485 | + | if len(r.targets) != 0 { | |||
| 486 | + | t.Fatalf("%d targets left", len(r.targets)) | |||
| 487 | + | } | |||
| 488 | + | }) | |||
| 489 | + | } | |||
| 490 | + | t.Run("second image refused drops the first", func(t *testing.T) { | |||
| 491 | + | // A real dmabuf for the first image: the renderer's own export. | |||
| 492 | + | exported, err := r.ExportTargets(1, []uint64{single}, true) | |||
| 493 | + | if err != nil { | |||
| 494 | + | t.Skipf("no exportable target to reuse: %v", err) | |||
| 495 | + | } | |||
| 496 | + | defer closeBufs(exported) | |||
| 497 | + | bad := good() | |||
| 498 | + | bad.Width = 1 | |||
| 499 | + | err = r.ImportTargets([]ports.DMABuf{exported[0], bad}) | |||
| 500 | + | if !errors.Is(err, errImageGeometry) { | |||
| 501 | + | t.Fatalf("error %v, want the second image's geometry refusal", err) | |||
| 502 | + | } | |||
| 503 | + | if len(r.targets) != 0 { | |||
| 504 | + | t.Fatalf("%d targets left", len(r.targets)) | |||
| 505 | + | } | |||
| 506 | + | }) | |||
| 507 | + | t.Run("dropping", func(t *testing.T) { | |||
| 508 | + | if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 { | |||
| 509 | + | t.Fatalf("%v, %d targets", err, len(r.targets)) | |||
| 510 | + | } | |||
| 511 | + | }) | |||
| 512 | + | t.Run("planes in different dmabufs", func(t *testing.T) { | |||
| 513 | + | multi := uint64(0) | |||
| 514 | + | for _, m := range r.TargetModifiers(nil, false) { | |||
| 515 | + | if r.modPlanes[m] > 1 { | |||
| 516 | + | multi = m | |||
| 517 | + | break | |||
| 518 | + | } | |||
| 519 | + | } | |||
| 520 | + | if multi == 0 { | |||
| 521 | + | t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf") | |||
| 522 | + | } | |||
| 523 | + | b := good() | |||
| 524 | + | b.Modifier = multi | |||
| 525 | + | b.Planes = nil | |||
| 526 | + | for range r.modPlanes[multi] { | |||
| 527 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |||
| 528 | + | } | |||
| 529 | + | if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) { | |||
| 530 | + | t.Fatalf("error %v", err) | |||
| 531 | + | } | |||
| 532 | + | }) | |||
| 533 | + | } | |||
| 534 | + | ||||
| 535 | + | func closeBufs(bufs []ports.DMABuf) { | |||
| 536 | + | for _, b := range bufs { | |||
| 537 | + | for _, p := range b.Planes { | |||
| 538 | + | p.File.Close() | |||
| 539 | + | } | |||
| 540 | + | } | |||
| 541 | + | } | |||
A internal/app/gbm_targets_test.go +151 -0
| @@ -0,0 +1,151 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "image/color" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | |
| 10 | + | "github.com/bnema/neferwl/internal/adapters/vulkan" | |
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // fourccXRGB8888 is DRM_FORMAT_XRGB8888. | |
| 16 | + | const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 17 | + | ||
| 18 | + | // renderNodeFor opens the render node of the GPU the renderer runs on. | |
| 19 | + | func renderNodeFor(t *testing.T, dev uint64) *os.File { | |
| 20 | + | t.Helper() | |
| 21 | + | nodes, _ := filepath.Glob("/dev/dri/renderD*") | |
| 22 | + | for _, n := range nodes { | |
| 23 | + | var st unix.Stat_t | |
| 24 | + | if unix.Stat(n, &st) != nil || st.Rdev != dev { | |
| 25 | + | continue | |
| 26 | + | } | |
| 27 | + | f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0) | |
| 28 | + | if err != nil { | |
| 29 | + | t.Skipf("cannot open %s: %v", n, err) | |
| 30 | + | } | |
| 31 | + | return f | |
| 32 | + | } | |
| 33 | + | t.Skipf("no render node for device %#x", dev) | |
| 34 | + | return nil | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // Images allocated by GBM are imported as the renderer's targets (the path | |
| 38 | + | // the DRM output takes) and frames drawn into them read back. Skips without | |
| 39 | + | // a GPU, libgbm or an openable render node. | |
| 40 | + | func TestGBMImagesAreRenderTargets(t *testing.T) { | |
| 41 | + | const w, h = 128, 64 | |
| 42 | + | r, err := vulkan.New(w, h) | |
| 43 | + | if err != nil { | |
| 44 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 45 | + | } | |
| 46 | + | defer r.Close() | |
| 47 | + | sup := r.DMABuf() | |
| 48 | + | if sup.Device == 0 || len(sup.Formats) == 0 { | |
| 49 | + | t.Skip("device does not import dmabufs") | |
| 50 | + | } | |
| 51 | + | mods := r.TargetModifiers(nil, false) | |
| 52 | + | if len(mods) == 0 { | |
| 53 | + | t.Skip("no importable target modifier") | |
| 54 | + | } | |
| 55 | + | t.Logf("renderer target modifiers: %#x", mods) | |
| 56 | + | node := renderNodeFor(t, sup.Device) | |
| 57 | + | defer node.Close() | |
| 58 | + | dev, err := gbm.Open(int(node.Fd())) | |
| 59 | + | if err != nil { | |
| 60 | + | t.Skipf("GBM unavailable: %v", err) | |
| 61 | + | } | |
| 62 | + | defer dev.Close() | |
| 63 | + | ||
| 64 | + | // A render node cannot always allocate scanout buffers. | |
| 65 | + | var bufs [2]*gbm.Buffer | |
| 66 | + | for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} { | |
| 67 | + | var cerr error | |
| 68 | + | for i := range bufs { | |
| 69 | + | if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil { | |
| 70 | + | break | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | if cerr == nil { | |
| 74 | + | t.Logf("GBM usage %#x", usage) | |
| 75 | + | break | |
| 76 | + | } | |
| 77 | + | t.Logf("GBM usage %#x: %v", usage, cerr) | |
| 78 | + | for i := range bufs { | |
| 79 | + | bufs[i].Close() | |
| 80 | + | bufs[i] = nil | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | if bufs[0] == nil || bufs[1] == nil { | |
| 84 | + | t.Skip("GBM allocated no image with the renderer's modifiers") | |
| 85 | + | } | |
| 86 | + | defer bufs[0].Close() | |
| 87 | + | defer bufs[1].Close() | |
| 88 | + | ||
| 89 | + | var dmas []ports.DMABuf | |
| 90 | + | for _, b := range bufs { | |
| 91 | + | d, err := b.DMABuf() | |
| 92 | + | if err != nil { | |
| 93 | + | t.Fatal(err) | |
| 94 | + | } | |
| 95 | + | dmas = append(dmas, d) | |
| 96 | + | } | |
| 97 | + | defer func() { | |
| 98 | + | for _, d := range dmas { | |
| 99 | + | for _, p := range d.Planes { | |
| 100 | + | p.File.Close() | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | }() | |
| 104 | + | t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride) | |
| 105 | + | if err := r.ImportTargets(dmas); err != nil { | |
| 106 | + | t.Fatalf("ImportTargets: %v", err) | |
| 107 | + | } | |
| 108 | + | t.Log("Vulkan import of the GBM images succeeded") | |
| 109 | + | ||
| 110 | + | // Target 0: red background with a blue window; target 1: green. | |
| 111 | + | scenes := []ports.Scene{ | |
| 112 | + | {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}}, | |
| 113 | + | {Background: "#00ff00"}, | |
| 114 | + | } | |
| 115 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 1, Height: 1, LogicalW: 64, LogicalH: 32, Opaque: true, Solid: &ports.SolidColor{B: 1, A: 1}}} | |
| 116 | + | type px struct { | |
| 117 | + | x, y int | |
| 118 | + | want color.RGBA | |
| 119 | + | } | |
| 120 | + | checks := [][]px{ | |
| 121 | + | {{4, 4, color.RGBA{255, 0, 0, 255}}, {w - 4, h - 4, color.RGBA{255, 0, 0, 255}}, {64, 32, color.RGBA{0, 0, 255, 255}}, {36, 20, color.RGBA{0, 0, 255, 255}}}, | |
| 122 | + | {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}}, | |
| 123 | + | } | |
| 124 | + | // Twice around, so each image is drawn into again after the other one. | |
| 125 | + | for round := range 2 { | |
| 126 | + | for i := range scenes { | |
| 127 | + | r.UseTarget(i) | |
| 128 | + | s := scenes[i] | |
| 129 | + | s.Seq = uint64(round*2 + i + 1) | |
| 130 | + | done, err := r.Render(s, contents) | |
| 131 | + | if err != nil { | |
| 132 | + | t.Fatalf("render %d: %v", i, err) | |
| 133 | + | } | |
| 134 | + | if done != nil { | |
| 135 | + | done.Close() | |
| 136 | + | } | |
| 137 | + | img := r.Pixels() | |
| 138 | + | if img == nil { | |
| 139 | + | t.Fatalf("target %d: no pixels", i) | |
| 140 | + | } | |
| 141 | + | for _, c := range checks[i] { | |
| 142 | + | if got := img.At(c.x, c.y); got != c.want { | |
| 143 | + | t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want) | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | if err := r.ImportTargets(nil); err != nil { | |
| 149 | + | t.Fatal(err) | |
| 150 | + | } | |
| 151 | + | } | |
| @@ -0,0 +1,151 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image/color" | |||
| 5 | + | "os" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | |||
| 10 | + | "github.com/bnema/neferwl/internal/adapters/vulkan" | |||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // fourccXRGB8888 is DRM_FORMAT_XRGB8888. | |||
| 16 | + | const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |||
| 17 | + | ||||
| 18 | + | // renderNodeFor opens the render node of the GPU the renderer runs on. | |||
| 19 | + | func renderNodeFor(t *testing.T, dev uint64) *os.File { | |||
| 20 | + | t.Helper() | |||
| 21 | + | nodes, _ := filepath.Glob("/dev/dri/renderD*") | |||
| 22 | + | for _, n := range nodes { | |||
| 23 | + | var st unix.Stat_t | |||
| 24 | + | if unix.Stat(n, &st) != nil || st.Rdev != dev { | |||
| 25 | + | continue | |||
| 26 | + | } | |||
| 27 | + | f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0) | |||
| 28 | + | if err != nil { | |||
| 29 | + | t.Skipf("cannot open %s: %v", n, err) | |||
| 30 | + | } | |||
| 31 | + | return f | |||
| 32 | + | } | |||
| 33 | + | t.Skipf("no render node for device %#x", dev) | |||
| 34 | + | return nil | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // Images allocated by GBM are imported as the renderer's targets (the path | |||
| 38 | + | // the DRM output takes) and frames drawn into them read back. Skips without | |||
| 39 | + | // a GPU, libgbm or an openable render node. | |||
| 40 | + | func TestGBMImagesAreRenderTargets(t *testing.T) { | |||
| 41 | + | const w, h = 128, 64 | |||
| 42 | + | r, err := vulkan.New(w, h) | |||
| 43 | + | if err != nil { | |||
| 44 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 45 | + | } | |||
| 46 | + | defer r.Close() | |||
| 47 | + | sup := r.DMABuf() | |||
| 48 | + | if sup.Device == 0 || len(sup.Formats) == 0 { | |||
| 49 | + | t.Skip("device does not import dmabufs") | |||
| 50 | + | } | |||
| 51 | + | mods := r.TargetModifiers(nil, false) | |||
| 52 | + | if len(mods) == 0 { | |||
| 53 | + | t.Skip("no importable target modifier") | |||
| 54 | + | } | |||
| 55 | + | t.Logf("renderer target modifiers: %#x", mods) | |||
| 56 | + | node := renderNodeFor(t, sup.Device) | |||
| 57 | + | defer node.Close() | |||
| 58 | + | dev, err := gbm.Open(int(node.Fd())) | |||
| 59 | + | if err != nil { | |||
| 60 | + | t.Skipf("GBM unavailable: %v", err) | |||
| 61 | + | } | |||
| 62 | + | defer dev.Close() | |||
| 63 | + | ||||
| 64 | + | // A render node cannot always allocate scanout buffers. | |||
| 65 | + | var bufs [2]*gbm.Buffer | |||
| 66 | + | for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} { | |||
| 67 | + | var cerr error | |||
| 68 | + | for i := range bufs { | |||
| 69 | + | if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil { | |||
| 70 | + | break | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | if cerr == nil { | |||
| 74 | + | t.Logf("GBM usage %#x", usage) | |||
| 75 | + | break | |||
| 76 | + | } | |||
| 77 | + | t.Logf("GBM usage %#x: %v", usage, cerr) | |||
| 78 | + | for i := range bufs { | |||
| 79 | + | bufs[i].Close() | |||
| 80 | + | bufs[i] = nil | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | if bufs[0] == nil || bufs[1] == nil { | |||
| 84 | + | t.Skip("GBM allocated no image with the renderer's modifiers") | |||
| 85 | + | } | |||
| 86 | + | defer bufs[0].Close() | |||
| 87 | + | defer bufs[1].Close() | |||
| 88 | + | ||||
| 89 | + | var dmas []ports.DMABuf | |||
| 90 | + | for _, b := range bufs { | |||
| 91 | + | d, err := b.DMABuf() | |||
| 92 | + | if err != nil { | |||
| 93 | + | t.Fatal(err) | |||
| 94 | + | } | |||
| 95 | + | dmas = append(dmas, d) | |||
| 96 | + | } | |||
| 97 | + | defer func() { | |||
| 98 | + | for _, d := range dmas { | |||
| 99 | + | for _, p := range d.Planes { | |||
| 100 | + | p.File.Close() | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | }() | |||
| 104 | + | t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride) | |||
| 105 | + | if err := r.ImportTargets(dmas); err != nil { | |||
| 106 | + | t.Fatalf("ImportTargets: %v", err) | |||
| 107 | + | } | |||
| 108 | + | t.Log("Vulkan import of the GBM images succeeded") | |||
| 109 | + | ||||
| 110 | + | // Target 0: red background with a blue window; target 1: green. | |||
| 111 | + | scenes := []ports.Scene{ | |||
| 112 | + | {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}}, | |||
| 113 | + | {Background: "#00ff00"}, | |||
| 114 | + | } | |||
| 115 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 1, Height: 1, LogicalW: 64, LogicalH: 32, Opaque: true, Solid: &ports.SolidColor{B: 1, A: 1}}} | |||
| 116 | + | type px struct { | |||
| 117 | + | x, y int | |||
| 118 | + | want color.RGBA | |||
| 119 | + | } | |||
| 120 | + | checks := [][]px{ | |||
| 121 | + | {{4, 4, color.RGBA{255, 0, 0, 255}}, {w - 4, h - 4, color.RGBA{255, 0, 0, 255}}, {64, 32, color.RGBA{0, 0, 255, 255}}, {36, 20, color.RGBA{0, 0, 255, 255}}}, | |||
| 122 | + | {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}}, | |||
| 123 | + | } | |||
| 124 | + | // Twice around, so each image is drawn into again after the other one. | |||
| 125 | + | for round := range 2 { | |||
| 126 | + | for i := range scenes { | |||
| 127 | + | r.UseTarget(i) | |||
| 128 | + | s := scenes[i] | |||
| 129 | + | s.Seq = uint64(round*2 + i + 1) | |||
| 130 | + | done, err := r.Render(s, contents) | |||
| 131 | + | if err != nil { | |||
| 132 | + | t.Fatalf("render %d: %v", i, err) | |||
| 133 | + | } | |||
| 134 | + | if done != nil { | |||
| 135 | + | done.Close() | |||
| 136 | + | } | |||
| 137 | + | img := r.Pixels() | |||
| 138 | + | if img == nil { | |||
| 139 | + | t.Fatalf("target %d: no pixels", i) | |||
| 140 | + | } | |||
| 141 | + | for _, c := range checks[i] { | |||
| 142 | + | if got := img.At(c.x, c.y); got != c.want { | |||
| 143 | + | t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want) | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | if err := r.ImportTargets(nil); err != nil { | |||
| 149 | + | t.Fatal(err) | |||
| 150 | + | } | |||
| 151 | + | } | |||
M internal/mocks/ports/devices_mock.go +110 -0
| @@ -504,6 +504,57 @@ func (_c *MockRenderer_HDRPixels_Call) RunAndReturn(run func() *image.RGBA64) *M | |||
| 504 | 504 | 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 | 558 | // Pixels provides a mock function for the type MockRenderer | |
| 508 | 559 | func (_mock *MockRenderer) Pixels() *image.RGBA { | |
| 509 | 560 | ret := _mock.Called() | |
| @@ -702,6 +753,65 @@ func (_c *MockRenderer_TakeRedrawn_Call) RunAndReturn(run func() int) *MockRende | |||
| 702 | 753 | 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 | 815 | // Trim provides a mock function for the type MockRenderer | |
| 706 | 816 | func (_mock *MockRenderer) Trim(now time.Time) error { | |
| 707 | 817 | ret := _mock.Called(now) | |
| @@ -504,6 +504,57 @@ func (_c *MockRenderer_HDRPixels_Call) RunAndReturn(run func() *image.RGBA64) *M | |||||
| 504 | return _c | 504 | 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 MockRenderer | 558 | // 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 _c | 753 | 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 MockRenderer | 815 | // Trim provides a mock function for the type MockRenderer | ||
| 706 | func (_mock *MockRenderer) Trim(now time.Time) error { | 816 | func (_mock *MockRenderer) Trim(now time.Time) error { | ||
| 707 | ret := _mock.Called(now) | 817 | ret := _mock.Called(now) | ||
M internal/ports/devices.go +20 -3
| @@ -36,11 +36,12 @@ type CaptureFrame interface { | |||
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | // 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). | |
| 41 | 42 | type Renderer interface { | |
| 42 | 43 | // SetHDR selects the HDR10 output transform (0 disables it). | |
| 43 | - | // The DRM output calls this before ExportTargets. | |
| 44 | + | // The DRM output calls this before ExportTargets or ImportTargets. | |
| 44 | 45 | SetHDR(sdrNits float64) | |
| 45 | 46 | // Render draws the scene. done is a sync file signalled when the GPU | |
| 46 | 47 | // finished the frame (nil: already finished); the caller closes it. | |
| @@ -53,6 +54,22 @@ type Renderer interface { | |||
| 53 | 54 | // restricts the choice to one-plane modifiers. | |
| 54 | 55 | // n = 0 drops the targets. | |
| 55 | 56 | ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]DMABuf, error) | |
| 57 | + | // TargetModifiers lists the modifiers an image allocated outside the | |
| 58 | + | // renderer (by the display's allocator) can have to be imported as a | |
| 59 | + | // target: those the device renders into and imports for the XRGB8888 | |
| 60 | + | // (SDR) or XRGB2101010 (HDR) format, that the display lists (empty: any), | |
| 61 | + | // without multi-plane ones when singlePlane. Empty when the device | |
| 62 | + | // cannot import dmabufs. | |
| 63 | + | TargetModifiers(display []uint64, singlePlane bool) []uint64 | |
| 64 | + | // ImportTargets makes images allocated outside the renderer its | |
| 65 | + | // targets, replacing the previous ones (no buffers: drops them). Each | |
| 66 | + | // is one memory object of the renderer's size and target format, with | |
| 67 | + | // a modifier from TargetModifiers: every plane file is the same | |
| 68 | + | // dmabuf (the planes differ by offset), and disjoint planes are | |
| 69 | + | // refused. The caller keeps its files and must keep the memory alive | |
| 70 | + | // while the targets are in use. An error leaves the renderer without | |
| 71 | + | // targets. | |
| 72 | + | ImportTargets(bufs []DMABuf) error | |
| 56 | 73 | // UseTarget selects the exported image the next Render draws into. | |
| 57 | 74 | UseTarget(i int) | |
| 58 | 75 | // CursorBuffers allocates two linear ARGB8888 images of size×size the | |
| @@ -36,11 +36,12 @@ type CaptureFrame interface { | |||||
| 36 | } | 36 | } | ||
| 37 | 37 | ||||
| 38 | // Renderer draws scenes for an output. One goroutine owns it. Frames go | 38 | // 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 | 39 | + | // to its own image until ExportTargets or ImportTargets gives it scanout |
| 40 | - | // Render draws into the target chosen by UseTarget (ADR 014: no CPU pass). | 40 | + | // images; then each Render draws into the target chosen by UseTarget |
| 41 | + | // (ADR 014: no CPU pass). | |||
| 41 | type Renderer interface { | 42 | type Renderer interface { | ||
| 42 | // SetHDR selects the HDR10 output transform (0 disables it). | 43 | // SetHDR selects the HDR10 output transform (0 disables it). | ||
| 43 | - | // The DRM output calls this before ExportTargets. | 44 | + | // The DRM output calls this before ExportTargets or ImportTargets. |
| 44 | SetHDR(sdrNits float64) | 45 | SetHDR(sdrNits float64) | ||
| 45 | // Render draws the scene. done is a sync file signalled when the GPU | 46 | // Render draws the scene. done is a sync file signalled when the GPU | ||
| 46 | // finished the frame (nil: already finished); the caller closes it. | 47 | // finished the frame (nil: already finished); the caller closes it. | ||
| @@ -53,6 +54,22 @@ type Renderer interface { | |||||
| 53 | // restricts the choice to one-plane modifiers. | 54 | // restricts the choice to one-plane modifiers. | ||
| 54 | // n = 0 drops the targets. | 55 | // n = 0 drops the targets. | ||
| 55 | ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]DMABuf, error) | 56 | ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]DMABuf, error) | ||
| 57 | + | // TargetModifiers lists the modifiers an image allocated outside the | |||
| 58 | + | // renderer (by the display's allocator) can have to be imported as a | |||
| 59 | + | // target: those the device renders into and imports for the XRGB8888 | |||
| 60 | + | // (SDR) or XRGB2101010 (HDR) format, that the display lists (empty: any), | |||
| 61 | + | // without multi-plane ones when singlePlane. Empty when the device | |||
| 62 | + | // cannot import dmabufs. | |||
| 63 | + | TargetModifiers(display []uint64, singlePlane bool) []uint64 | |||
| 64 | + | // ImportTargets makes images allocated outside the renderer its | |||
| 65 | + | // targets, replacing the previous ones (no buffers: drops them). Each | |||
| 66 | + | // is one memory object of the renderer's size and target format, with | |||
| 67 | + | // a modifier from TargetModifiers: every plane file is the same | |||
| 68 | + | // dmabuf (the planes differ by offset), and disjoint planes are | |||
| 69 | + | // refused. The caller keeps its files and must keep the memory alive | |||
| 70 | + | // while the targets are in use. An error leaves the renderer without | |||
| 71 | + | // targets. | |||
| 72 | + | ImportTargets(bufs []DMABuf) error | |||
| 56 | // UseTarget selects the exported image the next Render draws into. | 73 | // UseTarget selects the exported image the next Render draws into. | ||
| 57 | UseTarget(i int) | 74 | UseTarget(i int) | ||
| 58 | // CursorBuffers allocates two linear ARGB8888 images of size×size the | 75 | // CursorBuffers allocates two linear ARGB8888 images of size×size the | ||