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