fix(drm): allocate hardware cursor images as KMS dumb buffers
nvidia-drm refuses the cursor plane with the linear images Vulkan allocates and exports, which leaves NVIDIA on the software cursor. The cursor images are now KMS dumb buffers written by the CPU through their mapping. Vulkan cursor images stay the fallback when dumb buffers fail or the plane refuses them, then the software cursor. The HDR cursor conversion moves to internal/adapters/hdrcursor so the drm adapter writes the same pixels the renderer did. Fixes neferwl#22.
- commit
- a5f1806e4c7593621ea5e50f34cd9c102fe5ed8b
- author
- brice · · 2026-10-22 13:10 UTC
- parent
- 3c13b3d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
22 files · +1553 -150
M .mockery.yml +5 -0
| @@ -44,6 +44,11 @@ packages: | |||
| 44 | 44 | dir: internal/adapters/drm | |
| 45 | 45 | pkgname: drm | |
| 46 | 46 | filename: image_buffer_mock_test.go | |
| 47 | + | cursorImages: | |
| 48 | + | config: | |
| 49 | + | dir: internal/adapters/drm | |
| 50 | + | pkgname: drm | |
| 51 | + | filename: cursor_images_mock_test.go | |
| 47 | 52 | github.com/bnema/neferwl/internal/adapters/seat: | |
| 48 | 53 | interfaces: | |
| 49 | 54 | disabler: | |
| @@ -44,6 +44,11 @@ packages: | |||||
| 44 | dir: internal/adapters/drm | 44 | dir: internal/adapters/drm | ||
| 45 | pkgname: drm | 45 | pkgname: drm | ||
| 46 | filename: image_buffer_mock_test.go | 46 | filename: image_buffer_mock_test.go | ||
| 47 | + | cursorImages: | |||
| 48 | + | config: | |||
| 49 | + | dir: internal/adapters/drm | |||
| 50 | + | pkgname: drm | |||
| 51 | + | filename: cursor_images_mock_test.go | |||
| 47 | github.com/bnema/neferwl/internal/adapters/seat: | 52 | github.com/bnema/neferwl/internal/adapters/seat: | ||
| 48 | interfaces: | 53 | interfaces: | ||
| 49 | disabler: | 54 | disabler: | ||
M Makefile +1 -1
| @@ -61,7 +61,7 @@ arch: adapter-imports | |||
| 61 | 61 | # Adapters stay independent: production code may import only the shared | |
| 62 | 62 | # adapter libraries below. outputkit holds what both output backends (drm, | |
| 63 | 63 | # headless) run; the others are leaf helpers. | |
| 64 | - | ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb | |
| 64 | + | ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|hdrcursor|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb | |
| 65 | 65 | adapter-imports: | |
| 66 | 66 | @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \ | |
| 67 | 67 | bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \ | |
| @@ -61,7 +61,7 @@ arch: adapter-imports | |||||
| 61 | # Adapters stay independent: production code may import only the shared | 61 | # Adapters stay independent: production code may import only the shared | ||
| 62 | # adapter libraries below. outputkit holds what both output backends (drm, | 62 | # adapter libraries below. outputkit holds what both output backends (drm, | ||
| 63 | # headless) run; the others are leaf helpers. | 63 | # headless) run; the others are leaf helpers. | ||
| 64 | - | ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb | 64 | + | ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|hdrcursor|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb |
| 65 | adapter-imports: | 65 | adapter-imports: | ||
| 66 | @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \ | 66 | @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \ | ||
| 67 | bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \ | 67 | bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \ | ||
M docs/dev/architecture.md +7 -0
| @@ -32,6 +32,13 @@ A DRM output flips between two images the renderer draws into, with no CPU copy. | |||
| 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`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23). | |
| 33 | 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 | + | ### Cursor images | |
| 36 | + | ||
| 37 | + | The cursor plane shows the hardware cursor from two ARGB8888 images that the CPU writes only when the cursor image changes, never on a move. | |
| 38 | + | ||
| 39 | + | - **Allocation:** the images are KMS dumb buffers, mapped for the CPU, with the pitch the driver picks. The output converts them to HDR10 when it is in HDR. | |
| 40 | + | - **Fallback:** when KMS cannot make dumb buffers, or the plane refuses them in a TEST_ONLY commit, the renderer's linear cursor images are used. When the plane refuses those too, the cursor is drawn into every frame (software cursor). The log line `hardware cursor` names the allocator in its `alloc` field (`dumb` or `vulkan`), and is a warning for `vulkan`. | |
| 41 | + | ||
| 35 | 42 | 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 | 43 | ||
| 37 | 44 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | |
| @@ -32,6 +32,13 @@ A DRM output flips between two images the renderer draws into, with no CPU copy. | |||||
| 32 | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23). | 32 | - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23). | ||
| 33 | - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable. | 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 | + | ### Cursor images | |||
| 36 | + | ||||
| 37 | + | The cursor plane shows the hardware cursor from two ARGB8888 images that the CPU writes only when the cursor image changes, never on a move. | |||
| 38 | + | ||||
| 39 | + | - **Allocation:** the images are KMS dumb buffers, mapped for the CPU, with the pitch the driver picks. The output converts them to HDR10 when it is in HDR. | |||
| 40 | + | - **Fallback:** when KMS cannot make dumb buffers, or the plane refuses them in a TEST_ONLY commit, the renderer's linear cursor images are used. When the plane refuses those too, the cursor is drawn into every frame (software cursor). The log line `hardware cursor` names the allocator in its `alloc` field (`dumb` or `vulkan`), and is a warning for `vulkan`. | |||
| 41 | + | ||||
| 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: | 42 | 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 | 43 | ||||
| 37 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | 44 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | ||
M internal/adapters/drm/atomic.go +9 -0
| @@ -89,6 +89,15 @@ type kms interface { | |||
| 89 | 89 | // addFB imports a single-plane dmabuf as a framebuffer of format. | |
| 90 | 90 | addFB(b *ports.DMABuf, format uint32) (uint32, error) | |
| 91 | 91 | rmFB(id uint32) error | |
| 92 | + | // createDumb allocates a width×height 32 bpp dumb buffer; mapDumb maps it | |
| 93 | + | // for the CPU (read/write) and unmapDumb releases the mapping; | |
| 94 | + | // addFBHandle makes a framebuffer of format from it; destroyDumb frees | |
| 95 | + | // it. The framebuffer is removed with rmFB first. | |
| 96 | + | createDumb(width, height int) (dumbBuffer, error) | |
| 97 | + | mapDumb(b dumbBuffer) ([]byte, error) | |
| 98 | + | unmapDumb(mem []byte) error | |
| 99 | + | addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error) | |
| 100 | + | destroyDumb(handle uint32) error | |
| 92 | 101 | createLease(objects []uint32) (*os.File, uint32, error) | |
| 93 | 102 | revokeLease(id uint32) error | |
| 94 | 103 | resources() ([]uint32, []uint32, error) | |
| @@ -89,6 +89,15 @@ type kms interface { | |||||
| 89 | // addFB imports a single-plane dmabuf as a framebuffer of format. | 89 | // addFB imports a single-plane dmabuf as a framebuffer of format. | ||
| 90 | addFB(b *ports.DMABuf, format uint32) (uint32, error) | 90 | addFB(b *ports.DMABuf, format uint32) (uint32, error) | ||
| 91 | rmFB(id uint32) error | 91 | rmFB(id uint32) error | ||
| 92 | + | // createDumb allocates a width×height 32 bpp dumb buffer; mapDumb maps it | |||
| 93 | + | // for the CPU (read/write) and unmapDumb releases the mapping; | |||
| 94 | + | // addFBHandle makes a framebuffer of format from it; destroyDumb frees | |||
| 95 | + | // it. The framebuffer is removed with rmFB first. | |||
| 96 | + | createDumb(width, height int) (dumbBuffer, error) | |||
| 97 | + | mapDumb(b dumbBuffer) ([]byte, error) | |||
| 98 | + | unmapDumb(mem []byte) error | |||
| 99 | + | addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error) | |||
| 100 | + | destroyDumb(handle uint32) error | |||
| 92 | createLease(objects []uint32) (*os.File, uint32, error) | 101 | createLease(objects []uint32) (*os.File, uint32, error) | ||
| 93 | revokeLease(id uint32) error | 102 | revokeLease(id uint32) error | ||
| 94 | resources() ([]uint32, []uint32, error) | 103 | resources() ([]uint32, []uint32, error) | ||
M internal/adapters/drm/commit_test.go +8 -6
| @@ -55,9 +55,10 @@ func TestBusyFrameCommitsAfterEvent(t *testing.T) { | |||
| 55 | 55 | func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |
| 56 | 56 | o, _, _ := testOutput(t) | |
| 57 | 57 | c := o.cursor | |
| 58 | - | r := portsmocks.NewMockRenderer(t) | |
| 58 | + | imgs := newMockcursorImages(t) | |
| 59 | + | c.imgs = imgs | |
| 59 | 60 | var written []int | |
| 60 | - | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |
| 61 | + | imgs.EXPECT().write(mock.Anything, mock.Anything, 1, 1, float64(0)).RunAndReturn(func(i int, _ []byte, _, _ int, _ float64) error { | |
| 61 | 62 | written = append(written, i) | |
| 62 | 63 | return nil | |
| 63 | 64 | }) | |
| @@ -65,7 +66,7 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |||
| 65 | 66 | // Image 0 on screen: two changes before a commit both go to image 1. | |
| 66 | 67 | c.screen, c.cur, c.image = 90, 0, true | |
| 67 | 68 | for range 2 { | |
| 68 | - | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil { | |
| 69 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil { | |
| 69 | 70 | t.Fatal(err) | |
| 70 | 71 | } | |
| 71 | 72 | } | |
| @@ -74,12 +75,12 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |||
| 74 | 75 | } | |
| 75 | 76 | // Image 1 in flight, image 0 on screen: nothing is free. | |
| 76 | 77 | c.committed(cursorState{on: true, fb: 91}) | |
| 77 | - | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil { | |
| 78 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 || c.later == nil { | |
| 78 | 79 | t.Fatalf("wrote a busy image: %v %v", err, written) | |
| 79 | 80 | } | |
| 80 | 81 | // The flip frees image 0. | |
| 81 | 82 | c.landed() | |
| 82 | - | if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { | |
| 83 | + | if changed, err := c.flushLater(0); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { | |
| 83 | 84 | t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur) | |
| 84 | 85 | } | |
| 85 | 86 | } | |
| @@ -464,7 +465,7 @@ func TestCursorOffSkipsLoads(t *testing.T) { | |||
| 464 | 465 | o, _, _ := testOutput(t) | |
| 465 | 466 | o.cursor.off = true | |
| 466 | 467 | loaded := false | |
| 467 | - | o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) { | |
| 468 | + | o.setCursor(func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) { | |
| 468 | 469 | loaded = true | |
| 469 | 470 | return ports.CursorImage{}, nil | |
| 470 | 471 | }, ports.CursorChange{}, 1, 0) | |
| @@ -497,6 +498,7 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 497 | 498 | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 498 | 499 | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| 499 | 500 | r.EXPECT().Close().Return().Once() | |
| 501 | + | k.EXPECT().createDumb(64, 64).Return(dumbBuffer{}, unix.ENOSYS).Once() // renderer images | |
| 500 | 502 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once() | |
| 501 | 503 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once() | |
| 502 | 504 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |
| @@ -55,9 +55,10 @@ func TestBusyFrameCommitsAfterEvent(t *testing.T) { | |||||
| 55 | func TestCursorImageNeverOverwritesScreen(t *testing.T) { | 55 | func TestCursorImageNeverOverwritesScreen(t *testing.T) { | ||
| 56 | o, _, _ := testOutput(t) | 56 | o, _, _ := testOutput(t) | ||
| 57 | c := o.cursor | 57 | c := o.cursor | ||
| 58 | - | r := portsmocks.NewMockRenderer(t) | 58 | + | imgs := newMockcursorImages(t) |
| 59 | + | c.imgs = imgs | |||
| 59 | var written []int | 60 | var written []int | ||
| 60 | - | r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | 61 | + | imgs.EXPECT().write(mock.Anything, mock.Anything, 1, 1, float64(0)).RunAndReturn(func(i int, _ []byte, _, _ int, _ float64) error { |
| 61 | written = append(written, i) | 62 | written = append(written, i) | ||
| 62 | return nil | 63 | return nil | ||
| 63 | }) | 64 | }) | ||
| @@ -65,7 +66,7 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |||||
| 65 | // Image 0 on screen: two changes before a commit both go to image 1. | 66 | // Image 0 on screen: two changes before a commit both go to image 1. | ||
| 66 | c.screen, c.cur, c.image = 90, 0, true | 67 | c.screen, c.cur, c.image = 90, 0, true | ||
| 67 | for range 2 { | 68 | for range 2 { | ||
| 68 | - | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil { | 69 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil { |
| 69 | t.Fatal(err) | 70 | t.Fatal(err) | ||
| 70 | } | 71 | } | ||
| 71 | } | 72 | } | ||
| @@ -74,12 +75,12 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) { | |||||
| 74 | } | 75 | } | ||
| 75 | // Image 1 in flight, image 0 on screen: nothing is free. | 76 | // Image 1 in flight, image 0 on screen: nothing is free. | ||
| 76 | c.committed(cursorState{on: true, fb: 91}) | 77 | c.committed(cursorState{on: true, fb: 91}) | ||
| 77 | - | if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil { | 78 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 || c.later == nil { |
| 78 | t.Fatalf("wrote a busy image: %v %v", err, written) | 79 | t.Fatalf("wrote a busy image: %v %v", err, written) | ||
| 79 | } | 80 | } | ||
| 80 | // The flip frees image 0. | 81 | // The flip frees image 0. | ||
| 81 | c.landed() | 82 | c.landed() | ||
| 82 | - | if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { | 83 | + | if changed, err := c.flushLater(0); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 { |
| 83 | t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur) | 84 | t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur) | ||
| 84 | } | 85 | } | ||
| 85 | } | 86 | } | ||
| @@ -464,7 +465,7 @@ func TestCursorOffSkipsLoads(t *testing.T) { | |||||
| 464 | o, _, _ := testOutput(t) | 465 | o, _, _ := testOutput(t) | ||
| 465 | o.cursor.off = true | 466 | o.cursor.off = true | ||
| 466 | loaded := false | 467 | loaded := false | ||
| 467 | - | o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) { | 468 | + | o.setCursor(func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) { |
| 468 | loaded = true | 469 | loaded = true | ||
| 469 | return ports.CursorImage{}, nil | 470 | return ports.CursorImage{}, nil | ||
| 470 | }, ports.CursorChange{}, 1, 0) | 471 | }, ports.CursorChange{}, 1, 0) | ||
| @@ -497,6 +498,7 @@ func runReportsSeenAfterCursorCommit(t *testing.T) { | |||||
| 497 | r.EXPECT().UseTarget(mock.Anything).Return() | 498 | r.EXPECT().UseTarget(mock.Anything).Return() | ||
| 498 | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | 499 | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | ||
| 499 | r.EXPECT().Close().Return().Once() | 500 | r.EXPECT().Close().Return().Once() | ||
| 501 | + | k.EXPECT().createDumb(64, 64).Return(dumbBuffer{}, unix.ENOSYS).Once() // renderer images | |||
| 500 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once() | 502 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once() | ||
| 501 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once() | 503 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once() | ||
| 502 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | 504 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | ||
M internal/adapters/drm/cursor.go +73 -29
| @@ -31,14 +31,24 @@ type Cursor struct { | |||
| 31 | 31 | wake chan struct{} | |
| 32 | 32 | ||
| 33 | 33 | // Owned by the output goroutine. | |
| 34 | - | plane *plane | |
| 35 | - | size int | |
| 36 | - | fbs [2]uint32 // the two images; cur is on screen or next | |
| 34 | + | plane *plane | |
| 35 | + | size int | |
| 36 | + | // imgs are the two images (nil: none), alloc says who allocated them | |
| 37 | + | // (allocDumb or allocVulkan) and fallback makes the renderer's images | |
| 38 | + | // when KMS refuses the dumb ones. fbs mirrors their framebuffers; cur | |
| 39 | + | // is on screen or next. | |
| 40 | + | imgs cursorImages | |
| 41 | + | alloc string | |
| 42 | + | fallback func() (cursorImages, error) | |
| 43 | + | fbs [2]uint32 | |
| 37 | 44 | cur int | |
| 38 | 45 | image bool // an image is loaded | |
| 39 | 46 | hotX, hotY int | |
| 40 | - | applied cursorState // what the last commit put on the plane | |
| 41 | - | commits int | |
| 47 | + | // last is a copy of the loaded image, written again into new images | |
| 48 | + | // when the allocator changes. | |
| 49 | + | last laterImage | |
| 50 | + | applied cursorState // what the last commit put on the plane | |
| 51 | + | commits int | |
| 42 | 52 | // screen is the image the plane scans out (0: none); flight is what | |
| 43 | 53 | // the pending commit puts on it when flying. An image is written only | |
| 44 | 54 | // when it is neither, so the plane never shows a half-written image. | |
| @@ -111,37 +121,70 @@ func (c *Cursor) poke() { | |||
| 111 | 121 | } | |
| 112 | 122 | } | |
| 113 | 123 | ||
| 114 | - | // setup allocates the two cursor images and their framebuffers. | |
| 115 | - | func (c *Cursor) setup(k kms, r ports.Renderer) error { | |
| 116 | - | bufs, err := r.CursorBuffers(c.size) | |
| 124 | + | // setup allocates the two cursor images and their framebuffers: KMS dumb | |
| 125 | + | // buffers first, then the renderer's images. dumbErr is why the dumb buffers | |
| 126 | + | // were not used (nil when they are); err is set when no images exist. | |
| 127 | + | func (c *Cursor) setup(k kms, r ports.Renderer) (dumbErr, err error) { | |
| 128 | + | c.fallback = func() (cursorImages, error) { return newRendererCursor(k, r, c.size) } | |
| 129 | + | d, dumbErr := newDumbCursor(k, c.size) | |
| 130 | + | if dumbErr == nil { | |
| 131 | + | c.install(d, allocDumb) | |
| 132 | + | return nil, nil | |
| 133 | + | } | |
| 134 | + | imgs, err := c.fallback() | |
| 117 | 135 | if err != nil { | |
| 118 | - | return err | |
| 136 | + | return dumbErr, err | |
| 119 | 137 | } | |
| 120 | - | for i := range bufs { | |
| 121 | - | if err == nil { | |
| 122 | - | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) | |
| 123 | - | } | |
| 124 | - | for _, p := range bufs[i].Planes { | |
| 125 | - | p.File.Close() | |
| 126 | - | } | |
| 138 | + | c.install(imgs, allocVulkan) | |
| 139 | + | return dumbErr, nil | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // install makes imgs the cursor images; none is on screen or in flight. | |
| 143 | + | func (c *Cursor) install(imgs cursorImages, alloc string) { | |
| 144 | + | c.imgs, c.alloc = imgs, alloc | |
| 145 | + | for i := range c.fbs { | |
| 146 | + | c.fbs[i] = imgs.fb(i) | |
| 147 | + | } | |
| 148 | + | c.cur = 0 | |
| 149 | + | c.screen, c.flight, c.flying, c.applied = 0, cursorState{}, false, cursorState{} | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // useFallback replaces refused dumb images with the renderer's and writes | |
| 153 | + | // the current image into them. It reports false when it cannot: no fallback, | |
| 154 | + | // already used, or no images either. | |
| 155 | + | func (c *Cursor) useFallback(hdrNits float64) (bool, error) { | |
| 156 | + | if c.alloc != allocDumb || c.fallback == nil { | |
| 157 | + | return false, nil | |
| 127 | 158 | } | |
| 159 | + | imgs, err := c.fallback() | |
| 128 | 160 | if err != nil { | |
| 129 | - | c.free(k) | |
| 161 | + | return false, err | |
| 162 | + | } | |
| 163 | + | c.imgs.close() | |
| 164 | + | c.install(imgs, allocVulkan) | |
| 165 | + | l := c.later | |
| 166 | + | if l == nil && c.image { | |
| 167 | + | l = &c.last | |
| 130 | 168 | } | |
| 131 | - | return err | |
| 169 | + | c.image, c.later = false, nil | |
| 170 | + | if l == nil { | |
| 171 | + | return true, nil | |
| 172 | + | } | |
| 173 | + | return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits) | |
| 132 | 174 | } | |
| 133 | 175 | ||
| 134 | 176 | // setImage loads premultiplied ARGB8888 pixels into an image neither on | |
| 135 | 177 | // screen nor in flight; when there is none it waits for the next commit | |
| 136 | 178 | // event (flushLater). An empty image hides the cursor (a client asked for | |
| 137 | 179 | // none). | |
| 138 | - | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { | |
| 180 | + | // hdrNits is the SDR white of an HDR output (0: SDR). | |
| 181 | + | func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64) error { | |
| 139 | 182 | c.later = nil | |
| 140 | 183 | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | |
| 141 | 184 | c.image = false | |
| 142 | 185 | return nil | |
| 143 | 186 | } | |
| 144 | - | if c.fbs[0] == 0 { | |
| 187 | + | if c.imgs == nil { | |
| 145 | 188 | return errors.New("no cursor images") | |
| 146 | 189 | } | |
| 147 | 190 | next := c.freeSlot() | |
| @@ -149,10 +192,11 @@ func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) | |||
| 149 | 192 | c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY} | |
| 150 | 193 | return nil | |
| 151 | 194 | } | |
| 152 | - | if err := r.WriteCursor(next, pixels, w, h); err != nil { | |
| 195 | + | if err := c.imgs.write(next, pixels, w, h, hdrNits); err != nil { | |
| 153 | 196 | return err | |
| 154 | 197 | } | |
| 155 | 198 | c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY | |
| 199 | + | c.last = laterImage{pixels: append(c.last.pixels[:0], pixels[:w*h*4]...), w: w, h: h, hotX: hotX, hotY: hotY} | |
| 156 | 200 | return nil | |
| 157 | 201 | } | |
| 158 | 202 | ||
| @@ -169,12 +213,12 @@ func (c *Cursor) freeSlot() int { | |||
| 169 | 213 | ||
| 170 | 214 | // flushLater loads an image that waited for a free slot. It reports | |
| 171 | 215 | // whether the cursor changed. | |
| 172 | - | func (c *Cursor) flushLater(r ports.Renderer) (bool, error) { | |
| 216 | + | func (c *Cursor) flushLater(hdrNits float64) (bool, error) { | |
| 173 | 217 | l := c.later | |
| 174 | 218 | if l == nil || c.freeSlot() < 0 { | |
| 175 | 219 | return false, nil | |
| 176 | 220 | } | |
| 177 | - | return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY) | |
| 221 | + | return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits) | |
| 178 | 222 | } | |
| 179 | 223 | ||
| 180 | 224 | // committed records that a commit carrying state s is in flight. | |
| @@ -229,13 +273,13 @@ func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) { | |||
| 229 | 273 | req.set(p.id, p.prop("CRTC_H"), size) | |
| 230 | 274 | } | |
| 231 | 275 | ||
| 232 | - | func (c *Cursor) free(k kms) { | |
| 233 | - | for i, fb := range c.fbs { | |
| 234 | - | if fb != 0 { | |
| 235 | - | _ = k.rmFB(fb) | |
| 236 | - | c.fbs[i] = 0 | |
| 237 | - | } | |
| 276 | + | // free removes the framebuffers and releases the images. | |
| 277 | + | func (c *Cursor) free() { | |
| 278 | + | if c.imgs != nil { | |
| 279 | + | c.imgs.close() | |
| 280 | + | c.imgs, c.alloc = nil, "" | |
| 238 | 281 | } | |
| 282 | + | c.fbs = [2]uint32{} | |
| 239 | 283 | c.image, c.later = false, nil | |
| 240 | 284 | c.screen, c.flying = 0, false | |
| 241 | 285 | } | |
| @@ -31,14 +31,24 @@ type Cursor struct { | |||||
| 31 | wake chan struct{} | 31 | wake chan struct{} | ||
| 32 | 32 | ||||
| 33 | // Owned by the output goroutine. | 33 | // Owned by the output goroutine. | ||
| 34 | - | plane *plane | 34 | + | plane *plane |
| 35 | - | size int | 35 | + | size int |
| 36 | - | fbs [2]uint32 // the two images; cur is on screen or next | 36 | + | // imgs are the two images (nil: none), alloc says who allocated them |
| 37 | + | // (allocDumb or allocVulkan) and fallback makes the renderer's images | |||
| 38 | + | // when KMS refuses the dumb ones. fbs mirrors their framebuffers; cur | |||
| 39 | + | // is on screen or next. | |||
| 40 | + | imgs cursorImages | |||
| 41 | + | alloc string | |||
| 42 | + | fallback func() (cursorImages, error) | |||
| 43 | + | fbs [2]uint32 | |||
| 37 | cur int | 44 | cur int | ||
| 38 | image bool // an image is loaded | 45 | image bool // an image is loaded | ||
| 39 | hotX, hotY int | 46 | hotX, hotY int | ||
| 40 | - | applied cursorState // what the last commit put on the plane | 47 | + | // last is a copy of the loaded image, written again into new images |
| 41 | - | commits int | 48 | + | // when the allocator changes. |
| 49 | + | last laterImage | |||
| 50 | + | applied cursorState // what the last commit put on the plane | |||
| 51 | + | commits int | |||
| 42 | // screen is the image the plane scans out (0: none); flight is what | 52 | // screen is the image the plane scans out (0: none); flight is what | ||
| 43 | // the pending commit puts on it when flying. An image is written only | 53 | // the pending commit puts on it when flying. An image is written only | ||
| 44 | // when it is neither, so the plane never shows a half-written image. | 54 | // when it is neither, so the plane never shows a half-written image. | ||
| @@ -111,37 +121,70 @@ func (c *Cursor) poke() { | |||||
| 111 | } | 121 | } | ||
| 112 | } | 122 | } | ||
| 113 | 123 | ||||
| 114 | - | // setup allocates the two cursor images and their framebuffers. | 124 | + | // setup allocates the two cursor images and their framebuffers: KMS dumb |
| 115 | - | func (c *Cursor) setup(k kms, r ports.Renderer) error { | 125 | + | // buffers first, then the renderer's images. dumbErr is why the dumb buffers |
| 116 | - | bufs, err := r.CursorBuffers(c.size) | 126 | + | // were not used (nil when they are); err is set when no images exist. |
| 127 | + | func (c *Cursor) setup(k kms, r ports.Renderer) (dumbErr, err error) { | |||
| 128 | + | c.fallback = func() (cursorImages, error) { return newRendererCursor(k, r, c.size) } | |||
| 129 | + | d, dumbErr := newDumbCursor(k, c.size) | |||
| 130 | + | if dumbErr == nil { | |||
| 131 | + | c.install(d, allocDumb) | |||
| 132 | + | return nil, nil | |||
| 133 | + | } | |||
| 134 | + | imgs, err := c.fallback() | |||
| 117 | if err != nil { | 135 | if err != nil { | ||
| 118 | - | return err | 136 | + | return dumbErr, err |
| 119 | } | 137 | } | ||
| 120 | - | for i := range bufs { | 138 | + | c.install(imgs, allocVulkan) |
| 121 | - | if err == nil { | 139 | + | return dumbErr, nil |
| 122 | - | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) | 140 | + | } |
| 123 | - | } | 141 | + | |
| 124 | - | for _, p := range bufs[i].Planes { | 142 | + | // install makes imgs the cursor images; none is on screen or in flight. |
| 125 | - | p.File.Close() | 143 | + | func (c *Cursor) install(imgs cursorImages, alloc string) { |
| 126 | - | } | 144 | + | c.imgs, c.alloc = imgs, alloc |
| 145 | + | for i := range c.fbs { | |||
| 146 | + | c.fbs[i] = imgs.fb(i) | |||
| 147 | + | } | |||
| 148 | + | c.cur = 0 | |||
| 149 | + | c.screen, c.flight, c.flying, c.applied = 0, cursorState{}, false, cursorState{} | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // useFallback replaces refused dumb images with the renderer's and writes | |||
| 153 | + | // the current image into them. It reports false when it cannot: no fallback, | |||
| 154 | + | // already used, or no images either. | |||
| 155 | + | func (c *Cursor) useFallback(hdrNits float64) (bool, error) { | |||
| 156 | + | if c.alloc != allocDumb || c.fallback == nil { | |||
| 157 | + | return false, nil | |||
| 127 | } | 158 | } | ||
| 159 | + | imgs, err := c.fallback() | |||
| 128 | if err != nil { | 160 | if err != nil { | ||
| 129 | - | c.free(k) | 161 | + | return false, err |
| 162 | + | } | |||
| 163 | + | c.imgs.close() | |||
| 164 | + | c.install(imgs, allocVulkan) | |||
| 165 | + | l := c.later | |||
| 166 | + | if l == nil && c.image { | |||
| 167 | + | l = &c.last | |||
| 130 | } | 168 | } | ||
| 131 | - | return err | 169 | + | c.image, c.later = false, nil |
| 170 | + | if l == nil { | |||
| 171 | + | return true, nil | |||
| 172 | + | } | |||
| 173 | + | return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits) | |||
| 132 | } | 174 | } | ||
| 133 | 175 | ||||
| 134 | // setImage loads premultiplied ARGB8888 pixels into an image neither on | 176 | // setImage loads premultiplied ARGB8888 pixels into an image neither on | ||
| 135 | // screen nor in flight; when there is none it waits for the next commit | 177 | // screen nor in flight; when there is none it waits for the next commit | ||
| 136 | // event (flushLater). An empty image hides the cursor (a client asked for | 178 | // event (flushLater). An empty image hides the cursor (a client asked for | ||
| 137 | // none). | 179 | // none). | ||
| 138 | - | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { | 180 | + | // hdrNits is the SDR white of an HDR output (0: SDR). |
| 181 | + | func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64) error { | |||
| 139 | c.later = nil | 182 | c.later = nil | ||
| 140 | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | 183 | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | ||
| 141 | c.image = false | 184 | c.image = false | ||
| 142 | return nil | 185 | return nil | ||
| 143 | } | 186 | } | ||
| 144 | - | if c.fbs[0] == 0 { | 187 | + | if c.imgs == nil { |
| 145 | return errors.New("no cursor images") | 188 | return errors.New("no cursor images") | ||
| 146 | } | 189 | } | ||
| 147 | next := c.freeSlot() | 190 | next := c.freeSlot() | ||
| @@ -149,10 +192,11 @@ func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) | |||||
| 149 | c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY} | 192 | c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY} | ||
| 150 | return nil | 193 | return nil | ||
| 151 | } | 194 | } | ||
| 152 | - | if err := r.WriteCursor(next, pixels, w, h); err != nil { | 195 | + | if err := c.imgs.write(next, pixels, w, h, hdrNits); err != nil { |
| 153 | return err | 196 | return err | ||
| 154 | } | 197 | } | ||
| 155 | c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY | 198 | c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY | ||
| 199 | + | c.last = laterImage{pixels: append(c.last.pixels[:0], pixels[:w*h*4]...), w: w, h: h, hotX: hotX, hotY: hotY} | |||
| 156 | return nil | 200 | return nil | ||
| 157 | } | 201 | } | ||
| 158 | 202 | ||||
| @@ -169,12 +213,12 @@ func (c *Cursor) freeSlot() int { | |||||
| 169 | 213 | ||||
| 170 | // flushLater loads an image that waited for a free slot. It reports | 214 | // flushLater loads an image that waited for a free slot. It reports | ||
| 171 | // whether the cursor changed. | 215 | // whether the cursor changed. | ||
| 172 | - | func (c *Cursor) flushLater(r ports.Renderer) (bool, error) { | 216 | + | func (c *Cursor) flushLater(hdrNits float64) (bool, error) { |
| 173 | l := c.later | 217 | l := c.later | ||
| 174 | if l == nil || c.freeSlot() < 0 { | 218 | if l == nil || c.freeSlot() < 0 { | ||
| 175 | return false, nil | 219 | return false, nil | ||
| 176 | } | 220 | } | ||
| 177 | - | return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY) | 221 | + | return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits) |
| 178 | } | 222 | } | ||
| 179 | 223 | ||||
| 180 | // committed records that a commit carrying state s is in flight. | 224 | // committed records that a commit carrying state s is in flight. | ||
| @@ -229,13 +273,13 @@ func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) { | |||||
| 229 | req.set(p.id, p.prop("CRTC_H"), size) | 273 | req.set(p.id, p.prop("CRTC_H"), size) | ||
| 230 | } | 274 | } | ||
| 231 | 275 | ||||
| 232 | - | func (c *Cursor) free(k kms) { | 276 | + | // free removes the framebuffers and releases the images. |
| 233 | - | for i, fb := range c.fbs { | 277 | + | func (c *Cursor) free() { |
| 234 | - | if fb != 0 { | 278 | + | if c.imgs != nil { |
| 235 | - | _ = k.rmFB(fb) | 279 | + | c.imgs.close() |
| 236 | - | c.fbs[i] = 0 | 280 | + | c.imgs, c.alloc = nil, "" |
| 237 | - | } | |||
| 238 | } | 281 | } | ||
| 282 | + | c.fbs = [2]uint32{} | |||
| 239 | c.image, c.later = false, nil | 283 | c.image, c.later = false, nil | ||
| 240 | c.screen, c.flying = 0, false | 284 | c.screen, c.flying = 0, false | ||
| 241 | } | 285 | } | ||
A internal/adapters/drm/cursor_images.go +164 -0
| @@ -0,0 +1,164 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/hdrcursor" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Who allocated the cursor images (the "alloc" log field). | |
| 12 | + | // | |
| 13 | + | // The renderer's cursor images (allocVulkan, shared with the output images) | |
| 14 | + | // are the fallback. | |
| 15 | + | const allocDumb = "dumb" | |
| 16 | + | ||
| 17 | + | // cursorImages are the two ARGB8888 images of the cursor plane and their | |
| 18 | + | // framebuffers. The output goroutine owns them. | |
| 19 | + | type cursorImages interface { | |
| 20 | + | // fb is the framebuffer of image i. | |
| 21 | + | fb(i int) uint32 | |
| 22 | + | // write fills image i with premultiplied ARGB8888 pixels (w×h, w*4 per | |
| 23 | + | // row), cropped to the image and cleared around. hdrNits > 0 converts | |
| 24 | + | // them to the HDR10 output encoding for that SDR white. | |
| 25 | + | write(i int, pixels []byte, w, h int, hdrNits float64) error | |
| 26 | + | // close removes the framebuffers and releases the images. | |
| 27 | + | close() | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // dumbImage is one KMS dumb buffer mapped for the CPU. | |
| 31 | + | type dumbImage struct { | |
| 32 | + | handle, fb uint32 | |
| 33 | + | pitch int | |
| 34 | + | mem []byte | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // dumbCursor are two KMS dumb buffers: KMS allocates what it scans out, the | |
| 38 | + | // CPU writes the pixels. Renderer-allocated images are refused by some | |
| 39 | + | // drivers (nvidia-drm) on the cursor plane. | |
| 40 | + | type dumbCursor struct { | |
| 41 | + | k kms | |
| 42 | + | size int | |
| 43 | + | img [2]dumbImage | |
| 44 | + | cache hdrcursor.Cache | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func newDumbCursor(k kms, size int) (*dumbCursor, error) { | |
| 48 | + | d := &dumbCursor{k: k, size: size} | |
| 49 | + | for i := range d.img { | |
| 50 | + | if err := d.alloc(&d.img[i]); err != nil { | |
| 51 | + | d.close() | |
| 52 | + | return nil, err | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | return d, nil | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func (d *dumbCursor) alloc(img *dumbImage) error { | |
| 59 | + | b, err := d.k.createDumb(d.size, d.size) | |
| 60 | + | if err != nil { | |
| 61 | + | return fmt.Errorf("create dumb buffer: %w", err) | |
| 62 | + | } | |
| 63 | + | img.handle, img.pitch = b.handle, int(b.pitch) | |
| 64 | + | if img.pitch < d.size*4 { | |
| 65 | + | return fmt.Errorf("dumb buffer pitch %d, want at least %d", img.pitch, d.size*4) | |
| 66 | + | } | |
| 67 | + | if img.mem, err = d.k.mapDumb(b); err != nil { | |
| 68 | + | return fmt.Errorf("map dumb buffer: %w", err) | |
| 69 | + | } | |
| 70 | + | if len(img.mem) < img.pitch*d.size { | |
| 71 | + | return fmt.Errorf("dumb buffer mapping %d bytes, want %d", len(img.mem), img.pitch*d.size) | |
| 72 | + | } | |
| 73 | + | clear(img.mem) | |
| 74 | + | if img.fb, err = d.k.addFBHandle(b, d.size, d.size, fourccARGB); err != nil { | |
| 75 | + | return fmt.Errorf("dumb buffer framebuffer: %w", err) | |
| 76 | + | } | |
| 77 | + | return nil | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func (d *dumbCursor) fb(i int) uint32 { return d.img[i].fb } | |
| 81 | + | ||
| 82 | + | func (d *dumbCursor) write(i int, pixels []byte, w, h int, hdrNits float64) error { | |
| 83 | + | if i < 0 || i >= len(d.img) || d.img[i].mem == nil { | |
| 84 | + | return errors.New("no cursor image") | |
| 85 | + | } | |
| 86 | + | if w < 0 || h < 0 || len(pixels) < w*h*4 { | |
| 87 | + | return errors.New("short cursor image") | |
| 88 | + | } | |
| 89 | + | img := &d.img[i] | |
| 90 | + | mem := img.mem[:img.pitch*d.size] | |
| 91 | + | clear(mem) | |
| 92 | + | cw := min(w, d.size) * 4 | |
| 93 | + | src, stride := pixels, w*4 | |
| 94 | + | if hdrNits > 0 { | |
| 95 | + | // Called only on image changes, never on moves. | |
| 96 | + | src, stride = d.cache.Convert(pixels, w, h, d.size, hdrNits), cw | |
| 97 | + | } | |
| 98 | + | for y := range min(h, d.size) { | |
| 99 | + | copy(mem[y*img.pitch:y*img.pitch+cw], src[y*stride:y*stride+cw]) | |
| 100 | + | } | |
| 101 | + | return nil | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (d *dumbCursor) close() { | |
| 105 | + | for i := range d.img { | |
| 106 | + | img := &d.img[i] | |
| 107 | + | if img.fb != 0 { | |
| 108 | + | _ = d.k.rmFB(img.fb) | |
| 109 | + | } | |
| 110 | + | if img.mem != nil { | |
| 111 | + | _ = d.k.unmapDumb(img.mem) | |
| 112 | + | } | |
| 113 | + | if img.handle != 0 { | |
| 114 | + | _ = d.k.destroyDumb(img.handle) | |
| 115 | + | } | |
| 116 | + | *img = dumbImage{} | |
| 117 | + | } | |
| 118 | + | d.cache.Reset() | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // rendererCursor are the renderer's cursor images (linear dmabufs the | |
| 122 | + | // renderer allocates and writes), imported as framebuffers. | |
| 123 | + | type rendererCursor struct { | |
| 124 | + | k kms | |
| 125 | + | r ports.Renderer | |
| 126 | + | fbs [2]uint32 | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func newRendererCursor(k kms, r ports.Renderer, size int) (*rendererCursor, error) { | |
| 130 | + | bufs, err := r.CursorBuffers(size) | |
| 131 | + | if err != nil { | |
| 132 | + | return nil, err | |
| 133 | + | } | |
| 134 | + | c := &rendererCursor{k: k, r: r} | |
| 135 | + | for i := range bufs { | |
| 136 | + | if err == nil { | |
| 137 | + | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) | |
| 138 | + | } | |
| 139 | + | for _, p := range bufs[i].Planes { | |
| 140 | + | p.File.Close() | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | if err != nil { | |
| 144 | + | c.close() | |
| 145 | + | return nil, err | |
| 146 | + | } | |
| 147 | + | return c, nil | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func (c *rendererCursor) fb(i int) uint32 { return c.fbs[i] } | |
| 151 | + | ||
| 152 | + | // write relies on the renderer's own HDR state: hdrNits is not used. | |
| 153 | + | func (c *rendererCursor) write(i int, pixels []byte, w, h int, _ float64) error { | |
| 154 | + | return c.r.WriteCursor(i, pixels, w, h) | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | func (c *rendererCursor) close() { | |
| 158 | + | for i, fb := range c.fbs { | |
| 159 | + | if fb != 0 { | |
| 160 | + | _ = c.k.rmFB(fb) | |
| 161 | + | c.fbs[i] = 0 | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | } | |
| @@ -0,0 +1,164 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/hdrcursor" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Who allocated the cursor images (the "alloc" log field). | |||
| 12 | + | // | |||
| 13 | + | // The renderer's cursor images (allocVulkan, shared with the output images) | |||
| 14 | + | // are the fallback. | |||
| 15 | + | const allocDumb = "dumb" | |||
| 16 | + | ||||
| 17 | + | // cursorImages are the two ARGB8888 images of the cursor plane and their | |||
| 18 | + | // framebuffers. The output goroutine owns them. | |||
| 19 | + | type cursorImages interface { | |||
| 20 | + | // fb is the framebuffer of image i. | |||
| 21 | + | fb(i int) uint32 | |||
| 22 | + | // write fills image i with premultiplied ARGB8888 pixels (w×h, w*4 per | |||
| 23 | + | // row), cropped to the image and cleared around. hdrNits > 0 converts | |||
| 24 | + | // them to the HDR10 output encoding for that SDR white. | |||
| 25 | + | write(i int, pixels []byte, w, h int, hdrNits float64) error | |||
| 26 | + | // close removes the framebuffers and releases the images. | |||
| 27 | + | close() | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // dumbImage is one KMS dumb buffer mapped for the CPU. | |||
| 31 | + | type dumbImage struct { | |||
| 32 | + | handle, fb uint32 | |||
| 33 | + | pitch int | |||
| 34 | + | mem []byte | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // dumbCursor are two KMS dumb buffers: KMS allocates what it scans out, the | |||
| 38 | + | // CPU writes the pixels. Renderer-allocated images are refused by some | |||
| 39 | + | // drivers (nvidia-drm) on the cursor plane. | |||
| 40 | + | type dumbCursor struct { | |||
| 41 | + | k kms | |||
| 42 | + | size int | |||
| 43 | + | img [2]dumbImage | |||
| 44 | + | cache hdrcursor.Cache | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func newDumbCursor(k kms, size int) (*dumbCursor, error) { | |||
| 48 | + | d := &dumbCursor{k: k, size: size} | |||
| 49 | + | for i := range d.img { | |||
| 50 | + | if err := d.alloc(&d.img[i]); err != nil { | |||
| 51 | + | d.close() | |||
| 52 | + | return nil, err | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | return d, nil | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func (d *dumbCursor) alloc(img *dumbImage) error { | |||
| 59 | + | b, err := d.k.createDumb(d.size, d.size) | |||
| 60 | + | if err != nil { | |||
| 61 | + | return fmt.Errorf("create dumb buffer: %w", err) | |||
| 62 | + | } | |||
| 63 | + | img.handle, img.pitch = b.handle, int(b.pitch) | |||
| 64 | + | if img.pitch < d.size*4 { | |||
| 65 | + | return fmt.Errorf("dumb buffer pitch %d, want at least %d", img.pitch, d.size*4) | |||
| 66 | + | } | |||
| 67 | + | if img.mem, err = d.k.mapDumb(b); err != nil { | |||
| 68 | + | return fmt.Errorf("map dumb buffer: %w", err) | |||
| 69 | + | } | |||
| 70 | + | if len(img.mem) < img.pitch*d.size { | |||
| 71 | + | return fmt.Errorf("dumb buffer mapping %d bytes, want %d", len(img.mem), img.pitch*d.size) | |||
| 72 | + | } | |||
| 73 | + | clear(img.mem) | |||
| 74 | + | if img.fb, err = d.k.addFBHandle(b, d.size, d.size, fourccARGB); err != nil { | |||
| 75 | + | return fmt.Errorf("dumb buffer framebuffer: %w", err) | |||
| 76 | + | } | |||
| 77 | + | return nil | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func (d *dumbCursor) fb(i int) uint32 { return d.img[i].fb } | |||
| 81 | + | ||||
| 82 | + | func (d *dumbCursor) write(i int, pixels []byte, w, h int, hdrNits float64) error { | |||
| 83 | + | if i < 0 || i >= len(d.img) || d.img[i].mem == nil { | |||
| 84 | + | return errors.New("no cursor image") | |||
| 85 | + | } | |||
| 86 | + | if w < 0 || h < 0 || len(pixels) < w*h*4 { | |||
| 87 | + | return errors.New("short cursor image") | |||
| 88 | + | } | |||
| 89 | + | img := &d.img[i] | |||
| 90 | + | mem := img.mem[:img.pitch*d.size] | |||
| 91 | + | clear(mem) | |||
| 92 | + | cw := min(w, d.size) * 4 | |||
| 93 | + | src, stride := pixels, w*4 | |||
| 94 | + | if hdrNits > 0 { | |||
| 95 | + | // Called only on image changes, never on moves. | |||
| 96 | + | src, stride = d.cache.Convert(pixels, w, h, d.size, hdrNits), cw | |||
| 97 | + | } | |||
| 98 | + | for y := range min(h, d.size) { | |||
| 99 | + | copy(mem[y*img.pitch:y*img.pitch+cw], src[y*stride:y*stride+cw]) | |||
| 100 | + | } | |||
| 101 | + | return nil | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func (d *dumbCursor) close() { | |||
| 105 | + | for i := range d.img { | |||
| 106 | + | img := &d.img[i] | |||
| 107 | + | if img.fb != 0 { | |||
| 108 | + | _ = d.k.rmFB(img.fb) | |||
| 109 | + | } | |||
| 110 | + | if img.mem != nil { | |||
| 111 | + | _ = d.k.unmapDumb(img.mem) | |||
| 112 | + | } | |||
| 113 | + | if img.handle != 0 { | |||
| 114 | + | _ = d.k.destroyDumb(img.handle) | |||
| 115 | + | } | |||
| 116 | + | *img = dumbImage{} | |||
| 117 | + | } | |||
| 118 | + | d.cache.Reset() | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // rendererCursor are the renderer's cursor images (linear dmabufs the | |||
| 122 | + | // renderer allocates and writes), imported as framebuffers. | |||
| 123 | + | type rendererCursor struct { | |||
| 124 | + | k kms | |||
| 125 | + | r ports.Renderer | |||
| 126 | + | fbs [2]uint32 | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func newRendererCursor(k kms, r ports.Renderer, size int) (*rendererCursor, error) { | |||
| 130 | + | bufs, err := r.CursorBuffers(size) | |||
| 131 | + | if err != nil { | |||
| 132 | + | return nil, err | |||
| 133 | + | } | |||
| 134 | + | c := &rendererCursor{k: k, r: r} | |||
| 135 | + | for i := range bufs { | |||
| 136 | + | if err == nil { | |||
| 137 | + | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) | |||
| 138 | + | } | |||
| 139 | + | for _, p := range bufs[i].Planes { | |||
| 140 | + | p.File.Close() | |||
| 141 | + | } | |||
| 142 | + | } | |||
| 143 | + | if err != nil { | |||
| 144 | + | c.close() | |||
| 145 | + | return nil, err | |||
| 146 | + | } | |||
| 147 | + | return c, nil | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | func (c *rendererCursor) fb(i int) uint32 { return c.fbs[i] } | |||
| 151 | + | ||||
| 152 | + | // write relies on the renderer's own HDR state: hdrNits is not used. | |||
| 153 | + | func (c *rendererCursor) write(i int, pixels []byte, w, h int, _ float64) error { | |||
| 154 | + | return c.r.WriteCursor(i, pixels, w, h) | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | func (c *rendererCursor) close() { | |||
| 158 | + | for i, fb := range c.fbs { | |||
| 159 | + | if fb != 0 { | |||
| 160 | + | _ = c.k.rmFB(fb) | |||
| 161 | + | c.fbs[i] = 0 | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | } | |||
A internal/adapters/drm/cursor_images_mock_test.go +204 -0
| @@ -0,0 +1,204 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package drm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockcursorImages creates a new instance of mockcursorImages. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func newMockcursorImages(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockcursorImages { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockcursorImages{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { | |
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | + | helper.Helper() | |
| 27 | + | } | |
| 28 | + | mock.AssertExpectations(t) | |
| 29 | + | }) | |
| 30 | + | ||
| 31 | + | return mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // mockcursorImages is an autogenerated mock type for the cursorImages type | |
| 35 | + | type mockcursorImages struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockcursorImages_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockcursorImages) EXPECT() *mockcursorImages_Expecter { | |
| 44 | + | return &mockcursorImages_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // close provides a mock function for the type mockcursorImages | |
| 48 | + | func (_mock *mockcursorImages) close() { | |
| 49 | + | _mock.Called() | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // mockcursorImages_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close' | |
| 54 | + | type mockcursorImages_close_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // close is a helper method to define mock.On call | |
| 59 | + | func (_e *mockcursorImages_Expecter) close() *mockcursorImages_close_Call { | |
| 60 | + | return &mockcursorImages_close_Call{Call: _e.mock.On("close")} | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func (_c *mockcursorImages_close_Call) Run(run func()) *mockcursorImages_close_Call { | |
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 65 | + | run() | |
| 66 | + | }) | |
| 67 | + | return _c | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *mockcursorImages_close_Call) Return() *mockcursorImages_close_Call { | |
| 71 | + | _c.Call.Return() | |
| 72 | + | return _c | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (_c *mockcursorImages_close_Call) RunAndReturn(run func()) *mockcursorImages_close_Call { | |
| 76 | + | _c.Run(run) | |
| 77 | + | return _c | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // fb provides a mock function for the type mockcursorImages | |
| 81 | + | func (_mock *mockcursorImages) fb(i int) uint32 { | |
| 82 | + | ret := _mock.Called(i) | |
| 83 | + | ||
| 84 | + | if len(ret) == 0 { | |
| 85 | + | panic("no return value specified for fb") | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | var r0 uint32 | |
| 89 | + | if returnFunc, ok := ret.Get(0).(func(int) uint32); ok { | |
| 90 | + | r0 = returnFunc(i) | |
| 91 | + | } else { | |
| 92 | + | r0 = ret.Get(0).(uint32) | |
| 93 | + | } | |
| 94 | + | return r0 | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // mockcursorImages_fb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fb' | |
| 98 | + | type mockcursorImages_fb_Call struct { | |
| 99 | + | *mock.Call | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // fb is a helper method to define mock.On call | |
| 103 | + | // - i int | |
| 104 | + | func (_e *mockcursorImages_Expecter) fb(i any) *mockcursorImages_fb_Call { | |
| 105 | + | return &mockcursorImages_fb_Call{Call: _e.mock.On("fb", i)} | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func (_c *mockcursorImages_fb_Call) Run(run func(i int)) *mockcursorImages_fb_Call { | |
| 109 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 110 | + | var arg0 int | |
| 111 | + | if args[0] != nil { | |
| 112 | + | arg0 = args[0].(int) | |
| 113 | + | } | |
| 114 | + | run( | |
| 115 | + | arg0, | |
| 116 | + | ) | |
| 117 | + | }) | |
| 118 | + | return _c | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (_c *mockcursorImages_fb_Call) Return(v uint32) *mockcursorImages_fb_Call { | |
| 122 | + | _c.Call.Return(v) | |
| 123 | + | return _c | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func (_c *mockcursorImages_fb_Call) RunAndReturn(run func(i int) uint32) *mockcursorImages_fb_Call { | |
| 127 | + | _c.Call.Return(run) | |
| 128 | + | return _c | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // write provides a mock function for the type mockcursorImages | |
| 132 | + | func (_mock *mockcursorImages) write(i int, pixels []byte, w int, h int, hdrNits float64) error { | |
| 133 | + | ret := _mock.Called(i, pixels, w, h, hdrNits) | |
| 134 | + | ||
| 135 | + | if len(ret) == 0 { | |
| 136 | + | panic("no return value specified for write") | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | var r0 error | |
| 140 | + | if returnFunc, ok := ret.Get(0).(func(int, []byte, int, int, float64) error); ok { | |
| 141 | + | r0 = returnFunc(i, pixels, w, h, hdrNits) | |
| 142 | + | } else { | |
| 143 | + | r0 = ret.Error(0) | |
| 144 | + | } | |
| 145 | + | return r0 | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // mockcursorImages_write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'write' | |
| 149 | + | type mockcursorImages_write_Call struct { | |
| 150 | + | *mock.Call | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // write is a helper method to define mock.On call | |
| 154 | + | // - i int | |
| 155 | + | // - pixels []byte | |
| 156 | + | // - w int | |
| 157 | + | // - h int | |
| 158 | + | // - hdrNits float64 | |
| 159 | + | func (_e *mockcursorImages_Expecter) write(i any, pixels any, w any, h any, hdrNits any) *mockcursorImages_write_Call { | |
| 160 | + | return &mockcursorImages_write_Call{Call: _e.mock.On("write", i, pixels, w, h, hdrNits)} | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func (_c *mockcursorImages_write_Call) Run(run func(i int, pixels []byte, w int, h int, hdrNits float64)) *mockcursorImages_write_Call { | |
| 164 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 165 | + | var arg0 int | |
| 166 | + | if args[0] != nil { | |
| 167 | + | arg0 = args[0].(int) | |
| 168 | + | } | |
| 169 | + | var arg1 []byte | |
| 170 | + | if args[1] != nil { | |
| 171 | + | arg1 = args[1].([]byte) | |
| 172 | + | } | |
| 173 | + | var arg2 int | |
| 174 | + | if args[2] != nil { | |
| 175 | + | arg2 = args[2].(int) | |
| 176 | + | } | |
| 177 | + | var arg3 int | |
| 178 | + | if args[3] != nil { | |
| 179 | + | arg3 = args[3].(int) | |
| 180 | + | } | |
| 181 | + | var arg4 float64 | |
| 182 | + | if args[4] != nil { | |
| 183 | + | arg4 = args[4].(float64) | |
| 184 | + | } | |
| 185 | + | run( | |
| 186 | + | arg0, | |
| 187 | + | arg1, | |
| 188 | + | arg2, | |
| 189 | + | arg3, | |
| 190 | + | arg4, | |
| 191 | + | ) | |
| 192 | + | }) | |
| 193 | + | return _c | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | func (_c *mockcursorImages_write_Call) Return(err error) *mockcursorImages_write_Call { | |
| 197 | + | _c.Call.Return(err) | |
| 198 | + | return _c | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | func (_c *mockcursorImages_write_Call) RunAndReturn(run func(i int, pixels []byte, w int, h int, hdrNits float64) error) *mockcursorImages_write_Call { | |
| 202 | + | _c.Call.Return(run) | |
| 203 | + | return _c | |
| 204 | + | } | |
| @@ -0,0 +1,204 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package drm | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // newMockcursorImages creates a new instance of mockcursorImages. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 12 | + | // The first argument is typically a *testing.T value. | |||
| 13 | + | func newMockcursorImages(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *mockcursorImages { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &mockcursorImages{} | |||
| 22 | + | mock.Mock.Test(t) | |||
| 23 | + | ||||
| 24 | + | t.Cleanup(func() { | |||
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 26 | + | helper.Helper() | |||
| 27 | + | } | |||
| 28 | + | mock.AssertExpectations(t) | |||
| 29 | + | }) | |||
| 30 | + | ||||
| 31 | + | return mock | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // mockcursorImages is an autogenerated mock type for the cursorImages type | |||
| 35 | + | type mockcursorImages struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type mockcursorImages_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *mockcursorImages) EXPECT() *mockcursorImages_Expecter { | |||
| 44 | + | return &mockcursorImages_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // close provides a mock function for the type mockcursorImages | |||
| 48 | + | func (_mock *mockcursorImages) close() { | |||
| 49 | + | _mock.Called() | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // mockcursorImages_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close' | |||
| 54 | + | type mockcursorImages_close_Call struct { | |||
| 55 | + | *mock.Call | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // close is a helper method to define mock.On call | |||
| 59 | + | func (_e *mockcursorImages_Expecter) close() *mockcursorImages_close_Call { | |||
| 60 | + | return &mockcursorImages_close_Call{Call: _e.mock.On("close")} | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func (_c *mockcursorImages_close_Call) Run(run func()) *mockcursorImages_close_Call { | |||
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 65 | + | run() | |||
| 66 | + | }) | |||
| 67 | + | return _c | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (_c *mockcursorImages_close_Call) Return() *mockcursorImages_close_Call { | |||
| 71 | + | _c.Call.Return() | |||
| 72 | + | return _c | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (_c *mockcursorImages_close_Call) RunAndReturn(run func()) *mockcursorImages_close_Call { | |||
| 76 | + | _c.Run(run) | |||
| 77 | + | return _c | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // fb provides a mock function for the type mockcursorImages | |||
| 81 | + | func (_mock *mockcursorImages) fb(i int) uint32 { | |||
| 82 | + | ret := _mock.Called(i) | |||
| 83 | + | ||||
| 84 | + | if len(ret) == 0 { | |||
| 85 | + | panic("no return value specified for fb") | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | var r0 uint32 | |||
| 89 | + | if returnFunc, ok := ret.Get(0).(func(int) uint32); ok { | |||
| 90 | + | r0 = returnFunc(i) | |||
| 91 | + | } else { | |||
| 92 | + | r0 = ret.Get(0).(uint32) | |||
| 93 | + | } | |||
| 94 | + | return r0 | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // mockcursorImages_fb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fb' | |||
| 98 | + | type mockcursorImages_fb_Call struct { | |||
| 99 | + | *mock.Call | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // fb is a helper method to define mock.On call | |||
| 103 | + | // - i int | |||
| 104 | + | func (_e *mockcursorImages_Expecter) fb(i any) *mockcursorImages_fb_Call { | |||
| 105 | + | return &mockcursorImages_fb_Call{Call: _e.mock.On("fb", i)} | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | func (_c *mockcursorImages_fb_Call) Run(run func(i int)) *mockcursorImages_fb_Call { | |||
| 109 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 110 | + | var arg0 int | |||
| 111 | + | if args[0] != nil { | |||
| 112 | + | arg0 = args[0].(int) | |||
| 113 | + | } | |||
| 114 | + | run( | |||
| 115 | + | arg0, | |||
| 116 | + | ) | |||
| 117 | + | }) | |||
| 118 | + | return _c | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func (_c *mockcursorImages_fb_Call) Return(v uint32) *mockcursorImages_fb_Call { | |||
| 122 | + | _c.Call.Return(v) | |||
| 123 | + | return _c | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func (_c *mockcursorImages_fb_Call) RunAndReturn(run func(i int) uint32) *mockcursorImages_fb_Call { | |||
| 127 | + | _c.Call.Return(run) | |||
| 128 | + | return _c | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // write provides a mock function for the type mockcursorImages | |||
| 132 | + | func (_mock *mockcursorImages) write(i int, pixels []byte, w int, h int, hdrNits float64) error { | |||
| 133 | + | ret := _mock.Called(i, pixels, w, h, hdrNits) | |||
| 134 | + | ||||
| 135 | + | if len(ret) == 0 { | |||
| 136 | + | panic("no return value specified for write") | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | var r0 error | |||
| 140 | + | if returnFunc, ok := ret.Get(0).(func(int, []byte, int, int, float64) error); ok { | |||
| 141 | + | r0 = returnFunc(i, pixels, w, h, hdrNits) | |||
| 142 | + | } else { | |||
| 143 | + | r0 = ret.Error(0) | |||
| 144 | + | } | |||
| 145 | + | return r0 | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | // mockcursorImages_write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'write' | |||
| 149 | + | type mockcursorImages_write_Call struct { | |||
| 150 | + | *mock.Call | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // write is a helper method to define mock.On call | |||
| 154 | + | // - i int | |||
| 155 | + | // - pixels []byte | |||
| 156 | + | // - w int | |||
| 157 | + | // - h int | |||
| 158 | + | // - hdrNits float64 | |||
| 159 | + | func (_e *mockcursorImages_Expecter) write(i any, pixels any, w any, h any, hdrNits any) *mockcursorImages_write_Call { | |||
| 160 | + | return &mockcursorImages_write_Call{Call: _e.mock.On("write", i, pixels, w, h, hdrNits)} | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func (_c *mockcursorImages_write_Call) Run(run func(i int, pixels []byte, w int, h int, hdrNits float64)) *mockcursorImages_write_Call { | |||
| 164 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 165 | + | var arg0 int | |||
| 166 | + | if args[0] != nil { | |||
| 167 | + | arg0 = args[0].(int) | |||
| 168 | + | } | |||
| 169 | + | var arg1 []byte | |||
| 170 | + | if args[1] != nil { | |||
| 171 | + | arg1 = args[1].([]byte) | |||
| 172 | + | } | |||
| 173 | + | var arg2 int | |||
| 174 | + | if args[2] != nil { | |||
| 175 | + | arg2 = args[2].(int) | |||
| 176 | + | } | |||
| 177 | + | var arg3 int | |||
| 178 | + | if args[3] != nil { | |||
| 179 | + | arg3 = args[3].(int) | |||
| 180 | + | } | |||
| 181 | + | var arg4 float64 | |||
| 182 | + | if args[4] != nil { | |||
| 183 | + | arg4 = args[4].(float64) | |||
| 184 | + | } | |||
| 185 | + | run( | |||
| 186 | + | arg0, | |||
| 187 | + | arg1, | |||
| 188 | + | arg2, | |||
| 189 | + | arg3, | |||
| 190 | + | arg4, | |||
| 191 | + | ) | |||
| 192 | + | }) | |||
| 193 | + | return _c | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | func (_c *mockcursorImages_write_Call) Return(err error) *mockcursorImages_write_Call { | |||
| 197 | + | _c.Call.Return(err) | |||
| 198 | + | return _c | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | func (_c *mockcursorImages_write_Call) RunAndReturn(run func(i int, pixels []byte, w int, h int, hdrNits float64) error) *mockcursorImages_write_Call { | |||
| 202 | + | _c.Call.Return(run) | |||
| 203 | + | return _c | |||
| 204 | + | } | |||
A internal/adapters/drm/cursor_images_test.go +333 -0
| @@ -0,0 +1,333 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "errors" | |
| 6 | + | "os" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | const tSide = 16 | |
| 16 | + | ||
| 17 | + | // dumbOK expects two dumb buffers with handles 10 and 11, mappings (with | |
| 18 | + | // pitch bytes per row) returned in mem, and framebuffers 90 and 91. | |
| 19 | + | func dumbOK(k *mockkms, pitch int) (mem [2][]byte) { | |
| 20 | + | for i := range mem { | |
| 21 | + | mem[i] = make([]byte, pitch*tSide) | |
| 22 | + | h := uint32(10 + i) | |
| 23 | + | b := dumbBuffer{handle: h, pitch: uint32(pitch), size: uint64(len(mem[i]))} | |
| 24 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 25 | + | k.EXPECT().mapDumb(b).Return(mem[i], nil).Once() | |
| 26 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90+uint32(i), nil).Once() | |
| 27 | + | } | |
| 28 | + | return mem | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func dumbFree(k *mockkms) { | |
| 32 | + | for i := range 2 { | |
| 33 | + | k.EXPECT().rmFB(90 + uint32(i)).Return(nil).Once() | |
| 34 | + | k.EXPECT().destroyDumb(10 + uint32(i)).Return(nil).Once() | |
| 35 | + | } | |
| 36 | + | k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice() | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func rendererBufs() [2]ports.DMABuf { | |
| 40 | + | var b [2]ports.DMABuf | |
| 41 | + | for i := range b { | |
| 42 | + | f, w, _ := os.Pipe() | |
| 43 | + | w.Close() | |
| 44 | + | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 45 | + | } | |
| 46 | + | return b | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func rendererOK(k *mockkms, r *portsmocks.MockRenderer) { | |
| 50 | + | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | |
| 51 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |
| 52 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // Dumb buffers come first: the renderer allocates nothing. | |
| 56 | + | func TestCursorSetupUsesDumbBuffers(t *testing.T) { | |
| 57 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 58 | + | dumbOK(k, tSide*4) | |
| 59 | + | c := newCursor(&plane{}, tSide) | |
| 60 | + | dumbErr, err := c.setup(k, r) | |
| 61 | + | if dumbErr != nil || err != nil { | |
| 62 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |
| 63 | + | } | |
| 64 | + | if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} { | |
| 65 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |
| 66 | + | } | |
| 67 | + | dumbFree(k) | |
| 68 | + | c.free() | |
| 69 | + | if c.imgs != nil || c.fbs != [2]uint32{} { | |
| 70 | + | t.Fatal("images kept after free") | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // A dumb buffer that cannot be made (or mapped, or wrapped) falls back to | |
| 75 | + | // the renderer's images, and what was made is released. | |
| 76 | + | func TestCursorSetupFallsBackToRenderer(t *testing.T) { | |
| 77 | + | for name, fail := range map[string]func(k *mockkms){ | |
| 78 | + | "create": func(k *mockkms) { | |
| 79 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 80 | + | }, | |
| 81 | + | "map": func(k *mockkms) { | |
| 82 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |
| 83 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 84 | + | k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once() | |
| 85 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |
| 86 | + | }, | |
| 87 | + | "framebuffer": func(k *mockkms) { | |
| 88 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |
| 89 | + | mem := make([]byte, b.size) | |
| 90 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 91 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |
| 92 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |
| 93 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |
| 94 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |
| 95 | + | }, | |
| 96 | + | "second buffer": func(k *mockkms) { | |
| 97 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |
| 98 | + | mem := make([]byte, b.size) | |
| 99 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 100 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |
| 101 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once() | |
| 102 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once() | |
| 103 | + | k.EXPECT().rmFB(uint32(90)).Return(nil).Once() | |
| 104 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |
| 105 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |
| 106 | + | }, | |
| 107 | + | } { | |
| 108 | + | t.Run(name, func(t *testing.T) { | |
| 109 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 110 | + | fail(k) | |
| 111 | + | rendererOK(k, r) | |
| 112 | + | c := newCursor(&plane{}, tSide) | |
| 113 | + | dumbErr, err := c.setup(k, r) | |
| 114 | + | if dumbErr == nil || err != nil { | |
| 115 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |
| 116 | + | } | |
| 117 | + | if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} { | |
| 118 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |
| 119 | + | } | |
| 120 | + | }) | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // Without either allocator there are no images. | |
| 125 | + | func TestCursorSetupFailsWithoutAllocators(t *testing.T) { | |
| 126 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 127 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 128 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once() | |
| 129 | + | c := newCursor(&plane{}, tSide) | |
| 130 | + | dumbErr, err := c.setup(k, r) | |
| 131 | + | if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} { | |
| 132 | + | t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs) | |
| 133 | + | } | |
| 134 | + | if _, err := c.useFallback(0); err != nil { | |
| 135 | + | t.Fatal(err) | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // A renderer image whose framebuffer fails is released. | |
| 140 | + | func TestRendererCursorReleasesOnFailure(t *testing.T) { | |
| 141 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 142 | + | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | |
| 143 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |
| 144 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |
| 145 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 146 | + | if _, err := newRendererCursor(k, r, tSide); err == nil { | |
| 147 | + | t.Fatal("framebuffer failure not reported") | |
| 148 | + | } | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | // cursorOutput is a test output whose cursor plane has size tSide. | |
| 152 | + | func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) { | |
| 153 | + | o, k, commits := testOutput(t, errs...) | |
| 154 | + | o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide) | |
| 155 | + | return o, k, portsmocks.NewMockRenderer(t), commits | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | // The plane refuses the dumb images: the renderer's are tried (TEST_ONLY), | |
| 159 | + | // the image on the cursor is rewritten into them, and writes go there. | |
| 160 | + | func TestCursorDumbRefusedUsesRendererImages(t *testing.T) { | |
| 161 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 162 | + | dumbOK(k, tSide*4) | |
| 163 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 164 | + | t.Fatal(err) | |
| 165 | + | } | |
| 166 | + | px := []byte{1, 2, 3, 4} | |
| 167 | + | if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil { | |
| 168 | + | t.Fatal(err) | |
| 169 | + | } | |
| 170 | + | dumbFree(k) | |
| 171 | + | rendererOK(k, r) | |
| 172 | + | var written []int | |
| 173 | + | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |
| 174 | + | written = append(written, i) | |
| 175 | + | return nil | |
| 176 | + | }) | |
| 177 | + | o.testCursor() | |
| 178 | + | c := o.cursor | |
| 179 | + | if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 { | |
| 180 | + | t.Fatalf("off %v alloc %q fbs %v image %v hot %d,%d", c.off, c.alloc, c.fbs, c.image, c.hotX, c.hotY) | |
| 181 | + | } | |
| 182 | + | if len(written) != 1 || len(*commits) != 2 { | |
| 183 | + | t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits)) | |
| 184 | + | } | |
| 185 | + | if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly { | |
| 186 | + | t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags) | |
| 187 | + | } | |
| 188 | + | // Further changes are written by the renderer, not into the freed buffers. | |
| 189 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 { | |
| 190 | + | t.Fatalf("write after fallback: %v %v", err, written) | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // Both kinds refused: the software cursor takes over. | |
| 195 | + | func TestCursorBothRefusedGoesOff(t *testing.T) { | |
| 196 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL) | |
| 197 | + | dumbOK(k, tSide*4) | |
| 198 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 199 | + | t.Fatal(err) | |
| 200 | + | } | |
| 201 | + | dumbFree(k) | |
| 202 | + | rendererOK(k, r) | |
| 203 | + | if !o.testCursor() { | |
| 204 | + | t.Fatal("no change reported") | |
| 205 | + | } | |
| 206 | + | if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 { | |
| 207 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |
| 208 | + | } | |
| 209 | + | if o.cursor.desired().on { | |
| 210 | + | t.Fatal("cursor shown while off") | |
| 211 | + | } | |
| 212 | + | // The renderer's images are released with the output. | |
| 213 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 214 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |
| 215 | + | o.cursor.free() | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // Dumb images the plane takes stay, with no second test. | |
| 219 | + | func TestCursorDumbAcceptedStays(t *testing.T) { | |
| 220 | + | o, k, r, commits := cursorOutput(t) | |
| 221 | + | dumbOK(k, tSide*4) | |
| 222 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 223 | + | t.Fatal(err) | |
| 224 | + | } | |
| 225 | + | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |
| 226 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |
| 227 | + | } | |
| 228 | + | // An error other than a refusal changes nothing. | |
| 229 | + | dumbFree(k) | |
| 230 | + | o.cursor.free() | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | // Renderer images refused while no dumb ones exist: off, no retry. | |
| 234 | + | func TestCursorRendererRefusedGoesOff(t *testing.T) { | |
| 235 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 236 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 237 | + | rendererOK(k, r) | |
| 238 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 239 | + | t.Fatal(err) | |
| 240 | + | } | |
| 241 | + | if !o.testCursor() || !o.cursor.off || len(*commits) != 1 { | |
| 242 | + | t.Fatalf("off %v commits %d", o.cursor.off, len(*commits)) | |
| 243 | + | } | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | // Writes go to the images that own the cursor, and the output's HDR white | |
| 247 | + | // is passed along. | |
| 248 | + | func TestCursorWritesGoToCurrentImages(t *testing.T) { | |
| 249 | + | o, _, _, _ := cursorOutput(t) | |
| 250 | + | imgs := newMockcursorImages(t) | |
| 251 | + | imgs.EXPECT().fb(0).Return(90).Once() | |
| 252 | + | imgs.EXPECT().fb(1).Return(91).Once() | |
| 253 | + | o.cursor.install(imgs, allocDumb) | |
| 254 | + | px := []byte{1, 2, 3, 4} | |
| 255 | + | imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once() | |
| 256 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil { | |
| 257 | + | t.Fatal(err) | |
| 258 | + | } | |
| 259 | + | o.hdr.on, o.hdr.settings.SDRBrightness = true, 203 | |
| 260 | + | if got := o.cursorNits(); got != 203 { | |
| 261 | + | t.Fatalf("HDR white %v", got) | |
| 262 | + | } | |
| 263 | + | o.cursor.screen = 90 | |
| 264 | + | imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once() | |
| 265 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil { | |
| 266 | + | t.Fatal("write error swallowed") | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // Dumb pitch may exceed the row size: rows land at the pitch, the image is | |
| 271 | + | // cropped to the side and cleared around. | |
| 272 | + | func TestDumbCursorWritePitchCropClear(t *testing.T) { | |
| 273 | + | k := newMockkms(t) | |
| 274 | + | const pitch = tSide*4 + 32 | |
| 275 | + | mem := dumbOK(k, pitch) | |
| 276 | + | d, err := newDumbCursor(k, tSide) | |
| 277 | + | if err != nil { | |
| 278 | + | t.Fatal(err) | |
| 279 | + | } | |
| 280 | + | defer func() { dumbFree(k); d.close() }() | |
| 281 | + | big := bytes.Repeat([]byte{0xff}, 40*40*4) | |
| 282 | + | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | |
| 283 | + | t.Fatal(err) | |
| 284 | + | } | |
| 285 | + | last := (tSide - 1) * pitch | |
| 286 | + | if m := mem[1]; m[last+(tSide-1)*4] != 0xff || m[last+tSide*4] != 0 || m[pitch-1] != 0 { | |
| 287 | + | t.Fatal("cropped image misplaced at the pitch") | |
| 288 | + | } | |
| 289 | + | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | |
| 290 | + | t.Fatal(err) | |
| 291 | + | } | |
| 292 | + | m := mem[1] | |
| 293 | + | if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 { | |
| 294 | + | t.Fatal("image not cleared around the new pixels") | |
| 295 | + | } | |
| 296 | + | // A wide, short image keeps its rows apart. | |
| 297 | + | row := []byte{9, 9, 9, 9, 8, 8, 8, 8} | |
| 298 | + | if err := d.write(0, append(row, row...), 2, 2, 0); err != nil { | |
| 299 | + | t.Fatal(err) | |
| 300 | + | } | |
| 301 | + | if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 { | |
| 302 | + | t.Fatal("rows of a narrow image") | |
| 303 | + | } | |
| 304 | + | if err := d.write(0, []byte{1}, 1, 1, 0); err == nil { | |
| 305 | + | t.Fatal("short image accepted") | |
| 306 | + | } | |
| 307 | + | if err := d.write(2, nil, 0, 0, 0); err == nil { | |
| 308 | + | t.Fatal("bad index accepted") | |
| 309 | + | } | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | // HDR cursors convert exactly as the renderer did (see hdrcursor's vector). | |
| 313 | + | func TestDumbCursorWriteHDR(t *testing.T) { | |
| 314 | + | k := newMockkms(t) | |
| 315 | + | mem := dumbOK(k, tSide*4) | |
| 316 | + | d, err := newDumbCursor(k, tSide) | |
| 317 | + | if err != nil { | |
| 318 | + | t.Fatal(err) | |
| 319 | + | } | |
| 320 | + | defer func() { dumbFree(k); d.close() }() | |
| 321 | + | if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil { | |
| 322 | + | t.Fatal(err) | |
| 323 | + | } | |
| 324 | + | if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 { | |
| 325 | + | t.Fatalf("HDR white: %v", m[:4]) | |
| 326 | + | } | |
| 327 | + | if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 { | |
| 328 | + | t.Fatalf("SDR white: %v %v", err, mem[1][:4]) | |
| 329 | + | } | |
| 330 | + | if d.cache.Len() != 4 { | |
| 331 | + | t.Fatalf("cache %d", d.cache.Len()) | |
| 332 | + | } | |
| 333 | + | } | |
| @@ -0,0 +1,333 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | const tSide = 16 | |||
| 16 | + | ||||
| 17 | + | // dumbOK expects two dumb buffers with handles 10 and 11, mappings (with | |||
| 18 | + | // pitch bytes per row) returned in mem, and framebuffers 90 and 91. | |||
| 19 | + | func dumbOK(k *mockkms, pitch int) (mem [2][]byte) { | |||
| 20 | + | for i := range mem { | |||
| 21 | + | mem[i] = make([]byte, pitch*tSide) | |||
| 22 | + | h := uint32(10 + i) | |||
| 23 | + | b := dumbBuffer{handle: h, pitch: uint32(pitch), size: uint64(len(mem[i]))} | |||
| 24 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 25 | + | k.EXPECT().mapDumb(b).Return(mem[i], nil).Once() | |||
| 26 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90+uint32(i), nil).Once() | |||
| 27 | + | } | |||
| 28 | + | return mem | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func dumbFree(k *mockkms) { | |||
| 32 | + | for i := range 2 { | |||
| 33 | + | k.EXPECT().rmFB(90 + uint32(i)).Return(nil).Once() | |||
| 34 | + | k.EXPECT().destroyDumb(10 + uint32(i)).Return(nil).Once() | |||
| 35 | + | } | |||
| 36 | + | k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice() | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func rendererBufs() [2]ports.DMABuf { | |||
| 40 | + | var b [2]ports.DMABuf | |||
| 41 | + | for i := range b { | |||
| 42 | + | f, w, _ := os.Pipe() | |||
| 43 | + | w.Close() | |||
| 44 | + | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 45 | + | } | |||
| 46 | + | return b | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func rendererOK(k *mockkms, r *portsmocks.MockRenderer) { | |||
| 50 | + | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | |||
| 51 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |||
| 52 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // Dumb buffers come first: the renderer allocates nothing. | |||
| 56 | + | func TestCursorSetupUsesDumbBuffers(t *testing.T) { | |||
| 57 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 58 | + | dumbOK(k, tSide*4) | |||
| 59 | + | c := newCursor(&plane{}, tSide) | |||
| 60 | + | dumbErr, err := c.setup(k, r) | |||
| 61 | + | if dumbErr != nil || err != nil { | |||
| 62 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |||
| 63 | + | } | |||
| 64 | + | if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} { | |||
| 65 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |||
| 66 | + | } | |||
| 67 | + | dumbFree(k) | |||
| 68 | + | c.free() | |||
| 69 | + | if c.imgs != nil || c.fbs != [2]uint32{} { | |||
| 70 | + | t.Fatal("images kept after free") | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // A dumb buffer that cannot be made (or mapped, or wrapped) falls back to | |||
| 75 | + | // the renderer's images, and what was made is released. | |||
| 76 | + | func TestCursorSetupFallsBackToRenderer(t *testing.T) { | |||
| 77 | + | for name, fail := range map[string]func(k *mockkms){ | |||
| 78 | + | "create": func(k *mockkms) { | |||
| 79 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |||
| 80 | + | }, | |||
| 81 | + | "map": func(k *mockkms) { | |||
| 82 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |||
| 83 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 84 | + | k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once() | |||
| 85 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |||
| 86 | + | }, | |||
| 87 | + | "framebuffer": func(k *mockkms) { | |||
| 88 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |||
| 89 | + | mem := make([]byte, b.size) | |||
| 90 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 91 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |||
| 92 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |||
| 93 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |||
| 94 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |||
| 95 | + | }, | |||
| 96 | + | "second buffer": func(k *mockkms) { | |||
| 97 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |||
| 98 | + | mem := make([]byte, b.size) | |||
| 99 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 100 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |||
| 101 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once() | |||
| 102 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once() | |||
| 103 | + | k.EXPECT().rmFB(uint32(90)).Return(nil).Once() | |||
| 104 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |||
| 105 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |||
| 106 | + | }, | |||
| 107 | + | } { | |||
| 108 | + | t.Run(name, func(t *testing.T) { | |||
| 109 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 110 | + | fail(k) | |||
| 111 | + | rendererOK(k, r) | |||
| 112 | + | c := newCursor(&plane{}, tSide) | |||
| 113 | + | dumbErr, err := c.setup(k, r) | |||
| 114 | + | if dumbErr == nil || err != nil { | |||
| 115 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |||
| 116 | + | } | |||
| 117 | + | if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} { | |||
| 118 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |||
| 119 | + | } | |||
| 120 | + | }) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // Without either allocator there are no images. | |||
| 125 | + | func TestCursorSetupFailsWithoutAllocators(t *testing.T) { | |||
| 126 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 127 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |||
| 128 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once() | |||
| 129 | + | c := newCursor(&plane{}, tSide) | |||
| 130 | + | dumbErr, err := c.setup(k, r) | |||
| 131 | + | if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} { | |||
| 132 | + | t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs) | |||
| 133 | + | } | |||
| 134 | + | if _, err := c.useFallback(0); err != nil { | |||
| 135 | + | t.Fatal(err) | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | // A renderer image whose framebuffer fails is released. | |||
| 140 | + | func TestRendererCursorReleasesOnFailure(t *testing.T) { | |||
| 141 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 142 | + | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | |||
| 143 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |||
| 144 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |||
| 145 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |||
| 146 | + | if _, err := newRendererCursor(k, r, tSide); err == nil { | |||
| 147 | + | t.Fatal("framebuffer failure not reported") | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // cursorOutput is a test output whose cursor plane has size tSide. | |||
| 152 | + | func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) { | |||
| 153 | + | o, k, commits := testOutput(t, errs...) | |||
| 154 | + | o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide) | |||
| 155 | + | return o, k, portsmocks.NewMockRenderer(t), commits | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // The plane refuses the dumb images: the renderer's are tried (TEST_ONLY), | |||
| 159 | + | // the image on the cursor is rewritten into them, and writes go there. | |||
| 160 | + | func TestCursorDumbRefusedUsesRendererImages(t *testing.T) { | |||
| 161 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 162 | + | dumbOK(k, tSide*4) | |||
| 163 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 164 | + | t.Fatal(err) | |||
| 165 | + | } | |||
| 166 | + | px := []byte{1, 2, 3, 4} | |||
| 167 | + | if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil { | |||
| 168 | + | t.Fatal(err) | |||
| 169 | + | } | |||
| 170 | + | dumbFree(k) | |||
| 171 | + | rendererOK(k, r) | |||
| 172 | + | var written []int | |||
| 173 | + | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |||
| 174 | + | written = append(written, i) | |||
| 175 | + | return nil | |||
| 176 | + | }) | |||
| 177 | + | o.testCursor() | |||
| 178 | + | c := o.cursor | |||
| 179 | + | if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 { | |||
| 180 | + | t.Fatalf("off %v alloc %q fbs %v image %v hot %d,%d", c.off, c.alloc, c.fbs, c.image, c.hotX, c.hotY) | |||
| 181 | + | } | |||
| 182 | + | if len(written) != 1 || len(*commits) != 2 { | |||
| 183 | + | t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits)) | |||
| 184 | + | } | |||
| 185 | + | if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly { | |||
| 186 | + | t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags) | |||
| 187 | + | } | |||
| 188 | + | // Further changes are written by the renderer, not into the freed buffers. | |||
| 189 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 { | |||
| 190 | + | t.Fatalf("write after fallback: %v %v", err, written) | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // Both kinds refused: the software cursor takes over. | |||
| 195 | + | func TestCursorBothRefusedGoesOff(t *testing.T) { | |||
| 196 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 197 | + | dumbOK(k, tSide*4) | |||
| 198 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 199 | + | t.Fatal(err) | |||
| 200 | + | } | |||
| 201 | + | dumbFree(k) | |||
| 202 | + | rendererOK(k, r) | |||
| 203 | + | if !o.testCursor() { | |||
| 204 | + | t.Fatal("no change reported") | |||
| 205 | + | } | |||
| 206 | + | if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 { | |||
| 207 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |||
| 208 | + | } | |||
| 209 | + | if o.cursor.desired().on { | |||
| 210 | + | t.Fatal("cursor shown while off") | |||
| 211 | + | } | |||
| 212 | + | // The renderer's images are released with the output. | |||
| 213 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |||
| 214 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |||
| 215 | + | o.cursor.free() | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // Dumb images the plane takes stay, with no second test. | |||
| 219 | + | func TestCursorDumbAcceptedStays(t *testing.T) { | |||
| 220 | + | o, k, r, commits := cursorOutput(t) | |||
| 221 | + | dumbOK(k, tSide*4) | |||
| 222 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 223 | + | t.Fatal(err) | |||
| 224 | + | } | |||
| 225 | + | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |||
| 226 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |||
| 227 | + | } | |||
| 228 | + | // An error other than a refusal changes nothing. | |||
| 229 | + | dumbFree(k) | |||
| 230 | + | o.cursor.free() | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | // Renderer images refused while no dumb ones exist: off, no retry. | |||
| 234 | + | func TestCursorRendererRefusedGoesOff(t *testing.T) { | |||
| 235 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 236 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |||
| 237 | + | rendererOK(k, r) | |||
| 238 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 239 | + | t.Fatal(err) | |||
| 240 | + | } | |||
| 241 | + | if !o.testCursor() || !o.cursor.off || len(*commits) != 1 { | |||
| 242 | + | t.Fatalf("off %v commits %d", o.cursor.off, len(*commits)) | |||
| 243 | + | } | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // Writes go to the images that own the cursor, and the output's HDR white | |||
| 247 | + | // is passed along. | |||
| 248 | + | func TestCursorWritesGoToCurrentImages(t *testing.T) { | |||
| 249 | + | o, _, _, _ := cursorOutput(t) | |||
| 250 | + | imgs := newMockcursorImages(t) | |||
| 251 | + | imgs.EXPECT().fb(0).Return(90).Once() | |||
| 252 | + | imgs.EXPECT().fb(1).Return(91).Once() | |||
| 253 | + | o.cursor.install(imgs, allocDumb) | |||
| 254 | + | px := []byte{1, 2, 3, 4} | |||
| 255 | + | imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once() | |||
| 256 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil { | |||
| 257 | + | t.Fatal(err) | |||
| 258 | + | } | |||
| 259 | + | o.hdr.on, o.hdr.settings.SDRBrightness = true, 203 | |||
| 260 | + | if got := o.cursorNits(); got != 203 { | |||
| 261 | + | t.Fatalf("HDR white %v", got) | |||
| 262 | + | } | |||
| 263 | + | o.cursor.screen = 90 | |||
| 264 | + | imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once() | |||
| 265 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil { | |||
| 266 | + | t.Fatal("write error swallowed") | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // Dumb pitch may exceed the row size: rows land at the pitch, the image is | |||
| 271 | + | // cropped to the side and cleared around. | |||
| 272 | + | func TestDumbCursorWritePitchCropClear(t *testing.T) { | |||
| 273 | + | k := newMockkms(t) | |||
| 274 | + | const pitch = tSide*4 + 32 | |||
| 275 | + | mem := dumbOK(k, pitch) | |||
| 276 | + | d, err := newDumbCursor(k, tSide) | |||
| 277 | + | if err != nil { | |||
| 278 | + | t.Fatal(err) | |||
| 279 | + | } | |||
| 280 | + | defer func() { dumbFree(k); d.close() }() | |||
| 281 | + | big := bytes.Repeat([]byte{0xff}, 40*40*4) | |||
| 282 | + | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | |||
| 283 | + | t.Fatal(err) | |||
| 284 | + | } | |||
| 285 | + | last := (tSide - 1) * pitch | |||
| 286 | + | if m := mem[1]; m[last+(tSide-1)*4] != 0xff || m[last+tSide*4] != 0 || m[pitch-1] != 0 { | |||
| 287 | + | t.Fatal("cropped image misplaced at the pitch") | |||
| 288 | + | } | |||
| 289 | + | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | |||
| 290 | + | t.Fatal(err) | |||
| 291 | + | } | |||
| 292 | + | m := mem[1] | |||
| 293 | + | if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 { | |||
| 294 | + | t.Fatal("image not cleared around the new pixels") | |||
| 295 | + | } | |||
| 296 | + | // A wide, short image keeps its rows apart. | |||
| 297 | + | row := []byte{9, 9, 9, 9, 8, 8, 8, 8} | |||
| 298 | + | if err := d.write(0, append(row, row...), 2, 2, 0); err != nil { | |||
| 299 | + | t.Fatal(err) | |||
| 300 | + | } | |||
| 301 | + | if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 { | |||
| 302 | + | t.Fatal("rows of a narrow image") | |||
| 303 | + | } | |||
| 304 | + | if err := d.write(0, []byte{1}, 1, 1, 0); err == nil { | |||
| 305 | + | t.Fatal("short image accepted") | |||
| 306 | + | } | |||
| 307 | + | if err := d.write(2, nil, 0, 0, 0); err == nil { | |||
| 308 | + | t.Fatal("bad index accepted") | |||
| 309 | + | } | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | // HDR cursors convert exactly as the renderer did (see hdrcursor's vector). | |||
| 313 | + | func TestDumbCursorWriteHDR(t *testing.T) { | |||
| 314 | + | k := newMockkms(t) | |||
| 315 | + | mem := dumbOK(k, tSide*4) | |||
| 316 | + | d, err := newDumbCursor(k, tSide) | |||
| 317 | + | if err != nil { | |||
| 318 | + | t.Fatal(err) | |||
| 319 | + | } | |||
| 320 | + | defer func() { dumbFree(k); d.close() }() | |||
| 321 | + | if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil { | |||
| 322 | + | t.Fatal(err) | |||
| 323 | + | } | |||
| 324 | + | if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 { | |||
| 325 | + | t.Fatalf("HDR white: %v", m[:4]) | |||
| 326 | + | } | |||
| 327 | + | if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 { | |||
| 328 | + | t.Fatalf("SDR white: %v %v", err, mem[1][:4]) | |||
| 329 | + | } | |||
| 330 | + | if d.cache.Len() != 4 { | |||
| 331 | + | t.Fatalf("cache %d", d.cache.Len()) | |||
| 332 | + | } | |||
| 333 | + | } | |||
A internal/adapters/drm/dumb.go +77 -0
| @@ -0,0 +1,77 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "fmt" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // KMS dumb buffers: the driver allocates a linear buffer the CPU maps and | |
| 12 | + | // the display scans out. Cursor images use them (see cursor_images.go). | |
| 13 | + | ||
| 14 | + | const ( | |
| 15 | + | ioctlCreateDumb = 0xC02064B2 // DRM_IOWR('d', 0xB2, struct drm_mode_create_dumb) | |
| 16 | + | ioctlMapDumb = 0xC01064B3 // DRM_IOWR('d', 0xB3, struct drm_mode_map_dumb) | |
| 17 | + | ioctlDestroyDumb = 0xC00464B4 // DRM_IOWR('d', 0xB4, struct drm_mode_destroy_dumb) | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | type createDumb struct { | |
| 21 | + | height, width, bpp, flags uint32 | |
| 22 | + | handle, pitch uint32 | |
| 23 | + | size uint64 | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | type mapDumb struct { | |
| 27 | + | handle, pad uint32 | |
| 28 | + | offset uint64 | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // dumbBuffer is a dumb buffer's GEM handle, row pitch and size in bytes. | |
| 32 | + | type dumbBuffer struct { | |
| 33 | + | handle, pitch uint32 | |
| 34 | + | size uint64 | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (k kmsDevice) createDumb(width, height int) (dumbBuffer, error) { | |
| 38 | + | if width <= 0 || height <= 0 { | |
| 39 | + | return dumbBuffer{}, fmt.Errorf("dumb buffer size %dx%d", width, height) | |
| 40 | + | } | |
| 41 | + | c := createDumb{height: uint32(height), width: uint32(width), bpp: 32} | |
| 42 | + | if err := ioctl(k.fd, ioctlCreateDumb, unsafe.Pointer(&c)); err != nil { | |
| 43 | + | return dumbBuffer{}, err | |
| 44 | + | } | |
| 45 | + | return dumbBuffer{handle: c.handle, pitch: c.pitch, size: c.size}, nil | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func (k kmsDevice) mapDumb(b dumbBuffer) ([]byte, error) { | |
| 49 | + | m := mapDumb{handle: b.handle} | |
| 50 | + | if err := ioctl(k.fd, ioctlMapDumb, unsafe.Pointer(&m)); err != nil { | |
| 51 | + | return nil, err | |
| 52 | + | } | |
| 53 | + | if b.size == 0 || b.size > 1<<30 { | |
| 54 | + | return nil, fmt.Errorf("dumb buffer size %d", b.size) | |
| 55 | + | } | |
| 56 | + | return unix.Mmap(k.fd, int64(m.offset), int(b.size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func (k kmsDevice) unmapDumb(mem []byte) error { return unix.Munmap(mem) } | |
| 60 | + | ||
| 61 | + | // addFBHandle makes a framebuffer of format over the dumb buffer, without | |
| 62 | + | // modifier (the driver's linear layout). | |
| 63 | + | func (k kmsDevice) addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error) { | |
| 64 | + | if b.handle == 0 { | |
| 65 | + | return 0, errors.New("no dumb buffer") | |
| 66 | + | } | |
| 67 | + | cmd := fbCmd2{width: uint32(width), height: uint32(height), format: format} | |
| 68 | + | cmd.handles[0], cmd.pitches[0] = b.handle, b.pitch | |
| 69 | + | if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |
| 70 | + | return 0, errors.Join(errors.New("add fb2"), err) | |
| 71 | + | } | |
| 72 | + | return cmd.fbID, nil | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (k kmsDevice) destroyDumb(handle uint32) error { | |
| 76 | + | return ioctl(k.fd, ioctlDestroyDumb, unsafe.Pointer(&handle)) | |
| 77 | + | } | |
| @@ -0,0 +1,77 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "fmt" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // KMS dumb buffers: the driver allocates a linear buffer the CPU maps and | |||
| 12 | + | // the display scans out. Cursor images use them (see cursor_images.go). | |||
| 13 | + | ||||
| 14 | + | const ( | |||
| 15 | + | ioctlCreateDumb = 0xC02064B2 // DRM_IOWR('d', 0xB2, struct drm_mode_create_dumb) | |||
| 16 | + | ioctlMapDumb = 0xC01064B3 // DRM_IOWR('d', 0xB3, struct drm_mode_map_dumb) | |||
| 17 | + | ioctlDestroyDumb = 0xC00464B4 // DRM_IOWR('d', 0xB4, struct drm_mode_destroy_dumb) | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | type createDumb struct { | |||
| 21 | + | height, width, bpp, flags uint32 | |||
| 22 | + | handle, pitch uint32 | |||
| 23 | + | size uint64 | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | type mapDumb struct { | |||
| 27 | + | handle, pad uint32 | |||
| 28 | + | offset uint64 | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // dumbBuffer is a dumb buffer's GEM handle, row pitch and size in bytes. | |||
| 32 | + | type dumbBuffer struct { | |||
| 33 | + | handle, pitch uint32 | |||
| 34 | + | size uint64 | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func (k kmsDevice) createDumb(width, height int) (dumbBuffer, error) { | |||
| 38 | + | if width <= 0 || height <= 0 { | |||
| 39 | + | return dumbBuffer{}, fmt.Errorf("dumb buffer size %dx%d", width, height) | |||
| 40 | + | } | |||
| 41 | + | c := createDumb{height: uint32(height), width: uint32(width), bpp: 32} | |||
| 42 | + | if err := ioctl(k.fd, ioctlCreateDumb, unsafe.Pointer(&c)); err != nil { | |||
| 43 | + | return dumbBuffer{}, err | |||
| 44 | + | } | |||
| 45 | + | return dumbBuffer{handle: c.handle, pitch: c.pitch, size: c.size}, nil | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func (k kmsDevice) mapDumb(b dumbBuffer) ([]byte, error) { | |||
| 49 | + | m := mapDumb{handle: b.handle} | |||
| 50 | + | if err := ioctl(k.fd, ioctlMapDumb, unsafe.Pointer(&m)); err != nil { | |||
| 51 | + | return nil, err | |||
| 52 | + | } | |||
| 53 | + | if b.size == 0 || b.size > 1<<30 { | |||
| 54 | + | return nil, fmt.Errorf("dumb buffer size %d", b.size) | |||
| 55 | + | } | |||
| 56 | + | return unix.Mmap(k.fd, int64(m.offset), int(b.size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func (k kmsDevice) unmapDumb(mem []byte) error { return unix.Munmap(mem) } | |||
| 60 | + | ||||
| 61 | + | // addFBHandle makes a framebuffer of format over the dumb buffer, without | |||
| 62 | + | // modifier (the driver's linear layout). | |||
| 63 | + | func (k kmsDevice) addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error) { | |||
| 64 | + | if b.handle == 0 { | |||
| 65 | + | return 0, errors.New("no dumb buffer") | |||
| 66 | + | } | |||
| 67 | + | cmd := fbCmd2{width: uint32(width), height: uint32(height), format: format} | |||
| 68 | + | cmd.handles[0], cmd.pitches[0] = b.handle, b.pitch | |||
| 69 | + | if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |||
| 70 | + | return 0, errors.Join(errors.New("add fb2"), err) | |||
| 71 | + | } | |||
| 72 | + | return cmd.fbID, nil | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (k kmsDevice) destroyDumb(handle uint32) error { | |||
| 76 | + | return ioctl(k.fd, ioctlDestroyDumb, unsafe.Pointer(&handle)) | |||
| 77 | + | } | |||
A internal/adapters/drm/dumb_device_test.go +109 -0
| @@ -0,0 +1,109 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "errors" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "golang.org/x/sys/unix" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Real-device check of the dumb buffer layer: no modeset, no atomic commit. | |
| 14 | + | // It skips when no card node can be opened or makes dumb buffers. | |
| 15 | + | func TestDumbBuffersOnRealDevice(t *testing.T) { | |
| 16 | + | cards, _ := filepath.Glob("/dev/dri/card*") | |
| 17 | + | if len(cards) == 0 { | |
| 18 | + | t.Skip("no /dev/dri/card* node") | |
| 19 | + | } | |
| 20 | + | for _, path := range cards { | |
| 21 | + | f, err := os.OpenFile(path, os.O_RDWR|unix.O_CLOEXEC, 0) | |
| 22 | + | if err != nil { | |
| 23 | + | t.Logf("%s: %v", path, err) | |
| 24 | + | continue | |
| 25 | + | } | |
| 26 | + | done := dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |
| 27 | + | f.Close() | |
| 28 | + | if done { | |
| 29 | + | return | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | t.Skip("no card node makes dumb buffers for this user") | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // dumbDeviceRoundTrip runs the buffer life cycle on k and reports false | |
| 36 | + | // when the card cannot do it for a reason unrelated to the code under test | |
| 37 | + | // (the permission or the driver). | |
| 38 | + | func dumbDeviceRoundTrip(t *testing.T, path string, k kmsDevice) bool { | |
| 39 | + | size := cursorSize(k.fd) | |
| 40 | + | var imgs [2]dumbBuffer | |
| 41 | + | var mems [2][]byte | |
| 42 | + | var fbs [2]uint32 | |
| 43 | + | cleanup := func() { | |
| 44 | + | for i := range imgs { | |
| 45 | + | if fbs[i] != 0 { | |
| 46 | + | if err := k.rmFB(fbs[i]); err != nil { | |
| 47 | + | t.Errorf("rmfb: %v", err) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | if mems[i] != nil { | |
| 51 | + | if err := k.unmapDumb(mems[i]); err != nil { | |
| 52 | + | t.Errorf("munmap: %v", err) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | if imgs[i].handle != 0 { | |
| 56 | + | if err := k.destroyDumb(imgs[i].handle); err != nil { | |
| 57 | + | t.Errorf("destroy dumb: %v", err) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | defer cleanup() | |
| 63 | + | for i := range imgs { | |
| 64 | + | b, err := k.createDumb(size, size) | |
| 65 | + | if err != nil { | |
| 66 | + | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.ENOSYS) || errors.Is(err, unix.EINVAL) { | |
| 67 | + | t.Logf("%s: create dumb: %v", path, err) | |
| 68 | + | return false | |
| 69 | + | } | |
| 70 | + | t.Fatalf("%s: create dumb: %v", path, err) | |
| 71 | + | } | |
| 72 | + | imgs[i] = b | |
| 73 | + | if int(b.pitch) < size*4 || b.size < uint64(int(b.pitch)*size) { | |
| 74 | + | t.Fatalf("%s: dumb %dx%d pitch %d size %d", path, size, size, b.pitch, b.size) | |
| 75 | + | } | |
| 76 | + | if mems[i], err = k.mapDumb(b); err != nil { | |
| 77 | + | t.Fatalf("%s: map dumb: %v", path, err) | |
| 78 | + | } | |
| 79 | + | if fbs[i], err = k.addFBHandle(b, size, size, fourccARGB); err != nil { | |
| 80 | + | t.Fatalf("%s: add fb2 ARGB8888: %v", path, err) | |
| 81 | + | } | |
| 82 | + | t.Logf("%s: dumb buffer %d: %dx%d pitch %d size %d fb %d", path, i, size, size, b.pitch, b.size, fbs[i]) | |
| 83 | + | } | |
| 84 | + | // Write a pattern through the shared mapping; each buffer keeps its own. | |
| 85 | + | for i, mem := range mems { | |
| 86 | + | d := &dumbCursor{k: k, size: size} | |
| 87 | + | d.img[i] = dumbImage{pitch: int(imgs[i].pitch), mem: mem} | |
| 88 | + | px := bytes.Repeat([]byte{byte(0x10 + i), 0x20, 0x30, 0xff}, 3*2) | |
| 89 | + | if err := d.write(i, px, 3, 2, 0); err != nil { | |
| 90 | + | t.Fatal(err) | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | for i, mem := range mems { | |
| 94 | + | pitch := int(imgs[i].pitch) | |
| 95 | + | want := []byte{byte(0x10 + i), 0x20, 0x30, 0xff} | |
| 96 | + | for y := range 2 { | |
| 97 | + | if got := mem[y*pitch : y*pitch+4]; !bytes.Equal(got, want) { | |
| 98 | + | t.Fatalf("%s: buffer %d row %d read back %v, want %v", path, i, y, got, want) | |
| 99 | + | } | |
| 100 | + | if got := mem[y*pitch+3*4]; got != 0 { | |
| 101 | + | t.Fatalf("%s: buffer %d row %d not cleared after the image", path, i, y) | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | if mem[2*pitch] != 0 || mem[(size-1)*pitch+(size-1)*4] != 0 { | |
| 105 | + | t.Fatalf("%s: buffer %d not cleared below the image", path, i) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | return true | |
| 109 | + | } | |
| @@ -0,0 +1,109 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "errors" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Real-device check of the dumb buffer layer: no modeset, no atomic commit. | |||
| 14 | + | // It skips when no card node can be opened or makes dumb buffers. | |||
| 15 | + | func TestDumbBuffersOnRealDevice(t *testing.T) { | |||
| 16 | + | cards, _ := filepath.Glob("/dev/dri/card*") | |||
| 17 | + | if len(cards) == 0 { | |||
| 18 | + | t.Skip("no /dev/dri/card* node") | |||
| 19 | + | } | |||
| 20 | + | for _, path := range cards { | |||
| 21 | + | f, err := os.OpenFile(path, os.O_RDWR|unix.O_CLOEXEC, 0) | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Logf("%s: %v", path, err) | |||
| 24 | + | continue | |||
| 25 | + | } | |||
| 26 | + | done := dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |||
| 27 | + | f.Close() | |||
| 28 | + | if done { | |||
| 29 | + | return | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | t.Skip("no card node makes dumb buffers for this user") | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // dumbDeviceRoundTrip runs the buffer life cycle on k and reports false | |||
| 36 | + | // when the card cannot do it for a reason unrelated to the code under test | |||
| 37 | + | // (the permission or the driver). | |||
| 38 | + | func dumbDeviceRoundTrip(t *testing.T, path string, k kmsDevice) bool { | |||
| 39 | + | size := cursorSize(k.fd) | |||
| 40 | + | var imgs [2]dumbBuffer | |||
| 41 | + | var mems [2][]byte | |||
| 42 | + | var fbs [2]uint32 | |||
| 43 | + | cleanup := func() { | |||
| 44 | + | for i := range imgs { | |||
| 45 | + | if fbs[i] != 0 { | |||
| 46 | + | if err := k.rmFB(fbs[i]); err != nil { | |||
| 47 | + | t.Errorf("rmfb: %v", err) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | if mems[i] != nil { | |||
| 51 | + | if err := k.unmapDumb(mems[i]); err != nil { | |||
| 52 | + | t.Errorf("munmap: %v", err) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | if imgs[i].handle != 0 { | |||
| 56 | + | if err := k.destroyDumb(imgs[i].handle); err != nil { | |||
| 57 | + | t.Errorf("destroy dumb: %v", err) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | defer cleanup() | |||
| 63 | + | for i := range imgs { | |||
| 64 | + | b, err := k.createDumb(size, size) | |||
| 65 | + | if err != nil { | |||
| 66 | + | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.ENOSYS) || errors.Is(err, unix.EINVAL) { | |||
| 67 | + | t.Logf("%s: create dumb: %v", path, err) | |||
| 68 | + | return false | |||
| 69 | + | } | |||
| 70 | + | t.Fatalf("%s: create dumb: %v", path, err) | |||
| 71 | + | } | |||
| 72 | + | imgs[i] = b | |||
| 73 | + | if int(b.pitch) < size*4 || b.size < uint64(int(b.pitch)*size) { | |||
| 74 | + | t.Fatalf("%s: dumb %dx%d pitch %d size %d", path, size, size, b.pitch, b.size) | |||
| 75 | + | } | |||
| 76 | + | if mems[i], err = k.mapDumb(b); err != nil { | |||
| 77 | + | t.Fatalf("%s: map dumb: %v", path, err) | |||
| 78 | + | } | |||
| 79 | + | if fbs[i], err = k.addFBHandle(b, size, size, fourccARGB); err != nil { | |||
| 80 | + | t.Fatalf("%s: add fb2 ARGB8888: %v", path, err) | |||
| 81 | + | } | |||
| 82 | + | t.Logf("%s: dumb buffer %d: %dx%d pitch %d size %d fb %d", path, i, size, size, b.pitch, b.size, fbs[i]) | |||
| 83 | + | } | |||
| 84 | + | // Write a pattern through the shared mapping; each buffer keeps its own. | |||
| 85 | + | for i, mem := range mems { | |||
| 86 | + | d := &dumbCursor{k: k, size: size} | |||
| 87 | + | d.img[i] = dumbImage{pitch: int(imgs[i].pitch), mem: mem} | |||
| 88 | + | px := bytes.Repeat([]byte{byte(0x10 + i), 0x20, 0x30, 0xff}, 3*2) | |||
| 89 | + | if err := d.write(i, px, 3, 2, 0); err != nil { | |||
| 90 | + | t.Fatal(err) | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | for i, mem := range mems { | |||
| 94 | + | pitch := int(imgs[i].pitch) | |||
| 95 | + | want := []byte{byte(0x10 + i), 0x20, 0x30, 0xff} | |||
| 96 | + | for y := range 2 { | |||
| 97 | + | if got := mem[y*pitch : y*pitch+4]; !bytes.Equal(got, want) { | |||
| 98 | + | t.Fatalf("%s: buffer %d row %d read back %v, want %v", path, i, y, got, want) | |||
| 99 | + | } | |||
| 100 | + | if got := mem[y*pitch+3*4]; got != 0 { | |||
| 101 | + | t.Fatalf("%s: buffer %d row %d not cleared after the image", path, i, y) | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | if mem[2*pitch] != 0 || mem[(size-1)*pitch+(size-1)*4] != 0 { | |||
| 105 | + | t.Fatalf("%s: buffer %d not cleared below the image", path, i) | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | return true | |||
| 109 | + | } | |||
M internal/adapters/drm/kms_mock_test.go +308 -0
| @@ -113,6 +113,84 @@ func (_c *mockkms_addFB_Call) RunAndReturn(run func(b *ports.DMABuf, format uint | |||
| 113 | 113 | return _c | |
| 114 | 114 | } | |
| 115 | 115 | ||
| 116 | + | // addFBHandle provides a mock function for the type mockkms | |
| 117 | + | func (_mock *mockkms) addFBHandle(b dumbBuffer, width int, height int, format uint32) (uint32, error) { | |
| 118 | + | ret := _mock.Called(b, width, height, format) | |
| 119 | + | ||
| 120 | + | if len(ret) == 0 { | |
| 121 | + | panic("no return value specified for addFBHandle") | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | var r0 uint32 | |
| 125 | + | var r1 error | |
| 126 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) (uint32, error)); ok { | |
| 127 | + | return returnFunc(b, width, height, format) | |
| 128 | + | } | |
| 129 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) uint32); ok { | |
| 130 | + | r0 = returnFunc(b, width, height, format) | |
| 131 | + | } else { | |
| 132 | + | r0 = ret.Get(0).(uint32) | |
| 133 | + | } | |
| 134 | + | if returnFunc, ok := ret.Get(1).(func(dumbBuffer, int, int, uint32) error); ok { | |
| 135 | + | r1 = returnFunc(b, width, height, format) | |
| 136 | + | } else { | |
| 137 | + | r1 = ret.Error(1) | |
| 138 | + | } | |
| 139 | + | return r0, r1 | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // mockkms_addFBHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'addFBHandle' | |
| 143 | + | type mockkms_addFBHandle_Call struct { | |
| 144 | + | *mock.Call | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // addFBHandle is a helper method to define mock.On call | |
| 148 | + | // - b dumbBuffer | |
| 149 | + | // - width int | |
| 150 | + | // - height int | |
| 151 | + | // - format uint32 | |
| 152 | + | func (_e *mockkms_Expecter) addFBHandle(b any, width any, height any, format any) *mockkms_addFBHandle_Call { | |
| 153 | + | return &mockkms_addFBHandle_Call{Call: _e.mock.On("addFBHandle", b, width, height, format)} | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (_c *mockkms_addFBHandle_Call) Run(run func(b dumbBuffer, width int, height int, format uint32)) *mockkms_addFBHandle_Call { | |
| 157 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 158 | + | var arg0 dumbBuffer | |
| 159 | + | if args[0] != nil { | |
| 160 | + | arg0 = args[0].(dumbBuffer) | |
| 161 | + | } | |
| 162 | + | var arg1 int | |
| 163 | + | if args[1] != nil { | |
| 164 | + | arg1 = args[1].(int) | |
| 165 | + | } | |
| 166 | + | var arg2 int | |
| 167 | + | if args[2] != nil { | |
| 168 | + | arg2 = args[2].(int) | |
| 169 | + | } | |
| 170 | + | var arg3 uint32 | |
| 171 | + | if args[3] != nil { | |
| 172 | + | arg3 = args[3].(uint32) | |
| 173 | + | } | |
| 174 | + | run( | |
| 175 | + | arg0, | |
| 176 | + | arg1, | |
| 177 | + | arg2, | |
| 178 | + | arg3, | |
| 179 | + | ) | |
| 180 | + | }) | |
| 181 | + | return _c | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | func (_c *mockkms_addFBHandle_Call) Return(v uint32, err error) *mockkms_addFBHandle_Call { | |
| 185 | + | _c.Call.Return(v, err) | |
| 186 | + | return _c | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func (_c *mockkms_addFBHandle_Call) RunAndReturn(run func(b dumbBuffer, width int, height int, format uint32) (uint32, error)) *mockkms_addFBHandle_Call { | |
| 190 | + | _c.Call.Return(run) | |
| 191 | + | return _c | |
| 192 | + | } | |
| 193 | + | ||
| 116 | 194 | // commit provides a mock function for the type mockkms | |
| 117 | 195 | func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error { | |
| 118 | 196 | ret := _mock.Called(req, flags, userData) | |
| @@ -296,6 +374,72 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e | |||
| 296 | 374 | return _c | |
| 297 | 375 | } | |
| 298 | 376 | ||
| 377 | + | // createDumb provides a mock function for the type mockkms | |
| 378 | + | func (_mock *mockkms) createDumb(width int, height int) (dumbBuffer, error) { | |
| 379 | + | ret := _mock.Called(width, height) | |
| 380 | + | ||
| 381 | + | if len(ret) == 0 { | |
| 382 | + | panic("no return value specified for createDumb") | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | var r0 dumbBuffer | |
| 386 | + | var r1 error | |
| 387 | + | if returnFunc, ok := ret.Get(0).(func(int, int) (dumbBuffer, error)); ok { | |
| 388 | + | return returnFunc(width, height) | |
| 389 | + | } | |
| 390 | + | if returnFunc, ok := ret.Get(0).(func(int, int) dumbBuffer); ok { | |
| 391 | + | r0 = returnFunc(width, height) | |
| 392 | + | } else { | |
| 393 | + | r0 = ret.Get(0).(dumbBuffer) | |
| 394 | + | } | |
| 395 | + | if returnFunc, ok := ret.Get(1).(func(int, int) error); ok { | |
| 396 | + | r1 = returnFunc(width, height) | |
| 397 | + | } else { | |
| 398 | + | r1 = ret.Error(1) | |
| 399 | + | } | |
| 400 | + | return r0, r1 | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | // mockkms_createDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createDumb' | |
| 404 | + | type mockkms_createDumb_Call struct { | |
| 405 | + | *mock.Call | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | // createDumb is a helper method to define mock.On call | |
| 409 | + | // - width int | |
| 410 | + | // - height int | |
| 411 | + | func (_e *mockkms_Expecter) createDumb(width any, height any) *mockkms_createDumb_Call { | |
| 412 | + | return &mockkms_createDumb_Call{Call: _e.mock.On("createDumb", width, height)} | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | func (_c *mockkms_createDumb_Call) Run(run func(width int, height int)) *mockkms_createDumb_Call { | |
| 416 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 417 | + | var arg0 int | |
| 418 | + | if args[0] != nil { | |
| 419 | + | arg0 = args[0].(int) | |
| 420 | + | } | |
| 421 | + | var arg1 int | |
| 422 | + | if args[1] != nil { | |
| 423 | + | arg1 = args[1].(int) | |
| 424 | + | } | |
| 425 | + | run( | |
| 426 | + | arg0, | |
| 427 | + | arg1, | |
| 428 | + | ) | |
| 429 | + | }) | |
| 430 | + | return _c | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | func (_c *mockkms_createDumb_Call) Return(dumbBufferMoqParam dumbBuffer, err error) *mockkms_createDumb_Call { | |
| 434 | + | _c.Call.Return(dumbBufferMoqParam, err) | |
| 435 | + | return _c | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | func (_c *mockkms_createDumb_Call) RunAndReturn(run func(width int, height int) (dumbBuffer, error)) *mockkms_createDumb_Call { | |
| 439 | + | _c.Call.Return(run) | |
| 440 | + | return _c | |
| 441 | + | } | |
| 442 | + | ||
| 299 | 443 | // createLease provides a mock function for the type mockkms | |
| 300 | 444 | func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) { | |
| 301 | 445 | ret := _mock.Called(objects) | |
| @@ -415,6 +559,57 @@ func (_c *mockkms_destroyBlob_Call) RunAndReturn(run func(id uint32) error) *moc | |||
| 415 | 559 | return _c | |
| 416 | 560 | } | |
| 417 | 561 | ||
| 562 | + | // destroyDumb provides a mock function for the type mockkms | |
| 563 | + | func (_mock *mockkms) destroyDumb(handle uint32) error { | |
| 564 | + | ret := _mock.Called(handle) | |
| 565 | + | ||
| 566 | + | if len(ret) == 0 { | |
| 567 | + | panic("no return value specified for destroyDumb") | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | var r0 error | |
| 571 | + | if returnFunc, ok := ret.Get(0).(func(uint32) error); ok { | |
| 572 | + | r0 = returnFunc(handle) | |
| 573 | + | } else { | |
| 574 | + | r0 = ret.Error(0) | |
| 575 | + | } | |
| 576 | + | return r0 | |
| 577 | + | } | |
| 578 | + | ||
| 579 | + | // mockkms_destroyDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroyDumb' | |
| 580 | + | type mockkms_destroyDumb_Call struct { | |
| 581 | + | *mock.Call | |
| 582 | + | } | |
| 583 | + | ||
| 584 | + | // destroyDumb is a helper method to define mock.On call | |
| 585 | + | // - handle uint32 | |
| 586 | + | func (_e *mockkms_Expecter) destroyDumb(handle any) *mockkms_destroyDumb_Call { | |
| 587 | + | return &mockkms_destroyDumb_Call{Call: _e.mock.On("destroyDumb", handle)} | |
| 588 | + | } | |
| 589 | + | ||
| 590 | + | func (_c *mockkms_destroyDumb_Call) Run(run func(handle uint32)) *mockkms_destroyDumb_Call { | |
| 591 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 592 | + | var arg0 uint32 | |
| 593 | + | if args[0] != nil { | |
| 594 | + | arg0 = args[0].(uint32) | |
| 595 | + | } | |
| 596 | + | run( | |
| 597 | + | arg0, | |
| 598 | + | ) | |
| 599 | + | }) | |
| 600 | + | return _c | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | func (_c *mockkms_destroyDumb_Call) Return(err error) *mockkms_destroyDumb_Call { | |
| 604 | + | _c.Call.Return(err) | |
| 605 | + | return _c | |
| 606 | + | } | |
| 607 | + | ||
| 608 | + | func (_c *mockkms_destroyDumb_Call) RunAndReturn(run func(handle uint32) error) *mockkms_destroyDumb_Call { | |
| 609 | + | _c.Call.Return(run) | |
| 610 | + | return _c | |
| 611 | + | } | |
| 612 | + | ||
| 418 | 613 | // getBlob provides a mock function for the type mockkms | |
| 419 | 614 | func (_mock *mockkms) getBlob(id uint32) ([]byte, error) { | |
| 420 | 615 | ret := _mock.Called(id) | |
| @@ -477,6 +672,68 @@ func (_c *mockkms_getBlob_Call) RunAndReturn(run func(id uint32) ([]byte, error) | |||
| 477 | 672 | return _c | |
| 478 | 673 | } | |
| 479 | 674 | ||
| 675 | + | // mapDumb provides a mock function for the type mockkms | |
| 676 | + | func (_mock *mockkms) mapDumb(b dumbBuffer) ([]byte, error) { | |
| 677 | + | ret := _mock.Called(b) | |
| 678 | + | ||
| 679 | + | if len(ret) == 0 { | |
| 680 | + | panic("no return value specified for mapDumb") | |
| 681 | + | } | |
| 682 | + | ||
| 683 | + | var r0 []byte | |
| 684 | + | var r1 error | |
| 685 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer) ([]byte, error)); ok { | |
| 686 | + | return returnFunc(b) | |
| 687 | + | } | |
| 688 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer) []byte); ok { | |
| 689 | + | r0 = returnFunc(b) | |
| 690 | + | } else { | |
| 691 | + | if ret.Get(0) != nil { | |
| 692 | + | r0 = ret.Get(0).([]byte) | |
| 693 | + | } | |
| 694 | + | } | |
| 695 | + | if returnFunc, ok := ret.Get(1).(func(dumbBuffer) error); ok { | |
| 696 | + | r1 = returnFunc(b) | |
| 697 | + | } else { | |
| 698 | + | r1 = ret.Error(1) | |
| 699 | + | } | |
| 700 | + | return r0, r1 | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | // mockkms_mapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'mapDumb' | |
| 704 | + | type mockkms_mapDumb_Call struct { | |
| 705 | + | *mock.Call | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | // mapDumb is a helper method to define mock.On call | |
| 709 | + | // - b dumbBuffer | |
| 710 | + | func (_e *mockkms_Expecter) mapDumb(b any) *mockkms_mapDumb_Call { | |
| 711 | + | return &mockkms_mapDumb_Call{Call: _e.mock.On("mapDumb", b)} | |
| 712 | + | } | |
| 713 | + | ||
| 714 | + | func (_c *mockkms_mapDumb_Call) Run(run func(b dumbBuffer)) *mockkms_mapDumb_Call { | |
| 715 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 716 | + | var arg0 dumbBuffer | |
| 717 | + | if args[0] != nil { | |
| 718 | + | arg0 = args[0].(dumbBuffer) | |
| 719 | + | } | |
| 720 | + | run( | |
| 721 | + | arg0, | |
| 722 | + | ) | |
| 723 | + | }) | |
| 724 | + | return _c | |
| 725 | + | } | |
| 726 | + | ||
| 727 | + | func (_c *mockkms_mapDumb_Call) Return(bytes []byte, err error) *mockkms_mapDumb_Call { | |
| 728 | + | _c.Call.Return(bytes, err) | |
| 729 | + | return _c | |
| 730 | + | } | |
| 731 | + | ||
| 732 | + | func (_c *mockkms_mapDumb_Call) RunAndReturn(run func(b dumbBuffer) ([]byte, error)) *mockkms_mapDumb_Call { | |
| 733 | + | _c.Call.Return(run) | |
| 734 | + | return _c | |
| 735 | + | } | |
| 736 | + | ||
| 480 | 737 | // objProps provides a mock function for the type mockkms | |
| 481 | 738 | func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) { | |
| 482 | 739 | ret := _mock.Called(obj, typ) | |
| @@ -898,3 +1155,54 @@ func (_c *mockkms_rmFB_Call) RunAndReturn(run func(id uint32) error) *mockkms_rm | |||
| 898 | 1155 | _c.Call.Return(run) | |
| 899 | 1156 | return _c | |
| 900 | 1157 | } | |
| 1158 | + | ||
| 1159 | + | // unmapDumb provides a mock function for the type mockkms | |
| 1160 | + | func (_mock *mockkms) unmapDumb(mem []byte) error { | |
| 1161 | + | ret := _mock.Called(mem) | |
| 1162 | + | ||
| 1163 | + | if len(ret) == 0 { | |
| 1164 | + | panic("no return value specified for unmapDumb") | |
| 1165 | + | } | |
| 1166 | + | ||
| 1167 | + | var r0 error | |
| 1168 | + | if returnFunc, ok := ret.Get(0).(func([]byte) error); ok { | |
| 1169 | + | r0 = returnFunc(mem) | |
| 1170 | + | } else { | |
| 1171 | + | r0 = ret.Error(0) | |
| 1172 | + | } | |
| 1173 | + | return r0 | |
| 1174 | + | } | |
| 1175 | + | ||
| 1176 | + | // mockkms_unmapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'unmapDumb' | |
| 1177 | + | type mockkms_unmapDumb_Call struct { | |
| 1178 | + | *mock.Call | |
| 1179 | + | } | |
| 1180 | + | ||
| 1181 | + | // unmapDumb is a helper method to define mock.On call | |
| 1182 | + | // - mem []byte | |
| 1183 | + | func (_e *mockkms_Expecter) unmapDumb(mem any) *mockkms_unmapDumb_Call { | |
| 1184 | + | return &mockkms_unmapDumb_Call{Call: _e.mock.On("unmapDumb", mem)} | |
| 1185 | + | } | |
| 1186 | + | ||
| 1187 | + | func (_c *mockkms_unmapDumb_Call) Run(run func(mem []byte)) *mockkms_unmapDumb_Call { | |
| 1188 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1189 | + | var arg0 []byte | |
| 1190 | + | if args[0] != nil { | |
| 1191 | + | arg0 = args[0].([]byte) | |
| 1192 | + | } | |
| 1193 | + | run( | |
| 1194 | + | arg0, | |
| 1195 | + | ) | |
| 1196 | + | }) | |
| 1197 | + | return _c | |
| 1198 | + | } | |
| 1199 | + | ||
| 1200 | + | func (_c *mockkms_unmapDumb_Call) Return(err error) *mockkms_unmapDumb_Call { | |
| 1201 | + | _c.Call.Return(err) | |
| 1202 | + | return _c | |
| 1203 | + | } | |
| 1204 | + | ||
| 1205 | + | func (_c *mockkms_unmapDumb_Call) RunAndReturn(run func(mem []byte) error) *mockkms_unmapDumb_Call { | |
| 1206 | + | _c.Call.Return(run) | |
| 1207 | + | return _c | |
| 1208 | + | } | |
| @@ -113,6 +113,84 @@ func (_c *mockkms_addFB_Call) RunAndReturn(run func(b *ports.DMABuf, format uint | |||||
| 113 | return _c | 113 | return _c | ||
| 114 | } | 114 | } | ||
| 115 | 115 | ||||
| 116 | + | // addFBHandle provides a mock function for the type mockkms | |||
| 117 | + | func (_mock *mockkms) addFBHandle(b dumbBuffer, width int, height int, format uint32) (uint32, error) { | |||
| 118 | + | ret := _mock.Called(b, width, height, format) | |||
| 119 | + | ||||
| 120 | + | if len(ret) == 0 { | |||
| 121 | + | panic("no return value specified for addFBHandle") | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | var r0 uint32 | |||
| 125 | + | var r1 error | |||
| 126 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) (uint32, error)); ok { | |||
| 127 | + | return returnFunc(b, width, height, format) | |||
| 128 | + | } | |||
| 129 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) uint32); ok { | |||
| 130 | + | r0 = returnFunc(b, width, height, format) | |||
| 131 | + | } else { | |||
| 132 | + | r0 = ret.Get(0).(uint32) | |||
| 133 | + | } | |||
| 134 | + | if returnFunc, ok := ret.Get(1).(func(dumbBuffer, int, int, uint32) error); ok { | |||
| 135 | + | r1 = returnFunc(b, width, height, format) | |||
| 136 | + | } else { | |||
| 137 | + | r1 = ret.Error(1) | |||
| 138 | + | } | |||
| 139 | + | return r0, r1 | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // mockkms_addFBHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'addFBHandle' | |||
| 143 | + | type mockkms_addFBHandle_Call struct { | |||
| 144 | + | *mock.Call | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // addFBHandle is a helper method to define mock.On call | |||
| 148 | + | // - b dumbBuffer | |||
| 149 | + | // - width int | |||
| 150 | + | // - height int | |||
| 151 | + | // - format uint32 | |||
| 152 | + | func (_e *mockkms_Expecter) addFBHandle(b any, width any, height any, format any) *mockkms_addFBHandle_Call { | |||
| 153 | + | return &mockkms_addFBHandle_Call{Call: _e.mock.On("addFBHandle", b, width, height, format)} | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func (_c *mockkms_addFBHandle_Call) Run(run func(b dumbBuffer, width int, height int, format uint32)) *mockkms_addFBHandle_Call { | |||
| 157 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 158 | + | var arg0 dumbBuffer | |||
| 159 | + | if args[0] != nil { | |||
| 160 | + | arg0 = args[0].(dumbBuffer) | |||
| 161 | + | } | |||
| 162 | + | var arg1 int | |||
| 163 | + | if args[1] != nil { | |||
| 164 | + | arg1 = args[1].(int) | |||
| 165 | + | } | |||
| 166 | + | var arg2 int | |||
| 167 | + | if args[2] != nil { | |||
| 168 | + | arg2 = args[2].(int) | |||
| 169 | + | } | |||
| 170 | + | var arg3 uint32 | |||
| 171 | + | if args[3] != nil { | |||
| 172 | + | arg3 = args[3].(uint32) | |||
| 173 | + | } | |||
| 174 | + | run( | |||
| 175 | + | arg0, | |||
| 176 | + | arg1, | |||
| 177 | + | arg2, | |||
| 178 | + | arg3, | |||
| 179 | + | ) | |||
| 180 | + | }) | |||
| 181 | + | return _c | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | func (_c *mockkms_addFBHandle_Call) Return(v uint32, err error) *mockkms_addFBHandle_Call { | |||
| 185 | + | _c.Call.Return(v, err) | |||
| 186 | + | return _c | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func (_c *mockkms_addFBHandle_Call) RunAndReturn(run func(b dumbBuffer, width int, height int, format uint32) (uint32, error)) *mockkms_addFBHandle_Call { | |||
| 190 | + | _c.Call.Return(run) | |||
| 191 | + | return _c | |||
| 192 | + | } | |||
| 193 | + | ||||
| 116 | // commit provides a mock function for the type mockkms | 194 | // commit provides a mock function for the type mockkms | ||
| 117 | func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error { | 195 | func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error { | ||
| 118 | ret := _mock.Called(req, flags, userData) | 196 | ret := _mock.Called(req, flags, userData) | ||
| @@ -296,6 +374,72 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e | |||||
| 296 | return _c | 374 | return _c | ||
| 297 | } | 375 | } | ||
| 298 | 376 | ||||
| 377 | + | // createDumb provides a mock function for the type mockkms | |||
| 378 | + | func (_mock *mockkms) createDumb(width int, height int) (dumbBuffer, error) { | |||
| 379 | + | ret := _mock.Called(width, height) | |||
| 380 | + | ||||
| 381 | + | if len(ret) == 0 { | |||
| 382 | + | panic("no return value specified for createDumb") | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | var r0 dumbBuffer | |||
| 386 | + | var r1 error | |||
| 387 | + | if returnFunc, ok := ret.Get(0).(func(int, int) (dumbBuffer, error)); ok { | |||
| 388 | + | return returnFunc(width, height) | |||
| 389 | + | } | |||
| 390 | + | if returnFunc, ok := ret.Get(0).(func(int, int) dumbBuffer); ok { | |||
| 391 | + | r0 = returnFunc(width, height) | |||
| 392 | + | } else { | |||
| 393 | + | r0 = ret.Get(0).(dumbBuffer) | |||
| 394 | + | } | |||
| 395 | + | if returnFunc, ok := ret.Get(1).(func(int, int) error); ok { | |||
| 396 | + | r1 = returnFunc(width, height) | |||
| 397 | + | } else { | |||
| 398 | + | r1 = ret.Error(1) | |||
| 399 | + | } | |||
| 400 | + | return r0, r1 | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | // mockkms_createDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createDumb' | |||
| 404 | + | type mockkms_createDumb_Call struct { | |||
| 405 | + | *mock.Call | |||
| 406 | + | } | |||
| 407 | + | ||||
| 408 | + | // createDumb is a helper method to define mock.On call | |||
| 409 | + | // - width int | |||
| 410 | + | // - height int | |||
| 411 | + | func (_e *mockkms_Expecter) createDumb(width any, height any) *mockkms_createDumb_Call { | |||
| 412 | + | return &mockkms_createDumb_Call{Call: _e.mock.On("createDumb", width, height)} | |||
| 413 | + | } | |||
| 414 | + | ||||
| 415 | + | func (_c *mockkms_createDumb_Call) Run(run func(width int, height int)) *mockkms_createDumb_Call { | |||
| 416 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 417 | + | var arg0 int | |||
| 418 | + | if args[0] != nil { | |||
| 419 | + | arg0 = args[0].(int) | |||
| 420 | + | } | |||
| 421 | + | var arg1 int | |||
| 422 | + | if args[1] != nil { | |||
| 423 | + | arg1 = args[1].(int) | |||
| 424 | + | } | |||
| 425 | + | run( | |||
| 426 | + | arg0, | |||
| 427 | + | arg1, | |||
| 428 | + | ) | |||
| 429 | + | }) | |||
| 430 | + | return _c | |||
| 431 | + | } | |||
| 432 | + | ||||
| 433 | + | func (_c *mockkms_createDumb_Call) Return(dumbBufferMoqParam dumbBuffer, err error) *mockkms_createDumb_Call { | |||
| 434 | + | _c.Call.Return(dumbBufferMoqParam, err) | |||
| 435 | + | return _c | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | func (_c *mockkms_createDumb_Call) RunAndReturn(run func(width int, height int) (dumbBuffer, error)) *mockkms_createDumb_Call { | |||
| 439 | + | _c.Call.Return(run) | |||
| 440 | + | return _c | |||
| 441 | + | } | |||
| 442 | + | ||||
| 299 | // createLease provides a mock function for the type mockkms | 443 | // createLease provides a mock function for the type mockkms | ||
| 300 | func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) { | 444 | func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) { | ||
| 301 | ret := _mock.Called(objects) | 445 | ret := _mock.Called(objects) | ||
| @@ -415,6 +559,57 @@ func (_c *mockkms_destroyBlob_Call) RunAndReturn(run func(id uint32) error) *moc | |||||
| 415 | return _c | 559 | return _c | ||
| 416 | } | 560 | } | ||
| 417 | 561 | ||||
| 562 | + | // destroyDumb provides a mock function for the type mockkms | |||
| 563 | + | func (_mock *mockkms) destroyDumb(handle uint32) error { | |||
| 564 | + | ret := _mock.Called(handle) | |||
| 565 | + | ||||
| 566 | + | if len(ret) == 0 { | |||
| 567 | + | panic("no return value specified for destroyDumb") | |||
| 568 | + | } | |||
| 569 | + | ||||
| 570 | + | var r0 error | |||
| 571 | + | if returnFunc, ok := ret.Get(0).(func(uint32) error); ok { | |||
| 572 | + | r0 = returnFunc(handle) | |||
| 573 | + | } else { | |||
| 574 | + | r0 = ret.Error(0) | |||
| 575 | + | } | |||
| 576 | + | return r0 | |||
| 577 | + | } | |||
| 578 | + | ||||
| 579 | + | // mockkms_destroyDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroyDumb' | |||
| 580 | + | type mockkms_destroyDumb_Call struct { | |||
| 581 | + | *mock.Call | |||
| 582 | + | } | |||
| 583 | + | ||||
| 584 | + | // destroyDumb is a helper method to define mock.On call | |||
| 585 | + | // - handle uint32 | |||
| 586 | + | func (_e *mockkms_Expecter) destroyDumb(handle any) *mockkms_destroyDumb_Call { | |||
| 587 | + | return &mockkms_destroyDumb_Call{Call: _e.mock.On("destroyDumb", handle)} | |||
| 588 | + | } | |||
| 589 | + | ||||
| 590 | + | func (_c *mockkms_destroyDumb_Call) Run(run func(handle uint32)) *mockkms_destroyDumb_Call { | |||
| 591 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 592 | + | var arg0 uint32 | |||
| 593 | + | if args[0] != nil { | |||
| 594 | + | arg0 = args[0].(uint32) | |||
| 595 | + | } | |||
| 596 | + | run( | |||
| 597 | + | arg0, | |||
| 598 | + | ) | |||
| 599 | + | }) | |||
| 600 | + | return _c | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | func (_c *mockkms_destroyDumb_Call) Return(err error) *mockkms_destroyDumb_Call { | |||
| 604 | + | _c.Call.Return(err) | |||
| 605 | + | return _c | |||
| 606 | + | } | |||
| 607 | + | ||||
| 608 | + | func (_c *mockkms_destroyDumb_Call) RunAndReturn(run func(handle uint32) error) *mockkms_destroyDumb_Call { | |||
| 609 | + | _c.Call.Return(run) | |||
| 610 | + | return _c | |||
| 611 | + | } | |||
| 612 | + | ||||
| 418 | // getBlob provides a mock function for the type mockkms | 613 | // getBlob provides a mock function for the type mockkms | ||
| 419 | func (_mock *mockkms) getBlob(id uint32) ([]byte, error) { | 614 | func (_mock *mockkms) getBlob(id uint32) ([]byte, error) { | ||
| 420 | ret := _mock.Called(id) | 615 | ret := _mock.Called(id) | ||
| @@ -477,6 +672,68 @@ func (_c *mockkms_getBlob_Call) RunAndReturn(run func(id uint32) ([]byte, error) | |||||
| 477 | return _c | 672 | return _c | ||
| 478 | } | 673 | } | ||
| 479 | 674 | ||||
| 675 | + | // mapDumb provides a mock function for the type mockkms | |||
| 676 | + | func (_mock *mockkms) mapDumb(b dumbBuffer) ([]byte, error) { | |||
| 677 | + | ret := _mock.Called(b) | |||
| 678 | + | ||||
| 679 | + | if len(ret) == 0 { | |||
| 680 | + | panic("no return value specified for mapDumb") | |||
| 681 | + | } | |||
| 682 | + | ||||
| 683 | + | var r0 []byte | |||
| 684 | + | var r1 error | |||
| 685 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer) ([]byte, error)); ok { | |||
| 686 | + | return returnFunc(b) | |||
| 687 | + | } | |||
| 688 | + | if returnFunc, ok := ret.Get(0).(func(dumbBuffer) []byte); ok { | |||
| 689 | + | r0 = returnFunc(b) | |||
| 690 | + | } else { | |||
| 691 | + | if ret.Get(0) != nil { | |||
| 692 | + | r0 = ret.Get(0).([]byte) | |||
| 693 | + | } | |||
| 694 | + | } | |||
| 695 | + | if returnFunc, ok := ret.Get(1).(func(dumbBuffer) error); ok { | |||
| 696 | + | r1 = returnFunc(b) | |||
| 697 | + | } else { | |||
| 698 | + | r1 = ret.Error(1) | |||
| 699 | + | } | |||
| 700 | + | return r0, r1 | |||
| 701 | + | } | |||
| 702 | + | ||||
| 703 | + | // mockkms_mapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'mapDumb' | |||
| 704 | + | type mockkms_mapDumb_Call struct { | |||
| 705 | + | *mock.Call | |||
| 706 | + | } | |||
| 707 | + | ||||
| 708 | + | // mapDumb is a helper method to define mock.On call | |||
| 709 | + | // - b dumbBuffer | |||
| 710 | + | func (_e *mockkms_Expecter) mapDumb(b any) *mockkms_mapDumb_Call { | |||
| 711 | + | return &mockkms_mapDumb_Call{Call: _e.mock.On("mapDumb", b)} | |||
| 712 | + | } | |||
| 713 | + | ||||
| 714 | + | func (_c *mockkms_mapDumb_Call) Run(run func(b dumbBuffer)) *mockkms_mapDumb_Call { | |||
| 715 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 716 | + | var arg0 dumbBuffer | |||
| 717 | + | if args[0] != nil { | |||
| 718 | + | arg0 = args[0].(dumbBuffer) | |||
| 719 | + | } | |||
| 720 | + | run( | |||
| 721 | + | arg0, | |||
| 722 | + | ) | |||
| 723 | + | }) | |||
| 724 | + | return _c | |||
| 725 | + | } | |||
| 726 | + | ||||
| 727 | + | func (_c *mockkms_mapDumb_Call) Return(bytes []byte, err error) *mockkms_mapDumb_Call { | |||
| 728 | + | _c.Call.Return(bytes, err) | |||
| 729 | + | return _c | |||
| 730 | + | } | |||
| 731 | + | ||||
| 732 | + | func (_c *mockkms_mapDumb_Call) RunAndReturn(run func(b dumbBuffer) ([]byte, error)) *mockkms_mapDumb_Call { | |||
| 733 | + | _c.Call.Return(run) | |||
| 734 | + | return _c | |||
| 735 | + | } | |||
| 736 | + | ||||
| 480 | // objProps provides a mock function for the type mockkms | 737 | // objProps provides a mock function for the type mockkms | ||
| 481 | func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) { | 738 | func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) { | ||
| 482 | ret := _mock.Called(obj, typ) | 739 | ret := _mock.Called(obj, typ) | ||
| @@ -898,3 +1155,54 @@ func (_c *mockkms_rmFB_Call) RunAndReturn(run func(id uint32) error) *mockkms_rm | |||||
| 898 | _c.Call.Return(run) | 1155 | _c.Call.Return(run) | ||
| 899 | return _c | 1156 | return _c | ||
| 900 | } | 1157 | } | ||
| 1158 | + | ||||
| 1159 | + | // unmapDumb provides a mock function for the type mockkms | |||
| 1160 | + | func (_mock *mockkms) unmapDumb(mem []byte) error { | |||
| 1161 | + | ret := _mock.Called(mem) | |||
| 1162 | + | ||||
| 1163 | + | if len(ret) == 0 { | |||
| 1164 | + | panic("no return value specified for unmapDumb") | |||
| 1165 | + | } | |||
| 1166 | + | ||||
| 1167 | + | var r0 error | |||
| 1168 | + | if returnFunc, ok := ret.Get(0).(func([]byte) error); ok { | |||
| 1169 | + | r0 = returnFunc(mem) | |||
| 1170 | + | } else { | |||
| 1171 | + | r0 = ret.Error(0) | |||
| 1172 | + | } | |||
| 1173 | + | return r0 | |||
| 1174 | + | } | |||
| 1175 | + | ||||
| 1176 | + | // mockkms_unmapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'unmapDumb' | |||
| 1177 | + | type mockkms_unmapDumb_Call struct { | |||
| 1178 | + | *mock.Call | |||
| 1179 | + | } | |||
| 1180 | + | ||||
| 1181 | + | // unmapDumb is a helper method to define mock.On call | |||
| 1182 | + | // - mem []byte | |||
| 1183 | + | func (_e *mockkms_Expecter) unmapDumb(mem any) *mockkms_unmapDumb_Call { | |||
| 1184 | + | return &mockkms_unmapDumb_Call{Call: _e.mock.On("unmapDumb", mem)} | |||
| 1185 | + | } | |||
| 1186 | + | ||||
| 1187 | + | func (_c *mockkms_unmapDumb_Call) Run(run func(mem []byte)) *mockkms_unmapDumb_Call { | |||
| 1188 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1189 | + | var arg0 []byte | |||
| 1190 | + | if args[0] != nil { | |||
| 1191 | + | arg0 = args[0].([]byte) | |||
| 1192 | + | } | |||
| 1193 | + | run( | |||
| 1194 | + | arg0, | |||
| 1195 | + | ) | |||
| 1196 | + | }) | |||
| 1197 | + | return _c | |||
| 1198 | + | } | |||
| 1199 | + | ||||
| 1200 | + | func (_c *mockkms_unmapDumb_Call) Return(err error) *mockkms_unmapDumb_Call { | |||
| 1201 | + | _c.Call.Return(err) | |||
| 1202 | + | return _c | |||
| 1203 | + | } | |||
| 1204 | + | ||||
| 1205 | + | func (_c *mockkms_unmapDumb_Call) RunAndReturn(run func(mem []byte) error) *mockkms_unmapDumb_Call { | |||
| 1206 | + | _c.Call.Return(run) | |||
| 1207 | + | return _c | |||
| 1208 | + | } | |||
M internal/adapters/drm/kms_test.go +3 -0
| @@ -32,6 +32,9 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||
| 32 | 32 | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | |
| 33 | 33 | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | |
| 34 | 34 | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | |
| 35 | + | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | |
| 36 | + | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | |
| 37 | + | {"destroy_dumb", unsafe.Sizeof(uint32(0)), ioctlDestroyDumb}, | |
| 35 | 38 | {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob}, | |
| 36 | 39 | } { | |
| 37 | 40 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| @@ -32,6 +32,9 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||||
| 32 | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | 32 | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | ||
| 33 | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | 33 | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | ||
| 34 | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | 34 | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | ||
| 35 | + | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | |||
| 36 | + | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | |||
| 37 | + | {"destroy_dumb", unsafe.Sizeof(uint32(0)), ioctlDestroyDumb}, | |||
| 35 | {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob}, | 38 | {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob}, | ||
| 36 | } { | 39 | } { | ||
| 37 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | 40 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | ||
M internal/adapters/drm/output.go +51 -20
| @@ -579,18 +579,44 @@ func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) { | |||
| 579 | 579 | return blob, nil | |
| 580 | 580 | } | |
| 581 | 581 | ||
| 582 | - | // testCursor turns the hardware cursor off when KMS refuses its images on | |
| 583 | - | // the cursor plane (e.g. a pitch the plane cannot scan out). | |
| 584 | - | func (o *Output) testCursor() { | |
| 582 | + | // testCursor asks KMS whether the cursor plane takes the cursor images. | |
| 583 | + | // Refused dumb images are replaced by the renderer's; when those are refused | |
| 584 | + | // too, or there are none, the hardware cursor goes off. It reports whether | |
| 585 | + | // the cursor changed (new images or off). | |
| 586 | + | func (o *Output) testCursor() (changed bool) { | |
| 585 | 587 | c := o.cursor | |
| 586 | 588 | if c == nil || c.off || c.fbs[0] == 0 { | |
| 587 | - | return | |
| 589 | + | return false | |
| 588 | 590 | } | |
| 589 | - | req := &atomicReq{} | |
| 590 | - | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) | |
| 591 | - | if err := o.k.commit(req, atomicTestOnly, 0); refused(err) { | |
| 592 | - | o.cursorOff(err) | |
| 591 | + | for { | |
| 592 | + | req := &atomicReq{} | |
| 593 | + | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) | |
| 594 | + | err := o.k.commit(req, atomicTestOnly, 0) | |
| 595 | + | if !refused(err) { | |
| 596 | + | return changed | |
| 597 | + | } | |
| 598 | + | // Dumb images refused: the renderer's get one try. | |
| 599 | + | if c.alloc != allocDumb { | |
| 600 | + | o.cursorOff(err) | |
| 601 | + | return true | |
| 602 | + | } | |
| 603 | + | ok, ferr := c.useFallback(o.cursorNits()) | |
| 604 | + | if !ok || ferr != nil { | |
| 605 | + | o.cursorOff(errors.Join(err, ferr)) | |
| 606 | + | return true | |
| 607 | + | } | |
| 608 | + | changed = true | |
| 609 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Str("alloc", allocVulkan).Int("size", c.size).Msg("cursor plane refused dumb images; hardware cursor uses renderer images") | |
| 610 | + | } | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | // cursorNits is the SDR white the cursor images are converted for on an HDR | |
| 614 | + | // output (0: SDR). | |
| 615 | + | func (o *Output) cursorNits() float64 { | |
| 616 | + | if !o.hdr.on { | |
| 617 | + | return 0 | |
| 593 | 618 | } | |
| 619 | + | return float64(o.hdr.settings.SDRBrightness) | |
| 594 | 620 | } | |
| 595 | 621 | ||
| 596 | 622 | // cursorOff stops using the cursor plane for the rest of the output's life. | |
| @@ -797,11 +823,10 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState, | |||
| 797 | 823 | } | |
| 798 | 824 | ||
| 799 | 825 | // cursorRefused reports, after a commit carrying the cursor failed with | |
| 800 | - | // EINVAL, whether the cursor plane was the cause; then the cursor is off | |
| 801 | - | // and the commit can be retried without it. | |
| 826 | + | // EINVAL, whether the cursor plane was the cause; then the cursor is off or | |
| 827 | + | // on other images, and the commit can be retried. | |
| 802 | 828 | func (o *Output) cursorRefused() bool { | |
| 803 | - | o.testCursor() | |
| 804 | - | return o.cursor.off | |
| 829 | + | return o.testCursor() | |
| 805 | 830 | } | |
| 806 | 831 | ||
| 807 | 832 | // commitState commits a cursor or VRR change without a new frame. | |
| @@ -1042,7 +1067,7 @@ func (o *Output) Close() { | |||
| 1042 | 1067 | o.ctmBlob = 0 | |
| 1043 | 1068 | } | |
| 1044 | 1069 | if o.cursor != nil { | |
| 1045 | - | o.cursor.free(o.k) | |
| 1070 | + | o.cursor.free() | |
| 1046 | 1071 | } | |
| 1047 | 1072 | o.freeImages() | |
| 1048 | 1073 | if o.alloc != nil { | |
| @@ -1086,9 +1111,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1086 | 1111 | } | |
| 1087 | 1112 | defer r.Close() | |
| 1088 | 1113 | if o.cursor != nil { | |
| 1089 | - | if err := o.cursor.setup(o.k, r); err != nil { | |
| 1114 | + | dumbErr, err := o.cursor.setup(o.k, r) | |
| 1115 | + | switch { | |
| 1116 | + | case err != nil: | |
| 1090 | 1117 | o.cursor.off = true | |
| 1091 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") | |
| 1118 | + | o.log.Warn().Err(errors.Join(dumbErr, err)).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") | |
| 1119 | + | case dumbErr != nil: | |
| 1120 | + | o.log.Warn().Err(dumbErr).Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor") | |
| 1121 | + | default: | |
| 1122 | + | o.log.Info().Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor") | |
| 1092 | 1123 | } | |
| 1093 | 1124 | } | |
| 1094 | 1125 | // The seat sends its state first; while switched away (e.g. a monitor | |
| @@ -1463,7 +1494,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1463 | 1494 | stateDirty = true | |
| 1464 | 1495 | if o.cursor != nil && !o.protected { | |
| 1465 | 1496 | // An image that waited for a free slot loads now. | |
| 1466 | - | if _, err := o.cursor.flushLater(r); err != nil { | |
| 1497 | + | if _, err := o.cursor.flushLater(o.cursorNits()); err != nil { | |
| 1467 | 1498 | o.log.Warn().Err(err).Msg("cursor") | |
| 1468 | 1499 | } | |
| 1469 | 1500 | } | |
| @@ -1484,7 +1515,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1484 | 1515 | } | |
| 1485 | 1516 | if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) { | |
| 1486 | 1517 | cursorScale, cursorTransform = s.Scale, s.Transform | |
| 1487 | - | o.setCursor(r, loadCursor, want, s.Scale, s.Transform) | |
| 1518 | + | o.setCursor(loadCursor, want, s.Scale, s.Transform) | |
| 1488 | 1519 | stateDirty = true | |
| 1489 | 1520 | } | |
| 1490 | 1521 | scene, haveScene, dirty = s, true, true | |
| @@ -1499,7 +1530,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1499 | 1530 | want = c | |
| 1500 | 1531 | // Before the first scene the scale is unknown: loaded then. | |
| 1501 | 1532 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | |
| 1502 | - | o.setCursor(r, loadCursor, want, cursorScale, cursorTransform) | |
| 1533 | + | o.setCursor(loadCursor, want, cursorScale, cursorTransform) | |
| 1503 | 1534 | stateDirty = true | |
| 1504 | 1535 | } | |
| 1505 | 1536 | case b := <-contents: | |
| @@ -1736,14 +1767,14 @@ func (o *Output) wakeCore() { corewake.Send(o.Frames, o.conn.name) } | |||
| 1736 | 1767 | ||
| 1737 | 1768 | // setCursor loads a cursor for scale and output transform t into the cursor | |
| 1738 | 1769 | // images. | |
| 1739 | - | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) { | |
| 1770 | + | func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) { | |
| 1740 | 1771 | o.observeSecurity() | |
| 1741 | 1772 | if o.protected || o.cursor.off { | |
| 1742 | 1773 | return | |
| 1743 | 1774 | } | |
| 1744 | 1775 | img, err := load(c, scale, t, o.cursor.Limit()) | |
| 1745 | 1776 | if err == nil { | |
| 1746 | - | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) | |
| 1777 | + | err = o.cursor.setImage(img.Pixels, img.W, img.H, img.HotX, img.HotY, o.cursorNits()) | |
| 1747 | 1778 | } | |
| 1748 | 1779 | if err != nil { | |
| 1749 | 1780 | o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor") | |
| @@ -579,18 +579,44 @@ func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) { | |||||
| 579 | return blob, nil | 579 | return blob, nil | ||
| 580 | } | 580 | } | ||
| 581 | 581 | ||||
| 582 | - | // testCursor turns the hardware cursor off when KMS refuses its images on | 582 | + | // testCursor asks KMS whether the cursor plane takes the cursor images. |
| 583 | - | // the cursor plane (e.g. a pitch the plane cannot scan out). | 583 | + | // Refused dumb images are replaced by the renderer's; when those are refused |
| 584 | - | func (o *Output) testCursor() { | 584 | + | // too, or there are none, the hardware cursor goes off. It reports whether |
| 585 | + | // the cursor changed (new images or off). | |||
| 586 | + | func (o *Output) testCursor() (changed bool) { | |||
| 585 | c := o.cursor | 587 | c := o.cursor | ||
| 586 | if c == nil || c.off || c.fbs[0] == 0 { | 588 | if c == nil || c.off || c.fbs[0] == 0 { | ||
| 587 | - | return | 589 | + | return false |
| 588 | } | 590 | } | ||
| 589 | - | req := &atomicReq{} | 591 | + | for { |
| 590 | - | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) | 592 | + | req := &atomicReq{} |
| 591 | - | if err := o.k.commit(req, atomicTestOnly, 0); refused(err) { | 593 | + | c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]}) |
| 592 | - | o.cursorOff(err) | 594 | + | err := o.k.commit(req, atomicTestOnly, 0) |
| 595 | + | if !refused(err) { | |||
| 596 | + | return changed | |||
| 597 | + | } | |||
| 598 | + | // Dumb images refused: the renderer's get one try. | |||
| 599 | + | if c.alloc != allocDumb { | |||
| 600 | + | o.cursorOff(err) | |||
| 601 | + | return true | |||
| 602 | + | } | |||
| 603 | + | ok, ferr := c.useFallback(o.cursorNits()) | |||
| 604 | + | if !ok || ferr != nil { | |||
| 605 | + | o.cursorOff(errors.Join(err, ferr)) | |||
| 606 | + | return true | |||
| 607 | + | } | |||
| 608 | + | changed = true | |||
| 609 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Str("alloc", allocVulkan).Int("size", c.size).Msg("cursor plane refused dumb images; hardware cursor uses renderer images") | |||
| 610 | + | } | |||
| 611 | + | } | |||
| 612 | + | ||||
| 613 | + | // cursorNits is the SDR white the cursor images are converted for on an HDR | |||
| 614 | + | // output (0: SDR). | |||
| 615 | + | func (o *Output) cursorNits() float64 { | |||
| 616 | + | if !o.hdr.on { | |||
| 617 | + | return 0 | |||
| 593 | } | 618 | } | ||
| 619 | + | return float64(o.hdr.settings.SDRBrightness) | |||
| 594 | } | 620 | } | ||
| 595 | 621 | ||||
| 596 | // cursorOff stops using the cursor plane for the rest of the output's life. | 622 | // cursorOff stops using the cursor plane for the rest of the output's life. | ||
| @@ -797,11 +823,10 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState, | |||||
| 797 | } | 823 | } | ||
| 798 | 824 | ||||
| 799 | // cursorRefused reports, after a commit carrying the cursor failed with | 825 | // cursorRefused reports, after a commit carrying the cursor failed with | ||
| 800 | - | // EINVAL, whether the cursor plane was the cause; then the cursor is off | 826 | + | // EINVAL, whether the cursor plane was the cause; then the cursor is off or |
| 801 | - | // and the commit can be retried without it. | 827 | + | // on other images, and the commit can be retried. |
| 802 | func (o *Output) cursorRefused() bool { | 828 | func (o *Output) cursorRefused() bool { | ||
| 803 | - | o.testCursor() | 829 | + | return o.testCursor() |
| 804 | - | return o.cursor.off | |||
| 805 | } | 830 | } | ||
| 806 | 831 | ||||
| 807 | // commitState commits a cursor or VRR change without a new frame. | 832 | // commitState commits a cursor or VRR change without a new frame. | ||
| @@ -1042,7 +1067,7 @@ func (o *Output) Close() { | |||||
| 1042 | o.ctmBlob = 0 | 1067 | o.ctmBlob = 0 | ||
| 1043 | } | 1068 | } | ||
| 1044 | if o.cursor != nil { | 1069 | if o.cursor != nil { | ||
| 1045 | - | o.cursor.free(o.k) | 1070 | + | o.cursor.free() |
| 1046 | } | 1071 | } | ||
| 1047 | o.freeImages() | 1072 | o.freeImages() | ||
| 1048 | if o.alloc != nil { | 1073 | if o.alloc != nil { | ||
| @@ -1086,9 +1111,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1086 | } | 1111 | } | ||
| 1087 | defer r.Close() | 1112 | defer r.Close() | ||
| 1088 | if o.cursor != nil { | 1113 | if o.cursor != nil { | ||
| 1089 | - | if err := o.cursor.setup(o.k, r); err != nil { | 1114 | + | dumbErr, err := o.cursor.setup(o.k, r) |
| 1115 | + | switch { | |||
| 1116 | + | case err != nil: | |||
| 1090 | o.cursor.off = true | 1117 | o.cursor.off = true | ||
| 1091 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") | 1118 | + | o.log.Warn().Err(errors.Join(dumbErr, err)).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off") |
| 1119 | + | case dumbErr != nil: | |||
| 1120 | + | o.log.Warn().Err(dumbErr).Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor") | |||
| 1121 | + | default: | |||
| 1122 | + | o.log.Info().Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor") | |||
| 1092 | } | 1123 | } | ||
| 1093 | } | 1124 | } | ||
| 1094 | // The seat sends its state first; while switched away (e.g. a monitor | 1125 | // The seat sends its state first; while switched away (e.g. a monitor | ||
| @@ -1463,7 +1494,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1463 | stateDirty = true | 1494 | stateDirty = true | ||
| 1464 | if o.cursor != nil && !o.protected { | 1495 | if o.cursor != nil && !o.protected { | ||
| 1465 | // An image that waited for a free slot loads now. | 1496 | // An image that waited for a free slot loads now. | ||
| 1466 | - | if _, err := o.cursor.flushLater(r); err != nil { | 1497 | + | if _, err := o.cursor.flushLater(o.cursorNits()); err != nil { |
| 1467 | o.log.Warn().Err(err).Msg("cursor") | 1498 | o.log.Warn().Err(err).Msg("cursor") | ||
| 1468 | } | 1499 | } | ||
| 1469 | } | 1500 | } | ||
| @@ -1484,7 +1515,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1484 | } | 1515 | } | ||
| 1485 | if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) { | 1516 | if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) { | ||
| 1486 | cursorScale, cursorTransform = s.Scale, s.Transform | 1517 | cursorScale, cursorTransform = s.Scale, s.Transform | ||
| 1487 | - | o.setCursor(r, loadCursor, want, s.Scale, s.Transform) | 1518 | + | o.setCursor(loadCursor, want, s.Scale, s.Transform) |
| 1488 | stateDirty = true | 1519 | stateDirty = true | ||
| 1489 | } | 1520 | } | ||
| 1490 | scene, haveScene, dirty = s, true, true | 1521 | scene, haveScene, dirty = s, true, true | ||
| @@ -1499,7 +1530,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1499 | want = c | 1530 | want = c | ||
| 1500 | // Before the first scene the scale is unknown: loaded then. | 1531 | // Before the first scene the scale is unknown: loaded then. | ||
| 1501 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | 1532 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | ||
| 1502 | - | o.setCursor(r, loadCursor, want, cursorScale, cursorTransform) | 1533 | + | o.setCursor(loadCursor, want, cursorScale, cursorTransform) |
| 1503 | stateDirty = true | 1534 | stateDirty = true | ||
| 1504 | } | 1535 | } | ||
| 1505 | case b := <-contents: | 1536 | case b := <-contents: | ||
| @@ -1736,14 +1767,14 @@ func (o *Output) wakeCore() { corewake.Send(o.Frames, o.conn.name) } | |||||
| 1736 | 1767 | ||||
| 1737 | // setCursor loads a cursor for scale and output transform t into the cursor | 1768 | // setCursor loads a cursor for scale and output transform t into the cursor | ||
| 1738 | // images. | 1769 | // images. | ||
| 1739 | - | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) { | 1770 | + | func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) { |
| 1740 | o.observeSecurity() | 1771 | o.observeSecurity() | ||
| 1741 | if o.protected || o.cursor.off { | 1772 | if o.protected || o.cursor.off { | ||
| 1742 | return | 1773 | return | ||
| 1743 | } | 1774 | } | ||
| 1744 | img, err := load(c, scale, t, o.cursor.Limit()) | 1775 | img, err := load(c, scale, t, o.cursor.Limit()) | ||
| 1745 | if err == nil { | 1776 | if err == nil { | ||
| 1746 | - | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) | 1777 | + | err = o.cursor.setImage(img.Pixels, img.W, img.H, img.HotX, img.HotY, o.cursorNits()) |
| 1747 | } | 1778 | } | ||
| 1748 | if err != nil { | 1779 | if err != nil { | ||
| 1749 | o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor") | 1780 | o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor") | ||
A internal/adapters/hdrcursor/hdrcursor.go +100 -0
| @@ -0,0 +1,100 @@ | |||
| 1 | + | // Package hdrcursor converts cursor images to the HDR10 output encoding | |
| 2 | + | // (BT.2020 PQ) for cursor planes, whichever adapter owns the image memory. | |
| 3 | + | package hdrcursor | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "crypto/sha256" | |
| 7 | + | "encoding/binary" | |
| 8 | + | "math" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | var srgbTable = func() (table [256]float64) { | |
| 12 | + | for i := range table { | |
| 13 | + | table[i] = SRGBToLinear(float64(i) / 255) | |
| 14 | + | } | |
| 15 | + | return | |
| 16 | + | }() | |
| 17 | + | ||
| 18 | + | // SRGBToLinear decodes a unit-range sRGB component. | |
| 19 | + | func SRGBToLinear(v float64) float64 { | |
| 20 | + | if v <= 0.04045 { | |
| 21 | + | return v / 12.92 | |
| 22 | + | } | |
| 23 | + | return math.Pow((v+0.055)/1.055, 2.4) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // PQEncode encodes absolute luminance in nits using SMPTE ST 2084. | |
| 27 | + | func PQEncode(nits float64) float64 { | |
| 28 | + | if nits <= 0 { | |
| 29 | + | return 0 | |
| 30 | + | } | |
| 31 | + | x := math.Pow(min(nits, 10000)/10000, 2610.0/16384) | |
| 32 | + | return math.Pow((3424.0/4096+2413.0/128*x)/(1+2392.0/128*x), 2523.0/32) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Pixel converts premultiplied BGRA bytes in place, preserving alpha. The | |
| 36 | + | // source is quantized to its 8-bit unassociated sRGB value for the LUT. | |
| 37 | + | func Pixel(p []byte, nits float64) { | |
| 38 | + | a := float64(p[3]) / 255 | |
| 39 | + | if a == 0 { | |
| 40 | + | clear(p[:3]) | |
| 41 | + | return | |
| 42 | + | } | |
| 43 | + | index := func(v byte) int { return min(255, int(math.Round(float64(v)/a))) } | |
| 44 | + | r, g, b := srgbTable[index(p[2])], srgbTable[index(p[1])], srgbTable[index(p[0])] | |
| 45 | + | r, g, b = PQEncode((0.627404*r+0.329283*g+0.043313*b)*nits), PQEncode((0.069097*r+0.919540*g+0.011362*b)*nits), PQEncode((0.016391*r+0.088013*g+0.895595*b)*nits) | |
| 46 | + | p[0], p[1], p[2] = byte(math.Round(b*a*255)), byte(math.Round(g*a*255)), byte(math.Round(r*a*255)) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // Cache converts cursor images and keeps only the last result, bounded by | |
| 50 | + | // the cursor plane's maximum side regardless of cursor theme size. The | |
| 51 | + | // zero value is ready; it is not safe for concurrent use. | |
| 52 | + | type Cache struct { | |
| 53 | + | key [32]byte | |
| 54 | + | pixels []byte | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Len is the size of the cached conversion in bytes (0: empty). | |
| 58 | + | func (c *Cache) Len() int { return len(c.pixels) } | |
| 59 | + | ||
| 60 | + | // Key identifies the cached conversion (the zero key: empty). | |
| 61 | + | func (c *Cache) Key() [32]byte { return c.key } | |
| 62 | + | ||
| 63 | + | // Reset drops the cached conversion. | |
| 64 | + | func (c *Cache) Reset() { *c = Cache{} } | |
| 65 | + | ||
| 66 | + | func key(pixels []byte, w, h, side int, nits float64) [32]byte { | |
| 67 | + | var header [32]byte | |
| 68 | + | binary.LittleEndian.PutUint64(header[:8], uint64(w)) | |
| 69 | + | binary.LittleEndian.PutUint64(header[8:16], uint64(h)) | |
| 70 | + | binary.LittleEndian.PutUint64(header[16:24], math.Float64bits(nits)) | |
| 71 | + | binary.LittleEndian.PutUint64(header[24:], uint64(side)) | |
| 72 | + | digest := sha256.New() | |
| 73 | + | _, _ = digest.Write(header[:]) | |
| 74 | + | _, _ = digest.Write(pixels) | |
| 75 | + | var k [32]byte | |
| 76 | + | digest.Sum(k[:0]) | |
| 77 | + | return k | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // Convert returns the w×h premultiplied BGRA image (w*4 per row) cropped | |
| 81 | + | // to side×side and converted for an SDR white of nits, packed without row | |
| 82 | + | // padding: min(w, side) pixels per row, min(h, side) rows. The result is | |
| 83 | + | // owned by the cache and valid until the next call. | |
| 84 | + | func (c *Cache) Convert(pixels []byte, w, h, side int, nits float64) []byte { | |
| 85 | + | pixels = pixels[:w*h*4] | |
| 86 | + | k := key(pixels, w, h, side, nits) | |
| 87 | + | if c.pixels != nil && c.key == k { | |
| 88 | + | return c.pixels | |
| 89 | + | } | |
| 90 | + | cw, ch := min(w, side)*4, min(h, side) | |
| 91 | + | out := make([]byte, cw*ch) | |
| 92 | + | for y := range ch { | |
| 93 | + | copy(out[y*cw:(y+1)*cw], pixels[y*w*4:y*w*4+cw]) | |
| 94 | + | for x := 0; x < cw; x += 4 { | |
| 95 | + | Pixel(out[y*cw+x:y*cw+x+4], nits) | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | c.key, c.pixels = k, out | |
| 99 | + | return out | |
| 100 | + | } | |
| @@ -0,0 +1,100 @@ | |||||
| 1 | + | // Package hdrcursor converts cursor images to the HDR10 output encoding | |||
| 2 | + | // (BT.2020 PQ) for cursor planes, whichever adapter owns the image memory. | |||
| 3 | + | package hdrcursor | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "crypto/sha256" | |||
| 7 | + | "encoding/binary" | |||
| 8 | + | "math" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | var srgbTable = func() (table [256]float64) { | |||
| 12 | + | for i := range table { | |||
| 13 | + | table[i] = SRGBToLinear(float64(i) / 255) | |||
| 14 | + | } | |||
| 15 | + | return | |||
| 16 | + | }() | |||
| 17 | + | ||||
| 18 | + | // SRGBToLinear decodes a unit-range sRGB component. | |||
| 19 | + | func SRGBToLinear(v float64) float64 { | |||
| 20 | + | if v <= 0.04045 { | |||
| 21 | + | return v / 12.92 | |||
| 22 | + | } | |||
| 23 | + | return math.Pow((v+0.055)/1.055, 2.4) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // PQEncode encodes absolute luminance in nits using SMPTE ST 2084. | |||
| 27 | + | func PQEncode(nits float64) float64 { | |||
| 28 | + | if nits <= 0 { | |||
| 29 | + | return 0 | |||
| 30 | + | } | |||
| 31 | + | x := math.Pow(min(nits, 10000)/10000, 2610.0/16384) | |||
| 32 | + | return math.Pow((3424.0/4096+2413.0/128*x)/(1+2392.0/128*x), 2523.0/32) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Pixel converts premultiplied BGRA bytes in place, preserving alpha. The | |||
| 36 | + | // source is quantized to its 8-bit unassociated sRGB value for the LUT. | |||
| 37 | + | func Pixel(p []byte, nits float64) { | |||
| 38 | + | a := float64(p[3]) / 255 | |||
| 39 | + | if a == 0 { | |||
| 40 | + | clear(p[:3]) | |||
| 41 | + | return | |||
| 42 | + | } | |||
| 43 | + | index := func(v byte) int { return min(255, int(math.Round(float64(v)/a))) } | |||
| 44 | + | r, g, b := srgbTable[index(p[2])], srgbTable[index(p[1])], srgbTable[index(p[0])] | |||
| 45 | + | r, g, b = PQEncode((0.627404*r+0.329283*g+0.043313*b)*nits), PQEncode((0.069097*r+0.919540*g+0.011362*b)*nits), PQEncode((0.016391*r+0.088013*g+0.895595*b)*nits) | |||
| 46 | + | p[0], p[1], p[2] = byte(math.Round(b*a*255)), byte(math.Round(g*a*255)), byte(math.Round(r*a*255)) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // Cache converts cursor images and keeps only the last result, bounded by | |||
| 50 | + | // the cursor plane's maximum side regardless of cursor theme size. The | |||
| 51 | + | // zero value is ready; it is not safe for concurrent use. | |||
| 52 | + | type Cache struct { | |||
| 53 | + | key [32]byte | |||
| 54 | + | pixels []byte | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // Len is the size of the cached conversion in bytes (0: empty). | |||
| 58 | + | func (c *Cache) Len() int { return len(c.pixels) } | |||
| 59 | + | ||||
| 60 | + | // Key identifies the cached conversion (the zero key: empty). | |||
| 61 | + | func (c *Cache) Key() [32]byte { return c.key } | |||
| 62 | + | ||||
| 63 | + | // Reset drops the cached conversion. | |||
| 64 | + | func (c *Cache) Reset() { *c = Cache{} } | |||
| 65 | + | ||||
| 66 | + | func key(pixels []byte, w, h, side int, nits float64) [32]byte { | |||
| 67 | + | var header [32]byte | |||
| 68 | + | binary.LittleEndian.PutUint64(header[:8], uint64(w)) | |||
| 69 | + | binary.LittleEndian.PutUint64(header[8:16], uint64(h)) | |||
| 70 | + | binary.LittleEndian.PutUint64(header[16:24], math.Float64bits(nits)) | |||
| 71 | + | binary.LittleEndian.PutUint64(header[24:], uint64(side)) | |||
| 72 | + | digest := sha256.New() | |||
| 73 | + | _, _ = digest.Write(header[:]) | |||
| 74 | + | _, _ = digest.Write(pixels) | |||
| 75 | + | var k [32]byte | |||
| 76 | + | digest.Sum(k[:0]) | |||
| 77 | + | return k | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // Convert returns the w×h premultiplied BGRA image (w*4 per row) cropped | |||
| 81 | + | // to side×side and converted for an SDR white of nits, packed without row | |||
| 82 | + | // padding: min(w, side) pixels per row, min(h, side) rows. The result is | |||
| 83 | + | // owned by the cache and valid until the next call. | |||
| 84 | + | func (c *Cache) Convert(pixels []byte, w, h, side int, nits float64) []byte { | |||
| 85 | + | pixels = pixels[:w*h*4] | |||
| 86 | + | k := key(pixels, w, h, side, nits) | |||
| 87 | + | if c.pixels != nil && c.key == k { | |||
| 88 | + | return c.pixels | |||
| 89 | + | } | |||
| 90 | + | cw, ch := min(w, side)*4, min(h, side) | |||
| 91 | + | out := make([]byte, cw*ch) | |||
| 92 | + | for y := range ch { | |||
| 93 | + | copy(out[y*cw:(y+1)*cw], pixels[y*w*4:y*w*4+cw]) | |||
| 94 | + | for x := 0; x < cw; x += 4 { | |||
| 95 | + | Pixel(out[y*cw+x:y*cw+x+4], nits) | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | c.key, c.pixels = k, out | |||
| 99 | + | return out | |||
| 100 | + | } | |||
A internal/adapters/hdrcursor/hdrcursor_test.go +76 -0
| @@ -0,0 +1,76 @@ | |||
| 1 | + | package hdrcursor | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "math" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // vector is a 5×3 premultiplied BGRA image: alpha ramps up with one fully | |
| 10 | + | // transparent pixel (index 3). | |
| 11 | + | func vector() (pixels []byte, w, h int) { | |
| 12 | + | w, h = 5, 3 | |
| 13 | + | pixels = make([]byte, w*h*4) | |
| 14 | + | for i := range w * h { | |
| 15 | + | a := byte(i * 255 / (w*h - 1)) | |
| 16 | + | if i == 3 { | |
| 17 | + | a = 0 | |
| 18 | + | } | |
| 19 | + | c := func(v int) byte { return byte(v * int(a) / 255) } | |
| 20 | + | pixels[i*4], pixels[i*4+1], pixels[i*4+2], pixels[i*4+3] = c(i*37%256), c((i*91+20)%256), c((i*53+200)%256), a | |
| 21 | + | } | |
| 22 | + | return | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // The conversion is byte-identical to the one the Vulkan renderer did | |
| 26 | + | // before it moved here (203 nits, cropped to 4×4). | |
| 27 | + | func TestConvertMatchesRecordedVector(t *testing.T) { | |
| 28 | + | pixels, w, h := vector() | |
| 29 | + | want := []byte{0x0, 0x0, 0x0, 0x0, 0x5, 0x7, 0x9, 0x12, 0xd, 0x13, 0xf, 0x24, 0x0, 0x0, 0x0, 0x0, 0x2e, 0x31, 0x31, 0x5b, 0x3a, 0x1f, 0x20, 0x6d, 0x20, 0x39, 0x30, 0x7f, 0x33, 0x51, 0x46, 0x91, 0x4c, 0x58, 0x5f, 0xb6, 0x5f, 0x72, 0x5e, 0xc8, 0x6d, 0x4e, 0x4c, 0xda, 0x81, 0x75, 0x6c, 0xec} | |
| 30 | + | var c Cache | |
| 31 | + | if got := c.Convert(pixels, w, h, 4, 203); !bytes.Equal(got, want) { | |
| 32 | + | t.Fatalf("converted\n got %#v\nwant %#v", got, want) | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func TestPixelWhite(t *testing.T) { | |
| 37 | + | p := []byte{255, 255, 255, 255} | |
| 38 | + | Pixel(p, 203) | |
| 39 | + | if p[3] != 255 || math.Abs(float64(p[0])-148) > 1 || p[0] != p[1] || p[1] != p[2] { | |
| 40 | + | t.Fatalf("cursor white: %v", p) | |
| 41 | + | } | |
| 42 | + | if PQEncode(0) != 0 || math.Abs(PQEncode(10000)-1) > 1e-9 { | |
| 43 | + | t.Fatal("PQ endpoints") | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func TestCacheKeepsLastConversion(t *testing.T) { | |
| 48 | + | var c Cache | |
| 49 | + | px := []byte{255, 255, 255, 255} | |
| 50 | + | a := c.Convert(px, 1, 1, 64, 203) | |
| 51 | + | if c.Len() != 4 || a[0] != 148 { | |
| 52 | + | t.Fatalf("len %d pixel %v", c.Len(), a) | |
| 53 | + | } | |
| 54 | + | first := c.Key() | |
| 55 | + | if b := c.Convert(px, 1, 1, 64, 203); &a[0] != &b[0] { | |
| 56 | + | t.Fatal("same image converted again") | |
| 57 | + | } | |
| 58 | + | for i := range 12 { | |
| 59 | + | v := byte(i * 18) | |
| 60 | + | c.Convert([]byte{v, v, v, 255}, 1, 1, 64, 203) | |
| 61 | + | if c.Len() != 4 { | |
| 62 | + | t.Fatalf("cache grew after image %d: %d", i, c.Len()) | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | if c.Key() == first { | |
| 66 | + | t.Fatal("conversion not replaced") | |
| 67 | + | } | |
| 68 | + | c.Convert(px, 1, 1, 64, 100) | |
| 69 | + | if c.Key() == first { | |
| 70 | + | t.Fatal("nits not part of the key") | |
| 71 | + | } | |
| 72 | + | c.Reset() | |
| 73 | + | if c.Len() != 0 || c.Key() != [32]byte{} { | |
| 74 | + | t.Fatal("reset left a conversion") | |
| 75 | + | } | |
| 76 | + | } | |
| @@ -0,0 +1,76 @@ | |||||
| 1 | + | package hdrcursor | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "math" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // vector is a 5×3 premultiplied BGRA image: alpha ramps up with one fully | |||
| 10 | + | // transparent pixel (index 3). | |||
| 11 | + | func vector() (pixels []byte, w, h int) { | |||
| 12 | + | w, h = 5, 3 | |||
| 13 | + | pixels = make([]byte, w*h*4) | |||
| 14 | + | for i := range w * h { | |||
| 15 | + | a := byte(i * 255 / (w*h - 1)) | |||
| 16 | + | if i == 3 { | |||
| 17 | + | a = 0 | |||
| 18 | + | } | |||
| 19 | + | c := func(v int) byte { return byte(v * int(a) / 255) } | |||
| 20 | + | pixels[i*4], pixels[i*4+1], pixels[i*4+2], pixels[i*4+3] = c(i*37%256), c((i*91+20)%256), c((i*53+200)%256), a | |||
| 21 | + | } | |||
| 22 | + | return | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // The conversion is byte-identical to the one the Vulkan renderer did | |||
| 26 | + | // before it moved here (203 nits, cropped to 4×4). | |||
| 27 | + | func TestConvertMatchesRecordedVector(t *testing.T) { | |||
| 28 | + | pixels, w, h := vector() | |||
| 29 | + | want := []byte{0x0, 0x0, 0x0, 0x0, 0x5, 0x7, 0x9, 0x12, 0xd, 0x13, 0xf, 0x24, 0x0, 0x0, 0x0, 0x0, 0x2e, 0x31, 0x31, 0x5b, 0x3a, 0x1f, 0x20, 0x6d, 0x20, 0x39, 0x30, 0x7f, 0x33, 0x51, 0x46, 0x91, 0x4c, 0x58, 0x5f, 0xb6, 0x5f, 0x72, 0x5e, 0xc8, 0x6d, 0x4e, 0x4c, 0xda, 0x81, 0x75, 0x6c, 0xec} | |||
| 30 | + | var c Cache | |||
| 31 | + | if got := c.Convert(pixels, w, h, 4, 203); !bytes.Equal(got, want) { | |||
| 32 | + | t.Fatalf("converted\n got %#v\nwant %#v", got, want) | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func TestPixelWhite(t *testing.T) { | |||
| 37 | + | p := []byte{255, 255, 255, 255} | |||
| 38 | + | Pixel(p, 203) | |||
| 39 | + | if p[3] != 255 || math.Abs(float64(p[0])-148) > 1 || p[0] != p[1] || p[1] != p[2] { | |||
| 40 | + | t.Fatalf("cursor white: %v", p) | |||
| 41 | + | } | |||
| 42 | + | if PQEncode(0) != 0 || math.Abs(PQEncode(10000)-1) > 1e-9 { | |||
| 43 | + | t.Fatal("PQ endpoints") | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func TestCacheKeepsLastConversion(t *testing.T) { | |||
| 48 | + | var c Cache | |||
| 49 | + | px := []byte{255, 255, 255, 255} | |||
| 50 | + | a := c.Convert(px, 1, 1, 64, 203) | |||
| 51 | + | if c.Len() != 4 || a[0] != 148 { | |||
| 52 | + | t.Fatalf("len %d pixel %v", c.Len(), a) | |||
| 53 | + | } | |||
| 54 | + | first := c.Key() | |||
| 55 | + | if b := c.Convert(px, 1, 1, 64, 203); &a[0] != &b[0] { | |||
| 56 | + | t.Fatal("same image converted again") | |||
| 57 | + | } | |||
| 58 | + | for i := range 12 { | |||
| 59 | + | v := byte(i * 18) | |||
| 60 | + | c.Convert([]byte{v, v, v, 255}, 1, 1, 64, 203) | |||
| 61 | + | if c.Len() != 4 { | |||
| 62 | + | t.Fatalf("cache grew after image %d: %d", i, c.Len()) | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | if c.Key() == first { | |||
| 66 | + | t.Fatal("conversion not replaced") | |||
| 67 | + | } | |||
| 68 | + | c.Convert(px, 1, 1, 64, 100) | |||
| 69 | + | if c.Key() == first { | |||
| 70 | + | t.Fatal("nits not part of the key") | |||
| 71 | + | } | |||
| 72 | + | c.Reset() | |||
| 73 | + | if c.Len() != 0 || c.Key() != [32]byte{} { | |||
| 74 | + | t.Fatal("reset left a conversion") | |||
| 75 | + | } | |||
| 76 | + | } | |||
M internal/adapters/vulkan/cursor.go +5 -42
| @@ -1,11 +1,8 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "crypto/sha256" | |
| 5 | - | "encoding/binary" | |
| 6 | 4 | "errors" | |
| 7 | 5 | "fmt" | |
| 8 | - | "math" | |
| 9 | 6 | "os" | |
| 10 | 7 | "unsafe" | |
| 11 | 8 | ||
| @@ -108,26 +105,6 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) { | |||
| 108 | 105 | return c, ports.DMABuf{Width: size, Height: size, Format: fourccARGB, Modifier: 0, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}, nil | |
| 109 | 106 | } | |
| 110 | 107 | ||
| 111 | - | // cursorConversion keeps only the last converted image, bounded by the | |
| 112 | - | // cursor plane's maximum dimensions, regardless of cursor theme size. | |
| 113 | - | type cursorConversion struct { | |
| 114 | - | key [32]byte | |
| 115 | - | pixels []byte | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | func cursorKey(pixels []byte, w, h int, nits float64) [32]byte { | |
| 119 | - | var header [24]byte | |
| 120 | - | binary.LittleEndian.PutUint64(header[:8], uint64(w)) | |
| 121 | - | binary.LittleEndian.PutUint64(header[8:16], uint64(h)) | |
| 122 | - | binary.LittleEndian.PutUint64(header[16:], math.Float64bits(nits)) | |
| 123 | - | digest := sha256.New() | |
| 124 | - | _, _ = digest.Write(header[:]) | |
| 125 | - | _, _ = digest.Write(pixels) | |
| 126 | - | var key [32]byte | |
| 127 | - | digest.Sum(key[:0]) | |
| 128 | - | return key | |
| 129 | - | } | |
| 130 | - | ||
| 131 | 108 | // WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row) | |
| 132 | 109 | // into cursor image i at its row pitch, cropped and cleared around. | |
| 133 | 110 | func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | |
| @@ -141,27 +118,13 @@ func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | |||
| 141 | 118 | mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) | |
| 142 | 119 | clear(mem) | |
| 143 | 120 | cw := min(w, c.size) * 4 | |
| 121 | + | src, stride := pixels, w*4 | |
| 144 | 122 | if r.hdrNits > 0 { | |
| 145 | 123 | // WriteCursor is called only on image changes, never on moves. | |
| 146 | - | key := cursorKey(pixels[:w*h*4], w, h, r.hdrNits) | |
| 147 | - | converted := r.cursorCache.pixels | |
| 148 | - | if converted == nil || r.cursorCache.key != key { | |
| 149 | - | converted = make([]byte, cw*min(h, c.size)) | |
| 150 | - | for y := range min(h, c.size) { | |
| 151 | - | copy(converted[y*cw:(y+1)*cw], pixels[y*w*4:y*w*4+cw]) | |
| 152 | - | for x := 0; x < cw; x += 4 { | |
| 153 | - | hdrCursorPixel(converted[y*cw+x:y*cw+x+4], r.hdrNits) | |
| 154 | - | } | |
| 155 | - | } | |
| 156 | - | r.cursorCache = cursorConversion{key: key, pixels: converted} | |
| 157 | - | } | |
| 158 | - | for y := range min(h, c.size) { | |
| 159 | - | copy(mem[y*c.pitch:y*c.pitch+cw], converted[y*cw:(y+1)*cw]) | |
| 160 | - | } | |
| 161 | - | } else { | |
| 162 | - | for y := range min(h, c.size) { | |
| 163 | - | copy(mem[y*c.pitch:y*c.pitch+cw], pixels[y*w*4:y*w*4+cw]) | |
| 164 | - | } | |
| 124 | + | src, stride = r.cursorCache.Convert(pixels, w, h, c.size, r.hdrNits), cw | |
| 125 | + | } | |
| 126 | + | for y := range min(h, c.size) { | |
| 127 | + | copy(mem[y*c.pitch:y*c.pitch+cw], src[y*stride:y*stride+cw]) | |
| 165 | 128 | } | |
| 166 | 129 | return nil | |
| 167 | 130 | } | |
| @@ -1,11 +1,8 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "crypto/sha256" | |||
| 5 | - | "encoding/binary" | |||
| 6 | "errors" | 4 | "errors" | ||
| 7 | "fmt" | 5 | "fmt" | ||
| 8 | - | "math" | |||
| 9 | "os" | 6 | "os" | ||
| 10 | "unsafe" | 7 | "unsafe" | ||
| 11 | 8 | ||||
| @@ -108,26 +105,6 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) { | |||||
| 108 | return c, ports.DMABuf{Width: size, Height: size, Format: fourccARGB, Modifier: 0, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}, nil | 105 | return c, ports.DMABuf{Width: size, Height: size, Format: fourccARGB, Modifier: 0, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}, nil | ||
| 109 | } | 106 | } | ||
| 110 | 107 | ||||
| 111 | - | // cursorConversion keeps only the last converted image, bounded by the | |||
| 112 | - | // cursor plane's maximum dimensions, regardless of cursor theme size. | |||
| 113 | - | type cursorConversion struct { | |||
| 114 | - | key [32]byte | |||
| 115 | - | pixels []byte | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | func cursorKey(pixels []byte, w, h int, nits float64) [32]byte { | |||
| 119 | - | var header [24]byte | |||
| 120 | - | binary.LittleEndian.PutUint64(header[:8], uint64(w)) | |||
| 121 | - | binary.LittleEndian.PutUint64(header[8:16], uint64(h)) | |||
| 122 | - | binary.LittleEndian.PutUint64(header[16:], math.Float64bits(nits)) | |||
| 123 | - | digest := sha256.New() | |||
| 124 | - | _, _ = digest.Write(header[:]) | |||
| 125 | - | _, _ = digest.Write(pixels) | |||
| 126 | - | var key [32]byte | |||
| 127 | - | digest.Sum(key[:0]) | |||
| 128 | - | return key | |||
| 129 | - | } | |||
| 130 | - | ||||
| 131 | // WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row) | 108 | // WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row) | ||
| 132 | // into cursor image i at its row pitch, cropped and cleared around. | 109 | // into cursor image i at its row pitch, cropped and cleared around. | ||
| 133 | func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | 110 | func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | ||
| @@ -141,27 +118,13 @@ func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | |||||
| 141 | mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) | 118 | mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) | ||
| 142 | clear(mem) | 119 | clear(mem) | ||
| 143 | cw := min(w, c.size) * 4 | 120 | cw := min(w, c.size) * 4 | ||
| 121 | + | src, stride := pixels, w*4 | |||
| 144 | if r.hdrNits > 0 { | 122 | if r.hdrNits > 0 { | ||
| 145 | // WriteCursor is called only on image changes, never on moves. | 123 | // WriteCursor is called only on image changes, never on moves. | ||
| 146 | - | key := cursorKey(pixels[:w*h*4], w, h, r.hdrNits) | 124 | + | src, stride = r.cursorCache.Convert(pixels, w, h, c.size, r.hdrNits), cw |
| 147 | - | converted := r.cursorCache.pixels | 125 | + | } |
| 148 | - | if converted == nil || r.cursorCache.key != key { | 126 | + | for y := range min(h, c.size) { |
| 149 | - | converted = make([]byte, cw*min(h, c.size)) | 127 | + | copy(mem[y*c.pitch:y*c.pitch+cw], src[y*stride:y*stride+cw]) |
| 150 | - | for y := range min(h, c.size) { | |||
| 151 | - | copy(converted[y*cw:(y+1)*cw], pixels[y*w*4:y*w*4+cw]) | |||
| 152 | - | for x := 0; x < cw; x += 4 { | |||
| 153 | - | hdrCursorPixel(converted[y*cw+x:y*cw+x+4], r.hdrNits) | |||
| 154 | - | } | |||
| 155 | - | } | |||
| 156 | - | r.cursorCache = cursorConversion{key: key, pixels: converted} | |||
| 157 | - | } | |||
| 158 | - | for y := range min(h, c.size) { | |||
| 159 | - | copy(mem[y*c.pitch:y*c.pitch+cw], converted[y*cw:(y+1)*cw]) | |||
| 160 | - | } | |||
| 161 | - | } else { | |||
| 162 | - | for y := range min(h, c.size) { | |||
| 163 | - | copy(mem[y*c.pitch:y*c.pitch+cw], pixels[y*w*4:y*w*4+cw]) | |||
| 164 | - | } | |||
| 165 | } | 128 | } | ||
| 166 | return nil | 129 | return nil | ||
| 167 | } | 130 | } | ||
M internal/adapters/vulkan/cursor_test.go +10 -10
| @@ -65,26 +65,26 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||
| 65 | 65 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | |
| 66 | 66 | t.Fatal(err) | |
| 67 | 67 | } | |
| 68 | - | if len(r.cursorCache.pixels) != 4 { | |
| 69 | - | t.Fatalf("cache size %d", len(r.cursorCache.pixels)) | |
| 68 | + | if r.cursorCache.Len() != 4 { | |
| 69 | + | t.Fatalf("cache size %d", r.cursorCache.Len()) | |
| 70 | 70 | } | |
| 71 | 71 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |
| 72 | 72 | t.Fatal(err) | |
| 73 | 73 | } | |
| 74 | - | if len(r.cursorCache.pixels) != 4 { | |
| 75 | - | t.Fatalf("cache grew on reselect: %d", len(r.cursorCache.pixels)) | |
| 74 | + | if r.cursorCache.Len() != 4 { | |
| 75 | + | t.Fatalf("cache grew on reselect: %d", r.cursorCache.Len()) | |
| 76 | 76 | } | |
| 77 | - | first := r.cursorCache.key | |
| 77 | + | first := r.cursorCache.Key() | |
| 78 | 78 | for i := 0; i < 12; i++ { | |
| 79 | 79 | color := byte(i * 18) | |
| 80 | 80 | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | |
| 81 | 81 | t.Fatal(err) | |
| 82 | 82 | } | |
| 83 | - | if len(r.cursorCache.pixels) != 4 { | |
| 84 | - | t.Fatalf("cache grew after image %d: %d", i, len(r.cursorCache.pixels)) | |
| 83 | + | if r.cursorCache.Len() != 4 { | |
| 84 | + | t.Fatalf("cache grew after image %d: %d", i, r.cursorCache.Len()) | |
| 85 | 85 | } | |
| 86 | 86 | } | |
| 87 | - | if first == r.cursorCache.key { | |
| 87 | + | if first == r.cursorCache.Key() { | |
| 88 | 88 | t.Fatal("cursor conversion not replaced") | |
| 89 | 89 | } | |
| 90 | 90 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |
| @@ -100,7 +100,7 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||
| 100 | 100 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | |
| 101 | 101 | t.Fatal(err) | |
| 102 | 102 | } | |
| 103 | - | if got := unsafe.Slice((*byte)(r.cursors[0].mapped), 4); got[0] != 255 || len(r.cursorCache.pixels) != 0 { | |
| 104 | - | t.Fatalf("SDR cursor/cache: %v %d", got, len(r.cursorCache.pixels)) | |
| 103 | + | if got := unsafe.Slice((*byte)(r.cursors[0].mapped), 4); got[0] != 255 || r.cursorCache.Len() != 0 { | |
| 104 | + | t.Fatalf("SDR cursor/cache: %v %d", got, r.cursorCache.Len()) | |
| 105 | 105 | } | |
| 106 | 106 | } | |
| @@ -65,26 +65,26 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||||
| 65 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | 65 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | ||
| 66 | t.Fatal(err) | 66 | t.Fatal(err) | ||
| 67 | } | 67 | } | ||
| 68 | - | if len(r.cursorCache.pixels) != 4 { | 68 | + | if r.cursorCache.Len() != 4 { |
| 69 | - | t.Fatalf("cache size %d", len(r.cursorCache.pixels)) | 69 | + | t.Fatalf("cache size %d", r.cursorCache.Len()) |
| 70 | } | 70 | } | ||
| 71 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | 71 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | ||
| 72 | t.Fatal(err) | 72 | t.Fatal(err) | ||
| 73 | } | 73 | } | ||
| 74 | - | if len(r.cursorCache.pixels) != 4 { | 74 | + | if r.cursorCache.Len() != 4 { |
| 75 | - | t.Fatalf("cache grew on reselect: %d", len(r.cursorCache.pixels)) | 75 | + | t.Fatalf("cache grew on reselect: %d", r.cursorCache.Len()) |
| 76 | } | 76 | } | ||
| 77 | - | first := r.cursorCache.key | 77 | + | first := r.cursorCache.Key() |
| 78 | for i := 0; i < 12; i++ { | 78 | for i := 0; i < 12; i++ { | ||
| 79 | color := byte(i * 18) | 79 | color := byte(i * 18) | ||
| 80 | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | 80 | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | ||
| 81 | t.Fatal(err) | 81 | t.Fatal(err) | ||
| 82 | } | 82 | } | ||
| 83 | - | if len(r.cursorCache.pixels) != 4 { | 83 | + | if r.cursorCache.Len() != 4 { |
| 84 | - | t.Fatalf("cache grew after image %d: %d", i, len(r.cursorCache.pixels)) | 84 | + | t.Fatalf("cache grew after image %d: %d", i, r.cursorCache.Len()) |
| 85 | } | 85 | } | ||
| 86 | } | 86 | } | ||
| 87 | - | if first == r.cursorCache.key { | 87 | + | if first == r.cursorCache.Key() { |
| 88 | t.Fatal("cursor conversion not replaced") | 88 | t.Fatal("cursor conversion not replaced") | ||
| 89 | } | 89 | } | ||
| 90 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | 90 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | ||
| @@ -100,7 +100,7 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||||
| 100 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | 100 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | ||
| 101 | t.Fatal(err) | 101 | t.Fatal(err) | ||
| 102 | } | 102 | } | ||
| 103 | - | if got := unsafe.Slice((*byte)(r.cursors[0].mapped), 4); got[0] != 255 || len(r.cursorCache.pixels) != 0 { | 103 | + | if got := unsafe.Slice((*byte)(r.cursors[0].mapped), 4); got[0] != 255 || r.cursorCache.Len() != 0 { |
| 104 | - | t.Fatalf("SDR cursor/cache: %v %d", got, len(r.cursorCache.pixels)) | 104 | + | t.Fatalf("SDR cursor/cache: %v %d", got, r.cursorCache.Len()) |
| 105 | } | 105 | } | ||
| 106 | } | 106 | } | ||
M internal/adapters/vulkan/hdr.go +2 -2
| @@ -28,7 +28,7 @@ type hdrPass struct { | |||
| 28 | 28 | // remains owner-goroutine confined; all GPU objects are reused each frame. | |
| 29 | 29 | func (r *Renderer) SetHDR(nits float64) { | |
| 30 | 30 | if nits != r.hdrNits { | |
| 31 | - | r.cursorCache = cursorConversion{} | |
| 31 | + | r.cursorCache.Reset() | |
| 32 | 32 | if r.hdrNits > 0 && nits > 0 { | |
| 33 | 33 | r.hdrOwn.valid = false | |
| 34 | 34 | } | |
| @@ -47,7 +47,7 @@ func (r *Renderer) SetHDR(nits float64) { | |||
| 47 | 47 | } | |
| 48 | 48 | r.hdrNits = nits | |
| 49 | 49 | if nits <= 0 { | |
| 50 | - | r.cursorCache = cursorConversion{} | |
| 50 | + | r.cursorCache.Reset() | |
| 51 | 51 | r.hdrError = nil | |
| 52 | 52 | return | |
| 53 | 53 | } | |
| @@ -28,7 +28,7 @@ type hdrPass struct { | |||||
| 28 | // remains owner-goroutine confined; all GPU objects are reused each frame. | 28 | // remains owner-goroutine confined; all GPU objects are reused each frame. | ||
| 29 | func (r *Renderer) SetHDR(nits float64) { | 29 | func (r *Renderer) SetHDR(nits float64) { | ||
| 30 | if nits != r.hdrNits { | 30 | if nits != r.hdrNits { | ||
| 31 | - | r.cursorCache = cursorConversion{} | 31 | + | r.cursorCache.Reset() |
| 32 | if r.hdrNits > 0 && nits > 0 { | 32 | if r.hdrNits > 0 && nits > 0 { | ||
| 33 | r.hdrOwn.valid = false | 33 | r.hdrOwn.valid = false | ||
| 34 | } | 34 | } | ||
| @@ -47,7 +47,7 @@ func (r *Renderer) SetHDR(nits float64) { | |||||
| 47 | } | 47 | } | ||
| 48 | r.hdrNits = nits | 48 | r.hdrNits = nits | ||
| 49 | if nits <= 0 { | 49 | if nits <= 0 { | ||
| 50 | - | r.cursorCache = cursorConversion{} | 50 | + | r.cursorCache.Reset() |
| 51 | r.hdrError = nil | 51 | r.hdrError = nil | ||
| 52 | return | 52 | return | ||
| 53 | } | 53 | } | ||
M internal/adapters/vulkan/pq.go +6 -34
| @@ -1,21 +1,13 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | - | import "math" | |
| 3 | + | import ( | |
| 4 | + | "math" | |
| 4 | 5 | ||
| 5 | - | var srgbTable = func() (table [256]float64) { | |
| 6 | - | for i := range table { | |
| 7 | - | table[i] = srgbToLinear(float64(i) / 255) | |
| 8 | - | } | |
| 9 | - | return | |
| 10 | - | }() | |
| 6 | + | "github.com/bnema/neferwl/internal/adapters/hdrcursor" | |
| 7 | + | ) | |
| 11 | 8 | ||
| 12 | 9 | // srgbToLinear decodes a unit-range sRGB component. | |
| 13 | - | func srgbToLinear(v float64) float64 { | |
| 14 | - | if v <= 0.04045 { | |
| 15 | - | return v / 12.92 | |
| 16 | - | } | |
| 17 | - | return math.Pow((v+0.055)/1.055, 2.4) | |
| 18 | - | } | |
| 10 | + | func srgbToLinear(v float64) float64 { return hdrcursor.SRGBToLinear(v) } | |
| 19 | 11 | ||
| 20 | 12 | // linearToSRGB encodes a unit-range linear component; the inverse of | |
| 21 | 13 | // srgbToLinear. | |
| @@ -27,13 +19,7 @@ func linearToSRGB(v float64) float64 { | |||
| 27 | 19 | } | |
| 28 | 20 | ||
| 29 | 21 | // pqEncode encodes absolute luminance in nits using SMPTE ST 2084. | |
| 30 | - | func pqEncode(nits float64) float64 { | |
| 31 | - | if nits <= 0 { | |
| 32 | - | return 0 | |
| 33 | - | } | |
| 34 | - | x := math.Pow(min(nits, 10000)/10000, 2610.0/16384) | |
| 35 | - | return math.Pow((3424.0/4096+2413.0/128*x)/(1+2392.0/128*x), 2523.0/32) | |
| 36 | - | } | |
| 22 | + | func pqEncode(nits float64) float64 { return hdrcursor.PQEncode(nits) } | |
| 37 | 23 | ||
| 38 | 24 | // hdrPixel transforms an unassociated sRGB triplet into BT.2020 PQ. | |
| 39 | 25 | func hdrPixel(red, green, blue, nits float64) (float64, float64, float64) { | |
| @@ -42,17 +28,3 @@ func hdrPixel(red, green, blue, nits float64) (float64, float64, float64) { | |||
| 42 | 28 | pqEncode((0.069097*r + 0.919540*g + 0.011362*b) * nits), | |
| 43 | 29 | pqEncode((0.016391*r + 0.088013*g + 0.895595*b) * nits) | |
| 44 | 30 | } | |
| 45 | - | ||
| 46 | - | // hdrCursorPixel converts premultiplied BGRA bytes, preserving alpha. | |
| 47 | - | // The source is quantized to its 8-bit unassociated sRGB value for the LUT. | |
| 48 | - | func hdrCursorPixel(p []byte, nits float64) { | |
| 49 | - | a := float64(p[3]) / 255 | |
| 50 | - | if a == 0 { | |
| 51 | - | clear(p[:3]) | |
| 52 | - | return | |
| 53 | - | } | |
| 54 | - | index := func(v byte) int { return min(255, int(math.Round(float64(v)/a))) } | |
| 55 | - | r, g, b := srgbTable[index(p[2])], srgbTable[index(p[1])], srgbTable[index(p[0])] | |
| 56 | - | r, g, b = pqEncode((0.627404*r+0.329283*g+0.043313*b)*nits), pqEncode((0.069097*r+0.919540*g+0.011362*b)*nits), pqEncode((0.016391*r+0.088013*g+0.895595*b)*nits) | |
| 57 | - | p[0], p[1], p[2] = byte(math.Round(b*a*255)), byte(math.Round(g*a*255)), byte(math.Round(r*a*255)) | |
| 58 | - | } | |
| @@ -1,21 +1,13 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | - | import "math" | 3 | + | import ( |
| 4 | + | "math" | |||
| 4 | 5 | ||||
| 5 | - | var srgbTable = func() (table [256]float64) { | 6 | + | "github.com/bnema/neferwl/internal/adapters/hdrcursor" |
| 6 | - | for i := range table { | 7 | + | ) |
| 7 | - | table[i] = srgbToLinear(float64(i) / 255) | |||
| 8 | - | } | |||
| 9 | - | return | |||
| 10 | - | }() | |||
| 11 | 8 | ||||
| 12 | // srgbToLinear decodes a unit-range sRGB component. | 9 | // srgbToLinear decodes a unit-range sRGB component. | ||
| 13 | - | func srgbToLinear(v float64) float64 { | 10 | + | func srgbToLinear(v float64) float64 { return hdrcursor.SRGBToLinear(v) } |
| 14 | - | if v <= 0.04045 { | |||
| 15 | - | return v / 12.92 | |||
| 16 | - | } | |||
| 17 | - | return math.Pow((v+0.055)/1.055, 2.4) | |||
| 18 | - | } | |||
| 19 | 11 | ||||
| 20 | // linearToSRGB encodes a unit-range linear component; the inverse of | 12 | // linearToSRGB encodes a unit-range linear component; the inverse of | ||
| 21 | // srgbToLinear. | 13 | // srgbToLinear. | ||
| @@ -27,13 +19,7 @@ func linearToSRGB(v float64) float64 { | |||||
| 27 | } | 19 | } | ||
| 28 | 20 | ||||
| 29 | // pqEncode encodes absolute luminance in nits using SMPTE ST 2084. | 21 | // pqEncode encodes absolute luminance in nits using SMPTE ST 2084. | ||
| 30 | - | func pqEncode(nits float64) float64 { | 22 | + | func pqEncode(nits float64) float64 { return hdrcursor.PQEncode(nits) } |
| 31 | - | if nits <= 0 { | |||
| 32 | - | return 0 | |||
| 33 | - | } | |||
| 34 | - | x := math.Pow(min(nits, 10000)/10000, 2610.0/16384) | |||
| 35 | - | return math.Pow((3424.0/4096+2413.0/128*x)/(1+2392.0/128*x), 2523.0/32) | |||
| 36 | - | } | |||
| 37 | 23 | ||||
| 38 | // hdrPixel transforms an unassociated sRGB triplet into BT.2020 PQ. | 24 | // hdrPixel transforms an unassociated sRGB triplet into BT.2020 PQ. | ||
| 39 | func hdrPixel(red, green, blue, nits float64) (float64, float64, float64) { | 25 | func hdrPixel(red, green, blue, nits float64) (float64, float64, float64) { | ||
| @@ -42,17 +28,3 @@ func hdrPixel(red, green, blue, nits float64) (float64, float64, float64) { | |||||
| 42 | pqEncode((0.069097*r + 0.919540*g + 0.011362*b) * nits), | 28 | pqEncode((0.069097*r + 0.919540*g + 0.011362*b) * nits), | ||
| 43 | pqEncode((0.016391*r + 0.088013*g + 0.895595*b) * nits) | 29 | pqEncode((0.016391*r + 0.088013*g + 0.895595*b) * nits) | ||
| 44 | } | 30 | } | ||
| 45 | - | ||||
| 46 | - | // hdrCursorPixel converts premultiplied BGRA bytes, preserving alpha. | |||
| 47 | - | // The source is quantized to its 8-bit unassociated sRGB value for the LUT. | |||
| 48 | - | func hdrCursorPixel(p []byte, nits float64) { | |||
| 49 | - | a := float64(p[3]) / 255 | |||
| 50 | - | if a == 0 { | |||
| 51 | - | clear(p[:3]) | |||
| 52 | - | return | |||
| 53 | - | } | |||
| 54 | - | index := func(v byte) int { return min(255, int(math.Round(float64(v)/a))) } | |||
| 55 | - | r, g, b := srgbTable[index(p[2])], srgbTable[index(p[1])], srgbTable[index(p[0])] | |||
| 56 | - | r, g, b = pqEncode((0.627404*r+0.329283*g+0.043313*b)*nits), pqEncode((0.069097*r+0.919540*g+0.011362*b)*nits), pqEncode((0.016391*r+0.088013*g+0.895595*b)*nits) | |||
| 57 | - | p[0], p[1], p[2] = byte(math.Round(b*a*255)), byte(math.Round(g*a*255)), byte(math.Round(r*a*255)) | |||
| 58 | - | } | |||
M internal/adapters/vulkan/pq_test.go +0 -5
| @@ -13,9 +13,4 @@ func TestPQMath(t *testing.T) { | |||
| 13 | 13 | if pqEncode(0) != 0 || math.Abs(pqEncode(10000)-1) > 1e-9 { | |
| 14 | 14 | t.Fatal("PQ endpoints") | |
| 15 | 15 | } | |
| 16 | - | p := []byte{255, 255, 255, 255} | |
| 17 | - | hdrCursorPixel(p, 203) | |
| 18 | - | if p[3] != 255 || math.Abs(float64(p[0])-148) > 1 || p[0] != p[1] || p[1] != p[2] { | |
| 19 | - | t.Fatalf("cursor white: %v", p) | |
| 20 | - | } | |
| 21 | 16 | } | |
| @@ -13,9 +13,4 @@ func TestPQMath(t *testing.T) { | |||||
| 13 | if pqEncode(0) != 0 || math.Abs(pqEncode(10000)-1) > 1e-9 { | 13 | if pqEncode(0) != 0 || math.Abs(pqEncode(10000)-1) > 1e-9 { | ||
| 14 | t.Fatal("PQ endpoints") | 14 | t.Fatal("PQ endpoints") | ||
| 15 | } | 15 | } | ||
| 16 | - | p := []byte{255, 255, 255, 255} | |||
| 17 | - | hdrCursorPixel(p, 203) | |||
| 18 | - | if p[3] != 255 || math.Abs(float64(p[0])-148) > 1 || p[0] != p[1] || p[1] != p[2] { | |||
| 19 | - | t.Fatalf("cursor white: %v", p) | |||
| 20 | - | } | |||
| 21 | } | 16 | } | ||
M internal/adapters/vulkan/renderer.go +2 -1
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "strconv" | |
| 10 | 10 | "unsafe" | |
| 11 | 11 | ||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/hdrcursor" | |
| 12 | 13 | "github.com/bnema/neferwl/internal/adapters/hintlabel" | |
| 13 | 14 | "github.com/bnema/neferwl/internal/ports" | |
| 14 | 15 | "github.com/bnema/zerowrap" | |
| @@ -55,7 +56,7 @@ type Renderer struct { | |||
| 55 | 56 | virtual bool | |
| 56 | 57 | physical vk.PhysicalDevice | |
| 57 | 58 | // One converted cursor image is kept; changes replace it in bounded memory. | |
| 58 | - | cursorCache cursorConversion | |
| 59 | + | cursorCache hdrcursor.Cache | |
| 59 | 60 | // lastHDRTarget is the index in targets of the exported PQ target the | |
| 60 | 61 | // last HDR frame rendered into; -1 when the current targets hold none. | |
| 61 | 62 | lastHDRTarget int | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "strconv" | 9 | "strconv" | ||
| 10 | "unsafe" | 10 | "unsafe" | ||
| 11 | 11 | ||||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/hdrcursor" | |||
| 12 | "github.com/bnema/neferwl/internal/adapters/hintlabel" | 13 | "github.com/bnema/neferwl/internal/adapters/hintlabel" | ||
| 13 | "github.com/bnema/neferwl/internal/ports" | 14 | "github.com/bnema/neferwl/internal/ports" | ||
| 14 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| @@ -55,7 +56,7 @@ type Renderer struct { | |||||
| 55 | virtual bool | 56 | virtual bool | ||
| 56 | physical vk.PhysicalDevice | 57 | physical vk.PhysicalDevice | ||
| 57 | // One converted cursor image is kept; changes replace it in bounded memory. | 58 | // One converted cursor image is kept; changes replace it in bounded memory. | ||
| 58 | - | cursorCache cursorConversion | 59 | + | cursorCache hdrcursor.Cache |
| 59 | // lastHDRTarget is the index in targets of the exported PQ target the | 60 | // lastHDRTarget is the index in targets of the exported PQ target the | ||
| 60 | // last HDR frame rendered into; -1 when the current targets hold none. | 61 | // last HDR frame rendered into; -1 when the current targets hold none. | ||
| 61 | lastHDRTarget int | 62 | lastHDRTarget int | ||