refactor(drm): simplify GBM image lifecycle and harden the fence gate
Drop redundant image state (imageAlloc, dropLive), share the framebuffer setup tail between GBM and Vulkan images, and require gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane. On gated nvidia-drm, poll client fences without dup, compose when the poll fails, wait once per commit, and count and log fence timeouts. Cache the renderer's importable target modifiers and make the fence and import tests deterministic.
- commit
- f0c31c4b25a0570b34928bb1f86760bad05ba192
- author
- brice · · 2026-10-21 20:49 UTC
- parent
- 53269c8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +635 -402
M docs/dev/architecture.md +2 -2
| @@ -28,9 +28,9 @@ About 35 channels connect them, all created in one place, so the full data flow | |||
| 28 | 28 | ||
| 29 | 29 | A DRM output flips between two images the renderer draws into, with no CPU copy. | |
| 30 | 30 | ||
| 31 | - | - **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), the allocator of the display driver, with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. NVIDIA shows images allocated by Vulkan corrupted, which is why every driver uses GBM. | |
| 31 | + | - **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. GBM is the display driver's own allocator, so the layouts it picks scan out correctly on every driver. | |
| 32 | 32 | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`). | |
| 33 | - | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. | |
| 33 | + | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable. | |
| 34 | 34 | ||
| 35 | 35 | I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | |
| 36 | 36 | ||
| @@ -28,9 +28,9 @@ About 35 channels connect them, all created in one place, so the full data flow | |||||
| 28 | 28 | ||||
| 29 | A DRM output flips between two images the renderer draws into, with no CPU copy. | 29 | A DRM output flips between two images the renderer draws into, with no CPU copy. | ||
| 30 | 30 | ||||
| 31 | - | - **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), the allocator of the display driver, with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. NVIDIA shows images allocated by Vulkan corrupted, which is why every driver uses GBM. | 31 | + | - **Allocation:** the images are allocated with GBM (`GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING`), with the modifiers the primary plane lists that the renderer can render into and import. The renderer imports them as Vulkan images, and KMS gets framebuffers made from the same dmabufs. GBM is the display driver's own allocator, so the layouts it picks scan out correctly on every driver. |
| 32 | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`). | 32 | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`). | ||
| 33 | - | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. | 33 | + | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable. |
| 34 | 34 | ||||
| 35 | I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | 35 | I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | ||
| 36 | 36 | ||||
M internal/adapters/drm/alloc.go +5 -15
| @@ -3,7 +3,6 @@ package drm | |||
| 3 | 3 | import ( | |
| 4 | 4 | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | |
| 5 | 5 | "github.com/bnema/neferwl/internal/ports" | |
| 6 | - | "github.com/bnema/zerowrap" | |
| 7 | 6 | ) | |
| 8 | 7 | ||
| 9 | 8 | // imageAllocator makes the output images with the display driver's own | |
| @@ -24,35 +23,26 @@ type imageBuffer interface { | |||
| 24 | 23 | } | |
| 25 | 24 | ||
| 26 | 25 | // gbmAllocator allocates scanout-capable, renderable images with libgbm. | |
| 27 | - | type gbmAllocator struct { | |
| 28 | - | dev *gbm.Device | |
| 29 | - | log zerowrap.Logger | |
| 30 | - | // flaglessLogged: the missing usage flags were logged once. | |
| 31 | - | flaglessLogged bool | |
| 32 | - | } | |
| 26 | + | type gbmAllocator struct{ dev *gbm.Device } | |
| 33 | 27 | ||
| 34 | 28 | // openGBM opens a GBM allocator on a duplicate of the card fd. | |
| 35 | - | func openGBM(fd int, log zerowrap.Logger) (imageAllocator, error) { | |
| 29 | + | func openGBM(fd int) (imageAllocator, error) { | |
| 36 | 30 | dev, err := gbm.Open(fd) | |
| 37 | 31 | if err != nil { | |
| 38 | 32 | return nil, err | |
| 39 | 33 | } | |
| 40 | - | return &gbmAllocator{dev: dev, log: log}, nil | |
| 34 | + | return gbmAllocator{dev}, nil | |
| 41 | 35 | } | |
| 42 | 36 | ||
| 43 | - | func (a *gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | |
| 37 | + | func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | |
| 44 | 38 | b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering) | |
| 45 | 39 | if err != nil { | |
| 46 | 40 | return nil, err | |
| 47 | 41 | } | |
| 48 | - | if b.Flagless && !a.flaglessLogged { | |
| 49 | - | a.flaglessLogged = true | |
| 50 | - | a.log.Info().Msg("libgbm lacks gbm_bo_create_with_modifiers2: images are allocated without the scanout and rendering flags") | |
| 51 | - | } | |
| 52 | 42 | return gbmImage{b}, nil | |
| 53 | 43 | } | |
| 54 | 44 | ||
| 55 | - | func (a *gbmAllocator) close() { a.dev.Close() } | |
| 45 | + | func (a gbmAllocator) close() { a.dev.Close() } | |
| 56 | 46 | ||
| 57 | 47 | type gbmImage struct{ b *gbm.Buffer } | |
| 58 | 48 | ||
| @@ -3,7 +3,6 @@ package drm | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | 4 | "github.com/bnema/neferwl/internal/adapters/drm/gbm" | ||
| 5 | "github.com/bnema/neferwl/internal/ports" | 5 | "github.com/bnema/neferwl/internal/ports" | ||
| 6 | - | "github.com/bnema/zerowrap" | |||
| 7 | ) | 6 | ) | ||
| 8 | 7 | ||||
| 9 | // imageAllocator makes the output images with the display driver's own | 8 | // imageAllocator makes the output images with the display driver's own | ||
| @@ -24,35 +23,26 @@ type imageBuffer interface { | |||||
| 24 | } | 23 | } | ||
| 25 | 24 | ||||
| 26 | // gbmAllocator allocates scanout-capable, renderable images with libgbm. | 25 | // gbmAllocator allocates scanout-capable, renderable images with libgbm. | ||
| 27 | - | type gbmAllocator struct { | 26 | + | type gbmAllocator struct{ dev *gbm.Device } |
| 28 | - | dev *gbm.Device | |||
| 29 | - | log zerowrap.Logger | |||
| 30 | - | // flaglessLogged: the missing usage flags were logged once. | |||
| 31 | - | flaglessLogged bool | |||
| 32 | - | } | |||
| 33 | 27 | ||||
| 34 | // openGBM opens a GBM allocator on a duplicate of the card fd. | 28 | // openGBM opens a GBM allocator on a duplicate of the card fd. | ||
| 35 | - | func openGBM(fd int, log zerowrap.Logger) (imageAllocator, error) { | 29 | + | func openGBM(fd int) (imageAllocator, error) { |
| 36 | dev, err := gbm.Open(fd) | 30 | dev, err := gbm.Open(fd) | ||
| 37 | if err != nil { | 31 | if err != nil { | ||
| 38 | return nil, err | 32 | return nil, err | ||
| 39 | } | 33 | } | ||
| 40 | - | return &gbmAllocator{dev: dev, log: log}, nil | 34 | + | return gbmAllocator{dev}, nil |
| 41 | } | 35 | } | ||
| 42 | 36 | ||||
| 43 | - | func (a *gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) { | 37 | + | func (a gbmAllocator) create(width, height int, format uint32, modifiers []uint64) (imageBuffer, error) { |
| 44 | b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering) | 38 | b, err := a.dev.Create(width, height, format, modifiers, gbm.UseScanout|gbm.UseRendering) | ||
| 45 | if err != nil { | 39 | if err != nil { | ||
| 46 | return nil, err | 40 | return nil, err | ||
| 47 | } | 41 | } | ||
| 48 | - | if b.Flagless && !a.flaglessLogged { | |||
| 49 | - | a.flaglessLogged = true | |||
| 50 | - | a.log.Info().Msg("libgbm lacks gbm_bo_create_with_modifiers2: images are allocated without the scanout and rendering flags") | |||
| 51 | - | } | |||
| 52 | return gbmImage{b}, nil | 42 | return gbmImage{b}, nil | ||
| 53 | } | 43 | } | ||
| 54 | 44 | ||||
| 55 | - | func (a *gbmAllocator) close() { a.dev.Close() } | 45 | + | func (a gbmAllocator) close() { a.dev.Close() } |
| 56 | 46 | ||||
| 57 | type gbmImage struct{ b *gbm.Buffer } | 47 | type gbmImage struct{ b *gbm.Buffer } | ||
| 58 | 48 | ||||
M internal/adapters/drm/alloc_test.go +49 -93
| @@ -22,19 +22,19 @@ func gbmBuf(planes int) ports.DMABuf { | |||
| 22 | 22 | return b | |
| 23 | 23 | } | |
| 24 | 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) { | |
| 25 | + | // twoGBMImages makes an allocator that hands out two images of the given | |
| 26 | + | // plane count; each image closes exactly once, counted in closed. | |
| 27 | + | func twoGBMImages(t *testing.T, planes int) (a imageAllocator, closed *int) { | |
| 28 | + | m := newMockimageAllocator(t) | |
| 29 | + | closed = new(int) | |
| 30 | + | m.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) { | |
| 31 | 31 | img := newMockimageBuffer(t) | |
| 32 | 32 | img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil }) | |
| 33 | 33 | img.EXPECT().close().Run(func() { *closed++ }).Once() | |
| 34 | 34 | return img, nil | |
| 35 | 35 | }).Twice() | |
| 36 | - | a.EXPECT().close().Return().Maybe() | |
| 37 | - | return a, closed | |
| 36 | + | m.EXPECT().close().Return().Maybe() | |
| 37 | + | return m, closed | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | 40 | func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | |
| @@ -50,7 +50,7 @@ func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | |||
| 50 | 50 | func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | |
| 51 | 51 | o, k, _ := testOutput(t) | |
| 52 | 52 | o.cursor = nil | |
| 53 | - | a, closed := gbmImages(t, 1) | |
| 53 | + | a, closed := twoGBMImages(t, 1) | |
| 54 | 54 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 55 | 55 | r := gbmRenderer(t) | |
| 56 | 56 | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | |
| @@ -62,69 +62,71 @@ func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | |||
| 62 | 62 | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 63 | 63 | t.Fatal(err) | |
| 64 | 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) | |
| 65 | + | if o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 { | |
| 66 | + | t.Fatalf("kind %d modifier %#x images %d closed %d", o.kind, o.imageMod, len(o.images), *closed) | |
| 67 | 67 | } | |
| 68 | - | o.freeImages() | |
| 69 | - | if *closed != 2 || o.images != nil { | |
| 70 | - | t.Fatalf("closed %d images, %d left", *closed, len(o.images)) | |
| 68 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 69 | + | o.dropImages(r) | |
| 70 | + | if *closed != 2 || o.images != nil || o.fbs != [2]uint32{} { | |
| 71 | + | t.Fatalf("closed %d images, %d left, fbs %v", *closed, len(o.images), o.fbs) | |
| 71 | 72 | } | |
| 72 | 73 | } | |
| 73 | 74 | ||
| 74 | - | // The plane's modifiers reach the renderer with the single-plane rule, and | |
| 75 | - | // a GBM failure falls back to the renderer's own images of the same kind. | |
| 75 | + | // A GBM failure at any step falls back to the renderer's own images of the same kind. | |
| 76 | 76 | func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | |
| 77 | 77 | for name, tc := range map[string]struct { | |
| 78 | - | setup func(t *testing.T, o *Output, r *portsmocks.MockRenderer) | |
| 78 | + | setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) | |
| 79 | 79 | }{ | |
| 80 | - | "gbm unavailable": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | |
| 80 | + | "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) { | |
| 81 | 81 | o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") } | |
| 82 | 82 | }}, | |
| 83 | - | "no common modifier": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | |
| 83 | + | "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 84 | 84 | a := newMockimageAllocator(t) | |
| 85 | 85 | a.EXPECT().close().Return().Maybe() | |
| 86 | 86 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 87 | 87 | r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once() | |
| 88 | 88 | }}, | |
| 89 | - | "allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | |
| 89 | + | "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 90 | 90 | a := newMockimageAllocator(t) | |
| 91 | 91 | a.EXPECT().close().Return().Maybe() | |
| 92 | 92 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once() | |
| 93 | 93 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 94 | 94 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 95 | 95 | }}, | |
| 96 | - | "second allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | |
| 96 | + | "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 97 | 97 | a := newMockimageAllocator(t) | |
| 98 | 98 | a.EXPECT().close().Return().Maybe() | |
| 99 | 99 | img := newMockimageBuffer(t) | |
| 100 | 100 | img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once() | |
| 101 | 101 | img.EXPECT().close().Return().Once() // freed with the failed attempt | |
| 102 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 102 | 103 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once() | |
| 103 | 104 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once() | |
| 104 | 105 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 105 | 106 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 106 | 107 | }}, | |
| 107 | - | "import refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | |
| 108 | - | a, closed := gbmImages(t, 1) | |
| 109 | - | _ = closed | |
| 108 | + | "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 109 | + | a, _ := twoGBMImages(t, 1) | |
| 110 | 110 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 111 | 111 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 112 | 112 | r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once() | |
| 113 | + | // The images made before the refusal go with a renderer drop. | |
| 114 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 113 | 115 | }}, | |
| 114 | - | "framebuffer refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | |
| 115 | - | a, _ := gbmImages(t, 1) | |
| 116 | + | "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { | |
| 117 | + | a, _ := twoGBMImages(t, 1) | |
| 116 | 118 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 117 | 119 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | |
| 118 | 120 | r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once() | |
| 119 | 121 | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |
| 120 | - | o.k.(*mockkms).EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once() | |
| 122 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once() | |
| 121 | 123 | }}, | |
| 122 | 124 | } { | |
| 123 | 125 | t.Run(name, func(t *testing.T) { | |
| 124 | 126 | o, k, _ := testOutput(t) | |
| 125 | 127 | o.cursor = nil | |
| 126 | 128 | r := gbmRenderer(t) | |
| 127 | - | tc.setup(t, o, r) | |
| 129 | + | tc.setup(t, o, k, r) | |
| 128 | 130 | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | |
| 129 | 131 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe() | |
| 130 | 132 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| @@ -133,8 +135,9 @@ func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | |||
| 133 | 135 | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 134 | 136 | t.Fatal(err) | |
| 135 | 137 | } | |
| 136 | - | if o.imageAlloc != allocVulkan || o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 { | |
| 137 | - | t.Fatalf("alloc %q kind %d images %d fbs %v", o.imageAlloc, o.kind, len(o.images), o.fbs) | |
| 138 | + | // The renderer's own images: no GBM image is alive. | |
| 139 | + | if o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 { | |
| 140 | + | t.Fatalf("kind %d images %d fbs %v", o.kind, len(o.images), o.fbs) | |
| 138 | 141 | } | |
| 139 | 142 | }) | |
| 140 | 143 | } | |
| @@ -167,7 +170,7 @@ func TestGBMOpenFailureIsRemembered(t *testing.T) { | |||
| 167 | 170 | func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | |
| 168 | 171 | o, k, commits := testOutput(t, unix.EINVAL) | |
| 169 | 172 | o.cursor = nil | |
| 170 | - | a, closed := gbmImages(t, 1) | |
| 173 | + | a, closed := twoGBMImages(t, 1) | |
| 171 | 174 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | |
| 172 | 175 | r := gbmRenderer(t) | |
| 173 | 176 | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | |
| @@ -182,28 +185,33 @@ func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | |||
| 182 | 185 | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 183 | 186 | t.Fatal(err) | |
| 184 | 187 | } | |
| 185 | - | if o.imageAlloc != allocVulkan || o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 { | |
| 186 | - | t.Fatalf("alloc %q kind %d closed %d images %d commits %d", o.imageAlloc, o.kind, *closed, len(o.images), len(*commits)) | |
| 188 | + | if o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 { | |
| 189 | + | t.Fatalf("kind %d closed %d images %d commits %d", o.kind, *closed, len(o.images), len(*commits)) | |
| 187 | 190 | } | |
| 188 | 191 | } | |
| 189 | 192 | ||
| 190 | - | // Closing the output frees the GBM images and the allocator. | |
| 191 | - | func TestOutputFreeImagesClosesGBMImages(t *testing.T) { | |
| 193 | + | // Closing the output frees the GBM images and closes the allocator once. | |
| 194 | + | func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) { | |
| 192 | 195 | o, k, _ := testOutput(t) | |
| 196 | + | o.cursor = nil | |
| 193 | 197 | img := newMockimageBuffer(t) | |
| 194 | 198 | img.EXPECT().close().Return().Twice() | |
| 199 | + | alloc := newMockimageAllocator(t) | |
| 200 | + | alloc.EXPECT().close().Return().Once() | |
| 195 | 201 | o.images = []imageBuffer{img, img} | |
| 202 | + | o.alloc = alloc | |
| 196 | 203 | o.fbs = [2]uint32{70, 71} | |
| 197 | - | k.EXPECT().rmFB(mock.Anything).Return(nil).Twice() | |
| 198 | - | o.freeImages() | |
| 199 | - | if o.images != nil || o.fbs != [2]uint32{} { | |
| 200 | - | t.Fatalf("images %v fbs %v", o.images, o.fbs) | |
| 204 | + | k.EXPECT().rmFB(uint32(70)).Return(nil).Once() | |
| 205 | + | k.EXPECT().rmFB(uint32(71)).Return(nil).Once() | |
| 206 | + | o.Close() | |
| 207 | + | if o.images != nil || o.alloc != nil || o.fbs != [2]uint32{} { | |
| 208 | + | t.Fatalf("images %v alloc %v fbs %v", o.images, o.alloc, o.fbs) | |
| 201 | 209 | } | |
| 202 | 210 | } | |
| 203 | 211 | ||
| 204 | 212 | // A setup that finds images left over frees them first, with the renderer's | |
| 205 | - | // targets over GBM ones, and a refused modeset of GBM images (no fallback | |
| 206 | - | // left) drops the targets with the buffers. | |
| 213 | + | // targets over GBM ones: the renderer never keeps a target over a freed | |
| 214 | + | // image. | |
| 207 | 215 | func TestSetupStartsFromCleanState(t *testing.T) { | |
| 208 | 216 | o, k, _ := testOutput(t) | |
| 209 | 217 | o.cursor = nil | |
| @@ -224,55 +232,3 @@ func TestSetupStartsFromCleanState(t *testing.T) { | |||
| 224 | 232 | t.Fatalf("images %v fbs %v", o.images, o.fbs) | |
| 225 | 233 | } | |
| 226 | 234 | } | |
| 227 | - | ||
| 228 | - | // Without CPU fence waits every fence is ready; with them only a signalled | |
| 229 | - | // client fence is (a pipe stands in: readable means signalled). | |
| 230 | - | func TestClientFenceReady(t *testing.T) { | |
| 231 | - | r, w, err := os.Pipe() | |
| 232 | - | if err != nil { | |
| 233 | - | t.Fatal(err) | |
| 234 | - | } | |
| 235 | - | defer r.Close() | |
| 236 | - | defer w.Close() | |
| 237 | - | o, _, _ := testOutput(t) | |
| 238 | - | if !o.clientFenceReady(r) || !o.clientFenceReady(nil) { | |
| 239 | - | t.Fatal("fence not ready without CPU waits") | |
| 240 | - | } | |
| 241 | - | o.cpuFences = true | |
| 242 | - | if !o.clientFenceReady(nil) { | |
| 243 | - | t.Fatal("no fence must be ready") | |
| 244 | - | } | |
| 245 | - | if o.clientFenceReady(r) { | |
| 246 | - | t.Fatal("pending client fence ready") | |
| 247 | - | } | |
| 248 | - | _, _ = w.Write([]byte{1}) | |
| 249 | - | if !o.clientFenceReady(r) { | |
| 250 | - | t.Fatal("signalled client fence not ready") | |
| 251 | - | } | |
| 252 | - | } | |
| 253 | - | ||
| 254 | - | // With CPU waits a client buffer whose fence is pending is composed, not | |
| 255 | - | // scanned out and not put on the overlay: neither plane waits for it. Once | |
| 256 | - | // the fence signalled the same frame scans out. | |
| 257 | - | func TestPendingClientFenceComposes(t *testing.T) { | |
| 258 | - | r, w, err := os.Pipe() | |
| 259 | - | if err != nil { | |
| 260 | - | t.Fatal(err) | |
| 261 | - | } | |
| 262 | - | defer r.Close() | |
| 263 | - | defer w.Close() | |
| 264 | - | o, k, _ := testOutput(t) | |
| 265 | - | k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe() | |
| 266 | - | o.cpuFences = true | |
| 267 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 268 | - | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |
| 269 | - | c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, Acquire: r, DMABuf: &ports.DMABuf{ID: 5, Format: fourccXRGB, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{File: r}}}} | |
| 270 | - | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |
| 271 | - | if fb, _, _ := o.scanoutFrame(s, contents); fb != 0 || o.reason != "fence_pending" { | |
| 272 | - | t.Fatalf("pending fence: fb %d reason %q", fb, o.reason) | |
| 273 | - | } | |
| 274 | - | _, _ = w.Write([]byte{1}) | |
| 275 | - | if fb, _, _ := o.scanoutFrame(s, contents); fb == 0 { | |
| 276 | - | t.Fatalf("signalled fence: reason %q", o.reason) | |
| 277 | - | } | |
| 278 | - | } | |
| @@ -22,19 +22,19 @@ func gbmBuf(planes int) ports.DMABuf { | |||||
| 22 | return b | 22 | return b | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | - | // gbmImages makes an allocator that hands out two images; each image | 25 | + | // twoGBMImages makes an allocator that hands out two images of the given |
| 26 | - | // closes exactly once. | 26 | + | // plane count; each image closes exactly once, counted in closed. |
| 27 | - | func gbmImages(t *testing.T, planes int) (imageAllocator, *int) { | 27 | + | func twoGBMImages(t *testing.T, planes int) (a imageAllocator, closed *int) { |
| 28 | - | a := newMockimageAllocator(t) | 28 | + | m := newMockimageAllocator(t) |
| 29 | - | closed := new(int) | 29 | + | closed = new(int) |
| 30 | - | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) { | 30 | + | m.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).RunAndReturn(func(int, int, uint32, []uint64) (imageBuffer, error) { |
| 31 | img := newMockimageBuffer(t) | 31 | img := newMockimageBuffer(t) | ||
| 32 | img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil }) | 32 | img.EXPECT().dmabuf().RunAndReturn(func() (ports.DMABuf, error) { return gbmBuf(planes), nil }) | ||
| 33 | img.EXPECT().close().Run(func() { *closed++ }).Once() | 33 | img.EXPECT().close().Run(func() { *closed++ }).Once() | ||
| 34 | return img, nil | 34 | return img, nil | ||
| 35 | }).Twice() | 35 | }).Twice() | ||
| 36 | - | a.EXPECT().close().Return().Maybe() | 36 | + | m.EXPECT().close().Return().Maybe() |
| 37 | - | return a, closed | 37 | + | return m, closed |
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | 40 | func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | ||
| @@ -50,7 +50,7 @@ func gbmRenderer(t *testing.T) *portsmocks.MockRenderer { | |||||
| 50 | func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | 50 | func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | ||
| 51 | o, k, _ := testOutput(t) | 51 | o, k, _ := testOutput(t) | ||
| 52 | o.cursor = nil | 52 | o.cursor = nil | ||
| 53 | - | a, closed := gbmImages(t, 1) | 53 | + | a, closed := twoGBMImages(t, 1) |
| 54 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 54 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 55 | r := gbmRenderer(t) | 55 | r := gbmRenderer(t) | ||
| 56 | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | 56 | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | ||
| @@ -62,69 +62,71 @@ func TestShowImagesAllocatesWithGBMFirst(t *testing.T) { | |||||
| 62 | if err := o.showImages(r, imagesDriver, nil); err != nil { | 62 | if err := o.showImages(r, imagesDriver, nil); err != nil { | ||
| 63 | t.Fatal(err) | 63 | t.Fatal(err) | ||
| 64 | } | 64 | } | ||
| 65 | - | if o.imageAlloc != allocGBM || o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 { | 65 | + | if o.kind != imagesDriver || o.imageMod != tiledMod || len(o.images) != 2 || *closed != 0 { |
| 66 | - | t.Fatalf("alloc %q kind %d modifier %#x images %d closed %d", o.imageAlloc, o.kind, o.imageMod, len(o.images), *closed) | 66 | + | t.Fatalf("kind %d modifier %#x images %d closed %d", o.kind, o.imageMod, len(o.images), *closed) |
| 67 | } | 67 | } | ||
| 68 | - | o.freeImages() | 68 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() |
| 69 | - | if *closed != 2 || o.images != nil { | 69 | + | o.dropImages(r) |
| 70 | - | t.Fatalf("closed %d images, %d left", *closed, len(o.images)) | 70 | + | if *closed != 2 || o.images != nil || o.fbs != [2]uint32{} { |
| 71 | + | t.Fatalf("closed %d images, %d left, fbs %v", *closed, len(o.images), o.fbs) | |||
| 71 | } | 72 | } | ||
| 72 | } | 73 | } | ||
| 73 | 74 | ||||
| 74 | - | // The plane's modifiers reach the renderer with the single-plane rule, and | 75 | + | // A GBM failure at any step falls back to the renderer's own images of the same kind. |
| 75 | - | // a GBM failure falls back to the renderer's own images of the same kind. | |||
| 76 | func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | 76 | func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | ||
| 77 | for name, tc := range map[string]struct { | 77 | for name, tc := range map[string]struct { | ||
| 78 | - | setup func(t *testing.T, o *Output, r *portsmocks.MockRenderer) | 78 | + | setup func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) |
| 79 | }{ | 79 | }{ | ||
| 80 | - | "gbm unavailable": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | 80 | + | "gbm unavailable": {func(_ *testing.T, o *Output, _ *mockkms, _ *portsmocks.MockRenderer) { |
| 81 | o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") } | 81 | o.openAlloc = func() (imageAllocator, error) { return nil, errors.New("no libgbm") } | ||
| 82 | }}, | 82 | }}, | ||
| 83 | - | "no common modifier": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | 83 | + | "no common modifier": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { |
| 84 | a := newMockimageAllocator(t) | 84 | a := newMockimageAllocator(t) | ||
| 85 | a.EXPECT().close().Return().Maybe() | 85 | a.EXPECT().close().Return().Maybe() | ||
| 86 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 86 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 87 | r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once() | 87 | r.EXPECT().TargetModifiers(mock.Anything, false).Return(nil).Once() | ||
| 88 | }}, | 88 | }}, | ||
| 89 | - | "allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | 89 | + | "allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { |
| 90 | a := newMockimageAllocator(t) | 90 | a := newMockimageAllocator(t) | ||
| 91 | a.EXPECT().close().Return().Maybe() | 91 | a.EXPECT().close().Return().Maybe() | ||
| 92 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once() | 92 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("gbm: no buffer")).Once() | ||
| 93 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 93 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 94 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | 94 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | ||
| 95 | }}, | 95 | }}, | ||
| 96 | - | "second allocation fails": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | 96 | + | "second allocation fails": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { |
| 97 | a := newMockimageAllocator(t) | 97 | a := newMockimageAllocator(t) | ||
| 98 | a.EXPECT().close().Return().Maybe() | 98 | a.EXPECT().close().Return().Maybe() | ||
| 99 | img := newMockimageBuffer(t) | 99 | img := newMockimageBuffer(t) | ||
| 100 | img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once() | 100 | img.EXPECT().dmabuf().Return(gbmBuf(1), nil).Once() | ||
| 101 | img.EXPECT().close().Return().Once() // freed with the failed attempt | 101 | img.EXPECT().close().Return().Once() // freed with the failed attempt | ||
| 102 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 102 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once() | 103 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(img, nil).Once() | ||
| 103 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once() | 104 | a.EXPECT().create(200, 100, uint32(fourccXRGB), []uint64{tiledMod}).Return(nil, errors.New("out of memory")).Once() | ||
| 104 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 105 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 105 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | 106 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | ||
| 106 | }}, | 107 | }}, | ||
| 107 | - | "import refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | 108 | + | "import refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { |
| 108 | - | a, closed := gbmImages(t, 1) | 109 | + | a, _ := twoGBMImages(t, 1) |
| 109 | - | _ = closed | |||
| 110 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 110 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 111 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | 111 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | ||
| 112 | r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once() | 112 | r.EXPECT().ImportTargets(mock.Anything).Return(errors.New("explicit layout refused")).Once() | ||
| 113 | + | // The images made before the refusal go with a renderer drop. | |||
| 114 | + | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | |||
| 113 | }}, | 115 | }}, | ||
| 114 | - | "framebuffer refused": {func(t *testing.T, o *Output, r *portsmocks.MockRenderer) { | 116 | + | "framebuffer refused": {func(t *testing.T, o *Output, k *mockkms, r *portsmocks.MockRenderer) { |
| 115 | - | a, _ := gbmImages(t, 1) | 117 | + | a, _ := twoGBMImages(t, 1) |
| 116 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 118 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 117 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | 119 | r.EXPECT().TargetModifiers(mock.Anything, false).Return([]uint64{tiledMod}).Once() | ||
| 118 | r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once() | 120 | r.EXPECT().ImportTargets(mock.Anything).Return(nil).Once() | ||
| 119 | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | 121 | r.EXPECT().ImportTargets([]ports.DMABuf(nil)).Return(nil).Once() | ||
| 120 | - | o.k.(*mockkms).EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once() | 122 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(0, unix.EINVAL).Once() |
| 121 | }}, | 123 | }}, | ||
| 122 | } { | 124 | } { | ||
| 123 | t.Run(name, func(t *testing.T) { | 125 | t.Run(name, func(t *testing.T) { | ||
| 124 | o, k, _ := testOutput(t) | 126 | o, k, _ := testOutput(t) | ||
| 125 | o.cursor = nil | 127 | o.cursor = nil | ||
| 126 | r := gbmRenderer(t) | 128 | r := gbmRenderer(t) | ||
| 127 | - | tc.setup(t, o, r) | 129 | + | tc.setup(t, o, k, r) |
| 128 | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | 130 | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{gbmBuf(1), gbmBuf(1)}, nil).Once() | ||
| 129 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe() | 131 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe() | ||
| 130 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | 132 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | ||
| @@ -133,8 +135,9 @@ func TestShowImagesFallsBackToVulkanWhenGBMFails(t *testing.T) { | |||||
| 133 | if err := o.showImages(r, imagesDriver, nil); err != nil { | 135 | if err := o.showImages(r, imagesDriver, nil); err != nil { | ||
| 134 | t.Fatal(err) | 136 | t.Fatal(err) | ||
| 135 | } | 137 | } | ||
| 136 | - | if o.imageAlloc != allocVulkan || o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 { | 138 | + | // The renderer's own images: no GBM image is alive. |
| 137 | - | t.Fatalf("alloc %q kind %d images %d fbs %v", o.imageAlloc, o.kind, len(o.images), o.fbs) | 139 | + | if o.kind != imagesDriver || len(o.images) != 0 || o.fbs[0] == 0 || o.fbs[1] == 0 { |
| 140 | + | t.Fatalf("kind %d images %d fbs %v", o.kind, len(o.images), o.fbs) | |||
| 138 | } | 141 | } | ||
| 139 | }) | 142 | }) | ||
| 140 | } | 143 | } | ||
| @@ -167,7 +170,7 @@ func TestGBMOpenFailureIsRemembered(t *testing.T) { | |||||
| 167 | func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | 170 | func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | ||
| 168 | o, k, commits := testOutput(t, unix.EINVAL) | 171 | o, k, commits := testOutput(t, unix.EINVAL) | ||
| 169 | o.cursor = nil | 172 | o.cursor = nil | ||
| 170 | - | a, closed := gbmImages(t, 1) | 173 | + | a, closed := twoGBMImages(t, 1) |
| 171 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | 174 | o.openAlloc = func() (imageAllocator, error) { return a, nil } | ||
| 172 | r := gbmRenderer(t) | 175 | r := gbmRenderer(t) | ||
| 173 | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | 176 | r.EXPECT().TargetModifiers([]uint64(nil), false).Return([]uint64{tiledMod}).Once() | ||
| @@ -182,28 +185,33 @@ func TestShowImagesRefusedGBMImagesTryVulkanOfTheSameKind(t *testing.T) { | |||||
| 182 | if err := o.showImages(r, imagesDriver, nil); err != nil { | 185 | if err := o.showImages(r, imagesDriver, nil); err != nil { | ||
| 183 | t.Fatal(err) | 186 | t.Fatal(err) | ||
| 184 | } | 187 | } | ||
| 185 | - | if o.imageAlloc != allocVulkan || o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 { | 188 | + | if o.kind != imagesDriver || *closed != 2 || len(o.images) != 0 || len(*commits) != 3 { |
| 186 | - | t.Fatalf("alloc %q kind %d closed %d images %d commits %d", o.imageAlloc, o.kind, *closed, len(o.images), len(*commits)) | 189 | + | t.Fatalf("kind %d closed %d images %d commits %d", o.kind, *closed, len(o.images), len(*commits)) |
| 187 | } | 190 | } | ||
| 188 | } | 191 | } | ||
| 189 | 192 | ||||
| 190 | - | // Closing the output frees the GBM images and the allocator. | 193 | + | // Closing the output frees the GBM images and closes the allocator once. |
| 191 | - | func TestOutputFreeImagesClosesGBMImages(t *testing.T) { | 194 | + | func TestOutputCloseFreesGBMImagesAndAllocator(t *testing.T) { |
| 192 | o, k, _ := testOutput(t) | 195 | o, k, _ := testOutput(t) | ||
| 196 | + | o.cursor = nil | |||
| 193 | img := newMockimageBuffer(t) | 197 | img := newMockimageBuffer(t) | ||
| 194 | img.EXPECT().close().Return().Twice() | 198 | img.EXPECT().close().Return().Twice() | ||
| 199 | + | alloc := newMockimageAllocator(t) | |||
| 200 | + | alloc.EXPECT().close().Return().Once() | |||
| 195 | o.images = []imageBuffer{img, img} | 201 | o.images = []imageBuffer{img, img} | ||
| 202 | + | o.alloc = alloc | |||
| 196 | o.fbs = [2]uint32{70, 71} | 203 | o.fbs = [2]uint32{70, 71} | ||
| 197 | - | k.EXPECT().rmFB(mock.Anything).Return(nil).Twice() | 204 | + | k.EXPECT().rmFB(uint32(70)).Return(nil).Once() |
| 198 | - | o.freeImages() | 205 | + | k.EXPECT().rmFB(uint32(71)).Return(nil).Once() |
| 199 | - | if o.images != nil || o.fbs != [2]uint32{} { | 206 | + | o.Close() |
| 200 | - | t.Fatalf("images %v fbs %v", o.images, o.fbs) | 207 | + | if o.images != nil || o.alloc != nil || o.fbs != [2]uint32{} { |
| 208 | + | t.Fatalf("images %v alloc %v fbs %v", o.images, o.alloc, o.fbs) | |||
| 201 | } | 209 | } | ||
| 202 | } | 210 | } | ||
| 203 | 211 | ||||
| 204 | // A setup that finds images left over frees them first, with the renderer's | 212 | // 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 | 213 | + | // targets over GBM ones: the renderer never keeps a target over a freed |
| 206 | - | // left) drops the targets with the buffers. | 214 | + | // image. |
| 207 | func TestSetupStartsFromCleanState(t *testing.T) { | 215 | func TestSetupStartsFromCleanState(t *testing.T) { | ||
| 208 | o, k, _ := testOutput(t) | 216 | o, k, _ := testOutput(t) | ||
| 209 | o.cursor = nil | 217 | o.cursor = nil | ||
| @@ -224,55 +232,3 @@ func TestSetupStartsFromCleanState(t *testing.T) { | |||||
| 224 | t.Fatalf("images %v fbs %v", o.images, o.fbs) | 232 | t.Fatalf("images %v fbs %v", o.images, o.fbs) | ||
| 225 | } | 233 | } | ||
| 226 | } | 234 | } | ||
| 227 | - | ||||
| 228 | - | // Without CPU fence waits every fence is ready; with them only a signalled | |||
| 229 | - | // client fence is (a pipe stands in: readable means signalled). | |||
| 230 | - | func TestClientFenceReady(t *testing.T) { | |||
| 231 | - | r, w, err := os.Pipe() | |||
| 232 | - | if err != nil { | |||
| 233 | - | t.Fatal(err) | |||
| 234 | - | } | |||
| 235 | - | defer r.Close() | |||
| 236 | - | defer w.Close() | |||
| 237 | - | o, _, _ := testOutput(t) | |||
| 238 | - | if !o.clientFenceReady(r) || !o.clientFenceReady(nil) { | |||
| 239 | - | t.Fatal("fence not ready without CPU waits") | |||
| 240 | - | } | |||
| 241 | - | o.cpuFences = true | |||
| 242 | - | if !o.clientFenceReady(nil) { | |||
| 243 | - | t.Fatal("no fence must be ready") | |||
| 244 | - | } | |||
| 245 | - | if o.clientFenceReady(r) { | |||
| 246 | - | t.Fatal("pending client fence ready") | |||
| 247 | - | } | |||
| 248 | - | _, _ = w.Write([]byte{1}) | |||
| 249 | - | if !o.clientFenceReady(r) { | |||
| 250 | - | t.Fatal("signalled client fence not ready") | |||
| 251 | - | } | |||
| 252 | - | } | |||
| 253 | - | ||||
| 254 | - | // With CPU waits a client buffer whose fence is pending is composed, not | |||
| 255 | - | // scanned out and not put on the overlay: neither plane waits for it. Once | |||
| 256 | - | // the fence signalled the same frame scans out. | |||
| 257 | - | func TestPendingClientFenceComposes(t *testing.T) { | |||
| 258 | - | r, w, err := os.Pipe() | |||
| 259 | - | if err != nil { | |||
| 260 | - | t.Fatal(err) | |||
| 261 | - | } | |||
| 262 | - | defer r.Close() | |||
| 263 | - | defer w.Close() | |||
| 264 | - | o, k, _ := testOutput(t) | |||
| 265 | - | k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe() | |||
| 266 | - | o.cpuFences = true | |||
| 267 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 268 | - | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |||
| 269 | - | c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, Acquire: r, DMABuf: &ports.DMABuf{ID: 5, Format: fourccXRGB, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{File: r}}}} | |||
| 270 | - | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 271 | - | if fb, _, _ := o.scanoutFrame(s, contents); fb != 0 || o.reason != "fence_pending" { | |||
| 272 | - | t.Fatalf("pending fence: fb %d reason %q", fb, o.reason) | |||
| 273 | - | } | |||
| 274 | - | _, _ = w.Write([]byte{1}) | |||
| 275 | - | if fb, _, _ := o.scanoutFrame(s, contents); fb == 0 { | |||
| 276 | - | t.Fatalf("signalled fence: reason %q", o.reason) | |||
| 277 | - | } | |||
| 278 | - | } | |||
M internal/adapters/drm/fencegate.go +67 -23
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/neferwl/internal/adapters/syncfile" | |
| 14 | 14 | "github.com/bnema/zerowrap" | |
| 15 | + | "golang.org/x/sys/unix" | |
| 15 | 16 | ) | |
| 16 | 17 | ||
| 17 | 18 | // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for | |
| @@ -52,20 +53,19 @@ func driverName(fd int) (string, error) { | |||
| 52 | 53 | } | |
| 53 | 54 | ||
| 54 | 55 | // parseVersion reads a dotted version such as "560.35.03" (missing parts | |
| 55 | - | // are 0). | |
| 56 | + | // are 0). Parts are plain digits: no sign. | |
| 56 | 57 | func parseVersion(s string) ([3]int, error) { | |
| 57 | 58 | var v [3]int | |
| 58 | 59 | parts := strings.Split(strings.TrimSpace(s), ".") | |
| 59 | - | if len(parts) == 0 || len(parts) > 3 || parts[0] == "" { | |
| 60 | + | if len(parts) > len(v) { | |
| 60 | 61 | return v, fmt.Errorf("version %q", s) | |
| 61 | 62 | } | |
| 62 | 63 | for i, p := range parts { | |
| 63 | - | // Digits only: Atoi would also take a sign. | |
| 64 | - | n, err := strconv.Atoi(p) | |
| 65 | - | if err != nil || p[0] < '0' || p[0] > '9' { | |
| 64 | + | n, err := strconv.ParseUint(p, 10, 31) | |
| 65 | + | if err != nil { | |
| 66 | 66 | return v, fmt.Errorf("version %q", s) | |
| 67 | 67 | } | |
| 68 | - | v[i] = n | |
| 68 | + | v[i] = int(n) | |
| 69 | 69 | } | |
| 70 | 70 | return v, nil | |
| 71 | 71 | } | |
| @@ -110,7 +110,7 @@ func fenceGate(fd int, log zerowrap.Logger) bool { | |||
| 110 | 110 | if !brokenInFence(driver, version, verr) { | |
| 111 | 111 | return false | |
| 112 | 112 | } | |
| 113 | - | log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU") | |
| 113 | + | log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU") | |
| 114 | 114 | return true | |
| 115 | 115 | } | |
| 116 | 116 | ||
| @@ -123,32 +123,63 @@ func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) { | |||
| 123 | 123 | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 124 | 124 | } | |
| 125 | 125 | ||
| 126 | - | // fenceWaitLimit bounds a CPU wait on a fence of our own render. | |
| 126 | + | // fenceWaitLimit bounds the CPU wait of one commit, whatever its number of | |
| 127 | + | // fences. | |
| 127 | 128 | const fenceWaitLimit = time.Second | |
| 128 | 129 | ||
| 130 | + | // fenceWarnEvery is the least time between two warnings about a fence wait | |
| 131 | + | // that hit fenceWaitLimit. | |
| 132 | + | const fenceWarnEvery = 10 * time.Second | |
| 133 | + | ||
| 134 | + | // fencePoll polls one fence without blocking. fn is built once, so polling | |
| 135 | + | // allocates nothing; the output goroutine owns it. | |
| 136 | + | type fencePoll struct { | |
| 137 | + | fds [1]unix.PollFd | |
| 138 | + | ready bool | |
| 139 | + | fn func(fd uintptr) | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func (p *fencePoll) init() { | |
| 143 | + | p.fn = func(fd uintptr) { | |
| 144 | + | p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN} | |
| 145 | + | n, err := unix.Poll(p.fds[:], 0) | |
| 146 | + | p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0 | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 129 | 150 | // clientFenceReady reports whether a client's acquire fence lets its buffer | |
| 130 | 151 | // be shown by a plane this frame. Where fences are waited for on the CPU, a | |
| 131 | 152 | // client fence that has not signalled must not stall the output: the buffer | |
| 132 | - | // is composed instead (the renderer waits for it on the GPU). A nil or | |
| 133 | - | // unpollable fence counts as signalled, as in signalled. | |
| 153 | + | // is composed instead (the renderer waits for it on the GPU). Without CPU | |
| 154 | + | // waits, and for no fence, it is ready; with them, a fence that cannot be | |
| 155 | + | // polled is not ready. | |
| 134 | 156 | func (o *Output) clientFenceReady(acquire *os.File) bool { | |
| 135 | 157 | if !o.cpuFences || acquire == nil { | |
| 136 | 158 | return true | |
| 137 | 159 | } | |
| 138 | - | // Wayland may close its file at any time: poll a duplicate. | |
| 139 | - | d := dupFence(acquire) | |
| 140 | - | if d == nil { | |
| 141 | - | return true | |
| 160 | + | if o.fencePoll.fn == nil { | |
| 161 | + | o.fencePoll.init() | |
| 142 | 162 | } | |
| 143 | - | defer d.Close() | |
| 144 | - | return signalled([]*os.File{d}) | |
| 163 | + | raw, err := acquire.SyscallConn() | |
| 164 | + | if err != nil { | |
| 165 | + | return false | |
| 166 | + | } | |
| 167 | + | o.fencePoll.ready = false | |
| 168 | + | // Control keeps the descriptor open for the call: Wayland closing its | |
| 169 | + | // file meanwhile cannot hand us a reused number. | |
| 170 | + | if raw.Control(o.fencePoll.fn) != nil { | |
| 171 | + | return false | |
| 172 | + | } | |
| 173 | + | return o.fencePoll.ready | |
| 145 | 174 | } | |
| 146 | 175 | ||
| 147 | - | // waitFences blocks until our own render fences signalled when the driver | |
| 148 | - | // cannot wait for them in the commit, for at most fenceWaitLimit each, or | |
| 149 | - | // until the output stops. Client fences are not waited for here: planes take | |
| 150 | - | // only buffers whose fence signalled (clientFenceReady). A failed wait is not | |
| 151 | - | // fatal: the frame may then show early, as it would on any such driver. | |
| 176 | + | // waitFences blocks until the fences signalled when the driver cannot wait | |
| 177 | + | // for them in the commit, for at most fenceWaitLimit in all, or until the | |
| 178 | + | // output stops. A client fence passed here (direct scanout) already | |
| 179 | + | // signalled: planes take only such buffers (clientFenceReady), so it | |
| 180 | + | // returns at once. A failed or timed out wait is not fatal: the frame may | |
| 181 | + | // then show early, as it would on any such driver. A timeout is counted in | |
| 182 | + | // the stats and warned about at most every fenceWarnEvery. | |
| 152 | 183 | func (o *Output) waitFences(fences ...*os.File) { | |
| 153 | 184 | if !o.cpuFences { | |
| 154 | 185 | return | |
| @@ -157,13 +188,26 @@ func (o *Output) waitFences(fences ...*os.File) { | |||
| 157 | 188 | if ctx == nil { | |
| 158 | 189 | ctx = context.Background() | |
| 159 | 190 | } | |
| 160 | - | ctx, cancel := context.WithTimeout(ctx, fenceWaitLimit) | |
| 191 | + | limit := o.fenceLimit | |
| 192 | + | if limit == 0 { | |
| 193 | + | limit = fenceWaitLimit | |
| 194 | + | } | |
| 195 | + | ctx, cancel := context.WithTimeout(ctx, limit) | |
| 161 | 196 | defer cancel() | |
| 162 | 197 | for _, f := range fences { | |
| 163 | 198 | if f == nil { | |
| 164 | 199 | continue | |
| 165 | 200 | } | |
| 166 | - | if err := syncfile.Wait(ctx, f); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { | |
| 201 | + | err := syncfile.Wait(ctx, f) | |
| 202 | + | switch { | |
| 203 | + | case err == nil || errors.Is(err, context.Canceled): | |
| 204 | + | case errors.Is(err, context.DeadlineExceeded): | |
| 205 | + | o.flipStats.fenceTimeouts++ | |
| 206 | + | if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery { | |
| 207 | + | o.fenceWarnAt = now | |
| 208 | + | o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway") | |
| 209 | + | } | |
| 210 | + | default: | |
| 167 | 211 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence") | |
| 168 | 212 | } | |
| 169 | 213 | } | |
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | 12 | ||||
| 13 | "github.com/bnema/neferwl/internal/adapters/syncfile" | 13 | "github.com/bnema/neferwl/internal/adapters/syncfile" | ||
| 14 | "github.com/bnema/zerowrap" | 14 | "github.com/bnema/zerowrap" | ||
| 15 | + | "golang.org/x/sys/unix" | |||
| 15 | ) | 16 | ) | ||
| 16 | 17 | ||||
| 17 | // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for | 18 | // Old nvidia-drm drivers mishandle IN_FENCE_FD: the flip does not wait for | ||
| @@ -52,20 +53,19 @@ func driverName(fd int) (string, error) { | |||||
| 52 | } | 53 | } | ||
| 53 | 54 | ||||
| 54 | // parseVersion reads a dotted version such as "560.35.03" (missing parts | 55 | // parseVersion reads a dotted version such as "560.35.03" (missing parts | ||
| 55 | - | // are 0). | 56 | + | // are 0). Parts are plain digits: no sign. |
| 56 | func parseVersion(s string) ([3]int, error) { | 57 | func parseVersion(s string) ([3]int, error) { | ||
| 57 | var v [3]int | 58 | var v [3]int | ||
| 58 | parts := strings.Split(strings.TrimSpace(s), ".") | 59 | parts := strings.Split(strings.TrimSpace(s), ".") | ||
| 59 | - | if len(parts) == 0 || len(parts) > 3 || parts[0] == "" { | 60 | + | if len(parts) > len(v) { |
| 60 | return v, fmt.Errorf("version %q", s) | 61 | return v, fmt.Errorf("version %q", s) | ||
| 61 | } | 62 | } | ||
| 62 | for i, p := range parts { | 63 | for i, p := range parts { | ||
| 63 | - | // Digits only: Atoi would also take a sign. | 64 | + | n, err := strconv.ParseUint(p, 10, 31) |
| 64 | - | n, err := strconv.Atoi(p) | 65 | + | if err != nil { |
| 65 | - | if err != nil || p[0] < '0' || p[0] > '9' { | |||
| 66 | return v, fmt.Errorf("version %q", s) | 66 | return v, fmt.Errorf("version %q", s) | ||
| 67 | } | 67 | } | ||
| 68 | - | v[i] = n | 68 | + | v[i] = int(n) |
| 69 | } | 69 | } | ||
| 70 | return v, nil | 70 | return v, nil | ||
| 71 | } | 71 | } | ||
| @@ -110,7 +110,7 @@ func fenceGate(fd int, log zerowrap.Logger) bool { | |||||
| 110 | if !brokenInFence(driver, version, verr) { | 110 | if !brokenInFence(driver, version, verr) { | ||
| 111 | return false | 111 | return false | ||
| 112 | } | 112 | } | ||
| 113 | - | log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU") | 113 | + | log.Warn().Err(verr).Str("driver", driver).Str("version", strings.TrimSpace(version)).Msg("nvidia-drm version unknown or before 560.35.3 mishandles IN_FENCE_FD: waiting for frame fences on the CPU") |
| 114 | return true | 114 | return true | ||
| 115 | } | 115 | } | ||
| 116 | 116 | ||||
| @@ -123,32 +123,63 @@ func (o *Output) fenceProp(req *atomicReq, p *plane, fence *os.File) { | |||||
| 123 | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd())) | 123 | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(fence.Fd())) | ||
| 124 | } | 124 | } | ||
| 125 | 125 | ||||
| 126 | - | // fenceWaitLimit bounds a CPU wait on a fence of our own render. | 126 | + | // fenceWaitLimit bounds the CPU wait of one commit, whatever its number of |
| 127 | + | // fences. | |||
| 127 | const fenceWaitLimit = time.Second | 128 | const fenceWaitLimit = time.Second | ||
| 128 | 129 | ||||
| 130 | + | // fenceWarnEvery is the least time between two warnings about a fence wait | |||
| 131 | + | // that hit fenceWaitLimit. | |||
| 132 | + | const fenceWarnEvery = 10 * time.Second | |||
| 133 | + | ||||
| 134 | + | // fencePoll polls one fence without blocking. fn is built once, so polling | |||
| 135 | + | // allocates nothing; the output goroutine owns it. | |||
| 136 | + | type fencePoll struct { | |||
| 137 | + | fds [1]unix.PollFd | |||
| 138 | + | ready bool | |||
| 139 | + | fn func(fd uintptr) | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (p *fencePoll) init() { | |||
| 143 | + | p.fn = func(fd uintptr) { | |||
| 144 | + | p.fds[0] = unix.PollFd{Fd: int32(fd), Events: unix.POLLIN} | |||
| 145 | + | n, err := unix.Poll(p.fds[:], 0) | |||
| 146 | + | p.ready = err == nil && n > 0 && p.fds[0].Revents&unix.POLLIN != 0 | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 129 | // clientFenceReady reports whether a client's acquire fence lets its buffer | 150 | // 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 | 151 | // 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 | 152 | // 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 | 153 | + | // is composed instead (the renderer waits for it on the GPU). Without CPU |
| 133 | - | // unpollable fence counts as signalled, as in signalled. | 154 | + | // waits, and for no fence, it is ready; with them, a fence that cannot be |
| 155 | + | // polled is not ready. | |||
| 134 | func (o *Output) clientFenceReady(acquire *os.File) bool { | 156 | func (o *Output) clientFenceReady(acquire *os.File) bool { | ||
| 135 | if !o.cpuFences || acquire == nil { | 157 | if !o.cpuFences || acquire == nil { | ||
| 136 | return true | 158 | return true | ||
| 137 | } | 159 | } | ||
| 138 | - | // Wayland may close its file at any time: poll a duplicate. | 160 | + | if o.fencePoll.fn == nil { |
| 139 | - | d := dupFence(acquire) | 161 | + | o.fencePoll.init() |
| 140 | - | if d == nil { | |||
| 141 | - | return true | |||
| 142 | } | 162 | } | ||
| 143 | - | defer d.Close() | 163 | + | raw, err := acquire.SyscallConn() |
| 144 | - | return signalled([]*os.File{d}) | 164 | + | if err != nil { |
| 165 | + | return false | |||
| 166 | + | } | |||
| 167 | + | o.fencePoll.ready = false | |||
| 168 | + | // Control keeps the descriptor open for the call: Wayland closing its | |||
| 169 | + | // file meanwhile cannot hand us a reused number. | |||
| 170 | + | if raw.Control(o.fencePoll.fn) != nil { | |||
| 171 | + | return false | |||
| 172 | + | } | |||
| 173 | + | return o.fencePoll.ready | |||
| 145 | } | 174 | } | ||
| 146 | 175 | ||||
| 147 | - | // waitFences blocks until our own render fences signalled when the driver | 176 | + | // waitFences blocks until the fences signalled when the driver cannot wait |
| 148 | - | // cannot wait for them in the commit, for at most fenceWaitLimit each, or | 177 | + | // for them in the commit, for at most fenceWaitLimit in all, or until the |
| 149 | - | // until the output stops. Client fences are not waited for here: planes take | 178 | + | // output stops. A client fence passed here (direct scanout) already |
| 150 | - | // only buffers whose fence signalled (clientFenceReady). A failed wait is not | 179 | + | // signalled: planes take only such buffers (clientFenceReady), so it |
| 151 | - | // fatal: the frame may then show early, as it would on any such driver. | 180 | + | // returns at once. A failed or timed out wait is not fatal: the frame may |
| 181 | + | // then show early, as it would on any such driver. A timeout is counted in | |||
| 182 | + | // the stats and warned about at most every fenceWarnEvery. | |||
| 152 | func (o *Output) waitFences(fences ...*os.File) { | 183 | func (o *Output) waitFences(fences ...*os.File) { | ||
| 153 | if !o.cpuFences { | 184 | if !o.cpuFences { | ||
| 154 | return | 185 | return | ||
| @@ -157,13 +188,26 @@ func (o *Output) waitFences(fences ...*os.File) { | |||||
| 157 | if ctx == nil { | 188 | if ctx == nil { | ||
| 158 | ctx = context.Background() | 189 | ctx = context.Background() | ||
| 159 | } | 190 | } | ||
| 160 | - | ctx, cancel := context.WithTimeout(ctx, fenceWaitLimit) | 191 | + | limit := o.fenceLimit |
| 192 | + | if limit == 0 { | |||
| 193 | + | limit = fenceWaitLimit | |||
| 194 | + | } | |||
| 195 | + | ctx, cancel := context.WithTimeout(ctx, limit) | |||
| 161 | defer cancel() | 196 | defer cancel() | ||
| 162 | for _, f := range fences { | 197 | for _, f := range fences { | ||
| 163 | if f == nil { | 198 | if f == nil { | ||
| 164 | continue | 199 | continue | ||
| 165 | } | 200 | } | ||
| 166 | - | if err := syncfile.Wait(ctx, f); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { | 201 | + | err := syncfile.Wait(ctx, f) |
| 202 | + | switch { | |||
| 203 | + | case err == nil || errors.Is(err, context.Canceled): | |||
| 204 | + | case errors.Is(err, context.DeadlineExceeded): | |||
| 205 | + | o.flipStats.fenceTimeouts++ | |||
| 206 | + | if now := time.Now(); o.fenceWarnAt.IsZero() || now.Sub(o.fenceWarnAt) >= fenceWarnEvery { | |||
| 207 | + | o.fenceWarnAt = now | |||
| 208 | + | o.log.Warn().Str("connector", o.conn.name).Dur("limit", limit).Msg("frame fence did not signal in time; committing anyway") | |||
| 209 | + | } | |||
| 210 | + | default: | |||
| 167 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence") | 211 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("wait for frame fence") | ||
| 168 | } | 212 | } | ||
| 169 | } | 213 | } | ||
M internal/adapters/drm/fencegate_test.go +206 -45
| @@ -1,14 +1,31 @@ | |||
| 1 | 1 | package drm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "errors" | |
| 5 | 6 | "os" | |
| 7 | + | "path/filepath" | |
| 6 | 8 | "testing" | |
| 7 | 9 | "time" | |
| 8 | 10 | ||
| 9 | - | "github.com/bnema/zerowrap" | |
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |
| 12 | + | "github.com/stretchr/testify/mock" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 10 | 14 | ) | |
| 11 | 15 | ||
| 16 | + | // pipeFence returns a pipe standing in for a sync file: its read end is | |
| 17 | + | // readable (signalled) once the write end got a byte. Both ends close with | |
| 18 | + | // the test. | |
| 19 | + | func pipeFence(t *testing.T) (fence, signal *os.File) { | |
| 20 | + | t.Helper() | |
| 21 | + | r, w, err := os.Pipe() | |
| 22 | + | if err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |
| 26 | + | return r, w | |
| 27 | + | } | |
| 28 | + | ||
| 12 | 29 | func TestParseVersion(t *testing.T) { | |
| 13 | 30 | for _, tc := range []struct { | |
| 14 | 31 | in string | |
| @@ -26,7 +43,9 @@ func TestParseVersion(t *testing.T) { | |||
| 26 | 43 | {"-1.2", [3]int{}, false}, | |
| 27 | 44 | {"+560.35.3", [3]int{}, false}, | |
| 28 | 45 | {"560.+35.3", [3]int{}, false}, | |
| 46 | + | {"560.-5.3", [3]int{}, false}, | |
| 29 | 47 | {"560..3", [3]int{}, false}, | |
| 48 | + | {"99999999999.1.1", [3]int{}, false}, | |
| 30 | 49 | } { | |
| 31 | 50 | got, err := parseVersion(tc.in) | |
| 32 | 51 | if (err == nil) != tc.ok || tc.ok && got != tc.want { | |
| @@ -35,25 +54,8 @@ func TestParseVersion(t *testing.T) { | |||
| 35 | 54 | } | |
| 36 | 55 | } | |
| 37 | 56 | ||
| 38 | - | func TestVersionLess(t *testing.T) { | |
| 39 | - | for _, tc := range []struct { | |
| 40 | - | a, b [3]int | |
| 41 | - | want bool | |
| 42 | - | }{ | |
| 43 | - | {[3]int{560, 35, 2}, [3]int{560, 35, 3}, true}, | |
| 44 | - | {[3]int{560, 35, 3}, [3]int{560, 35, 3}, false}, | |
| 45 | - | {[3]int{560, 35, 5}, [3]int{560, 35, 3}, false}, | |
| 46 | - | {[3]int{550, 120, 0}, [3]int{560, 35, 3}, true}, | |
| 47 | - | {[3]int{560, 36, 0}, [3]int{560, 35, 3}, false}, | |
| 48 | - | {[3]int{615, 78, 8}, [3]int{560, 35, 3}, false}, | |
| 49 | - | {[3]int{470, 99, 99}, [3]int{560, 0, 0}, true}, | |
| 50 | - | } { | |
| 51 | - | if got := versionLess(tc.a, tc.b); got != tc.want { | |
| 52 | - | t.Errorf("versionLess(%v, %v) = %v", tc.a, tc.b, got) | |
| 53 | - | } | |
| 54 | - | } | |
| 55 | - | } | |
| 56 | - | ||
| 57 | + | // The gate opens for nvidia-drm before 560.35.3 or of unknown version, and | |
| 58 | + | // for no other driver. | |
| 57 | 59 | func TestBrokenInFence(t *testing.T) { | |
| 58 | 60 | for _, tc := range []struct { | |
| 59 | 61 | name, driver, version string | |
| @@ -63,8 +65,12 @@ func TestBrokenInFence(t *testing.T) { | |||
| 63 | 65 | {"amd", "amdgpu", "", nil, false}, | |
| 64 | 66 | {"intel", "i915", "garbage", nil, false}, | |
| 65 | 67 | {"old nvidia", driverNVIDIA, "555.58.02", nil, true}, | |
| 66 | - | {"just before", driverNVIDIA, "560.35.2", nil, true}, | |
| 68 | + | {"one patch before", driverNVIDIA, "560.35.2", nil, true}, | |
| 69 | + | {"minor before", driverNVIDIA, "560.34.99", nil, true}, | |
| 67 | 70 | {"fixed", driverNVIDIA, "560.35.03\n", nil, false}, | |
| 71 | + | {"patch after", driverNVIDIA, "560.35.5", nil, false}, | |
| 72 | + | {"minor after", driverNVIDIA, "560.36.0", nil, false}, | |
| 73 | + | {"major after", driverNVIDIA, "561.0.0", nil, false}, | |
| 68 | 74 | {"newer", driverNVIDIA, "615.78.08", nil, false}, | |
| 69 | 75 | {"unreadable", driverNVIDIA, "", errors.New("no such file"), true}, | |
| 70 | 76 | {"unparsable", driverNVIDIA, "n/a", nil, true}, | |
| @@ -75,14 +81,32 @@ func TestBrokenInFence(t *testing.T) { | |||
| 75 | 81 | } | |
| 76 | 82 | } | |
| 77 | 83 | ||
| 84 | + | // The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node | |
| 85 | + | // answers it too: skips without one. | |
| 86 | + | func TestDriverName(t *testing.T) { | |
| 87 | + | nodes, _ := filepath.Glob("/dev/dri/renderD*") | |
| 88 | + | if len(nodes) == 0 { | |
| 89 | + | t.Skip("no /dev/dri/renderD* node") | |
| 90 | + | } | |
| 91 | + | for _, n := range nodes { | |
| 92 | + | f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0) | |
| 93 | + | if err != nil { | |
| 94 | + | continue | |
| 95 | + | } | |
| 96 | + | defer f.Close() | |
| 97 | + | name, err := driverName(int(f.Fd())) | |
| 98 | + | if err != nil || name == "" { | |
| 99 | + | t.Fatalf("driverName(%s) = %q, %v", n, name, err) | |
| 100 | + | } | |
| 101 | + | t.Logf("%s: %s", n, name) | |
| 102 | + | return | |
| 103 | + | } | |
| 104 | + | t.Skip("no render node can be opened") | |
| 105 | + | } | |
| 106 | + | ||
| 78 | 107 | // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does. | |
| 79 | 108 | func TestFenceProp(t *testing.T) { | |
| 80 | - | f, w, err := os.Pipe() | |
| 81 | - | if err != nil { | |
| 82 | - | t.Fatal(err) | |
| 83 | - | } | |
| 84 | - | defer f.Close() | |
| 85 | - | defer w.Close() | |
| 109 | + | f, _ := pipeFence(t) | |
| 86 | 110 | for _, cpu := range []bool{false, true} { | |
| 87 | 111 | o, _, _ := testOutput(t) | |
| 88 | 112 | o.cpuFences = cpu | |
| @@ -96,27 +120,164 @@ func TestFenceProp(t *testing.T) { | |||
| 96 | 120 | } | |
| 97 | 121 | } | |
| 98 | 122 | ||
| 99 | - | // The CPU wait returns once the fence signals (a pipe stands in: readable | |
| 100 | - | // means signalled) and gives up after fenceWaitLimit. | |
| 123 | + | // waitFences returns for a fence that signalled, for one that never will | |
| 124 | + | // once the output stops, and at once where fences go to the kernel. | |
| 101 | 125 | func TestWaitFences(t *testing.T) { | |
| 126 | + | returns := func(t *testing.T, o *Output, fences ...*os.File) { | |
| 127 | + | t.Helper() | |
| 128 | + | done := make(chan struct{}) | |
| 129 | + | go func() { o.waitFences(fences...); close(done) }() | |
| 130 | + | select { | |
| 131 | + | case <-done: | |
| 132 | + | case <-time.After(30 * time.Second): // failure guard only | |
| 133 | + | t.Fatal("waitFences did not return") | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | t.Run("signalled", func(t *testing.T) { | |
| 137 | + | o, _, _ := testOutput(t) | |
| 138 | + | o.cpuFences = true | |
| 139 | + | f, w := pipeFence(t) | |
| 140 | + | _, _ = w.Write([]byte{1}) | |
| 141 | + | returns(t, o, nil, f) | |
| 142 | + | }) | |
| 143 | + | t.Run("never signalled, output stopped", func(t *testing.T) { | |
| 144 | + | o, _, _ := testOutput(t) | |
| 145 | + | o.cpuFences = true | |
| 146 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 147 | + | cancel() | |
| 148 | + | o.runContext = ctx | |
| 149 | + | f, _ := pipeFence(t) | |
| 150 | + | returns(t, o, f) | |
| 151 | + | if o.flipStats.fenceTimeouts != 0 { | |
| 152 | + | t.Fatal("a stopped output counted a fence timeout") | |
| 153 | + | } | |
| 154 | + | }) | |
| 155 | + | t.Run("kernel waits", func(t *testing.T) { | |
| 156 | + | o, _, _ := testOutput(t) | |
| 157 | + | f, _ := pipeFence(t) | |
| 158 | + | returns(t, o, f) | |
| 159 | + | }) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // A fence wait that hit its limit counts in the stats, and warns at most | |
| 163 | + | // once per fenceWarnEvery. | |
| 164 | + | func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) { | |
| 102 | 165 | o, _, _ := testOutput(t) | |
| 103 | 166 | o.cpuFences = true | |
| 104 | - | o.log = zerowrap.Default() | |
| 105 | - | r, w, err := os.Pipe() | |
| 106 | - | if err != nil { | |
| 167 | + | o.fenceLimit = time.Millisecond | |
| 168 | + | f, _ := pipeFence(t) | |
| 169 | + | o.waitFences(f) | |
| 170 | + | if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() { | |
| 171 | + | t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt) | |
| 172 | + | } | |
| 173 | + | first := o.fenceWarnAt | |
| 174 | + | o.waitFences(f) | |
| 175 | + | if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) { | |
| 176 | + | t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // Without CPU fence waits every fence is ready; with them only a signalled | |
| 181 | + | // client fence is, and one that cannot be polled is not. | |
| 182 | + | func TestClientFenceReady(t *testing.T) { | |
| 183 | + | f, w := pipeFence(t) | |
| 184 | + | o, _, _ := testOutput(t) | |
| 185 | + | if !o.clientFenceReady(f) || !o.clientFenceReady(nil) { | |
| 186 | + | t.Fatal("fence not ready without CPU waits") | |
| 187 | + | } | |
| 188 | + | o.cpuFences = true | |
| 189 | + | if !o.clientFenceReady(nil) { | |
| 190 | + | t.Fatal("no fence must be ready") | |
| 191 | + | } | |
| 192 | + | if o.clientFenceReady(f) { | |
| 193 | + | t.Fatal("pending client fence ready") | |
| 194 | + | } | |
| 195 | + | _, _ = w.Write([]byte{1}) | |
| 196 | + | if !o.clientFenceReady(f) { | |
| 197 | + | t.Fatal("signalled client fence not ready") | |
| 198 | + | } | |
| 199 | + | closed, cw := pipeFence(t) | |
| 200 | + | closed.Close() | |
| 201 | + | cw.Close() | |
| 202 | + | if o.clientFenceReady(closed) { | |
| 203 | + | t.Fatal("closed fence ready") | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | // Polling a fence allocates nothing. | |
| 208 | + | func TestClientFenceReadyAllocatesNothing(t *testing.T) { | |
| 209 | + | f, _ := pipeFence(t) | |
| 210 | + | o, _, _ := testOutput(t) | |
| 211 | + | o.cpuFences = true | |
| 212 | + | o.clientFenceReady(f) // builds the poll closure | |
| 213 | + | if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 { | |
| 214 | + | t.Fatalf("%v allocations per poll", n) | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // With CPU waits a client buffer whose fence is pending is composed, on the | |
| 219 | + | // primary plane and on the overlay alike: neither plane waits for it. Once | |
| 220 | + | // the fence signalled the same frame uses the plane. | |
| 221 | + | func TestPendingClientFenceComposes(t *testing.T) { | |
| 222 | + | for _, tc := range []struct { | |
| 223 | + | name string | |
| 224 | + | reason func(o *Output) string | |
| 225 | + | // decide returns whether the plane took the buffer. | |
| 226 | + | decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool | |
| 227 | + | }{ | |
| 228 | + | {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |
| 229 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 230 | + | s, c := fullscreenScene() | |
| 231 | + | content := c[1] | |
| 232 | + | content.Acquire = fence | |
| 233 | + | fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content}) | |
| 234 | + | return fb != 0 | |
| 235 | + | }}, | |
| 236 | + | {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |
| 237 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 238 | + | s, c := overlayScene() | |
| 239 | + | content := c[2] | |
| 240 | + | content.Acquire = fence | |
| 241 | + | ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content}) | |
| 242 | + | ov.close() | |
| 243 | + | return ov.fb != 0 | |
| 244 | + | }}, | |
| 245 | + | } { | |
| 246 | + | t.Run(tc.name, func(t *testing.T) { | |
| 247 | + | o, k, _ := overlayOutput(t) | |
| 248 | + | k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe() | |
| 249 | + | o.cpuFences = true | |
| 250 | + | fence, w := pipeFence(t) | |
| 251 | + | if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" { | |
| 252 | + | t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o)) | |
| 253 | + | } | |
| 254 | + | _, _ = w.Write([]byte{1}) | |
| 255 | + | if !tc.decide(t, o, k, fence) { | |
| 256 | + | t.Fatalf("signalled fence: reason %q", tc.reason(o)) | |
| 257 | + | } | |
| 258 | + | }) | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // A commit that is retried (here: VRR refused, then the same frame without | |
| 263 | + | // it) waits for its fence once, not once per attempt. | |
| 264 | + | func TestCommitWaitsForItsFenceOnce(t *testing.T) { | |
| 265 | + | o, _, commits := testOutput(t, unix.EINVAL) | |
| 266 | + | o.cpuFences = true | |
| 267 | + | o.fenceLimit = time.Millisecond | |
| 268 | + | fence, _ := pipeFence(t) | |
| 269 | + | if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil { | |
| 107 | 270 | t.Fatal(err) | |
| 108 | 271 | } | |
| 109 | - | defer r.Close() | |
| 110 | - | go func() { | |
| 111 | - | time.Sleep(20 * time.Millisecond) | |
| 112 | - | _, _ = w.Write([]byte{1}) | |
| 113 | - | }() | |
| 114 | - | start := time.Now() | |
| 115 | - | o.waitFences(nil, r) | |
| 116 | - | if d := time.Since(start); d < 15*time.Millisecond || d > fenceWaitLimit/2 { | |
| 117 | - | t.Fatalf("waited %v for the fence", d) | |
| 118 | - | } | |
| 119 | - | w.Close() | |
| 120 | - | o.cpuFences = false | |
| 121 | - | o.waitFences(r) // not waited for: returns at once | |
| 272 | + | if len(*commits) < 2 { | |
| 273 | + | t.Fatalf("%d commits: no retry happened", len(*commits)) | |
| 274 | + | } | |
| 275 | + | if o.flipStats.fenceTimeouts != 1 { | |
| 276 | + | t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits)) | |
| 277 | + | } | |
| 278 | + | for i, c := range *commits { | |
| 279 | + | if _, ok := c.req.value(tPrimary, pFence); ok { | |
| 280 | + | t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i) | |
| 281 | + | } | |
| 282 | + | } | |
| 122 | 283 | } | |
| @@ -1,14 +1,31 @@ | |||||
| 1 | package drm | 1 | package drm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "errors" | 5 | "errors" | ||
| 5 | "os" | 6 | "os" | ||
| 7 | + | "path/filepath" | |||
| 6 | "testing" | 8 | "testing" | ||
| 7 | "time" | 9 | "time" | ||
| 8 | 10 | ||||
| 9 | - | "github.com/bnema/zerowrap" | 11 | + | "github.com/bnema/neferwl/internal/ports" |
| 12 | + | "github.com/stretchr/testify/mock" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 10 | ) | 14 | ) | ||
| 11 | 15 | ||||
| 16 | + | // pipeFence returns a pipe standing in for a sync file: its read end is | |||
| 17 | + | // readable (signalled) once the write end got a byte. Both ends close with | |||
| 18 | + | // the test. | |||
| 19 | + | func pipeFence(t *testing.T) (fence, signal *os.File) { | |||
| 20 | + | t.Helper() | |||
| 21 | + | r, w, err := os.Pipe() | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |||
| 26 | + | return r, w | |||
| 27 | + | } | |||
| 28 | + | ||||
| 12 | func TestParseVersion(t *testing.T) { | 29 | func TestParseVersion(t *testing.T) { | ||
| 13 | for _, tc := range []struct { | 30 | for _, tc := range []struct { | ||
| 14 | in string | 31 | in string | ||
| @@ -26,7 +43,9 @@ func TestParseVersion(t *testing.T) { | |||||
| 26 | {"-1.2", [3]int{}, false}, | 43 | {"-1.2", [3]int{}, false}, | ||
| 27 | {"+560.35.3", [3]int{}, false}, | 44 | {"+560.35.3", [3]int{}, false}, | ||
| 28 | {"560.+35.3", [3]int{}, false}, | 45 | {"560.+35.3", [3]int{}, false}, | ||
| 46 | + | {"560.-5.3", [3]int{}, false}, | |||
| 29 | {"560..3", [3]int{}, false}, | 47 | {"560..3", [3]int{}, false}, | ||
| 48 | + | {"99999999999.1.1", [3]int{}, false}, | |||
| 30 | } { | 49 | } { | ||
| 31 | got, err := parseVersion(tc.in) | 50 | got, err := parseVersion(tc.in) | ||
| 32 | if (err == nil) != tc.ok || tc.ok && got != tc.want { | 51 | if (err == nil) != tc.ok || tc.ok && got != tc.want { | ||
| @@ -35,25 +54,8 @@ func TestParseVersion(t *testing.T) { | |||||
| 35 | } | 54 | } | ||
| 36 | } | 55 | } | ||
| 37 | 56 | ||||
| 38 | - | func TestVersionLess(t *testing.T) { | 57 | + | // The gate opens for nvidia-drm before 560.35.3 or of unknown version, and |
| 39 | - | for _, tc := range []struct { | 58 | + | // for no other driver. |
| 40 | - | a, b [3]int | |||
| 41 | - | want bool | |||
| 42 | - | }{ | |||
| 43 | - | {[3]int{560, 35, 2}, [3]int{560, 35, 3}, true}, | |||
| 44 | - | {[3]int{560, 35, 3}, [3]int{560, 35, 3}, false}, | |||
| 45 | - | {[3]int{560, 35, 5}, [3]int{560, 35, 3}, false}, | |||
| 46 | - | {[3]int{550, 120, 0}, [3]int{560, 35, 3}, true}, | |||
| 47 | - | {[3]int{560, 36, 0}, [3]int{560, 35, 3}, false}, | |||
| 48 | - | {[3]int{615, 78, 8}, [3]int{560, 35, 3}, false}, | |||
| 49 | - | {[3]int{470, 99, 99}, [3]int{560, 0, 0}, true}, | |||
| 50 | - | } { | |||
| 51 | - | if got := versionLess(tc.a, tc.b); got != tc.want { | |||
| 52 | - | t.Errorf("versionLess(%v, %v) = %v", tc.a, tc.b, got) | |||
| 53 | - | } | |||
| 54 | - | } | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | func TestBrokenInFence(t *testing.T) { | 59 | func TestBrokenInFence(t *testing.T) { | ||
| 58 | for _, tc := range []struct { | 60 | for _, tc := range []struct { | ||
| 59 | name, driver, version string | 61 | name, driver, version string | ||
| @@ -63,8 +65,12 @@ func TestBrokenInFence(t *testing.T) { | |||||
| 63 | {"amd", "amdgpu", "", nil, false}, | 65 | {"amd", "amdgpu", "", nil, false}, | ||
| 64 | {"intel", "i915", "garbage", nil, false}, | 66 | {"intel", "i915", "garbage", nil, false}, | ||
| 65 | {"old nvidia", driverNVIDIA, "555.58.02", nil, true}, | 67 | {"old nvidia", driverNVIDIA, "555.58.02", nil, true}, | ||
| 66 | - | {"just before", driverNVIDIA, "560.35.2", nil, true}, | 68 | + | {"one patch before", driverNVIDIA, "560.35.2", nil, true}, |
| 69 | + | {"minor before", driverNVIDIA, "560.34.99", nil, true}, | |||
| 67 | {"fixed", driverNVIDIA, "560.35.03\n", nil, false}, | 70 | {"fixed", driverNVIDIA, "560.35.03\n", nil, false}, | ||
| 71 | + | {"patch after", driverNVIDIA, "560.35.5", nil, false}, | |||
| 72 | + | {"minor after", driverNVIDIA, "560.36.0", nil, false}, | |||
| 73 | + | {"major after", driverNVIDIA, "561.0.0", nil, false}, | |||
| 68 | {"newer", driverNVIDIA, "615.78.08", nil, false}, | 74 | {"newer", driverNVIDIA, "615.78.08", nil, false}, | ||
| 69 | {"unreadable", driverNVIDIA, "", errors.New("no such file"), true}, | 75 | {"unreadable", driverNVIDIA, "", errors.New("no such file"), true}, | ||
| 70 | {"unparsable", driverNVIDIA, "n/a", nil, true}, | 76 | {"unparsable", driverNVIDIA, "n/a", nil, true}, | ||
| @@ -75,14 +81,32 @@ func TestBrokenInFence(t *testing.T) { | |||||
| 75 | } | 81 | } | ||
| 76 | } | 82 | } | ||
| 77 | 83 | ||||
| 84 | + | // The driver of a DRM device is read with DRM_IOCTL_VERSION. A render node | |||
| 85 | + | // answers it too: skips without one. | |||
| 86 | + | func TestDriverName(t *testing.T) { | |||
| 87 | + | nodes, _ := filepath.Glob("/dev/dri/renderD*") | |||
| 88 | + | if len(nodes) == 0 { | |||
| 89 | + | t.Skip("no /dev/dri/renderD* node") | |||
| 90 | + | } | |||
| 91 | + | for _, n := range nodes { | |||
| 92 | + | f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0) | |||
| 93 | + | if err != nil { | |||
| 94 | + | continue | |||
| 95 | + | } | |||
| 96 | + | defer f.Close() | |||
| 97 | + | name, err := driverName(int(f.Fd())) | |||
| 98 | + | if err != nil || name == "" { | |||
| 99 | + | t.Fatalf("driverName(%s) = %q, %v", n, name, err) | |||
| 100 | + | } | |||
| 101 | + | t.Logf("%s: %s", n, name) | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | t.Skip("no render node can be opened") | |||
| 105 | + | } | |||
| 106 | + | ||||
| 78 | // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does. | 107 | // With CPU waits no commit carries IN_FENCE_FD; otherwise the plane does. | ||
| 79 | func TestFenceProp(t *testing.T) { | 108 | func TestFenceProp(t *testing.T) { | ||
| 80 | - | f, w, err := os.Pipe() | 109 | + | f, _ := pipeFence(t) |
| 81 | - | if err != nil { | |||
| 82 | - | t.Fatal(err) | |||
| 83 | - | } | |||
| 84 | - | defer f.Close() | |||
| 85 | - | defer w.Close() | |||
| 86 | for _, cpu := range []bool{false, true} { | 110 | for _, cpu := range []bool{false, true} { | ||
| 87 | o, _, _ := testOutput(t) | 111 | o, _, _ := testOutput(t) | ||
| 88 | o.cpuFences = cpu | 112 | o.cpuFences = cpu | ||
| @@ -96,27 +120,164 @@ func TestFenceProp(t *testing.T) { | |||||
| 96 | } | 120 | } | ||
| 97 | } | 121 | } | ||
| 98 | 122 | ||||
| 99 | - | // The CPU wait returns once the fence signals (a pipe stands in: readable | 123 | + | // waitFences returns for a fence that signalled, for one that never will |
| 100 | - | // means signalled) and gives up after fenceWaitLimit. | 124 | + | // once the output stops, and at once where fences go to the kernel. |
| 101 | func TestWaitFences(t *testing.T) { | 125 | func TestWaitFences(t *testing.T) { | ||
| 126 | + | returns := func(t *testing.T, o *Output, fences ...*os.File) { | |||
| 127 | + | t.Helper() | |||
| 128 | + | done := make(chan struct{}) | |||
| 129 | + | go func() { o.waitFences(fences...); close(done) }() | |||
| 130 | + | select { | |||
| 131 | + | case <-done: | |||
| 132 | + | case <-time.After(30 * time.Second): // failure guard only | |||
| 133 | + | t.Fatal("waitFences did not return") | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | t.Run("signalled", func(t *testing.T) { | |||
| 137 | + | o, _, _ := testOutput(t) | |||
| 138 | + | o.cpuFences = true | |||
| 139 | + | f, w := pipeFence(t) | |||
| 140 | + | _, _ = w.Write([]byte{1}) | |||
| 141 | + | returns(t, o, nil, f) | |||
| 142 | + | }) | |||
| 143 | + | t.Run("never signalled, output stopped", func(t *testing.T) { | |||
| 144 | + | o, _, _ := testOutput(t) | |||
| 145 | + | o.cpuFences = true | |||
| 146 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 147 | + | cancel() | |||
| 148 | + | o.runContext = ctx | |||
| 149 | + | f, _ := pipeFence(t) | |||
| 150 | + | returns(t, o, f) | |||
| 151 | + | if o.flipStats.fenceTimeouts != 0 { | |||
| 152 | + | t.Fatal("a stopped output counted a fence timeout") | |||
| 153 | + | } | |||
| 154 | + | }) | |||
| 155 | + | t.Run("kernel waits", func(t *testing.T) { | |||
| 156 | + | o, _, _ := testOutput(t) | |||
| 157 | + | f, _ := pipeFence(t) | |||
| 158 | + | returns(t, o, f) | |||
| 159 | + | }) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // A fence wait that hit its limit counts in the stats, and warns at most | |||
| 163 | + | // once per fenceWarnEvery. | |||
| 164 | + | func TestWaitFencesTimeoutIsCountedAndRateLimited(t *testing.T) { | |||
| 102 | o, _, _ := testOutput(t) | 165 | o, _, _ := testOutput(t) | ||
| 103 | o.cpuFences = true | 166 | o.cpuFences = true | ||
| 104 | - | o.log = zerowrap.Default() | 167 | + | o.fenceLimit = time.Millisecond |
| 105 | - | r, w, err := os.Pipe() | 168 | + | f, _ := pipeFence(t) |
| 106 | - | if err != nil { | 169 | + | o.waitFences(f) |
| 170 | + | if o.flipStats.fenceTimeouts != 1 || o.fenceWarnAt.IsZero() { | |||
| 171 | + | t.Fatalf("timeouts %d, warned at %v", o.flipStats.fenceTimeouts, o.fenceWarnAt) | |||
| 172 | + | } | |||
| 173 | + | first := o.fenceWarnAt | |||
| 174 | + | o.waitFences(f) | |||
| 175 | + | if o.flipStats.fenceTimeouts != 2 || !o.fenceWarnAt.Equal(first) { | |||
| 176 | + | t.Fatalf("timeouts %d; warning not rate-limited (%v then %v)", o.flipStats.fenceTimeouts, first, o.fenceWarnAt) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // Without CPU fence waits every fence is ready; with them only a signalled | |||
| 181 | + | // client fence is, and one that cannot be polled is not. | |||
| 182 | + | func TestClientFenceReady(t *testing.T) { | |||
| 183 | + | f, w := pipeFence(t) | |||
| 184 | + | o, _, _ := testOutput(t) | |||
| 185 | + | if !o.clientFenceReady(f) || !o.clientFenceReady(nil) { | |||
| 186 | + | t.Fatal("fence not ready without CPU waits") | |||
| 187 | + | } | |||
| 188 | + | o.cpuFences = true | |||
| 189 | + | if !o.clientFenceReady(nil) { | |||
| 190 | + | t.Fatal("no fence must be ready") | |||
| 191 | + | } | |||
| 192 | + | if o.clientFenceReady(f) { | |||
| 193 | + | t.Fatal("pending client fence ready") | |||
| 194 | + | } | |||
| 195 | + | _, _ = w.Write([]byte{1}) | |||
| 196 | + | if !o.clientFenceReady(f) { | |||
| 197 | + | t.Fatal("signalled client fence not ready") | |||
| 198 | + | } | |||
| 199 | + | closed, cw := pipeFence(t) | |||
| 200 | + | closed.Close() | |||
| 201 | + | cw.Close() | |||
| 202 | + | if o.clientFenceReady(closed) { | |||
| 203 | + | t.Fatal("closed fence ready") | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | // Polling a fence allocates nothing. | |||
| 208 | + | func TestClientFenceReadyAllocatesNothing(t *testing.T) { | |||
| 209 | + | f, _ := pipeFence(t) | |||
| 210 | + | o, _, _ := testOutput(t) | |||
| 211 | + | o.cpuFences = true | |||
| 212 | + | o.clientFenceReady(f) // builds the poll closure | |||
| 213 | + | if n := testing.AllocsPerRun(100, func() { o.clientFenceReady(f) }); n != 0 { | |||
| 214 | + | t.Fatalf("%v allocations per poll", n) | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // With CPU waits a client buffer whose fence is pending is composed, on the | |||
| 219 | + | // primary plane and on the overlay alike: neither plane waits for it. Once | |||
| 220 | + | // the fence signalled the same frame uses the plane. | |||
| 221 | + | func TestPendingClientFenceComposes(t *testing.T) { | |||
| 222 | + | for _, tc := range []struct { | |||
| 223 | + | name string | |||
| 224 | + | reason func(o *Output) string | |||
| 225 | + | // decide returns whether the plane took the buffer. | |||
| 226 | + | decide func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool | |||
| 227 | + | }{ | |||
| 228 | + | {"primary scanout", func(o *Output) string { return o.reason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |||
| 229 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 230 | + | s, c := fullscreenScene() | |||
| 231 | + | content := c[1] | |||
| 232 | + | content.Acquire = fence | |||
| 233 | + | fb, _, _ := o.scanoutFrame(s, map[ports.WindowID]ports.SurfaceContent{1: content}) | |||
| 234 | + | return fb != 0 | |||
| 235 | + | }}, | |||
| 236 | + | {"overlay", func(o *Output) string { return o.overlayReason }, func(t *testing.T, o *Output, k *mockkms, fence *os.File) bool { | |||
| 237 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 238 | + | s, c := overlayScene() | |||
| 239 | + | content := c[2] | |||
| 240 | + | content.Acquire = fence | |||
| 241 | + | ov, _ := o.overlayFrame(s, map[ports.WindowID]ports.SurfaceContent{2: content}) | |||
| 242 | + | ov.close() | |||
| 243 | + | return ov.fb != 0 | |||
| 244 | + | }}, | |||
| 245 | + | } { | |||
| 246 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 247 | + | o, k, _ := overlayOutput(t) | |||
| 248 | + | k.EXPECT().addFB(mock.Anything, mock.Anything).Return(60, nil).Maybe() | |||
| 249 | + | o.cpuFences = true | |||
| 250 | + | fence, w := pipeFence(t) | |||
| 251 | + | if tc.decide(t, o, k, fence) || tc.reason(o) != "fence_pending" { | |||
| 252 | + | t.Fatalf("pending fence: plane took the buffer or reason %q", tc.reason(o)) | |||
| 253 | + | } | |||
| 254 | + | _, _ = w.Write([]byte{1}) | |||
| 255 | + | if !tc.decide(t, o, k, fence) { | |||
| 256 | + | t.Fatalf("signalled fence: reason %q", tc.reason(o)) | |||
| 257 | + | } | |||
| 258 | + | }) | |||
| 259 | + | } | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // A commit that is retried (here: VRR refused, then the same frame without | |||
| 263 | + | // it) waits for its fence once, not once per attempt. | |||
| 264 | + | func TestCommitWaitsForItsFenceOnce(t *testing.T) { | |||
| 265 | + | o, _, commits := testOutput(t, unix.EINVAL) | |||
| 266 | + | o.cpuFences = true | |||
| 267 | + | o.fenceLimit = time.Millisecond | |||
| 268 | + | fence, _ := pipeFence(t) | |||
| 269 | + | if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil { | |||
| 107 | t.Fatal(err) | 270 | t.Fatal(err) | ||
| 108 | } | 271 | } | ||
| 109 | - | defer r.Close() | 272 | + | if len(*commits) < 2 { |
| 110 | - | go func() { | 273 | + | t.Fatalf("%d commits: no retry happened", len(*commits)) |
| 111 | - | time.Sleep(20 * time.Millisecond) | 274 | + | } |
| 112 | - | _, _ = w.Write([]byte{1}) | 275 | + | if o.flipStats.fenceTimeouts != 1 { |
| 113 | - | }() | 276 | + | t.Fatalf("%d fence waits for %d commits, want one", o.flipStats.fenceTimeouts, len(*commits)) |
| 114 | - | start := time.Now() | 277 | + | } |
| 115 | - | o.waitFences(nil, r) | 278 | + | for i, c := range *commits { |
| 116 | - | if d := time.Since(start); d < 15*time.Millisecond || d > fenceWaitLimit/2 { | 279 | + | if _, ok := c.req.value(tPrimary, pFence); ok { |
| 117 | - | t.Fatalf("waited %v for the fence", d) | 280 | + | t.Fatalf("commit %d carries IN_FENCE_FD with CPU waits", i) |
| 118 | - | } | 281 | + | } |
| 119 | - | w.Close() | 282 | + | } |
| 120 | - | o.cpuFences = false | |||
| 121 | - | o.waitFences(r) // not waited for: returns at once | |||
| 122 | } | 283 | } | ||
M internal/adapters/drm/flip_stats.go +3 -0
| @@ -30,6 +30,9 @@ type flipStats struct { | |||
| 30 | 30 | // stateCarried counts deferred state changes a frame carried instead. | |
| 31 | 31 | // Many state commits while a video plays point at a lost vblank each. | |
| 32 | 32 | stateCommits, stateCarried int | |
| 33 | + | // fenceTimeouts counts commits whose CPU fence wait hit its limit | |
| 34 | + | // (fencegate.go). | |
| 35 | + | fenceTimeouts int | |
| 33 | 36 | } | |
| 34 | 37 | ||
| 35 | 38 | // nominalPeriod is the time between two vblanks at the mode's refresh | |
| @@ -30,6 +30,9 @@ type flipStats struct { | |||||
| 30 | // stateCarried counts deferred state changes a frame carried instead. | 30 | // stateCarried counts deferred state changes a frame carried instead. | ||
| 31 | // Many state commits while a video plays point at a lost vblank each. | 31 | // Many state commits while a video plays point at a lost vblank each. | ||
| 32 | stateCommits, stateCarried int | 32 | stateCommits, stateCarried int | ||
| 33 | + | // fenceTimeouts counts commits whose CPU fence wait hit its limit | |||
| 34 | + | // (fencegate.go). | |||
| 35 | + | fenceTimeouts int | |||
| 33 | } | 36 | } | ||
| 34 | 37 | ||||
| 35 | // nominalPeriod is the time between two vblanks at the mode's refresh | 38 | // nominalPeriod is the time between two vblanks at the mode's refresh | ||
M internal/adapters/drm/gbm/gbm.go +23 -59
| @@ -18,9 +18,6 @@ import ( | |||
| 18 | 18 | "golang.org/x/sys/unix" | |
| 19 | 19 | ) | |
| 20 | 20 | ||
| 21 | - | // ErrUnavailable marks a missing or unusable libgbm. | |
| 22 | - | var ErrUnavailable = errors.New("gbm unavailable") | |
| 23 | - | ||
| 24 | 21 | // Usage is a set of GBM_BO_USE_* flags. | |
| 25 | 22 | type Usage uint32 | |
| 26 | 23 | ||
| @@ -29,8 +26,8 @@ const ( | |||
| 29 | 26 | UseScanout Usage = 1 << 0 | |
| 30 | 27 | // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it. | |
| 31 | 28 | UseRendering Usage = 1 << 2 | |
| 32 | - | // UseLinear (GBM_BO_USE_LINEAR): the layout is linear. | |
| 33 | - | UseLinear Usage = 1 << 4 | |
| 29 | + | // useLinear (GBM_BO_USE_LINEAR): the layout is linear. | |
| 30 | + | useLinear Usage = 1 << 4 | |
| 34 | 31 | ) | |
| 35 | 32 | ||
| 36 | 33 | const ( | |
| @@ -49,56 +46,42 @@ var ( | |||
| 49 | 46 | createDevice func(fd int32) uintptr | |
| 50 | 47 | destroyDevice func(dev uintptr) | |
| 51 | 48 | 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 | 49 | createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr | |
| 54 | 50 | boModifier func(bo uintptr) uint64 | |
| 55 | 51 | boPlaneCount func(bo uintptr) int32 | |
| 56 | 52 | boFDForPlane func(bo uintptr, plane int32) int32 | |
| 57 | - | boFD func(bo uintptr) int32 | |
| 58 | 53 | boStrideForPlane func(bo uintptr, plane int32) uint32 | |
| 59 | 54 | boOffset func(bo uintptr, plane int32) uint32 | |
| 60 | 55 | boDestroy func(bo uintptr) | |
| 61 | 56 | ) | |
| 62 | 57 | ||
| 63 | - | // load opens libgbm once. Without it (or with a lacking one) the allocator | |
| 64 | - | // is unavailable and callers fall back to another way to make images. | |
| 58 | + | // load opens libgbm once. Without it, or with one that lacks a function | |
| 59 | + | // below (gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane came with | |
| 60 | + | // Mesa 21.1), the allocator is unavailable and callers fall back to another | |
| 61 | + | // way to make images. | |
| 65 | 62 | func load() error { | |
| 66 | 63 | loadOnce.Do(func() { | |
| 67 | 64 | lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL) | |
| 68 | 65 | if err != nil { | |
| 69 | - | loadErr = fmt.Errorf("%w: open libgbm.so.1: %v", ErrUnavailable, err) | |
| 66 | + | loadErr = fmt.Errorf("open libgbm.so.1: %w", err) | |
| 70 | 67 | return | |
| 71 | 68 | } | |
| 72 | 69 | defer func() { | |
| 73 | 70 | if r := recover(); r != nil { | |
| 74 | - | loadErr = fmt.Errorf("%w: libgbm: %v", ErrUnavailable, r) | |
| 71 | + | loadErr = fmt.Errorf("libgbm: %v", r) | |
| 75 | 72 | } | |
| 76 | 73 | }() | |
| 77 | 74 | reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) } | |
| 78 | - | have := func(name string) bool { p, err := purego.Dlsym(lib, name); return err == nil && p != 0 } | |
| 79 | 75 | reg(&createDevice, "gbm_create_device") | |
| 80 | 76 | reg(&destroyDevice, "gbm_device_destroy") | |
| 77 | + | reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2") | |
| 81 | 78 | reg(&createPlain, "gbm_bo_create") | |
| 82 | 79 | reg(&boModifier, "gbm_bo_get_modifier") | |
| 83 | 80 | reg(&boPlaneCount, "gbm_bo_get_plane_count") | |
| 84 | - | if have("gbm_bo_get_fd_for_plane") { | |
| 85 | - | reg(&boFDForPlane, "gbm_bo_get_fd_for_plane") | |
| 86 | - | } else { | |
| 87 | - | // Older libgbm: one fd for the whole object. | |
| 88 | - | reg(&boFD, "gbm_bo_get_fd") | |
| 89 | - | } | |
| 81 | + | reg(&boFDForPlane, "gbm_bo_get_fd_for_plane") | |
| 90 | 82 | reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane") | |
| 91 | 83 | reg(&boOffset, "gbm_bo_get_offset") | |
| 92 | 84 | reg(&boDestroy, "gbm_bo_destroy") | |
| 93 | - | if have("gbm_bo_create_with_modifiers2") { | |
| 94 | - | reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2") | |
| 95 | - | } | |
| 96 | - | if have("gbm_bo_create_with_modifiers") { | |
| 97 | - | reg(&createWithModifiers, "gbm_bo_create_with_modifiers") | |
| 98 | - | } | |
| 99 | - | if createWithModifiers2 == nil && createWithModifiers == nil { | |
| 100 | - | loadErr = fmt.Errorf("%w: libgbm lacks gbm_bo_create_with_modifiers", ErrUnavailable) | |
| 101 | - | } | |
| 102 | 85 | }) | |
| 103 | 86 | return loadErr | |
| 104 | 87 | } | |
| @@ -141,13 +124,10 @@ func (d *Device) Close() { | |||
| 141 | 124 | // object, kept alive for as long as the image is used. | |
| 142 | 125 | type Buffer struct { | |
| 143 | 126 | bo uintptr | |
| 144 | - | Width, Height int | |
| 145 | - | Format uint32 | |
| 146 | - | Modifier uint64 | |
| 147 | - | // Flagless: the buffer came from gbm_bo_create_with_modifiers, which | |
| 148 | - | // takes no usage flags (libgbm before 21.1): scanout was not asked for. | |
| 149 | - | Flagless bool | |
| 150 | - | planes []plane | |
| 127 | + | width, height int | |
| 128 | + | format uint32 | |
| 129 | + | modifier uint64 | |
| 130 | + | planes []plane | |
| 151 | 131 | } | |
| 152 | 132 | ||
| 153 | 133 | type plane struct { | |
| @@ -158,7 +138,7 @@ type plane struct { | |||
| 158 | 138 | // Create allocates an image whose modifier is one of modifiers (a | |
| 159 | 139 | // non-empty list). A list holding only linear falls back to a plain | |
| 160 | 140 | // linear allocation when GBM refuses the modifier call, and accepts an | |
| 161 | - | // implicit layout (DRM_FORMAT_MOD_INVALID) as linear on either path. The | |
| 141 | + | // implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The | |
| 162 | 142 | // result must be one memory object, because the renderer imports one | |
| 163 | 143 | // dmabuf for all planes. | |
| 164 | 144 | func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) { | |
| @@ -168,21 +148,11 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us | |||
| 168 | 148 | if width <= 0 || height <= 0 || len(modifiers) == 0 { | |
| 169 | 149 | return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers)) | |
| 170 | 150 | } | |
| 171 | - | var bo uintptr | |
| 172 | - | flagless := false | |
| 173 | - | switch { | |
| 174 | - | case createWithModifiers2 != nil: | |
| 175 | - | bo = createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage)) | |
| 176 | - | case createWithModifiers != nil: | |
| 177 | - | // This entry point cannot ask for scanout. | |
| 178 | - | flagless = true | |
| 179 | - | bo = createWithModifiers(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers))) | |
| 180 | - | } | |
| 151 | + | bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage)) | |
| 181 | 152 | runtime.KeepAlive(modifiers) | |
| 182 | 153 | onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear | |
| 183 | 154 | if bo == 0 && onlyLinear { | |
| 184 | - | bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|UseLinear)) | |
| 185 | - | flagless = false | |
| 155 | + | bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear)) | |
| 186 | 156 | } | |
| 187 | 157 | if bo == 0 { | |
| 188 | 158 | return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers) | |
| @@ -192,10 +162,9 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us | |||
| 192 | 162 | boDestroy(bo) | |
| 193 | 163 | return nil, err | |
| 194 | 164 | } | |
| 195 | - | if onlyLinear && b.Modifier == modInvalid { | |
| 196 | - | b.Modifier = modLinear | |
| 165 | + | if onlyLinear && b.modifier == modInvalid { | |
| 166 | + | b.modifier = modLinear | |
| 197 | 167 | } | |
| 198 | - | b.Flagless = flagless | |
| 199 | 168 | return b, nil | |
| 200 | 169 | } | |
| 201 | 170 | ||
| @@ -211,8 +180,8 @@ func acceptedModifiers(requested []uint64) []uint64 { | |||
| 211 | 180 | // readBuffer reads the layout of bo and validates it against the request. | |
| 212 | 181 | // It does not destroy bo on error. | |
| 213 | 182 | func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) { | |
| 214 | - | b := &Buffer{bo: bo, Width: width, Height: height, Format: format, Modifier: boModifier(bo)} | |
| 215 | - | if err := checkModifier(modifiers, b.Modifier); err != nil { | |
| 183 | + | b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)} | |
| 184 | + | if err := checkModifier(modifiers, b.modifier); err != nil { | |
| 216 | 185 | return nil, err | |
| 217 | 186 | } | |
| 218 | 187 | n := int(boPlaneCount(bo)) | |
| @@ -220,12 +189,7 @@ func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64 | |||
| 220 | 189 | return nil, fmt.Errorf("gbm: buffer has %d planes", n) | |
| 221 | 190 | } | |
| 222 | 191 | for i := range n { | |
| 223 | - | var fd int32 | |
| 224 | - | if boFDForPlane != nil { | |
| 225 | - | fd = boFDForPlane(bo, int32(i)) | |
| 226 | - | } else { | |
| 227 | - | fd = boFD(bo) | |
| 228 | - | } | |
| 192 | + | fd := boFDForPlane(bo, int32(i)) | |
| 229 | 193 | if fd < 0 { | |
| 230 | 194 | b.closeFDs() | |
| 231 | 195 | return nil, fmt.Errorf("gbm: no fd for plane %d", i) | |
| @@ -287,7 +251,7 @@ func (b *Buffer) DMABuf() (ports.DMABuf, error) { | |||
| 287 | 251 | if b == nil || b.bo == 0 { | |
| 288 | 252 | return ports.DMABuf{}, errors.New("gbm buffer closed") | |
| 289 | 253 | } | |
| 290 | - | out := ports.DMABuf{Width: b.Width, Height: b.Height, Format: b.Format, Modifier: b.Modifier} | |
| 254 | + | out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier} | |
| 291 | 255 | for _, p := range b.planes { | |
| 292 | 256 | fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0) | |
| 293 | 257 | if err != nil { | |
| @@ -18,9 +18,6 @@ import ( | |||||
| 18 | "golang.org/x/sys/unix" | 18 | "golang.org/x/sys/unix" | ||
| 19 | ) | 19 | ) | ||
| 20 | 20 | ||||
| 21 | - | // ErrUnavailable marks a missing or unusable libgbm. | |||
| 22 | - | var ErrUnavailable = errors.New("gbm unavailable") | |||
| 23 | - | ||||
| 24 | // Usage is a set of GBM_BO_USE_* flags. | 21 | // Usage is a set of GBM_BO_USE_* flags. | ||
| 25 | type Usage uint32 | 22 | type Usage uint32 | ||
| 26 | 23 | ||||
| @@ -29,8 +26,8 @@ const ( | |||||
| 29 | UseScanout Usage = 1 << 0 | 26 | UseScanout Usage = 1 << 0 | ||
| 30 | // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it. | 27 | // UseRendering (GBM_BO_USE_RENDERING): a GPU can render into it. | ||
| 31 | UseRendering Usage = 1 << 2 | 28 | UseRendering Usage = 1 << 2 | ||
| 32 | - | // UseLinear (GBM_BO_USE_LINEAR): the layout is linear. | 29 | + | // useLinear (GBM_BO_USE_LINEAR): the layout is linear. |
| 33 | - | UseLinear Usage = 1 << 4 | 30 | + | useLinear Usage = 1 << 4 |
| 34 | ) | 31 | ) | ||
| 35 | 32 | ||||
| 36 | const ( | 33 | const ( | ||
| @@ -49,56 +46,42 @@ var ( | |||||
| 49 | createDevice func(fd int32) uintptr | 46 | createDevice func(fd int32) uintptr | ||
| 50 | destroyDevice func(dev uintptr) | 47 | destroyDevice func(dev uintptr) | ||
| 51 | createWithModifiers2 func(dev uintptr, w, h, format uint32, mods unsafe.Pointer, count uint32, flags uint32) uintptr | 48 | 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 | 49 | createPlain func(dev uintptr, w, h, format uint32, flags uint32) uintptr | ||
| 54 | boModifier func(bo uintptr) uint64 | 50 | boModifier func(bo uintptr) uint64 | ||
| 55 | boPlaneCount func(bo uintptr) int32 | 51 | boPlaneCount func(bo uintptr) int32 | ||
| 56 | boFDForPlane func(bo uintptr, plane int32) int32 | 52 | boFDForPlane func(bo uintptr, plane int32) int32 | ||
| 57 | - | boFD func(bo uintptr) int32 | |||
| 58 | boStrideForPlane func(bo uintptr, plane int32) uint32 | 53 | boStrideForPlane func(bo uintptr, plane int32) uint32 | ||
| 59 | boOffset func(bo uintptr, plane int32) uint32 | 54 | boOffset func(bo uintptr, plane int32) uint32 | ||
| 60 | boDestroy func(bo uintptr) | 55 | boDestroy func(bo uintptr) | ||
| 61 | ) | 56 | ) | ||
| 62 | 57 | ||||
| 63 | - | // load opens libgbm once. Without it (or with a lacking one) the allocator | 58 | + | // load opens libgbm once. Without it, or with one that lacks a function |
| 64 | - | // is unavailable and callers fall back to another way to make images. | 59 | + | // below (gbm_bo_create_with_modifiers2 and gbm_bo_get_fd_for_plane came with |
| 60 | + | // Mesa 21.1), the allocator is unavailable and callers fall back to another | |||
| 61 | + | // way to make images. | |||
| 65 | func load() error { | 62 | func load() error { | ||
| 66 | loadOnce.Do(func() { | 63 | loadOnce.Do(func() { | ||
| 67 | lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL) | 64 | lib, err := purego.Dlopen("libgbm.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL) | ||
| 68 | if err != nil { | 65 | if err != nil { | ||
| 69 | - | loadErr = fmt.Errorf("%w: open libgbm.so.1: %v", ErrUnavailable, err) | 66 | + | loadErr = fmt.Errorf("open libgbm.so.1: %w", err) |
| 70 | return | 67 | return | ||
| 71 | } | 68 | } | ||
| 72 | defer func() { | 69 | defer func() { | ||
| 73 | if r := recover(); r != nil { | 70 | if r := recover(); r != nil { | ||
| 74 | - | loadErr = fmt.Errorf("%w: libgbm: %v", ErrUnavailable, r) | 71 | + | loadErr = fmt.Errorf("libgbm: %v", r) |
| 75 | } | 72 | } | ||
| 76 | }() | 73 | }() | ||
| 77 | reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) } | 74 | reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, name) } | ||
| 78 | - | have := func(name string) bool { p, err := purego.Dlsym(lib, name); return err == nil && p != 0 } | |||
| 79 | reg(&createDevice, "gbm_create_device") | 75 | reg(&createDevice, "gbm_create_device") | ||
| 80 | reg(&destroyDevice, "gbm_device_destroy") | 76 | reg(&destroyDevice, "gbm_device_destroy") | ||
| 77 | + | reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2") | |||
| 81 | reg(&createPlain, "gbm_bo_create") | 78 | reg(&createPlain, "gbm_bo_create") | ||
| 82 | reg(&boModifier, "gbm_bo_get_modifier") | 79 | reg(&boModifier, "gbm_bo_get_modifier") | ||
| 83 | reg(&boPlaneCount, "gbm_bo_get_plane_count") | 80 | reg(&boPlaneCount, "gbm_bo_get_plane_count") | ||
| 84 | - | if have("gbm_bo_get_fd_for_plane") { | 81 | + | reg(&boFDForPlane, "gbm_bo_get_fd_for_plane") |
| 85 | - | reg(&boFDForPlane, "gbm_bo_get_fd_for_plane") | |||
| 86 | - | } else { | |||
| 87 | - | // Older libgbm: one fd for the whole object. | |||
| 88 | - | reg(&boFD, "gbm_bo_get_fd") | |||
| 89 | - | } | |||
| 90 | reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane") | 82 | reg(&boStrideForPlane, "gbm_bo_get_stride_for_plane") | ||
| 91 | reg(&boOffset, "gbm_bo_get_offset") | 83 | reg(&boOffset, "gbm_bo_get_offset") | ||
| 92 | reg(&boDestroy, "gbm_bo_destroy") | 84 | reg(&boDestroy, "gbm_bo_destroy") | ||
| 93 | - | if have("gbm_bo_create_with_modifiers2") { | |||
| 94 | - | reg(&createWithModifiers2, "gbm_bo_create_with_modifiers2") | |||
| 95 | - | } | |||
| 96 | - | if have("gbm_bo_create_with_modifiers") { | |||
| 97 | - | reg(&createWithModifiers, "gbm_bo_create_with_modifiers") | |||
| 98 | - | } | |||
| 99 | - | if createWithModifiers2 == nil && createWithModifiers == nil { | |||
| 100 | - | loadErr = fmt.Errorf("%w: libgbm lacks gbm_bo_create_with_modifiers", ErrUnavailable) | |||
| 101 | - | } | |||
| 102 | }) | 85 | }) | ||
| 103 | return loadErr | 86 | return loadErr | ||
| 104 | } | 87 | } | ||
| @@ -141,13 +124,10 @@ func (d *Device) Close() { | |||||
| 141 | // object, kept alive for as long as the image is used. | 124 | // object, kept alive for as long as the image is used. | ||
| 142 | type Buffer struct { | 125 | type Buffer struct { | ||
| 143 | bo uintptr | 126 | bo uintptr | ||
| 144 | - | Width, Height int | 127 | + | width, height int |
| 145 | - | Format uint32 | 128 | + | format uint32 |
| 146 | - | Modifier uint64 | 129 | + | modifier uint64 |
| 147 | - | // Flagless: the buffer came from gbm_bo_create_with_modifiers, which | 130 | + | planes []plane |
| 148 | - | // takes no usage flags (libgbm before 21.1): scanout was not asked for. | |||
| 149 | - | Flagless bool | |||
| 150 | - | planes []plane | |||
| 151 | } | 131 | } | ||
| 152 | 132 | ||||
| 153 | type plane struct { | 133 | type plane struct { | ||
| @@ -158,7 +138,7 @@ type plane struct { | |||||
| 158 | // Create allocates an image whose modifier is one of modifiers (a | 138 | // 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 | 139 | // 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 | 140 | // linear allocation when GBM refuses the modifier call, and accepts an | ||
| 161 | - | // implicit layout (DRM_FORMAT_MOD_INVALID) as linear on either path. The | 141 | + | // implicit layout (DRM_FORMAT_MOD_INVALID) as linear. The |
| 162 | // result must be one memory object, because the renderer imports one | 142 | // result must be one memory object, because the renderer imports one | ||
| 163 | // dmabuf for all planes. | 143 | // dmabuf for all planes. | ||
| 164 | func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) { | 144 | func (d *Device) Create(width, height int, format uint32, modifiers []uint64, usage Usage) (*Buffer, error) { | ||
| @@ -168,21 +148,11 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us | |||||
| 168 | if width <= 0 || height <= 0 || len(modifiers) == 0 { | 148 | if width <= 0 || height <= 0 || len(modifiers) == 0 { | ||
| 169 | return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers)) | 149 | return nil, fmt.Errorf("gbm: bad request %dx%d with %d modifiers", width, height, len(modifiers)) | ||
| 170 | } | 150 | } | ||
| 171 | - | var bo uintptr | 151 | + | bo := createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage)) |
| 172 | - | flagless := false | |||
| 173 | - | switch { | |||
| 174 | - | case createWithModifiers2 != nil: | |||
| 175 | - | bo = createWithModifiers2(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers)), uint32(usage)) | |||
| 176 | - | case createWithModifiers != nil: | |||
| 177 | - | // This entry point cannot ask for scanout. | |||
| 178 | - | flagless = true | |||
| 179 | - | bo = createWithModifiers(d.dev, uint32(width), uint32(height), format, unsafe.Pointer(&modifiers[0]), uint32(len(modifiers))) | |||
| 180 | - | } | |||
| 181 | runtime.KeepAlive(modifiers) | 152 | runtime.KeepAlive(modifiers) | ||
| 182 | onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear | 153 | onlyLinear := len(modifiers) == 1 && modifiers[0] == modLinear | ||
| 183 | if bo == 0 && onlyLinear { | 154 | if bo == 0 && onlyLinear { | ||
| 184 | - | bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|UseLinear)) | 155 | + | bo = createPlain(d.dev, uint32(width), uint32(height), format, uint32(usage|useLinear)) |
| 185 | - | flagless = false | |||
| 186 | } | 156 | } | ||
| 187 | if bo == 0 { | 157 | if bo == 0 { | ||
| 188 | return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers) | 158 | return nil, fmt.Errorf("gbm: no %dx%d buffer of format %#x with modifiers %#x", width, height, format, modifiers) | ||
| @@ -192,10 +162,9 @@ func (d *Device) Create(width, height int, format uint32, modifiers []uint64, us | |||||
| 192 | boDestroy(bo) | 162 | boDestroy(bo) | ||
| 193 | return nil, err | 163 | return nil, err | ||
| 194 | } | 164 | } | ||
| 195 | - | if onlyLinear && b.Modifier == modInvalid { | 165 | + | if onlyLinear && b.modifier == modInvalid { |
| 196 | - | b.Modifier = modLinear | 166 | + | b.modifier = modLinear |
| 197 | } | 167 | } | ||
| 198 | - | b.Flagless = flagless | |||
| 199 | return b, nil | 168 | return b, nil | ||
| 200 | } | 169 | } | ||
| 201 | 170 | ||||
| @@ -211,8 +180,8 @@ func acceptedModifiers(requested []uint64) []uint64 { | |||||
| 211 | // readBuffer reads the layout of bo and validates it against the request. | 180 | // readBuffer reads the layout of bo and validates it against the request. | ||
| 212 | // It does not destroy bo on error. | 181 | // It does not destroy bo on error. | ||
| 213 | func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) { | 182 | func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64) (*Buffer, error) { | ||
| 214 | - | b := &Buffer{bo: bo, Width: width, Height: height, Format: format, Modifier: boModifier(bo)} | 183 | + | b := &Buffer{bo: bo, width: width, height: height, format: format, modifier: boModifier(bo)} |
| 215 | - | if err := checkModifier(modifiers, b.Modifier); err != nil { | 184 | + | if err := checkModifier(modifiers, b.modifier); err != nil { |
| 216 | return nil, err | 185 | return nil, err | ||
| 217 | } | 186 | } | ||
| 218 | n := int(boPlaneCount(bo)) | 187 | n := int(boPlaneCount(bo)) | ||
| @@ -220,12 +189,7 @@ func readBuffer(bo uintptr, width, height int, format uint32, modifiers []uint64 | |||||
| 220 | return nil, fmt.Errorf("gbm: buffer has %d planes", n) | 189 | return nil, fmt.Errorf("gbm: buffer has %d planes", n) | ||
| 221 | } | 190 | } | ||
| 222 | for i := range n { | 191 | for i := range n { | ||
| 223 | - | var fd int32 | 192 | + | fd := boFDForPlane(bo, int32(i)) |
| 224 | - | if boFDForPlane != nil { | |||
| 225 | - | fd = boFDForPlane(bo, int32(i)) | |||
| 226 | - | } else { | |||
| 227 | - | fd = boFD(bo) | |||
| 228 | - | } | |||
| 229 | if fd < 0 { | 193 | if fd < 0 { | ||
| 230 | b.closeFDs() | 194 | b.closeFDs() | ||
| 231 | return nil, fmt.Errorf("gbm: no fd for plane %d", i) | 195 | return nil, fmt.Errorf("gbm: no fd for plane %d", i) | ||
| @@ -287,7 +251,7 @@ func (b *Buffer) DMABuf() (ports.DMABuf, error) { | |||||
| 287 | if b == nil || b.bo == 0 { | 251 | if b == nil || b.bo == 0 { | ||
| 288 | return ports.DMABuf{}, errors.New("gbm buffer closed") | 252 | return ports.DMABuf{}, errors.New("gbm buffer closed") | ||
| 289 | } | 253 | } | ||
| 290 | - | out := ports.DMABuf{Width: b.Width, Height: b.Height, Format: b.Format, Modifier: b.Modifier} | 254 | + | out := ports.DMABuf{Width: b.width, Height: b.height, Format: b.format, Modifier: b.modifier} |
| 291 | for _, p := range b.planes { | 255 | for _, p := range b.planes { | ||
| 292 | fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0) | 256 | fd, err := unix.FcntlInt(uintptr(p.fd), unix.F_DUPFD_CLOEXEC, 0) | ||
| 293 | if err != nil { | 257 | if err != nil { | ||
M internal/adapters/drm/gbm/gbm_test.go +10 -17
| @@ -2,15 +2,15 @@ package gbm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "os" | |
| 5 | - | "slices" | |
| 6 | 5 | "strings" | |
| 7 | 6 | "testing" | |
| 8 | 7 | ||
| 9 | 8 | "golang.org/x/sys/unix" | |
| 10 | 9 | ) | |
| 11 | 10 | ||
| 12 | - | // An allocator answer outside the requested list is refused, including an | |
| 13 | - | // implicit layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import. | |
| 11 | + | // An allocator answer outside the request is refused, including an implicit | |
| 12 | + | // layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import; only a | |
| 13 | + | // linear-only request takes it, as linear. | |
| 14 | 14 | func TestCheckModifier(t *testing.T) { | |
| 15 | 15 | const tiled = 0x300000000606014 | |
| 16 | 16 | for _, tc := range []struct { | |
| @@ -23,10 +23,15 @@ func TestCheckModifier(t *testing.T) { | |||
| 23 | 23 | {"linear listed", []uint64{tiled, 0}, 0, true}, | |
| 24 | 24 | {"not listed", []uint64{tiled}, 0, false}, | |
| 25 | 25 | {"other vendor", []uint64{tiled}, 0x200000000401b03, false}, | |
| 26 | - | {"invalid", []uint64{tiled, 0}, modInvalid, false}, | |
| 27 | 26 | {"empty list", nil, 0, false}, | |
| 27 | + | {"linear only takes linear", []uint64{0}, 0, true}, | |
| 28 | + | {"linear only takes invalid", []uint64{0}, modInvalid, true}, | |
| 29 | + | {"linear only refuses others", []uint64{0}, tiled, false}, | |
| 30 | + | {"tiled refuses invalid", []uint64{tiled}, modInvalid, false}, | |
| 31 | + | {"tiled or linear refuses invalid", []uint64{tiled, 0}, modInvalid, false}, | |
| 32 | + | {"0 and 5 refuses invalid", []uint64{0, 5}, modInvalid, false}, | |
| 28 | 33 | } { | |
| 29 | - | if err := checkModifier(tc.requested, tc.got); (err == nil) != tc.ok { | |
| 34 | + | if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok { | |
| 30 | 35 | t.Errorf("%s: %v", tc.name, err) | |
| 31 | 36 | } | |
| 32 | 37 | } | |
| @@ -81,15 +86,3 @@ func TestCreateRejectsBadRequests(t *testing.T) { | |||
| 81 | 86 | } | |
| 82 | 87 | } | |
| 83 | 88 | } | |
| 84 | - | ||
| 85 | - | // Only a linear-only request accepts an implicit layout, as linear. | |
| 86 | - | func TestAcceptedModifiers(t *testing.T) { | |
| 87 | - | if got := acceptedModifiers([]uint64{0}); checkModifier(got, modInvalid) != nil || checkModifier(got, 0) != nil || checkModifier(got, 5) == nil { | |
| 88 | - | t.Errorf("linear-only request accepts %#x", got) | |
| 89 | - | } | |
| 90 | - | for _, req := range [][]uint64{{5}, {0, 5}, {modInvalid}} { | |
| 91 | - | if got := acceptedModifiers(req); checkModifier(got, modInvalid) == nil && !slices.Contains(req, modInvalid) { | |
| 92 | - | t.Errorf("request %#x accepts an implicit layout", req) | |
| 93 | - | } | |
| 94 | - | } | |
| 95 | - | } | |
| @@ -2,15 +2,15 @@ package gbm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "os" | 4 | "os" | ||
| 5 | - | "slices" | |||
| 6 | "strings" | 5 | "strings" | ||
| 7 | "testing" | 6 | "testing" | ||
| 8 | 7 | ||||
| 9 | "golang.org/x/sys/unix" | 8 | "golang.org/x/sys/unix" | ||
| 10 | ) | 9 | ) | ||
| 11 | 10 | ||||
| 12 | - | // An allocator answer outside the requested list is refused, including an | 11 | + | // An allocator answer outside the request is refused, including an implicit |
| 13 | - | // implicit layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import. | 12 | + | // layout (DRM_FORMAT_MOD_INVALID) that Vulkan cannot import; only a |
| 13 | + | // linear-only request takes it, as linear. | |||
| 14 | func TestCheckModifier(t *testing.T) { | 14 | func TestCheckModifier(t *testing.T) { | ||
| 15 | const tiled = 0x300000000606014 | 15 | const tiled = 0x300000000606014 | ||
| 16 | for _, tc := range []struct { | 16 | for _, tc := range []struct { | ||
| @@ -23,10 +23,15 @@ func TestCheckModifier(t *testing.T) { | |||||
| 23 | {"linear listed", []uint64{tiled, 0}, 0, true}, | 23 | {"linear listed", []uint64{tiled, 0}, 0, true}, | ||
| 24 | {"not listed", []uint64{tiled}, 0, false}, | 24 | {"not listed", []uint64{tiled}, 0, false}, | ||
| 25 | {"other vendor", []uint64{tiled}, 0x200000000401b03, false}, | 25 | {"other vendor", []uint64{tiled}, 0x200000000401b03, false}, | ||
| 26 | - | {"invalid", []uint64{tiled, 0}, modInvalid, false}, | |||
| 27 | {"empty list", nil, 0, false}, | 26 | {"empty list", nil, 0, false}, | ||
| 27 | + | {"linear only takes linear", []uint64{0}, 0, true}, | |||
| 28 | + | {"linear only takes invalid", []uint64{0}, modInvalid, true}, | |||
| 29 | + | {"linear only refuses others", []uint64{0}, tiled, false}, | |||
| 30 | + | {"tiled refuses invalid", []uint64{tiled}, modInvalid, false}, | |||
| 31 | + | {"tiled or linear refuses invalid", []uint64{tiled, 0}, modInvalid, false}, | |||
| 32 | + | {"0 and 5 refuses invalid", []uint64{0, 5}, modInvalid, false}, | |||
| 28 | } { | 33 | } { | ||
| 29 | - | if err := checkModifier(tc.requested, tc.got); (err == nil) != tc.ok { | 34 | + | if err := checkModifier(acceptedModifiers(tc.requested), tc.got); (err == nil) != tc.ok { |
| 30 | t.Errorf("%s: %v", tc.name, err) | 35 | t.Errorf("%s: %v", tc.name, err) | ||
| 31 | } | 36 | } | ||
| 32 | } | 37 | } | ||
| @@ -81,15 +86,3 @@ func TestCreateRejectsBadRequests(t *testing.T) { | |||||
| 81 | } | 86 | } | ||
| 82 | } | 87 | } | ||
| 83 | } | 88 | } | ||
| 84 | - | ||||
| 85 | - | // Only a linear-only request accepts an implicit layout, as linear. | |||
| 86 | - | func TestAcceptedModifiers(t *testing.T) { | |||
| 87 | - | if got := acceptedModifiers([]uint64{0}); checkModifier(got, modInvalid) != nil || checkModifier(got, 0) != nil || checkModifier(got, 5) == nil { | |||
| 88 | - | t.Errorf("linear-only request accepts %#x", got) | |||
| 89 | - | } | |||
| 90 | - | for _, req := range [][]uint64{{5}, {0, 5}, {modInvalid}} { | |||
| 91 | - | if got := acceptedModifiers(req); checkModifier(got, modInvalid) == nil && !slices.Contains(req, modInvalid) { | |||
| 92 | - | t.Errorf("request %#x accepts an implicit layout", req) | |||
| 93 | - | } | |||
| 94 | - | } | |||
| 95 | - | } | |||
M internal/adapters/drm/images.go +44 -64
| @@ -112,7 +112,8 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||
| 112 | 112 | o.dropImages(r) | |
| 113 | 113 | return err | |
| 114 | 114 | } | |
| 115 | - | if o.imageAlloc == allocGBM { | |
| 115 | + | if len(o.images) > 0 { | |
| 116 | + | // GBM images (the renderer imported them). | |
| 116 | 117 | // The renderer's own images of this kind may still pass. | |
| 117 | 118 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images") | |
| 118 | 119 | o.dropImages(r) | |
| @@ -147,9 +148,9 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||
| 147 | 148 | // GPU that cannot export a scanout-capable image cannot drive the output. | |
| 148 | 149 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) { | |
| 149 | 150 | err := cause | |
| 150 | - | for first := true; kind <= imagesLinear; kind++ { | |
| 151 | - | noGBM := first && skipGBM | |
| 152 | - | first = false | |
| 151 | + | start := kind | |
| 152 | + | for ; kind <= imagesLinear; kind++ { | |
| 153 | + | noGBM := skipGBM && kind == start | |
| 153 | 154 | if o.runContext != nil && o.runContext.Err() != nil { | |
| 154 | 155 | return kind, o.runContext.Err() | |
| 155 | 156 | } | |
| @@ -220,38 +221,52 @@ func (o *Output) primaryModifiers(format uint32) []uint64 { | |||
| 220 | 221 | return mods | |
| 221 | 222 | } | |
| 222 | 223 | ||
| 224 | + | // closeBufs closes every plane file of bufs. The callers own them: the | |
| 225 | + | // renderer and the framebuffers keep their own references to the memory. | |
| 226 | + | func closeBufs(bufs []ports.DMABuf) { | |
| 227 | + | for _, b := range bufs { | |
| 228 | + | for _, p := range b.Planes { | |
| 229 | + | _ = p.File.Close() | |
| 230 | + | } | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | ||
| 223 | 234 | // exportImages makes the renderer's exported targets the output images. | |
| 224 | 235 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | |
| 225 | - | o.dropLive(r) | |
| 236 | + | o.dropImages(r) | |
| 226 | 237 | bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane) | |
| 227 | 238 | if err != nil { | |
| 228 | 239 | return err | |
| 229 | 240 | } | |
| 241 | + | defer closeBufs(bufs) | |
| 242 | + | if err = o.showBufs(r, bufs, allocVulkan); err != nil { | |
| 243 | + | o.dropImages(r) | |
| 244 | + | _, _ = r.ExportTargets(0, nil, false) | |
| 245 | + | return err | |
| 246 | + | } | |
| 247 | + | return nil | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // showBufs makes framebuffers of the output images bufs (already the | |
| 251 | + | // renderer's targets), clears them and records their layout. The caller | |
| 252 | + | // closes the plane files and drops the images on error. | |
| 253 | + | func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error { | |
| 230 | 254 | o.planes, o.imageMod = 0, 0 | |
| 231 | 255 | for i := range bufs { | |
| 232 | - | if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes { | |
| 233 | - | err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes))) | |
| 234 | - | } | |
| 235 | - | if err == nil { | |
| 236 | - | o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()) | |
| 256 | + | if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes { | |
| 257 | + | return fmt.Errorf("output image has %d planes", n) | |
| 237 | 258 | } | |
| 238 | - | // Every plane owns its file; the framebuffer keeps its own | |
| 239 | - | // reference to the buffer. | |
| 240 | - | for _, p := range bufs[i].Planes { | |
| 241 | - | p.File.Close() | |
| 259 | + | fb, err := o.k.addFB(&bufs[i], o.imageFormat()) | |
| 260 | + | if err != nil { | |
| 261 | + | return err | |
| 242 | 262 | } | |
| 263 | + | o.fbs[i] = fb | |
| 243 | 264 | } | |
| 244 | - | if err == nil { | |
| 245 | - | err = o.clearImages(r) | |
| 246 | - | } | |
| 247 | - | if err != nil { | |
| 248 | - | o.dropImages(r) | |
| 249 | - | _, _ = r.ExportTargets(0, nil, false) | |
| 265 | + | if err := o.clearImages(r); err != nil { | |
| 250 | 266 | return err | |
| 251 | 267 | } | |
| 252 | 268 | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | |
| 253 | - | o.imageAlloc = allocVulkan | |
| 254 | - | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocVulkan).Msg("zero-copy output") | |
| 269 | + | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output") | |
| 255 | 270 | return nil | |
| 256 | 271 | } | |
| 257 | 272 | ||
| @@ -284,7 +299,7 @@ func (o *Output) clearImages(r ports.Renderer) error { | |||
| 284 | 299 | // those the plane lists (mods, as for exportImages) that the renderer can | |
| 285 | 300 | // render into and import. On failure nothing is left allocated. | |
| 286 | 301 | func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) { | |
| 287 | - | o.dropLive(r) | |
| 302 | + | o.dropImages(r) | |
| 288 | 303 | if o.alloc == nil { | |
| 289 | 304 | if o.allocErr != nil { | |
| 290 | 305 | return o.allocErr | |
| @@ -299,21 +314,10 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e | |||
| 299 | 314 | return errors.New("no modifier both the plane and the renderer accept for GBM images") | |
| 300 | 315 | } | |
| 301 | 316 | var bufs []ports.DMABuf | |
| 302 | - | imported := false | |
| 303 | 317 | defer func() { | |
| 304 | - | for _, b := range bufs { | |
| 305 | - | for _, p := range b.Planes { | |
| 306 | - | p.File.Close() | |
| 307 | - | } | |
| 308 | - | } | |
| 318 | + | closeBufs(bufs) | |
| 309 | 319 | if err != nil { | |
| 310 | - | // A failed import leaves no renderer targets; a later failure | |
| 311 | - | // leaves ones over the images about to be freed. | |
| 312 | - | if imported { | |
| 313 | - | o.dropImages(r) | |
| 314 | - | } else { | |
| 315 | - | o.freeImages() | |
| 316 | - | } | |
| 320 | + | o.dropImages(r) | |
| 317 | 321 | } | |
| 318 | 322 | }() | |
| 319 | 323 | for range o.fbs { | |
| @@ -331,35 +335,12 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e | |||
| 331 | 335 | if err = r.ImportTargets(bufs); err != nil { | |
| 332 | 336 | return err | |
| 333 | 337 | } | |
| 334 | - | imported = true | |
| 335 | - | for i := range bufs { | |
| 336 | - | if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes { | |
| 337 | - | return fmt.Errorf("output image has %d planes", n) | |
| 338 | - | } | |
| 339 | - | if o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()); err != nil { | |
| 340 | - | return err | |
| 341 | - | } | |
| 342 | - | } | |
| 343 | - | if err = o.clearImages(r); err != nil { | |
| 344 | - | return err | |
| 345 | - | } | |
| 346 | - | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | |
| 347 | - | o.imageAlloc = allocGBM | |
| 348 | - | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocGBM).Msg("zero-copy output") | |
| 349 | - | return nil | |
| 350 | - | } | |
| 351 | - | ||
| 352 | - | // dropLive makes sure no image is left from an earlier setup: setup starts | |
| 353 | - | // from nothing, so what is still there is freed first. | |
| 354 | - | func (o *Output) dropLive(r ports.Renderer) { | |
| 355 | - | if o.fbs != [2]uint32{} || len(o.images) > 0 { | |
| 356 | - | o.dropImages(r) | |
| 357 | - | } | |
| 338 | + | return o.showBufs(r, bufs, allocGBM) | |
| 358 | 339 | } | |
| 359 | 340 | ||
| 360 | - | // dropImages frees the output images with the renderer targets built over | |
| 361 | - | // the GBM ones: they would keep their memory (VRAM) alive after the | |
| 362 | - | // buffers are gone. | |
| 341 | + | // dropImages frees the output images: their framebuffers, the GBM buffers | |
| 342 | + | // and the renderer targets built over them (they would keep the memory | |
| 343 | + | // alive after the buffers are gone). It is a no-op when there are none. | |
| 363 | 344 | func (o *Output) dropImages(r ports.Renderer) { | |
| 364 | 345 | if len(o.images) > 0 { | |
| 365 | 346 | _ = r.ImportTargets(nil) | |
| @@ -384,5 +365,4 @@ func (o *Output) freeImages() { | |||
| 384 | 365 | img.close() | |
| 385 | 366 | } | |
| 386 | 367 | o.images = nil | |
| 387 | - | o.imageAlloc = "" | |
| 388 | 368 | } | |
| @@ -112,7 +112,8 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||||
| 112 | o.dropImages(r) | 112 | o.dropImages(r) | ||
| 113 | return err | 113 | return err | ||
| 114 | } | 114 | } | ||
| 115 | - | if o.imageAlloc == allocGBM { | 115 | + | if len(o.images) > 0 { |
| 116 | + | // GBM images (the renderer imported them). | |||
| 116 | // The renderer's own images of this kind may still pass. | 117 | // The renderer's own images of this kind may still pass. | ||
| 117 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images") | 118 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images") | ||
| 118 | o.dropImages(r) | 119 | o.dropImages(r) | ||
| @@ -147,9 +148,9 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||||
| 147 | // GPU that cannot export a scanout-capable image cannot drive the output. | 148 | // GPU that cannot export a scanout-capable image cannot drive the output. | ||
| 148 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) { | 149 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) { | ||
| 149 | err := cause | 150 | err := cause | ||
| 150 | - | for first := true; kind <= imagesLinear; kind++ { | 151 | + | start := kind |
| 151 | - | noGBM := first && skipGBM | 152 | + | for ; kind <= imagesLinear; kind++ { |
| 152 | - | first = false | 153 | + | noGBM := skipGBM && kind == start |
| 153 | if o.runContext != nil && o.runContext.Err() != nil { | 154 | if o.runContext != nil && o.runContext.Err() != nil { | ||
| 154 | return kind, o.runContext.Err() | 155 | return kind, o.runContext.Err() | ||
| 155 | } | 156 | } | ||
| @@ -220,38 +221,52 @@ func (o *Output) primaryModifiers(format uint32) []uint64 { | |||||
| 220 | return mods | 221 | return mods | ||
| 221 | } | 222 | } | ||
| 222 | 223 | ||||
| 224 | + | // closeBufs closes every plane file of bufs. The callers own them: the | |||
| 225 | + | // renderer and the framebuffers keep their own references to the memory. | |||
| 226 | + | func closeBufs(bufs []ports.DMABuf) { | |||
| 227 | + | for _, b := range bufs { | |||
| 228 | + | for _, p := range b.Planes { | |||
| 229 | + | _ = p.File.Close() | |||
| 230 | + | } | |||
| 231 | + | } | |||
| 232 | + | } | |||
| 233 | + | ||||
| 223 | // exportImages makes the renderer's exported targets the output images. | 234 | // exportImages makes the renderer's exported targets the output images. | ||
| 224 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | 235 | func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error { | ||
| 225 | - | o.dropLive(r) | 236 | + | o.dropImages(r) |
| 226 | bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane) | 237 | bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane) | ||
| 227 | if err != nil { | 238 | if err != nil { | ||
| 228 | return err | 239 | return err | ||
| 229 | } | 240 | } | ||
| 241 | + | defer closeBufs(bufs) | |||
| 242 | + | if err = o.showBufs(r, bufs, allocVulkan); err != nil { | |||
| 243 | + | o.dropImages(r) | |||
| 244 | + | _, _ = r.ExportTargets(0, nil, false) | |||
| 245 | + | return err | |||
| 246 | + | } | |||
| 247 | + | return nil | |||
| 248 | + | } | |||
| 249 | + | ||||
| 250 | + | // showBufs makes framebuffers of the output images bufs (already the | |||
| 251 | + | // renderer's targets), clears them and records their layout. The caller | |||
| 252 | + | // closes the plane files and drops the images on error. | |||
| 253 | + | func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error { | |||
| 230 | o.planes, o.imageMod = 0, 0 | 254 | o.planes, o.imageMod = 0, 0 | ||
| 231 | for i := range bufs { | 255 | for i := range bufs { | ||
| 232 | - | if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes { | 256 | + | if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes { |
| 233 | - | err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes))) | 257 | + | return fmt.Errorf("output image has %d planes", n) |
| 234 | - | } | |||
| 235 | - | if err == nil { | |||
| 236 | - | o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()) | |||
| 237 | } | 258 | } | ||
| 238 | - | // Every plane owns its file; the framebuffer keeps its own | 259 | + | fb, err := o.k.addFB(&bufs[i], o.imageFormat()) |
| 239 | - | // reference to the buffer. | 260 | + | if err != nil { |
| 240 | - | for _, p := range bufs[i].Planes { | 261 | + | return err |
| 241 | - | p.File.Close() | |||
| 242 | } | 262 | } | ||
| 263 | + | o.fbs[i] = fb | |||
| 243 | } | 264 | } | ||
| 244 | - | if err == nil { | 265 | + | if err := o.clearImages(r); err != nil { |
| 245 | - | err = o.clearImages(r) | |||
| 246 | - | } | |||
| 247 | - | if err != nil { | |||
| 248 | - | o.dropImages(r) | |||
| 249 | - | _, _ = r.ExportTargets(0, nil, false) | |||
| 250 | return err | 266 | return err | ||
| 251 | } | 267 | } | ||
| 252 | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | 268 | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | ||
| 253 | - | o.imageAlloc = allocVulkan | 269 | + | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output") |
| 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 | 270 | return nil | ||
| 256 | } | 271 | } | ||
| 257 | 272 | ||||
| @@ -284,7 +299,7 @@ func (o *Output) clearImages(r ports.Renderer) error { | |||||
| 284 | // those the plane lists (mods, as for exportImages) that the renderer can | 299 | // those the plane lists (mods, as for exportImages) that the renderer can | ||
| 285 | // render into and import. On failure nothing is left allocated. | 300 | // render into and import. On failure nothing is left allocated. | ||
| 286 | func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) { | 301 | func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) { | ||
| 287 | - | o.dropLive(r) | 302 | + | o.dropImages(r) |
| 288 | if o.alloc == nil { | 303 | if o.alloc == nil { | ||
| 289 | if o.allocErr != nil { | 304 | if o.allocErr != nil { | ||
| 290 | return o.allocErr | 305 | return o.allocErr | ||
| @@ -299,21 +314,10 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e | |||||
| 299 | return errors.New("no modifier both the plane and the renderer accept for GBM images") | 314 | return errors.New("no modifier both the plane and the renderer accept for GBM images") | ||
| 300 | } | 315 | } | ||
| 301 | var bufs []ports.DMABuf | 316 | var bufs []ports.DMABuf | ||
| 302 | - | imported := false | |||
| 303 | defer func() { | 317 | defer func() { | ||
| 304 | - | for _, b := range bufs { | 318 | + | closeBufs(bufs) |
| 305 | - | for _, p := range b.Planes { | |||
| 306 | - | p.File.Close() | |||
| 307 | - | } | |||
| 308 | - | } | |||
| 309 | if err != nil { | 319 | if err != nil { | ||
| 310 | - | // A failed import leaves no renderer targets; a later failure | 320 | + | o.dropImages(r) |
| 311 | - | // leaves ones over the images about to be freed. | |||
| 312 | - | if imported { | |||
| 313 | - | o.dropImages(r) | |||
| 314 | - | } else { | |||
| 315 | - | o.freeImages() | |||
| 316 | - | } | |||
| 317 | } | 321 | } | ||
| 318 | }() | 322 | }() | ||
| 319 | for range o.fbs { | 323 | for range o.fbs { | ||
| @@ -331,35 +335,12 @@ func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (e | |||||
| 331 | if err = r.ImportTargets(bufs); err != nil { | 335 | if err = r.ImportTargets(bufs); err != nil { | ||
| 332 | return err | 336 | return err | ||
| 333 | } | 337 | } | ||
| 334 | - | imported = true | 338 | + | return o.showBufs(r, bufs, allocGBM) |
| 335 | - | for i := range bufs { | |||
| 336 | - | if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes { | |||
| 337 | - | return fmt.Errorf("output image has %d planes", n) | |||
| 338 | - | } | |||
| 339 | - | if o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat()); err != nil { | |||
| 340 | - | return err | |||
| 341 | - | } | |||
| 342 | - | } | |||
| 343 | - | if err = o.clearImages(r); err != nil { | |||
| 344 | - | return err | |||
| 345 | - | } | |||
| 346 | - | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | |||
| 347 | - | o.imageAlloc = allocGBM | |||
| 348 | - | o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Str("alloc", allocGBM).Msg("zero-copy output") | |||
| 349 | - | return nil | |||
| 350 | - | } | |||
| 351 | - | ||||
| 352 | - | // dropLive makes sure no image is left from an earlier setup: setup starts | |||
| 353 | - | // from nothing, so what is still there is freed first. | |||
| 354 | - | func (o *Output) dropLive(r ports.Renderer) { | |||
| 355 | - | if o.fbs != [2]uint32{} || len(o.images) > 0 { | |||
| 356 | - | o.dropImages(r) | |||
| 357 | - | } | |||
| 358 | } | 339 | } | ||
| 359 | 340 | ||||
| 360 | - | // dropImages frees the output images with the renderer targets built over | 341 | + | // dropImages frees the output images: their framebuffers, the GBM buffers |
| 361 | - | // the GBM ones: they would keep their memory (VRAM) alive after the | 342 | + | // and the renderer targets built over them (they would keep the memory |
| 362 | - | // buffers are gone. | 343 | + | // alive after the buffers are gone). It is a no-op when there are none. |
| 363 | func (o *Output) dropImages(r ports.Renderer) { | 344 | func (o *Output) dropImages(r ports.Renderer) { | ||
| 364 | if len(o.images) > 0 { | 345 | if len(o.images) > 0 { | ||
| 365 | _ = r.ImportTargets(nil) | 346 | _ = r.ImportTargets(nil) | ||
| @@ -384,5 +365,4 @@ func (o *Output) freeImages() { | |||||
| 384 | img.close() | 365 | img.close() | ||
| 385 | } | 366 | } | ||
| 386 | o.images = nil | 367 | o.images = nil | ||
| 387 | - | o.imageAlloc = "" | |||
| 388 | } | 368 | } | ||
M internal/adapters/drm/output.go +27 -12
| @@ -197,12 +197,10 @@ type Output struct { | |||
| 197 | 197 | protectNotBefore time.Time | |
| 198 | 198 | // kind is how the images were made. | |
| 199 | 199 | kind imageKind | |
| 200 | - | // planes is the memory plane count of the last exported images. | |
| 200 | + | // planes is the memory plane count of the output images. | |
| 201 | 201 | planes int | |
| 202 | - | // imageMod is the modifier of the exported images (0: linear). | |
| 202 | + | // imageMod is the modifier of the output images (0: linear). | |
| 203 | 203 | imageMod uint64 | |
| 204 | - | // imageAlloc is who allocated the images (allocGBM, allocVulkan). | |
| 205 | - | imageAlloc string | |
| 206 | 204 | // openAlloc opens the GBM image allocator, once per output (nil: GBM is | |
| 207 | 205 | // not used); alloc is it, allocErr why it failed. images are the GBM | |
| 208 | 206 | // images alive while they are shown. | |
| @@ -213,6 +211,12 @@ type Output struct { | |||
| 213 | 211 | // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so | |
| 214 | 212 | // commits wait for their fences on the CPU instead. | |
| 215 | 213 | cpuFences bool | |
| 214 | + | // fencePoll polls client fences; fenceWarnAt is when a fence wait | |
| 215 | + | // timeout was last warned about. | |
| 216 | + | // fenceLimit overrides fenceWaitLimit (zero: no override; tests). | |
| 217 | + | fencePoll fencePoll | |
| 218 | + | fenceWarnAt time.Time | |
| 219 | + | fenceLimit time.Duration | |
| 216 | 220 | // formats receives the direct scanout formats after each modeset; | |
| 217 | 221 | // sampled and device are what they are built from (Want). | |
| 218 | 222 | formats chan<- ports.OutputFormats | |
| @@ -234,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf | |||
| 234 | 238 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 235 | 239 | log := card.log | |
| 236 | 240 | pipe := slices.Index(card.crtcs, crtc) | |
| 237 | - | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd, card.log) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} | |
| 241 | + | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} | |
| 238 | 242 | var err error | |
| 239 | 243 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 240 | 244 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| @@ -650,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 650 | 654 | // (overlay) say. A frame that changes a plane's colour state is never async: | |
| 651 | 655 | // only FB_ID may change in an async commit. | |
| 652 | 656 | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | |
| 657 | + | // Where fences are waited for on the CPU, once per commit and before | |
| 658 | + | // any state is read: the retries below go through commitRect. The | |
| 659 | + | // fence is our own render's, or a client's that already signalled | |
| 660 | + | // (clientFenceReady). | |
| 661 | + | o.waitFences(fence) | |
| 662 | + | return o.commitRect(fb, fence, async, vrr, f, ov, rect, pc) | |
| 663 | + | } | |
| 664 | + | ||
| 665 | + | // commitRect is commitWithRect once the fence was waited for; its retries | |
| 666 | + | // call it again, without waiting again. | |
| 667 | + | func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | |
| 653 | 668 | o.observeSecurity() | |
| 654 | 669 | if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) { | |
| 655 | 670 | return errSecurityScene | |
| @@ -661,9 +676,6 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||
| 661 | 676 | if o.protected { | |
| 662 | 677 | async, vrr, ov, pc = false, false, overlayWin{}, colorUse{} | |
| 663 | 678 | } | |
| 664 | - | // A client fence (direct scanout, overlay) signalled already | |
| 665 | - | // (clientFenceReady): this waits only for our own render. | |
| 666 | - | o.waitFences(fence) | |
| 667 | 679 | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) { | |
| 668 | 680 | async = false | |
| 669 | 681 | } | |
| @@ -704,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||
| 704 | 716 | return errColorRefused | |
| 705 | 717 | } | |
| 706 | 718 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 707 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 719 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 708 | 720 | } | |
| 709 | 721 | // The same frame without the connector hint tells whether the hint | |
| 710 | 722 | // is the cause. | |
| 711 | 723 | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) { | |
| 712 | 724 | o.log.Warn().Err(err).Msg("content type refused; disabled") | |
| 713 | 725 | o.contentProp = 0 | |
| 714 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 726 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) | |
| 715 | 727 | } | |
| 716 | 728 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |
| 717 | 729 | // Retry without turning VRR on: if that passes, the driver | |
| 718 | 730 | // refuses VRR on this output. The overlay stays: the composed | |
| 719 | 731 | // image left its window out. | |
| 720 | - | if o.commitWithRect(fb, fence, false, false, f, ov, rect, pc) == nil { | |
| 732 | + | if o.commitRect(fb, fence, false, false, f, ov, rect, pc) == nil { | |
| 721 | 733 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | |
| 722 | 734 | o.vrrProp = 0 | |
| 723 | 735 | return nil | |
| @@ -923,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF | |||
| 923 | 935 | // from now on. | |
| 924 | 936 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | |
| 925 | 937 | cfb.noAsync = true | |
| 926 | - | err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) | |
| 938 | + | err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) | |
| 927 | 939 | } | |
| 928 | 940 | if errors.Is(err, errColorRefused) { | |
| 929 | 941 | // KMS takes the buffer without the pipeline: compose it; the | |
| @@ -1513,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1513 | 1525 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | |
| 1514 | 1526 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | |
| 1515 | 1527 | ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried) | |
| 1528 | + | if fs.fenceTimeouts > 0 { | |
| 1529 | + | ev = ev.Int("fence_timeouts", fs.fenceTimeouts) | |
| 1530 | + | } | |
| 1516 | 1531 | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | |
| 1517 | 1532 | if o.traceFlips { | |
| 1518 | 1533 | ev = ev.Int("late_fences", fs.lateFences) | |
| @@ -197,12 +197,10 @@ type Output struct { | |||||
| 197 | protectNotBefore time.Time | 197 | protectNotBefore time.Time | ||
| 198 | // kind is how the images were made. | 198 | // kind is how the images were made. | ||
| 199 | kind imageKind | 199 | kind imageKind | ||
| 200 | - | // planes is the memory plane count of the last exported images. | 200 | + | // planes is the memory plane count of the output images. |
| 201 | planes int | 201 | planes int | ||
| 202 | - | // imageMod is the modifier of the exported images (0: linear). | 202 | + | // imageMod is the modifier of the output images (0: linear). |
| 203 | imageMod uint64 | 203 | imageMod uint64 | ||
| 204 | - | // imageAlloc is who allocated the images (allocGBM, allocVulkan). | |||
| 205 | - | imageAlloc string | |||
| 206 | // openAlloc opens the GBM image allocator, once per output (nil: GBM is | 204 | // 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 | 205 | // not used); alloc is it, allocErr why it failed. images are the GBM | ||
| 208 | // images alive while they are shown. | 206 | // images alive while they are shown. | ||
| @@ -213,6 +211,12 @@ type Output struct { | |||||
| 213 | // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so | 211 | // cpuFences: the driver mishandles IN_FENCE_FD (fencegate.go), so | ||
| 214 | // commits wait for their fences on the CPU instead. | 212 | // commits wait for their fences on the CPU instead. | ||
| 215 | cpuFences bool | 213 | cpuFences bool | ||
| 214 | + | // fencePoll polls client fences; fenceWarnAt is when a fence wait | |||
| 215 | + | // timeout was last warned about. | |||
| 216 | + | // fenceLimit overrides fenceWaitLimit (zero: no override; tests). | |||
| 217 | + | fencePoll fencePoll | |||
| 218 | + | fenceWarnAt time.Time | |||
| 219 | + | fenceLimit time.Duration | |||
| 216 | // formats receives the direct scanout formats after each modeset; | 220 | // formats receives the direct scanout formats after each modeset; | ||
| 217 | // sampled and device are what they are built from (Want). | 221 | // sampled and device are what they are built from (Want). | ||
| 218 | formats chan<- ports.OutputFormats | 222 | formats chan<- ports.OutputFormats | ||
| @@ -234,7 +238,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransf | |||||
| 234 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 238 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 235 | log := card.log | 239 | log := card.log | ||
| 236 | pipe := slices.Index(card.crtcs, crtc) | 240 | pipe := slices.Index(card.crtcs, crtc) | ||
| 237 | - | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd, card.log) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} | 241 | + | o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{serials: &card.serials}, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, renderLog: card.renderLog, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, cpuFences: card.cpuFences, openAlloc: func() (imageAllocator, error) { return openGBM(card.fd) }, reason: "start", ready: make(chan error, 1), seatDisable: make(chan chan struct{}), ping: make(chan chan struct{}), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap} |
| 238 | var err error | 242 | var err error | ||
| 239 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 243 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 240 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 244 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| @@ -650,6 +654,17 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 650 | // (overlay) say. A frame that changes a plane's colour state is never async: | 654 | // (overlay) say. A frame that changes a plane's colour state is never async: | ||
| 651 | // only FB_ID may change in an async commit. | 655 | // only FB_ID may change in an async commit. | ||
| 652 | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | 656 | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | ||
| 657 | + | // Where fences are waited for on the CPU, once per commit and before | |||
| 658 | + | // any state is read: the retries below go through commitRect. The | |||
| 659 | + | // fence is our own render's, or a client's that already signalled | |||
| 660 | + | // (clientFenceReady). | |||
| 661 | + | o.waitFences(fence) | |||
| 662 | + | return o.commitRect(fb, fence, async, vrr, f, ov, rect, pc) | |||
| 663 | + | } | |||
| 664 | + | ||||
| 665 | + | // commitRect is commitWithRect once the fence was waited for; its retries | |||
| 666 | + | // call it again, without waiting again. | |||
| 667 | + | func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect, pc colorUse) error { | |||
| 653 | o.observeSecurity() | 668 | o.observeSecurity() | ||
| 654 | if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) { | 669 | if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) { | ||
| 655 | return errSecurityScene | 670 | return errSecurityScene | ||
| @@ -661,9 +676,6 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||||
| 661 | if o.protected { | 676 | if o.protected { | ||
| 662 | async, vrr, ov, pc = false, false, overlayWin{}, colorUse{} | 677 | async, vrr, ov, pc = false, false, overlayWin{}, colorUse{} | ||
| 663 | } | 678 | } | ||
| 664 | - | // A client fence (direct scanout, overlay) signalled already | |||
| 665 | - | // (clientFenceReady): this waits only for our own render. | |||
| 666 | - | o.waitFences(fence) | |||
| 667 | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) { | 679 | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue || o.colorStaleFor(pc, ov)) { | ||
| 668 | async = false | 680 | async = false | ||
| 669 | } | 681 | } | ||
| @@ -704,20 +716,20 @@ func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, | |||||
| 704 | return errColorRefused | 716 | return errColorRefused | ||
| 705 | } | 717 | } | ||
| 706 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | 718 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | ||
| 707 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | 719 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) |
| 708 | } | 720 | } | ||
| 709 | // The same frame without the connector hint tells whether the hint | 721 | // The same frame without the connector hint tells whether the hint | ||
| 710 | // is the cause. | 722 | // is the cause. | ||
| 711 | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) { | 723 | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.frameTest(fb, fence, vrr, cur, ov, rect, pc, false) { | ||
| 712 | o.log.Warn().Err(err).Msg("content type refused; disabled") | 724 | o.log.Warn().Err(err).Msg("content type refused; disabled") | ||
| 713 | o.contentProp = 0 | 725 | o.contentProp = 0 | ||
| 714 | - | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect, pc) | 726 | + | return o.commitRect(fb, fence, false, vrr, f, ov, rect, pc) |
| 715 | } | 727 | } | ||
| 716 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | 728 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | ||
| 717 | // Retry without turning VRR on: if that passes, the driver | 729 | // Retry without turning VRR on: if that passes, the driver | ||
| 718 | // refuses VRR on this output. The overlay stays: the composed | 730 | // refuses VRR on this output. The overlay stays: the composed | ||
| 719 | // image left its window out. | 731 | // image left its window out. | ||
| 720 | - | if o.commitWithRect(fb, fence, false, false, f, ov, rect, pc) == nil { | 732 | + | if o.commitRect(fb, fence, false, false, f, ov, rect, pc) == nil { |
| 721 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | 733 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | ||
| 722 | o.vrrProp = 0 | 734 | o.vrrProp = 0 | ||
| 723 | return nil | 735 | return nil | ||
| @@ -923,7 +935,7 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF | |||||
| 923 | // from now on. | 935 | // from now on. | ||
| 924 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 936 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 925 | cfb.noAsync = true | 937 | cfb.noAsync = true | ||
| 926 | - | err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) | 938 | + | err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc) |
| 927 | } | 939 | } | ||
| 928 | if errors.Is(err, errColorRefused) { | 940 | if errors.Is(err, errColorRefused) { | ||
| 929 | // KMS takes the buffer without the pipeline: compose it; the | 941 | // KMS takes the buffer without the pipeline: compose it; the | ||
| @@ -1513,6 +1525,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1513 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | 1525 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | ||
| 1514 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | 1526 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | ||
| 1515 | ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried) | 1527 | ev = ev.Int("state_commits", fs.stateCommits).Int("state_carried", fs.stateCarried) | ||
| 1528 | + | if fs.fenceTimeouts > 0 { | |||
| 1529 | + | ev = ev.Int("fence_timeouts", fs.fenceTimeouts) | |||
| 1530 | + | } | |||
| 1516 | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | 1531 | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | ||
| 1517 | if o.traceFlips { | 1532 | if o.traceFlips { | ||
| 1518 | ev = ev.Int("late_fences", fs.lateFences) | 1533 | ev = ev.Int("late_fences", fs.lateFences) | ||
M internal/adapters/vulkan/renderer.go +3 -0
| @@ -34,6 +34,9 @@ type Renderer struct { | |||
| 34 | 34 | current int | |
| 35 | 35 | renderMods []uint64 | |
| 36 | 36 | hdrMods []uint64 | |
| 37 | + | // importMods caches the modifiers a target of a format can be imported | |
| 38 | + | // with (probeImportModifiers), by what the answer depends on. | |
| 39 | + | importMods map[importKey][]uint64 | |
| 37 | 40 | hdrNits float64 | |
| 38 | 41 | hdr hdrPass | |
| 39 | 42 | hdrError error | |
| @@ -34,6 +34,9 @@ type Renderer struct { | |||||
| 34 | current int | 34 | current int | ||
| 35 | renderMods []uint64 | 35 | renderMods []uint64 | ||
| 36 | hdrMods []uint64 | 36 | hdrMods []uint64 | ||
| 37 | + | // importMods caches the modifiers a target of a format can be imported | |||
| 38 | + | // with (probeImportModifiers), by what the answer depends on. | |||
| 39 | + | importMods map[importKey][]uint64 | |||
| 37 | hdrNits float64 | 40 | hdrNits float64 | ||
| 38 | hdr hdrPass | 41 | hdr hdrPass | ||
| 39 | hdrError error | 42 | hdrError error | ||
M internal/adapters/vulkan/targets.go +35 -9
| @@ -214,11 +214,28 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) | |||
| 214 | 214 | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit) | |
| 215 | 215 | } | |
| 216 | 216 | ||
| 217 | + | // importKey is what the importable modifiers of a target depend on: its | |
| 218 | + | // format and whether test readback adds the transfer-source requirement. | |
| 219 | + | type importKey struct { | |
| 220 | + | format vk.Format | |
| 221 | + | readback bool | |
| 222 | + | } | |
| 223 | + | ||
| 217 | 224 | // probeImportModifiers lists the modifiers of format a target can be | |
| 218 | 225 | // imported with: probeModifiers with the importable bit, for images the | |
| 219 | - | // display allocator made. | |
| 226 | + | // display allocator made. The answer is probed once per key: it does not | |
| 227 | + | // change while the device lives. | |
| 220 | 228 | func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | |
| 221 | - | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit) | |
| 229 | + | key := importKey{format, r.hdrReadback} | |
| 230 | + | if mods, ok := r.importMods[key]; ok { | |
| 231 | + | return mods | |
| 232 | + | } | |
| 233 | + | mods := r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit) | |
| 234 | + | if r.importMods == nil { | |
| 235 | + | r.importMods = make(map[importKey][]uint64) | |
| 236 | + | } | |
| 237 | + | r.importMods[key] = mods | |
| 238 | + | return mods | |
| 222 | 239 | } | |
| 223 | 240 | ||
| 224 | 241 | func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 { | |
| @@ -425,6 +442,14 @@ func (r *Renderer) dropTargets() { | |||
| 425 | 442 | r.targets, r.current, r.lastHDRTarget = nil, 0, -1 | |
| 426 | 443 | } | |
| 427 | 444 | ||
| 445 | + | // What ImportTargets refuses an image for, before asking the driver. | |
| 446 | + | var ( | |
| 447 | + | errImageGeometry = errors.New("image size or format does not match the target") | |
| 448 | + | errImageModifier = errors.New("image modifier is not an importable target modifier") | |
| 449 | + | errImagePlanes = errors.New("image plane count does not match its modifier") | |
| 450 | + | errImageDMABuf = errors.New("image planes are not in one dmabuf") | |
| 451 | + | ) | |
| 452 | + | ||
| 428 | 453 | // importTarget makes a target of an image allocated outside the renderer. | |
| 429 | 454 | // The target is shared with the display like an exported one: frames | |
| 430 | 455 | // acquire it from and release it to the foreign queue family. | |
| @@ -432,15 +457,16 @@ func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) { | |||
| 432 | 457 | d := r.dd | |
| 433 | 458 | format, fourcc := r.targetFormat() | |
| 434 | 459 | n := len(b.Planes) | |
| 460 | + | want, known := r.modPlanes[b.Modifier] | |
| 435 | 461 | switch { | |
| 436 | 462 | case b.Format != fourcc || b.Width != r.width || b.Height != r.height: | |
| 437 | - | return nil, fmt.Errorf("image %dx%d format %#x does not match target %dx%d format %#x", b.Width, b.Height, b.Format, r.width, r.height, fourcc) | |
| 438 | - | case n < 1 || n > maxModifierPlanes: | |
| 439 | - | return nil, fmt.Errorf("image has %d planes", n) | |
| 440 | - | case r.modPlanes[b.Modifier] != uint32(n): | |
| 441 | - | return nil, fmt.Errorf("modifier %#x: image has %d planes, device expects %d", b.Modifier, n, r.modPlanes[b.Modifier]) | |
| 463 | + | return nil, fmt.Errorf("%w: image %dx%d format %#x, target %dx%d format %#x", errImageGeometry, b.Width, b.Height, b.Format, r.width, r.height, fourcc) | |
| 464 | + | case !known: | |
| 465 | + | return nil, fmt.Errorf("%w: %#x is not a target modifier of the device", errImageModifier, b.Modifier) | |
| 466 | + | case n < 1 || n > maxModifierPlanes || want != uint32(n): | |
| 467 | + | return nil, fmt.Errorf("%w: modifier %#x: image has %d planes, device expects %d", errImagePlanes, b.Modifier, n, want) | |
| 442 | 468 | case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit): | |
| 443 | - | return nil, fmt.Errorf("modifier %#x cannot be imported as a target", b.Modifier) | |
| 469 | + | return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier) | |
| 444 | 470 | } | |
| 445 | 471 | if err := sameDMABuf(b.Planes); err != nil { | |
| 446 | 472 | return nil, err | |
| @@ -523,7 +549,7 @@ func sameDMABuf(planes []ports.DMABufPlane) error { | |||
| 523 | 549 | if i == 0 { | |
| 524 | 550 | first = st | |
| 525 | 551 | } else if st.Dev != first.Dev || st.Ino != first.Ino { | |
| 526 | - | return fmt.Errorf("plane %d is not in the same dmabuf as plane 0", i) | |
| 552 | + | return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i) | |
| 527 | 553 | } | |
| 528 | 554 | } | |
| 529 | 555 | return nil | |
| @@ -214,11 +214,28 @@ func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) | |||||
| 214 | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit) | 214 | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureExportableBit) | ||
| 215 | } | 215 | } | ||
| 216 | 216 | ||||
| 217 | + | // importKey is what the importable modifiers of a target depend on: its | |||
| 218 | + | // format and whether test readback adds the transfer-source requirement. | |||
| 219 | + | type importKey struct { | |||
| 220 | + | format vk.Format | |||
| 221 | + | readback bool | |||
| 222 | + | } | |||
| 223 | + | ||||
| 217 | // probeImportModifiers lists the modifiers of format a target can be | 224 | // probeImportModifiers lists the modifiers of format a target can be | ||
| 218 | // imported with: probeModifiers with the importable bit, for images the | 225 | // imported with: probeModifiers with the importable bit, for images the | ||
| 219 | - | // display allocator made. | 226 | + | // display allocator made. The answer is probed once per key: it does not |
| 227 | + | // change while the device lives. | |||
| 220 | func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | 228 | func (r *Renderer) probeImportModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 { | ||
| 221 | - | return r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit) | 229 | + | key := importKey{format, r.hdrReadback} |
| 230 | + | if mods, ok := r.importMods[key]; ok { | |||
| 231 | + | return mods | |||
| 232 | + | } | |||
| 233 | + | mods := r.probeTargetModifiers(physical, format, vk.ExternalMemoryFeatureImportableBit) | |||
| 234 | + | if r.importMods == nil { | |||
| 235 | + | r.importMods = make(map[importKey][]uint64) | |||
| 236 | + | } | |||
| 237 | + | r.importMods[key] = mods | |||
| 238 | + | return mods | |||
| 222 | } | 239 | } | ||
| 223 | 240 | ||||
| 224 | func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 { | 241 | func (r *Renderer) probeTargetModifiers(physical vk.PhysicalDevice, format vk.Format, feature vk.ExternalMemoryFeatureFlags) []uint64 { | ||
| @@ -425,6 +442,14 @@ func (r *Renderer) dropTargets() { | |||||
| 425 | r.targets, r.current, r.lastHDRTarget = nil, 0, -1 | 442 | r.targets, r.current, r.lastHDRTarget = nil, 0, -1 | ||
| 426 | } | 443 | } | ||
| 427 | 444 | ||||
| 445 | + | // What ImportTargets refuses an image for, before asking the driver. | |||
| 446 | + | var ( | |||
| 447 | + | errImageGeometry = errors.New("image size or format does not match the target") | |||
| 448 | + | errImageModifier = errors.New("image modifier is not an importable target modifier") | |||
| 449 | + | errImagePlanes = errors.New("image plane count does not match its modifier") | |||
| 450 | + | errImageDMABuf = errors.New("image planes are not in one dmabuf") | |||
| 451 | + | ) | |||
| 452 | + | ||||
| 428 | // importTarget makes a target of an image allocated outside the renderer. | 453 | // importTarget makes a target of an image allocated outside the renderer. | ||
| 429 | // The target is shared with the display like an exported one: frames | 454 | // The target is shared with the display like an exported one: frames | ||
| 430 | // acquire it from and release it to the foreign queue family. | 455 | // acquire it from and release it to the foreign queue family. | ||
| @@ -432,15 +457,16 @@ func (r *Renderer) importTarget(b *ports.DMABuf) (*target, error) { | |||||
| 432 | d := r.dd | 457 | d := r.dd | ||
| 433 | format, fourcc := r.targetFormat() | 458 | format, fourcc := r.targetFormat() | ||
| 434 | n := len(b.Planes) | 459 | n := len(b.Planes) | ||
| 460 | + | want, known := r.modPlanes[b.Modifier] | |||
| 435 | switch { | 461 | switch { | ||
| 436 | case b.Format != fourcc || b.Width != r.width || b.Height != r.height: | 462 | case b.Format != fourcc || b.Width != r.width || b.Height != r.height: | ||
| 437 | - | return nil, fmt.Errorf("image %dx%d format %#x does not match target %dx%d format %#x", b.Width, b.Height, b.Format, r.width, r.height, fourcc) | 463 | + | return nil, fmt.Errorf("%w: image %dx%d format %#x, target %dx%d format %#x", errImageGeometry, b.Width, b.Height, b.Format, r.width, r.height, fourcc) |
| 438 | - | case n < 1 || n > maxModifierPlanes: | 464 | + | case !known: |
| 439 | - | return nil, fmt.Errorf("image has %d planes", n) | 465 | + | return nil, fmt.Errorf("%w: %#x is not a target modifier of the device", errImageModifier, b.Modifier) |
| 440 | - | case r.modPlanes[b.Modifier] != uint32(n): | 466 | + | case n < 1 || n > maxModifierPlanes || want != uint32(n): |
| 441 | - | return nil, fmt.Errorf("modifier %#x: image has %d planes, device expects %d", b.Modifier, n, r.modPlanes[b.Modifier]) | 467 | + | return nil, fmt.Errorf("%w: modifier %#x: image has %d planes, device expects %d", errImagePlanes, b.Modifier, n, want) |
| 442 | case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit): | 468 | case !r.externalTarget(r.physical, format, b.Modifier, vk.ExternalMemoryFeatureImportableBit): | ||
| 443 | - | return nil, fmt.Errorf("modifier %#x cannot be imported as a target", b.Modifier) | 469 | + | return nil, fmt.Errorf("%w: %#x cannot be imported as a target", errImageModifier, b.Modifier) |
| 444 | } | 470 | } | ||
| 445 | if err := sameDMABuf(b.Planes); err != nil { | 471 | if err := sameDMABuf(b.Planes); err != nil { | ||
| 446 | return nil, err | 472 | return nil, err | ||
| @@ -523,7 +549,7 @@ func sameDMABuf(planes []ports.DMABufPlane) error { | |||||
| 523 | if i == 0 { | 549 | if i == 0 { | ||
| 524 | first = st | 550 | first = st | ||
| 525 | } else if st.Dev != first.Dev || st.Ino != first.Ino { | 551 | } else if st.Dev != first.Dev || st.Ino != first.Ino { | ||
| 526 | - | return fmt.Errorf("plane %d is not in the same dmabuf as plane 0", i) | 552 | + | return fmt.Errorf("%w: plane %d is not in the dmabuf of plane 0", errImageDMABuf, i) |
| 527 | } | 553 | } | ||
| 528 | } | 554 | } | ||
| 529 | return nil | 555 | return nil | ||
M internal/adapters/vulkan/targets_test.go +149 -54
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "errors" | |
| 4 | 5 | "image/color" | |
| 5 | 6 | "math" | |
| 6 | 7 | "os" | |
| @@ -369,10 +370,46 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) { | |||
| 369 | 370 | } | |
| 370 | 371 | } | |
| 371 | 372 | ||
| 372 | - | // ImportTargets refuses images that do not fit the renderer (size, format, | |
| 373 | - | // plane count, modifier) before touching the driver, and a refusal leaves | |
| 374 | - | // no targets: frames go back to the renderer's own image. | |
| 375 | - | func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |
| 373 | + | // pipeFile is a file for plane tests: a distinct inode per call. | |
| 374 | + | func pipeFile(t *testing.T) *os.File { | |
| 375 | + | t.Helper() | |
| 376 | + | f, w, err := os.Pipe() | |
| 377 | + | if err != nil { | |
| 378 | + | t.Fatal(err) | |
| 379 | + | } | |
| 380 | + | w.Close() | |
| 381 | + | t.Cleanup(func() { f.Close() }) | |
| 382 | + | return f | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | // Planes must be in one dmabuf: the same file or duplicates of it are, two | |
| 386 | + | // files are not. No device needed. | |
| 387 | + | func TestSameDMABuf(t *testing.T) { | |
| 388 | + | one, other := pipeFile(t), pipeFile(t) | |
| 389 | + | for _, tc := range []struct { | |
| 390 | + | name string | |
| 391 | + | planes []ports.DMABufPlane | |
| 392 | + | ok bool | |
| 393 | + | }{ | |
| 394 | + | {"one plane", []ports.DMABufPlane{{File: one}}, true}, | |
| 395 | + | {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true}, | |
| 396 | + | {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false}, | |
| 397 | + | {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false}, | |
| 398 | + | {"no file", []ports.DMABufPlane{{File: one}, {}}, false}, | |
| 399 | + | } { | |
| 400 | + | err := sameDMABuf(tc.planes) | |
| 401 | + | if (err == nil) != tc.ok { | |
| 402 | + | t.Errorf("%s: %v", tc.name, err) | |
| 403 | + | } | |
| 404 | + | if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) { | |
| 405 | + | t.Errorf("%s: %v is not errImageDMABuf", tc.name, err) | |
| 406 | + | } | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | // TargetModifiers keeps the modifiers the display lists and drops | |
| 411 | + | // multi-plane ones for singlePlane. | |
| 412 | + | func TestTargetModifiersFiltering(t *testing.T) { | |
| 376 | 413 | r, err := New(64, 32) | |
| 377 | 414 | if err != nil { | |
| 378 | 415 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -382,56 +419,6 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |||
| 382 | 419 | if len(mods) == 0 { | |
| 383 | 420 | t.Skip("no importable target modifier") | |
| 384 | 421 | } | |
| 385 | - | pipe := func() *os.File { | |
| 386 | - | f, w, err := os.Pipe() | |
| 387 | - | if err != nil { | |
| 388 | - | t.Fatal(err) | |
| 389 | - | } | |
| 390 | - | w.Close() | |
| 391 | - | t.Cleanup(func() { f.Close() }) | |
| 392 | - | return f | |
| 393 | - | } | |
| 394 | - | good := func() ports.DMABuf { | |
| 395 | - | return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: mods[0], Planes: make([]ports.DMABufPlane, r.modPlanes[mods[0]])} | |
| 396 | - | } | |
| 397 | - | for name, mutate := range map[string]func(*ports.DMABuf){ | |
| 398 | - | "width": func(b *ports.DMABuf) { b.Width = 65 }, | |
| 399 | - | "height": func(b *ports.DMABuf) { b.Height = 31 }, | |
| 400 | - | "format": func(b *ports.DMABuf) { b.Format = fourccXR30 }, | |
| 401 | - | "planes": func(b *ports.DMABuf) { b.Planes = nil }, | |
| 402 | - | "modifier": func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, | |
| 403 | - | } { | |
| 404 | - | b := good() | |
| 405 | - | for i := range b.Planes { | |
| 406 | - | b.Planes[i].File = pipe() | |
| 407 | - | } | |
| 408 | - | mutate(&b) | |
| 409 | - | if err := r.ImportTargets([]ports.DMABuf{b}); err == nil { | |
| 410 | - | t.Errorf("%s: mismatched image imported", name) | |
| 411 | - | } | |
| 412 | - | if len(r.targets) != 0 { | |
| 413 | - | t.Errorf("%s: %d targets left", name, len(r.targets)) | |
| 414 | - | } | |
| 415 | - | } | |
| 416 | - | // Planes in two different dmabufs: refused whatever the plane count. | |
| 417 | - | if n := r.modPlanes[mods[0]]; n == 1 { | |
| 418 | - | two := good() | |
| 419 | - | two.Planes = []ports.DMABufPlane{{File: pipe()}, {File: pipe()}} | |
| 420 | - | if err := r.ImportTargets([]ports.DMABuf{two}); err == nil { | |
| 421 | - | t.Error("two-plane image imported for a one-plane modifier") | |
| 422 | - | } | |
| 423 | - | } | |
| 424 | - | if err := sameDMABuf([]ports.DMABufPlane{{File: pipe()}, {File: pipe()}}); err == nil { | |
| 425 | - | t.Error("planes in different files accepted") | |
| 426 | - | } | |
| 427 | - | same := pipe() | |
| 428 | - | if err := sameDMABuf([]ports.DMABufPlane{{File: same}, {File: same}}); err != nil { | |
| 429 | - | t.Errorf("planes in one file refused: %v", err) | |
| 430 | - | } | |
| 431 | - | if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 { | |
| 432 | - | t.Errorf("dropping targets: %v, %d left", err, len(r.targets)) | |
| 433 | - | } | |
| 434 | - | // A display list without the renderer's modifiers leaves nothing. | |
| 435 | 422 | if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 { | |
| 436 | 423 | t.Errorf("modifiers outside the display list: %#x", got) | |
| 437 | 424 | } | |
| @@ -443,4 +430,112 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |||
| 443 | 430 | t.Errorf("single-plane list holds multi-plane modifier %#x", m) | |
| 444 | 431 | } | |
| 445 | 432 | } | |
| 433 | + | // The answer is probed once and kept. | |
| 434 | + | if len(r.importMods) == 0 { | |
| 435 | + | t.Error("import modifiers not cached") | |
| 436 | + | } | |
| 437 | + | again := r.TargetModifiers(nil, false) | |
| 438 | + | if !slices.Equal(mods, again) { | |
| 439 | + | t.Errorf("cached list differs: %#x vs %#x", mods, again) | |
| 440 | + | } | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | // ImportTargets refuses an image that does not fit the renderer before | |
| 444 | + | // asking the driver, says why, and leaves no targets: frames go back to the | |
| 445 | + | // renderer's own image. | |
| 446 | + | func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |
| 447 | + | r, err := New(64, 32) | |
| 448 | + | if err != nil { | |
| 449 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 450 | + | } | |
| 451 | + | defer r.Close() | |
| 452 | + | var single uint64 | |
| 453 | + | found := false | |
| 454 | + | for _, m := range r.TargetModifiers(nil, true) { | |
| 455 | + | single, found = m, true | |
| 456 | + | break | |
| 457 | + | } | |
| 458 | + | if !found { | |
| 459 | + | t.Skip("no importable one-plane target modifier") | |
| 460 | + | } | |
| 461 | + | good := func() ports.DMABuf { | |
| 462 | + | return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}} | |
| 463 | + | } | |
| 464 | + | for _, tc := range []struct { | |
| 465 | + | name string | |
| 466 | + | mutate func(*ports.DMABuf) | |
| 467 | + | want error | |
| 468 | + | }{ | |
| 469 | + | {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry}, | |
| 470 | + | {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry}, | |
| 471 | + | {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry}, | |
| 472 | + | {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier}, | |
| 473 | + | {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes}, | |
| 474 | + | {"too many planes", func(b *ports.DMABuf) { | |
| 475 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |
| 476 | + | }, errImagePlanes}, | |
| 477 | + | } { | |
| 478 | + | t.Run(tc.name, func(t *testing.T) { | |
| 479 | + | b := good() | |
| 480 | + | tc.mutate(&b) | |
| 481 | + | err := r.ImportTargets([]ports.DMABuf{b}) | |
| 482 | + | if !errors.Is(err, tc.want) { | |
| 483 | + | t.Fatalf("error %v, want %v", err, tc.want) | |
| 484 | + | } | |
| 485 | + | if len(r.targets) != 0 { | |
| 486 | + | t.Fatalf("%d targets left", len(r.targets)) | |
| 487 | + | } | |
| 488 | + | }) | |
| 489 | + | } | |
| 490 | + | t.Run("second image refused drops the first", func(t *testing.T) { | |
| 491 | + | // A real dmabuf for the first image: the renderer's own export. | |
| 492 | + | exported, err := r.ExportTargets(1, []uint64{single}, true) | |
| 493 | + | if err != nil { | |
| 494 | + | t.Skipf("no exportable target to reuse: %v", err) | |
| 495 | + | } | |
| 496 | + | defer closeBufs(exported) | |
| 497 | + | bad := good() | |
| 498 | + | bad.Width = 1 | |
| 499 | + | err = r.ImportTargets([]ports.DMABuf{exported[0], bad}) | |
| 500 | + | if !errors.Is(err, errImageGeometry) { | |
| 501 | + | t.Fatalf("error %v, want the second image's geometry refusal", err) | |
| 502 | + | } | |
| 503 | + | if len(r.targets) != 0 { | |
| 504 | + | t.Fatalf("%d targets left", len(r.targets)) | |
| 505 | + | } | |
| 506 | + | }) | |
| 507 | + | t.Run("dropping", func(t *testing.T) { | |
| 508 | + | if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 { | |
| 509 | + | t.Fatalf("%v, %d targets", err, len(r.targets)) | |
| 510 | + | } | |
| 511 | + | }) | |
| 512 | + | t.Run("planes in different dmabufs", func(t *testing.T) { | |
| 513 | + | multi := uint64(0) | |
| 514 | + | for _, m := range r.TargetModifiers(nil, false) { | |
| 515 | + | if r.modPlanes[m] > 1 { | |
| 516 | + | multi = m | |
| 517 | + | break | |
| 518 | + | } | |
| 519 | + | } | |
| 520 | + | if multi == 0 { | |
| 521 | + | t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf") | |
| 522 | + | } | |
| 523 | + | b := good() | |
| 524 | + | b.Modifier = multi | |
| 525 | + | b.Planes = nil | |
| 526 | + | for range r.modPlanes[multi] { | |
| 527 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |
| 528 | + | } | |
| 529 | + | if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) { | |
| 530 | + | t.Fatalf("error %v", err) | |
| 531 | + | } | |
| 532 | + | }) | |
| 533 | + | } | |
| 534 | + | ||
| 535 | + | func closeBufs(bufs []ports.DMABuf) { | |
| 536 | + | for _, b := range bufs { | |
| 537 | + | for _, p := range b.Planes { | |
| 538 | + | p.File.Close() | |
| 539 | + | } | |
| 540 | + | } | |
| 446 | 541 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "errors" | |||
| 4 | "image/color" | 5 | "image/color" | ||
| 5 | "math" | 6 | "math" | ||
| 6 | "os" | 7 | "os" | ||
| @@ -369,10 +370,46 @@ func TestProbedModifiersHaveNoDCCRetile(t *testing.T) { | |||||
| 369 | } | 370 | } | ||
| 370 | } | 371 | } | ||
| 371 | 372 | ||||
| 372 | - | // ImportTargets refuses images that do not fit the renderer (size, format, | 373 | + | // pipeFile is a file for plane tests: a distinct inode per call. |
| 373 | - | // plane count, modifier) before touching the driver, and a refusal leaves | 374 | + | func pipeFile(t *testing.T) *os.File { |
| 374 | - | // no targets: frames go back to the renderer's own image. | 375 | + | t.Helper() |
| 375 | - | func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | 376 | + | f, w, err := os.Pipe() |
| 377 | + | if err != nil { | |||
| 378 | + | t.Fatal(err) | |||
| 379 | + | } | |||
| 380 | + | w.Close() | |||
| 381 | + | t.Cleanup(func() { f.Close() }) | |||
| 382 | + | return f | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | // Planes must be in one dmabuf: the same file or duplicates of it are, two | |||
| 386 | + | // files are not. No device needed. | |||
| 387 | + | func TestSameDMABuf(t *testing.T) { | |||
| 388 | + | one, other := pipeFile(t), pipeFile(t) | |||
| 389 | + | for _, tc := range []struct { | |||
| 390 | + | name string | |||
| 391 | + | planes []ports.DMABufPlane | |||
| 392 | + | ok bool | |||
| 393 | + | }{ | |||
| 394 | + | {"one plane", []ports.DMABufPlane{{File: one}}, true}, | |||
| 395 | + | {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true}, | |||
| 396 | + | {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false}, | |||
| 397 | + | {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false}, | |||
| 398 | + | {"no file", []ports.DMABufPlane{{File: one}, {}}, false}, | |||
| 399 | + | } { | |||
| 400 | + | err := sameDMABuf(tc.planes) | |||
| 401 | + | if (err == nil) != tc.ok { | |||
| 402 | + | t.Errorf("%s: %v", tc.name, err) | |||
| 403 | + | } | |||
| 404 | + | if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) { | |||
| 405 | + | t.Errorf("%s: %v is not errImageDMABuf", tc.name, err) | |||
| 406 | + | } | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | // TargetModifiers keeps the modifiers the display lists and drops | |||
| 411 | + | // multi-plane ones for singlePlane. | |||
| 412 | + | func TestTargetModifiersFiltering(t *testing.T) { | |||
| 376 | r, err := New(64, 32) | 413 | r, err := New(64, 32) | ||
| 377 | if err != nil { | 414 | if err != nil { | ||
| 378 | t.Skipf("Vulkan unavailable: %v", err) | 415 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -382,56 +419,6 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |||||
| 382 | if len(mods) == 0 { | 419 | if len(mods) == 0 { | ||
| 383 | t.Skip("no importable target modifier") | 420 | t.Skip("no importable target modifier") | ||
| 384 | } | 421 | } | ||
| 385 | - | pipe := func() *os.File { | |||
| 386 | - | f, w, err := os.Pipe() | |||
| 387 | - | if err != nil { | |||
| 388 | - | t.Fatal(err) | |||
| 389 | - | } | |||
| 390 | - | w.Close() | |||
| 391 | - | t.Cleanup(func() { f.Close() }) | |||
| 392 | - | return f | |||
| 393 | - | } | |||
| 394 | - | good := func() ports.DMABuf { | |||
| 395 | - | return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: mods[0], Planes: make([]ports.DMABufPlane, r.modPlanes[mods[0]])} | |||
| 396 | - | } | |||
| 397 | - | for name, mutate := range map[string]func(*ports.DMABuf){ | |||
| 398 | - | "width": func(b *ports.DMABuf) { b.Width = 65 }, | |||
| 399 | - | "height": func(b *ports.DMABuf) { b.Height = 31 }, | |||
| 400 | - | "format": func(b *ports.DMABuf) { b.Format = fourccXR30 }, | |||
| 401 | - | "planes": func(b *ports.DMABuf) { b.Planes = nil }, | |||
| 402 | - | "modifier": func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, | |||
| 403 | - | } { | |||
| 404 | - | b := good() | |||
| 405 | - | for i := range b.Planes { | |||
| 406 | - | b.Planes[i].File = pipe() | |||
| 407 | - | } | |||
| 408 | - | mutate(&b) | |||
| 409 | - | if err := r.ImportTargets([]ports.DMABuf{b}); err == nil { | |||
| 410 | - | t.Errorf("%s: mismatched image imported", name) | |||
| 411 | - | } | |||
| 412 | - | if len(r.targets) != 0 { | |||
| 413 | - | t.Errorf("%s: %d targets left", name, len(r.targets)) | |||
| 414 | - | } | |||
| 415 | - | } | |||
| 416 | - | // Planes in two different dmabufs: refused whatever the plane count. | |||
| 417 | - | if n := r.modPlanes[mods[0]]; n == 1 { | |||
| 418 | - | two := good() | |||
| 419 | - | two.Planes = []ports.DMABufPlane{{File: pipe()}, {File: pipe()}} | |||
| 420 | - | if err := r.ImportTargets([]ports.DMABuf{two}); err == nil { | |||
| 421 | - | t.Error("two-plane image imported for a one-plane modifier") | |||
| 422 | - | } | |||
| 423 | - | } | |||
| 424 | - | if err := sameDMABuf([]ports.DMABufPlane{{File: pipe()}, {File: pipe()}}); err == nil { | |||
| 425 | - | t.Error("planes in different files accepted") | |||
| 426 | - | } | |||
| 427 | - | same := pipe() | |||
| 428 | - | if err := sameDMABuf([]ports.DMABufPlane{{File: same}, {File: same}}); err != nil { | |||
| 429 | - | t.Errorf("planes in one file refused: %v", err) | |||
| 430 | - | } | |||
| 431 | - | if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 { | |||
| 432 | - | t.Errorf("dropping targets: %v, %d left", err, len(r.targets)) | |||
| 433 | - | } | |||
| 434 | - | // A display list without the renderer's modifiers leaves nothing. | |||
| 435 | if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 { | 422 | if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 { | ||
| 436 | t.Errorf("modifiers outside the display list: %#x", got) | 423 | t.Errorf("modifiers outside the display list: %#x", got) | ||
| 437 | } | 424 | } | ||
| @@ -443,4 +430,112 @@ func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |||||
| 443 | t.Errorf("single-plane list holds multi-plane modifier %#x", m) | 430 | t.Errorf("single-plane list holds multi-plane modifier %#x", m) | ||
| 444 | } | 431 | } | ||
| 445 | } | 432 | } | ||
| 433 | + | // The answer is probed once and kept. | |||
| 434 | + | if len(r.importMods) == 0 { | |||
| 435 | + | t.Error("import modifiers not cached") | |||
| 436 | + | } | |||
| 437 | + | again := r.TargetModifiers(nil, false) | |||
| 438 | + | if !slices.Equal(mods, again) { | |||
| 439 | + | t.Errorf("cached list differs: %#x vs %#x", mods, again) | |||
| 440 | + | } | |||
| 441 | + | } | |||
| 442 | + | ||||
| 443 | + | // ImportTargets refuses an image that does not fit the renderer before | |||
| 444 | + | // asking the driver, says why, and leaves no targets: frames go back to the | |||
| 445 | + | // renderer's own image. | |||
| 446 | + | func TestImportTargetsRefusesMismatchedImages(t *testing.T) { | |||
| 447 | + | r, err := New(64, 32) | |||
| 448 | + | if err != nil { | |||
| 449 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 450 | + | } | |||
| 451 | + | defer r.Close() | |||
| 452 | + | var single uint64 | |||
| 453 | + | found := false | |||
| 454 | + | for _, m := range r.TargetModifiers(nil, true) { | |||
| 455 | + | single, found = m, true | |||
| 456 | + | break | |||
| 457 | + | } | |||
| 458 | + | if !found { | |||
| 459 | + | t.Skip("no importable one-plane target modifier") | |||
| 460 | + | } | |||
| 461 | + | good := func() ports.DMABuf { | |||
| 462 | + | return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}} | |||
| 463 | + | } | |||
| 464 | + | for _, tc := range []struct { | |||
| 465 | + | name string | |||
| 466 | + | mutate func(*ports.DMABuf) | |||
| 467 | + | want error | |||
| 468 | + | }{ | |||
| 469 | + | {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry}, | |||
| 470 | + | {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry}, | |||
| 471 | + | {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry}, | |||
| 472 | + | {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier}, | |||
| 473 | + | {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes}, | |||
| 474 | + | {"too many planes", func(b *ports.DMABuf) { | |||
| 475 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |||
| 476 | + | }, errImagePlanes}, | |||
| 477 | + | } { | |||
| 478 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 479 | + | b := good() | |||
| 480 | + | tc.mutate(&b) | |||
| 481 | + | err := r.ImportTargets([]ports.DMABuf{b}) | |||
| 482 | + | if !errors.Is(err, tc.want) { | |||
| 483 | + | t.Fatalf("error %v, want %v", err, tc.want) | |||
| 484 | + | } | |||
| 485 | + | if len(r.targets) != 0 { | |||
| 486 | + | t.Fatalf("%d targets left", len(r.targets)) | |||
| 487 | + | } | |||
| 488 | + | }) | |||
| 489 | + | } | |||
| 490 | + | t.Run("second image refused drops the first", func(t *testing.T) { | |||
| 491 | + | // A real dmabuf for the first image: the renderer's own export. | |||
| 492 | + | exported, err := r.ExportTargets(1, []uint64{single}, true) | |||
| 493 | + | if err != nil { | |||
| 494 | + | t.Skipf("no exportable target to reuse: %v", err) | |||
| 495 | + | } | |||
| 496 | + | defer closeBufs(exported) | |||
| 497 | + | bad := good() | |||
| 498 | + | bad.Width = 1 | |||
| 499 | + | err = r.ImportTargets([]ports.DMABuf{exported[0], bad}) | |||
| 500 | + | if !errors.Is(err, errImageGeometry) { | |||
| 501 | + | t.Fatalf("error %v, want the second image's geometry refusal", err) | |||
| 502 | + | } | |||
| 503 | + | if len(r.targets) != 0 { | |||
| 504 | + | t.Fatalf("%d targets left", len(r.targets)) | |||
| 505 | + | } | |||
| 506 | + | }) | |||
| 507 | + | t.Run("dropping", func(t *testing.T) { | |||
| 508 | + | if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 { | |||
| 509 | + | t.Fatalf("%v, %d targets", err, len(r.targets)) | |||
| 510 | + | } | |||
| 511 | + | }) | |||
| 512 | + | t.Run("planes in different dmabufs", func(t *testing.T) { | |||
| 513 | + | multi := uint64(0) | |||
| 514 | + | for _, m := range r.TargetModifiers(nil, false) { | |||
| 515 | + | if r.modPlanes[m] > 1 { | |||
| 516 | + | multi = m | |||
| 517 | + | break | |||
| 518 | + | } | |||
| 519 | + | } | |||
| 520 | + | if multi == 0 { | |||
| 521 | + | t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf") | |||
| 522 | + | } | |||
| 523 | + | b := good() | |||
| 524 | + | b.Modifier = multi | |||
| 525 | + | b.Planes = nil | |||
| 526 | + | for range r.modPlanes[multi] { | |||
| 527 | + | b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)}) | |||
| 528 | + | } | |||
| 529 | + | if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) { | |||
| 530 | + | t.Fatalf("error %v", err) | |||
| 531 | + | } | |||
| 532 | + | }) | |||
| 533 | + | } | |||
| 534 | + | ||||
| 535 | + | func closeBufs(bufs []ports.DMABuf) { | |||
| 536 | + | for _, b := range bufs { | |||
| 537 | + | for _, p := range b.Planes { | |||
| 538 | + | p.File.Close() | |||
| 539 | + | } | |||
| 540 | + | } | |||
| 446 | } | 541 | } | ||
M internal/app/gbm_targets_test.go +4 -1
| @@ -12,6 +12,9 @@ import ( | |||
| 12 | 12 | "golang.org/x/sys/unix" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | + | // fourccXRGB8888 is DRM_FORMAT_XRGB8888. | |
| 16 | + | const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 17 | + | ||
| 15 | 18 | // renderNodeFor opens the render node of the GPU the renderer runs on. | |
| 16 | 19 | func renderNodeFor(t *testing.T, dev uint64) *os.File { | |
| 17 | 20 | t.Helper() | |
| @@ -63,7 +66,7 @@ func TestGBMImagesAreRenderTargets(t *testing.T) { | |||
| 63 | 66 | for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} { | |
| 64 | 67 | var cerr error | |
| 65 | 68 | for i := range bufs { | |
| 66 | - | if bufs[i], cerr = dev.Create(w, h, 'X'|'R'<<8|'2'<<16|'4'<<24, mods, usage); cerr != nil { | |
| 69 | + | if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil { | |
| 67 | 70 | break | |
| 68 | 71 | } | |
| 69 | 72 | } | |
| @@ -12,6 +12,9 @@ import ( | |||||
| 12 | "golang.org/x/sys/unix" | 12 | "golang.org/x/sys/unix" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | + | // fourccXRGB8888 is DRM_FORMAT_XRGB8888. | |||
| 16 | + | const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |||
| 17 | + | ||||
| 15 | // renderNodeFor opens the render node of the GPU the renderer runs on. | 18 | // renderNodeFor opens the render node of the GPU the renderer runs on. | ||
| 16 | func renderNodeFor(t *testing.T, dev uint64) *os.File { | 19 | func renderNodeFor(t *testing.T, dev uint64) *os.File { | ||
| 17 | t.Helper() | 20 | t.Helper() | ||
| @@ -63,7 +66,7 @@ func TestGBMImagesAreRenderTargets(t *testing.T) { | |||||
| 63 | for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} { | 66 | for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} { | ||
| 64 | var cerr error | 67 | var cerr error | ||
| 65 | for i := range bufs { | 68 | for i := range bufs { | ||
| 66 | - | if bufs[i], cerr = dev.Create(w, h, 'X'|'R'<<8|'2'<<16|'4'<<24, mods, usage); cerr != nil { | 69 | + | if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil { |
| 67 | break | 70 | break | ||
| 68 | } | 71 | } | ||
| 69 | } | 72 | } | ||
M internal/ports/devices.go +8 -8
| @@ -41,7 +41,7 @@ type CaptureFrame interface { | |||
| 41 | 41 | // (ADR 014: no CPU pass). | |
| 42 | 42 | type Renderer interface { | |
| 43 | 43 | // SetHDR selects the HDR10 output transform (0 disables it). | |
| 44 | - | // The DRM output calls this before ExportTargets. | |
| 44 | + | // The DRM output calls this before ExportTargets or ImportTargets. | |
| 45 | 45 | SetHDR(sdrNits float64) | |
| 46 | 46 | // Render draws the scene. done is a sync file signalled when the GPU | |
| 47 | 47 | // finished the frame (nil: already finished); the caller closes it. | |
| @@ -62,13 +62,13 @@ type Renderer interface { | |||
| 62 | 62 | // cannot import dmabufs. | |
| 63 | 63 | TargetModifiers(display []uint64, singlePlane bool) []uint64 | |
| 64 | 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. | |
| 65 | + | // targets, replacing the previous ones (no buffers: drops them). Each | |
| 66 | + | // is one memory object of the renderer's size and target format, with | |
| 67 | + | // a modifier from TargetModifiers: every plane file is the same | |
| 68 | + | // dmabuf (the planes differ by offset), and disjoint planes are | |
| 69 | + | // refused. The caller keeps its files and must keep the memory alive | |
| 70 | + | // while the targets are in use. An error leaves the renderer without | |
| 71 | + | // targets. | |
| 72 | 72 | ImportTargets(bufs []DMABuf) error | |
| 73 | 73 | // UseTarget selects the exported image the next Render draws into. | |
| 74 | 74 | UseTarget(i int) | |
| @@ -41,7 +41,7 @@ type CaptureFrame interface { | |||||
| 41 | // (ADR 014: no CPU pass). | 41 | // (ADR 014: no CPU pass). | ||
| 42 | type Renderer interface { | 42 | type Renderer interface { | ||
| 43 | // SetHDR selects the HDR10 output transform (0 disables it). | 43 | // SetHDR selects the HDR10 output transform (0 disables it). | ||
| 44 | - | // The DRM output calls this before ExportTargets. | 44 | + | // The DRM output calls this before ExportTargets or ImportTargets. |
| 45 | SetHDR(sdrNits float64) | 45 | SetHDR(sdrNits float64) | ||
| 46 | // Render draws the scene. done is a sync file signalled when the GPU | 46 | // Render draws the scene. done is a sync file signalled when the GPU | ||
| 47 | // finished the frame (nil: already finished); the caller closes it. | 47 | // finished the frame (nil: already finished); the caller closes it. | ||
| @@ -62,13 +62,13 @@ type Renderer interface { | |||||
| 62 | // cannot import dmabufs. | 62 | // cannot import dmabufs. | ||
| 63 | TargetModifiers(display []uint64, singlePlane bool) []uint64 | 63 | TargetModifiers(display []uint64, singlePlane bool) []uint64 | ||
| 64 | // ImportTargets makes images allocated outside the renderer its | 64 | // ImportTargets makes images allocated outside the renderer its | ||
| 65 | - | // targets, replacing the previous ones (no buffers: drops them). Each is | 65 | + | // targets, replacing the previous ones (no buffers: drops them). Each |
| 66 | - | // one memory object (every plane file is the same dmabuf, the planes | 66 | + | // is one memory object of the renderer's size and target format, with |
| 67 | - | // differing by offset; disjoint planes are refused) of the renderer's | 67 | + | // a modifier from TargetModifiers: every plane file is the same |
| 68 | - | // size and target format, with a modifier from TargetModifiers. The | 68 | + | // dmabuf (the planes differ by offset), and disjoint planes are |
| 69 | - | // caller keeps its files and must | 69 | + | // refused. The caller keeps its files and must keep the memory alive |
| 70 | - | // keep the memory alive while the targets are in use. An error leaves | 70 | + | // while the targets are in use. An error leaves the renderer without |
| 71 | - | // the renderer without targets. | 71 | + | // targets. |
| 72 | ImportTargets(bufs []DMABuf) error | 72 | ImportTargets(bufs []DMABuf) error | ||
| 73 | // UseTarget selects the exported image the next Render draws into. | 73 | // UseTarget selects the exported image the next Render draws into. | ||
| 74 | UseTarget(i int) | 74 | UseTarget(i int) | ||