Merge branch 'fix/nvidia-cursor-plane' into main (!26)
fix(drm): allocate hardware cursor images as KMS dumb buffers
23 files · +1738 -165
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 +491 -0
| @@ -0,0 +1,491 @@ | |||
| 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(t *testing.T) [2]ports.DMABuf { | |
| 40 | + | var b [2]ports.DMABuf | |
| 41 | + | for i := range b { | |
| 42 | + | f, w, err := os.Pipe() | |
| 43 | + | if err != nil { | |
| 44 | + | t.Fatal(err) | |
| 45 | + | } | |
| 46 | + | t.Cleanup(func() { _ = f.Close(); _ = w.Close() }) | |
| 47 | + | if err := w.Close(); err != nil { | |
| 48 | + | t.Fatal(err) | |
| 49 | + | } | |
| 50 | + | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 51 | + | } | |
| 52 | + | return b | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) { | |
| 56 | + | b := rendererBufs(t) | |
| 57 | + | r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once() | |
| 58 | + | t.Cleanup(func() { assertPlanesClosed(t, b) }) | |
| 59 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |
| 60 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Dumb buffers come first: the renderer allocates nothing. | |
| 64 | + | func TestCursorSetupUsesDumbBuffers(t *testing.T) { | |
| 65 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 66 | + | dumbOK(k, tSide*4) | |
| 67 | + | c := newCursor(&plane{}, tSide) | |
| 68 | + | dumbErr, err := c.setup(k, r) | |
| 69 | + | if dumbErr != nil || err != nil { | |
| 70 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |
| 71 | + | } | |
| 72 | + | if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} { | |
| 73 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |
| 74 | + | } | |
| 75 | + | dumbFree(k) | |
| 76 | + | c.free() | |
| 77 | + | if c.imgs != nil || c.fbs != [2]uint32{} { | |
| 78 | + | t.Fatal("images kept after free") | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // A dumb buffer that cannot be made (or mapped, or wrapped) falls back to | |
| 83 | + | // the renderer's images, and what was made is released. | |
| 84 | + | func TestCursorSetupFallsBackToRenderer(t *testing.T) { | |
| 85 | + | for name, fail := range map[string]func(k *mockkms){ | |
| 86 | + | "create": func(k *mockkms) { | |
| 87 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 88 | + | }, | |
| 89 | + | "map": func(k *mockkms) { | |
| 90 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |
| 91 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 92 | + | k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once() | |
| 93 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |
| 94 | + | }, | |
| 95 | + | "framebuffer": func(k *mockkms) { | |
| 96 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |
| 97 | + | mem := make([]byte, b.size) | |
| 98 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 99 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |
| 100 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |
| 101 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |
| 102 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |
| 103 | + | }, | |
| 104 | + | "second buffer": func(k *mockkms) { | |
| 105 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |
| 106 | + | mem := make([]byte, b.size) | |
| 107 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |
| 108 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |
| 109 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once() | |
| 110 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once() | |
| 111 | + | k.EXPECT().rmFB(uint32(90)).Return(nil).Once() | |
| 112 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |
| 113 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |
| 114 | + | }, | |
| 115 | + | } { | |
| 116 | + | t.Run(name, func(t *testing.T) { | |
| 117 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 118 | + | fail(k) | |
| 119 | + | rendererOK(t, k, r) | |
| 120 | + | c := newCursor(&plane{}, tSide) | |
| 121 | + | dumbErr, err := c.setup(k, r) | |
| 122 | + | if dumbErr == nil || err != nil { | |
| 123 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |
| 124 | + | } | |
| 125 | + | if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} { | |
| 126 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |
| 127 | + | } | |
| 128 | + | }) | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Without either allocator there are no images. | |
| 133 | + | func TestCursorSetupFailsWithoutAllocators(t *testing.T) { | |
| 134 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 135 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 136 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once() | |
| 137 | + | c := newCursor(&plane{}, tSide) | |
| 138 | + | dumbErr, err := c.setup(k, r) | |
| 139 | + | if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} { | |
| 140 | + | t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs) | |
| 141 | + | } | |
| 142 | + | if _, err := c.useFallback(0); err != nil { | |
| 143 | + | t.Fatal(err) | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // A renderer image whose framebuffer fails is released. | |
| 148 | + | func TestRendererCursorReleasesOnFailure(t *testing.T) { | |
| 149 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 150 | + | bufs := rendererBufs(t) | |
| 151 | + | r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once() | |
| 152 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |
| 153 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |
| 154 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 155 | + | if _, err := newRendererCursor(k, r, tSide); err == nil { | |
| 156 | + | t.Fatal("framebuffer failure not reported") | |
| 157 | + | } | |
| 158 | + | assertPlanesClosed(t, bufs) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) { | |
| 162 | + | t.Helper() | |
| 163 | + | for i, b := range bufs { | |
| 164 | + | for j, p := range b.Planes { | |
| 165 | + | if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) { | |
| 166 | + | t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err) | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // cursorOutput is a test output whose cursor plane has size tSide. | |
| 173 | + | func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) { | |
| 174 | + | o, k, commits := testOutput(t, errs...) | |
| 175 | + | o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide) | |
| 176 | + | return o, k, portsmocks.NewMockRenderer(t), commits | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // The plane refuses the dumb images: the renderer's are tried (TEST_ONLY), | |
| 180 | + | // the image on the cursor is rewritten into them, and writes go there. | |
| 181 | + | func TestCursorDumbRefusedUsesRendererImages(t *testing.T) { | |
| 182 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 183 | + | dumbOK(k, tSide*4) | |
| 184 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 185 | + | t.Fatal(err) | |
| 186 | + | } | |
| 187 | + | px := []byte{1, 2, 3, 4} | |
| 188 | + | if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil { | |
| 189 | + | t.Fatal(err) | |
| 190 | + | } | |
| 191 | + | dumbFree(k) | |
| 192 | + | rendererOK(t, k, r) | |
| 193 | + | var written []int | |
| 194 | + | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |
| 195 | + | written = append(written, i) | |
| 196 | + | return nil | |
| 197 | + | }) | |
| 198 | + | o.testCursor() | |
| 199 | + | c := o.cursor | |
| 200 | + | if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 { | |
| 201 | + | 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) | |
| 202 | + | } | |
| 203 | + | if len(written) != 1 || len(*commits) != 2 { | |
| 204 | + | t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits)) | |
| 205 | + | } | |
| 206 | + | if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly { | |
| 207 | + | t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags) | |
| 208 | + | } | |
| 209 | + | // Further changes are written by the renderer, not into the freed buffers. | |
| 210 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 { | |
| 211 | + | t.Fatalf("write after fallback: %v %v", err, written) | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // Both kinds refused: the software cursor takes over. | |
| 216 | + | func TestCursorBothRefusedGoesOff(t *testing.T) { | |
| 217 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL) | |
| 218 | + | dumbOK(k, tSide*4) | |
| 219 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 220 | + | t.Fatal(err) | |
| 221 | + | } | |
| 222 | + | dumbFree(k) | |
| 223 | + | rendererOK(t, k, r) | |
| 224 | + | if !o.testCursor() { | |
| 225 | + | t.Fatal("no change reported") | |
| 226 | + | } | |
| 227 | + | if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 { | |
| 228 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |
| 229 | + | } | |
| 230 | + | if o.cursor.desired().on { | |
| 231 | + | t.Fatal("cursor shown while off") | |
| 232 | + | } | |
| 233 | + | // The renderer's images are released with the output. | |
| 234 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 235 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |
| 236 | + | o.cursor.free() | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | // Dumb images the plane takes stay, with no second test. | |
| 240 | + | func TestCursorDumbAcceptedStays(t *testing.T) { | |
| 241 | + | for _, tc := range []struct { | |
| 242 | + | name string | |
| 243 | + | err error | |
| 244 | + | }{{"accepted", nil}, {"non-refusal", unix.EIO}} { | |
| 245 | + | t.Run(tc.name, func(t *testing.T) { | |
| 246 | + | o, k, r, commits := cursorOutput(t, tc.err) | |
| 247 | + | dumbOK(k, tSide*4) | |
| 248 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 249 | + | t.Fatal(err) | |
| 250 | + | } | |
| 251 | + | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |
| 252 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |
| 253 | + | } | |
| 254 | + | dumbFree(k) | |
| 255 | + | o.cursor.free() | |
| 256 | + | }) | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | // Renderer images refused while no dumb ones exist: off, no retry. | |
| 261 | + | func TestCursorRendererRefusedGoesOff(t *testing.T) { | |
| 262 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 263 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 264 | + | rendererOK(t, k, r) | |
| 265 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 266 | + | t.Fatal(err) | |
| 267 | + | } | |
| 268 | + | if !o.testCursor() || !o.cursor.off || len(*commits) != 1 { | |
| 269 | + | t.Fatalf("off %v commits %d", o.cursor.off, len(*commits)) | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // Writes go to the images that own the cursor, and the output's HDR white | |
| 274 | + | // is passed along. | |
| 275 | + | func TestCursorWritesGoToCurrentImages(t *testing.T) { | |
| 276 | + | o, _, _, _ := cursorOutput(t) | |
| 277 | + | imgs := newMockcursorImages(t) | |
| 278 | + | imgs.EXPECT().fb(0).Return(90).Once() | |
| 279 | + | imgs.EXPECT().fb(1).Return(91).Once() | |
| 280 | + | o.cursor.install(imgs, allocDumb) | |
| 281 | + | px := []byte{1, 2, 3, 4} | |
| 282 | + | imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once() | |
| 283 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil { | |
| 284 | + | t.Fatal(err) | |
| 285 | + | } | |
| 286 | + | o.hdr.on, o.hdr.settings.SDRBrightness = true, 203 | |
| 287 | + | if got := o.cursorNits(); got != 203 { | |
| 288 | + | t.Fatalf("HDR white %v", got) | |
| 289 | + | } | |
| 290 | + | o.cursor.screen = 90 | |
| 291 | + | imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once() | |
| 292 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil { | |
| 293 | + | t.Fatal("write error swallowed") | |
| 294 | + | } | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // Dumb pitch may exceed the row size: rows land at the pitch, the image is | |
| 298 | + | // cropped to the side and cleared around. | |
| 299 | + | func TestDumbCursorWritePitchCropClear(t *testing.T) { | |
| 300 | + | k := newMockkms(t) | |
| 301 | + | const pitch = tSide*4 + 32 | |
| 302 | + | mem := dumbOK(k, pitch) | |
| 303 | + | d, err := newDumbCursor(k, tSide) | |
| 304 | + | if err != nil { | |
| 305 | + | t.Fatal(err) | |
| 306 | + | } | |
| 307 | + | defer func() { dumbFree(k); d.close() }() | |
| 308 | + | big := make([]byte, 40*40*4) | |
| 309 | + | for y := range 40 { | |
| 310 | + | for x := range 40 { | |
| 311 | + | copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255}) | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | |
| 315 | + | t.Fatal(err) | |
| 316 | + | } | |
| 317 | + | last := (tSide - 1) * pitch | |
| 318 | + | for _, y := range []int{0, 1, 7, tSide - 1} { | |
| 319 | + | for _, x := range []int{0, 3, tSide - 1} { | |
| 320 | + | got := mem[1][y*pitch+x*4 : y*pitch+x*4+4] | |
| 321 | + | want := big[(y*40+x)*4 : (y*40+x)*4+4] | |
| 322 | + | if !bytes.Equal(got, want) { | |
| 323 | + | t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want) | |
| 324 | + | } | |
| 325 | + | } | |
| 326 | + | } | |
| 327 | + | for y := range tSide { | |
| 328 | + | if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) { | |
| 329 | + | t.Fatalf("row %d padding written", y) | |
| 330 | + | } | |
| 331 | + | } | |
| 332 | + | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | |
| 333 | + | t.Fatal(err) | |
| 334 | + | } | |
| 335 | + | m := mem[1] | |
| 336 | + | if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 { | |
| 337 | + | t.Fatal("image not cleared around the new pixels") | |
| 338 | + | } | |
| 339 | + | // A wide, short image keeps its rows apart. | |
| 340 | + | row := []byte{9, 9, 9, 9, 8, 8, 8, 8} | |
| 341 | + | if err := d.write(0, append(row, row...), 2, 2, 0); err != nil { | |
| 342 | + | t.Fatal(err) | |
| 343 | + | } | |
| 344 | + | if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 { | |
| 345 | + | t.Fatal("rows of a narrow image") | |
| 346 | + | } | |
| 347 | + | if err := d.write(0, []byte{1}, 1, 1, 0); err == nil { | |
| 348 | + | t.Fatal("short image accepted") | |
| 349 | + | } | |
| 350 | + | if err := d.write(2, nil, 0, 0, 0); err == nil { | |
| 351 | + | t.Fatal("bad index accepted") | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | // HDR cursors convert exactly as the renderer did (see hdrcursor's vector). | |
| 356 | + | func TestDumbCursorWriteHDR(t *testing.T) { | |
| 357 | + | k := newMockkms(t) | |
| 358 | + | mem := dumbOK(k, tSide*4) | |
| 359 | + | d, err := newDumbCursor(k, tSide) | |
| 360 | + | if err != nil { | |
| 361 | + | t.Fatal(err) | |
| 362 | + | } | |
| 363 | + | defer func() { dumbFree(k); d.close() }() | |
| 364 | + | if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil { | |
| 365 | + | t.Fatal(err) | |
| 366 | + | } | |
| 367 | + | if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 { | |
| 368 | + | t.Fatalf("HDR white: %v", m[:4]) | |
| 369 | + | } | |
| 370 | + | if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 { | |
| 371 | + | t.Fatalf("SDR white: %v %v", err, mem[1][:4]) | |
| 372 | + | } | |
| 373 | + | if d.cache.Len() != 4 { | |
| 374 | + | t.Fatalf("cache %d", d.cache.Len()) | |
| 375 | + | } | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | func TestCursorFallbackState(t *testing.T) { | |
| 379 | + | for _, mode := range []string{"loaded", "queued", "hidden"} { | |
| 380 | + | t.Run(mode, func(t *testing.T) { | |
| 381 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 382 | + | dumbOK(k, tSide*4) | |
| 383 | + | c := o.cursor | |
| 384 | + | if _, err := c.setup(k, r); err != nil { | |
| 385 | + | t.Fatal(err) | |
| 386 | + | } | |
| 387 | + | px := []byte{1, 2, 3, 4} | |
| 388 | + | if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil { | |
| 389 | + | t.Fatal(err) | |
| 390 | + | } | |
| 391 | + | c.screen = 90 | |
| 392 | + | c.committed(cursorState{on: true, fb: 91, x: 8, y: 9}) | |
| 393 | + | want := []byte{1, 2, 3, 4} | |
| 394 | + | if mode == "queued" { | |
| 395 | + | px = []byte{5, 6, 7, 8} | |
| 396 | + | want = bytes.Clone(px) | |
| 397 | + | if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil { | |
| 398 | + | t.Fatal(err) | |
| 399 | + | } | |
| 400 | + | if c.later == nil { | |
| 401 | + | t.Fatal("image not queued") | |
| 402 | + | } | |
| 403 | + | } | |
| 404 | + | if mode == "hidden" { | |
| 405 | + | if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil { | |
| 406 | + | t.Fatal(err) | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | px[0] = 99 // Neither the loaded nor queued image may alias caller memory. | |
| 410 | + | dumbFree(k) | |
| 411 | + | rendererOK(t, k, r) | |
| 412 | + | if mode != "hidden" { | |
| 413 | + | r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once() | |
| 414 | + | } | |
| 415 | + | if !o.testCursor() { | |
| 416 | + | t.Fatal("swap not reported") | |
| 417 | + | } | |
| 418 | + | if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil { | |
| 419 | + | t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied) | |
| 420 | + | } | |
| 421 | + | c.landed() | |
| 422 | + | if c.screen != 0 { | |
| 423 | + | t.Fatal("old pending flight resurrected") | |
| 424 | + | } | |
| 425 | + | s := c.desired() | |
| 426 | + | if mode == "hidden" { | |
| 427 | + | if s.on || c.image { | |
| 428 | + | t.Fatal("hidden cursor shown") | |
| 429 | + | } | |
| 430 | + | } else { | |
| 431 | + | if !s.on || s.fb != 80 { | |
| 432 | + | t.Fatalf("desired %+v", s) | |
| 433 | + | } | |
| 434 | + | if mode == "queued" && (c.hotX != 4 || c.hotY != 5) { | |
| 435 | + | t.Fatal("queued hotspot lost") | |
| 436 | + | } | |
| 437 | + | } | |
| 438 | + | req := &atomicReq{} | |
| 439 | + | c.props(req, o.crtc, s) | |
| 440 | + | if err := k.commit(req, 0, 0); err != nil { | |
| 441 | + | t.Fatal(err) | |
| 442 | + | } | |
| 443 | + | wantFB := uint64(80) | |
| 444 | + | if mode == "hidden" { | |
| 445 | + | wantFB = 0 | |
| 446 | + | } | |
| 447 | + | if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB { | |
| 448 | + | t.Fatalf("next commit fb %d want %d", fb, wantFB) | |
| 449 | + | } | |
| 450 | + | c.committed(s) | |
| 451 | + | c.landed() | |
| 452 | + | if c.screen != uint32(wantFB) { | |
| 453 | + | t.Fatalf("screen %d", c.screen) | |
| 454 | + | } | |
| 455 | + | }) | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | func TestCursorFallbackFailureGoesOff(t *testing.T) { | |
| 460 | + | for _, mode := range []string{"allocation", "rewrite"} { | |
| 461 | + | t.Run(mode, func(t *testing.T) { | |
| 462 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 463 | + | dumbOK(k, tSide*4) | |
| 464 | + | c := o.cursor | |
| 465 | + | if _, err := c.setup(k, r); err != nil { | |
| 466 | + | t.Fatal(err) | |
| 467 | + | } | |
| 468 | + | px := []byte{1, 2, 3, 4} | |
| 469 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil { | |
| 470 | + | t.Fatal(err) | |
| 471 | + | } | |
| 472 | + | if mode == "allocation" { | |
| 473 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once() | |
| 474 | + | } else { | |
| 475 | + | dumbFree(k) | |
| 476 | + | rendererOK(t, k, r) | |
| 477 | + | r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once() | |
| 478 | + | } | |
| 479 | + | if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 { | |
| 480 | + | t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits)) | |
| 481 | + | } | |
| 482 | + | if mode == "allocation" { | |
| 483 | + | dumbFree(k) | |
| 484 | + | } else { | |
| 485 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 486 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |
| 487 | + | } | |
| 488 | + | c.free() | |
| 489 | + | }) | |
| 490 | + | } | |
| 491 | + | } | |
| @@ -0,0 +1,491 @@ | |||||
| 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(t *testing.T) [2]ports.DMABuf { | |||
| 40 | + | var b [2]ports.DMABuf | |||
| 41 | + | for i := range b { | |||
| 42 | + | f, w, err := os.Pipe() | |||
| 43 | + | if err != nil { | |||
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | t.Cleanup(func() { _ = f.Close(); _ = w.Close() }) | |||
| 47 | + | if err := w.Close(); err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 50 | + | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |||
| 51 | + | } | |||
| 52 | + | return b | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) { | |||
| 56 | + | b := rendererBufs(t) | |||
| 57 | + | r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once() | |||
| 58 | + | t.Cleanup(func() { assertPlanesClosed(t, b) }) | |||
| 59 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |||
| 60 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // Dumb buffers come first: the renderer allocates nothing. | |||
| 64 | + | func TestCursorSetupUsesDumbBuffers(t *testing.T) { | |||
| 65 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 66 | + | dumbOK(k, tSide*4) | |||
| 67 | + | c := newCursor(&plane{}, tSide) | |||
| 68 | + | dumbErr, err := c.setup(k, r) | |||
| 69 | + | if dumbErr != nil || err != nil { | |||
| 70 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |||
| 71 | + | } | |||
| 72 | + | if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} { | |||
| 73 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |||
| 74 | + | } | |||
| 75 | + | dumbFree(k) | |||
| 76 | + | c.free() | |||
| 77 | + | if c.imgs != nil || c.fbs != [2]uint32{} { | |||
| 78 | + | t.Fatal("images kept after free") | |||
| 79 | + | } | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // A dumb buffer that cannot be made (or mapped, or wrapped) falls back to | |||
| 83 | + | // the renderer's images, and what was made is released. | |||
| 84 | + | func TestCursorSetupFallsBackToRenderer(t *testing.T) { | |||
| 85 | + | for name, fail := range map[string]func(k *mockkms){ | |||
| 86 | + | "create": func(k *mockkms) { | |||
| 87 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |||
| 88 | + | }, | |||
| 89 | + | "map": func(k *mockkms) { | |||
| 90 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |||
| 91 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 92 | + | k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once() | |||
| 93 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |||
| 94 | + | }, | |||
| 95 | + | "framebuffer": func(k *mockkms) { | |||
| 96 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |||
| 97 | + | mem := make([]byte, b.size) | |||
| 98 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 99 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |||
| 100 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |||
| 101 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |||
| 102 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |||
| 103 | + | }, | |||
| 104 | + | "second buffer": func(k *mockkms) { | |||
| 105 | + | b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4} | |||
| 106 | + | mem := make([]byte, b.size) | |||
| 107 | + | k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once() | |||
| 108 | + | k.EXPECT().mapDumb(b).Return(mem, nil).Once() | |||
| 109 | + | k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once() | |||
| 110 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once() | |||
| 111 | + | k.EXPECT().rmFB(uint32(90)).Return(nil).Once() | |||
| 112 | + | k.EXPECT().unmapDumb(mem).Return(nil).Once() | |||
| 113 | + | k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once() | |||
| 114 | + | }, | |||
| 115 | + | } { | |||
| 116 | + | t.Run(name, func(t *testing.T) { | |||
| 117 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 118 | + | fail(k) | |||
| 119 | + | rendererOK(t, k, r) | |||
| 120 | + | c := newCursor(&plane{}, tSide) | |||
| 121 | + | dumbErr, err := c.setup(k, r) | |||
| 122 | + | if dumbErr == nil || err != nil { | |||
| 123 | + | t.Fatalf("setup: %v %v", dumbErr, err) | |||
| 124 | + | } | |||
| 125 | + | if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} { | |||
| 126 | + | t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs) | |||
| 127 | + | } | |||
| 128 | + | }) | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Without either allocator there are no images. | |||
| 133 | + | func TestCursorSetupFailsWithoutAllocators(t *testing.T) { | |||
| 134 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 135 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |||
| 136 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once() | |||
| 137 | + | c := newCursor(&plane{}, tSide) | |||
| 138 | + | dumbErr, err := c.setup(k, r) | |||
| 139 | + | if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} { | |||
| 140 | + | t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs) | |||
| 141 | + | } | |||
| 142 | + | if _, err := c.useFallback(0); err != nil { | |||
| 143 | + | t.Fatal(err) | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // A renderer image whose framebuffer fails is released. | |||
| 148 | + | func TestRendererCursorReleasesOnFailure(t *testing.T) { | |||
| 149 | + | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |||
| 150 | + | bufs := rendererBufs(t) | |||
| 151 | + | r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once() | |||
| 152 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |||
| 153 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |||
| 154 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |||
| 155 | + | if _, err := newRendererCursor(k, r, tSide); err == nil { | |||
| 156 | + | t.Fatal("framebuffer failure not reported") | |||
| 157 | + | } | |||
| 158 | + | assertPlanesClosed(t, bufs) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) { | |||
| 162 | + | t.Helper() | |||
| 163 | + | for i, b := range bufs { | |||
| 164 | + | for j, p := range b.Planes { | |||
| 165 | + | if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) { | |||
| 166 | + | t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err) | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // cursorOutput is a test output whose cursor plane has size tSide. | |||
| 173 | + | func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) { | |||
| 174 | + | o, k, commits := testOutput(t, errs...) | |||
| 175 | + | o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide) | |||
| 176 | + | return o, k, portsmocks.NewMockRenderer(t), commits | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // The plane refuses the dumb images: the renderer's are tried (TEST_ONLY), | |||
| 180 | + | // the image on the cursor is rewritten into them, and writes go there. | |||
| 181 | + | func TestCursorDumbRefusedUsesRendererImages(t *testing.T) { | |||
| 182 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 183 | + | dumbOK(k, tSide*4) | |||
| 184 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 185 | + | t.Fatal(err) | |||
| 186 | + | } | |||
| 187 | + | px := []byte{1, 2, 3, 4} | |||
| 188 | + | if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil { | |||
| 189 | + | t.Fatal(err) | |||
| 190 | + | } | |||
| 191 | + | dumbFree(k) | |||
| 192 | + | rendererOK(t, k, r) | |||
| 193 | + | var written []int | |||
| 194 | + | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |||
| 195 | + | written = append(written, i) | |||
| 196 | + | return nil | |||
| 197 | + | }) | |||
| 198 | + | o.testCursor() | |||
| 199 | + | c := o.cursor | |||
| 200 | + | if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 { | |||
| 201 | + | 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) | |||
| 202 | + | } | |||
| 203 | + | if len(written) != 1 || len(*commits) != 2 { | |||
| 204 | + | t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits)) | |||
| 205 | + | } | |||
| 206 | + | if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly { | |||
| 207 | + | t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags) | |||
| 208 | + | } | |||
| 209 | + | // Further changes are written by the renderer, not into the freed buffers. | |||
| 210 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 { | |||
| 211 | + | t.Fatalf("write after fallback: %v %v", err, written) | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // Both kinds refused: the software cursor takes over. | |||
| 216 | + | func TestCursorBothRefusedGoesOff(t *testing.T) { | |||
| 217 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL) | |||
| 218 | + | dumbOK(k, tSide*4) | |||
| 219 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 220 | + | t.Fatal(err) | |||
| 221 | + | } | |||
| 222 | + | dumbFree(k) | |||
| 223 | + | rendererOK(t, k, r) | |||
| 224 | + | if !o.testCursor() { | |||
| 225 | + | t.Fatal("no change reported") | |||
| 226 | + | } | |||
| 227 | + | if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 { | |||
| 228 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |||
| 229 | + | } | |||
| 230 | + | if o.cursor.desired().on { | |||
| 231 | + | t.Fatal("cursor shown while off") | |||
| 232 | + | } | |||
| 233 | + | // The renderer's images are released with the output. | |||
| 234 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |||
| 235 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |||
| 236 | + | o.cursor.free() | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | // Dumb images the plane takes stay, with no second test. | |||
| 240 | + | func TestCursorDumbAcceptedStays(t *testing.T) { | |||
| 241 | + | for _, tc := range []struct { | |||
| 242 | + | name string | |||
| 243 | + | err error | |||
| 244 | + | }{{"accepted", nil}, {"non-refusal", unix.EIO}} { | |||
| 245 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 246 | + | o, k, r, commits := cursorOutput(t, tc.err) | |||
| 247 | + | dumbOK(k, tSide*4) | |||
| 248 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 249 | + | t.Fatal(err) | |||
| 250 | + | } | |||
| 251 | + | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |||
| 252 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |||
| 253 | + | } | |||
| 254 | + | dumbFree(k) | |||
| 255 | + | o.cursor.free() | |||
| 256 | + | }) | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | // Renderer images refused while no dumb ones exist: off, no retry. | |||
| 261 | + | func TestCursorRendererRefusedGoesOff(t *testing.T) { | |||
| 262 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 263 | + | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |||
| 264 | + | rendererOK(t, k, r) | |||
| 265 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 266 | + | t.Fatal(err) | |||
| 267 | + | } | |||
| 268 | + | if !o.testCursor() || !o.cursor.off || len(*commits) != 1 { | |||
| 269 | + | t.Fatalf("off %v commits %d", o.cursor.off, len(*commits)) | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | // Writes go to the images that own the cursor, and the output's HDR white | |||
| 274 | + | // is passed along. | |||
| 275 | + | func TestCursorWritesGoToCurrentImages(t *testing.T) { | |||
| 276 | + | o, _, _, _ := cursorOutput(t) | |||
| 277 | + | imgs := newMockcursorImages(t) | |||
| 278 | + | imgs.EXPECT().fb(0).Return(90).Once() | |||
| 279 | + | imgs.EXPECT().fb(1).Return(91).Once() | |||
| 280 | + | o.cursor.install(imgs, allocDumb) | |||
| 281 | + | px := []byte{1, 2, 3, 4} | |||
| 282 | + | imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once() | |||
| 283 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil { | |||
| 284 | + | t.Fatal(err) | |||
| 285 | + | } | |||
| 286 | + | o.hdr.on, o.hdr.settings.SDRBrightness = true, 203 | |||
| 287 | + | if got := o.cursorNits(); got != 203 { | |||
| 288 | + | t.Fatalf("HDR white %v", got) | |||
| 289 | + | } | |||
| 290 | + | o.cursor.screen = 90 | |||
| 291 | + | imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once() | |||
| 292 | + | if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil { | |||
| 293 | + | t.Fatal("write error swallowed") | |||
| 294 | + | } | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | // Dumb pitch may exceed the row size: rows land at the pitch, the image is | |||
| 298 | + | // cropped to the side and cleared around. | |||
| 299 | + | func TestDumbCursorWritePitchCropClear(t *testing.T) { | |||
| 300 | + | k := newMockkms(t) | |||
| 301 | + | const pitch = tSide*4 + 32 | |||
| 302 | + | mem := dumbOK(k, pitch) | |||
| 303 | + | d, err := newDumbCursor(k, tSide) | |||
| 304 | + | if err != nil { | |||
| 305 | + | t.Fatal(err) | |||
| 306 | + | } | |||
| 307 | + | defer func() { dumbFree(k); d.close() }() | |||
| 308 | + | big := make([]byte, 40*40*4) | |||
| 309 | + | for y := range 40 { | |||
| 310 | + | for x := range 40 { | |||
| 311 | + | copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255}) | |||
| 312 | + | } | |||
| 313 | + | } | |||
| 314 | + | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | |||
| 315 | + | t.Fatal(err) | |||
| 316 | + | } | |||
| 317 | + | last := (tSide - 1) * pitch | |||
| 318 | + | for _, y := range []int{0, 1, 7, tSide - 1} { | |||
| 319 | + | for _, x := range []int{0, 3, tSide - 1} { | |||
| 320 | + | got := mem[1][y*pitch+x*4 : y*pitch+x*4+4] | |||
| 321 | + | want := big[(y*40+x)*4 : (y*40+x)*4+4] | |||
| 322 | + | if !bytes.Equal(got, want) { | |||
| 323 | + | t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want) | |||
| 324 | + | } | |||
| 325 | + | } | |||
| 326 | + | } | |||
| 327 | + | for y := range tSide { | |||
| 328 | + | if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) { | |||
| 329 | + | t.Fatalf("row %d padding written", y) | |||
| 330 | + | } | |||
| 331 | + | } | |||
| 332 | + | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | |||
| 333 | + | t.Fatal(err) | |||
| 334 | + | } | |||
| 335 | + | m := mem[1] | |||
| 336 | + | if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 { | |||
| 337 | + | t.Fatal("image not cleared around the new pixels") | |||
| 338 | + | } | |||
| 339 | + | // A wide, short image keeps its rows apart. | |||
| 340 | + | row := []byte{9, 9, 9, 9, 8, 8, 8, 8} | |||
| 341 | + | if err := d.write(0, append(row, row...), 2, 2, 0); err != nil { | |||
| 342 | + | t.Fatal(err) | |||
| 343 | + | } | |||
| 344 | + | if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 { | |||
| 345 | + | t.Fatal("rows of a narrow image") | |||
| 346 | + | } | |||
| 347 | + | if err := d.write(0, []byte{1}, 1, 1, 0); err == nil { | |||
| 348 | + | t.Fatal("short image accepted") | |||
| 349 | + | } | |||
| 350 | + | if err := d.write(2, nil, 0, 0, 0); err == nil { | |||
| 351 | + | t.Fatal("bad index accepted") | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | // HDR cursors convert exactly as the renderer did (see hdrcursor's vector). | |||
| 356 | + | func TestDumbCursorWriteHDR(t *testing.T) { | |||
| 357 | + | k := newMockkms(t) | |||
| 358 | + | mem := dumbOK(k, tSide*4) | |||
| 359 | + | d, err := newDumbCursor(k, tSide) | |||
| 360 | + | if err != nil { | |||
| 361 | + | t.Fatal(err) | |||
| 362 | + | } | |||
| 363 | + | defer func() { dumbFree(k); d.close() }() | |||
| 364 | + | if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil { | |||
| 365 | + | t.Fatal(err) | |||
| 366 | + | } | |||
| 367 | + | if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 { | |||
| 368 | + | t.Fatalf("HDR white: %v", m[:4]) | |||
| 369 | + | } | |||
| 370 | + | if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 { | |||
| 371 | + | t.Fatalf("SDR white: %v %v", err, mem[1][:4]) | |||
| 372 | + | } | |||
| 373 | + | if d.cache.Len() != 4 { | |||
| 374 | + | t.Fatalf("cache %d", d.cache.Len()) | |||
| 375 | + | } | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | func TestCursorFallbackState(t *testing.T) { | |||
| 379 | + | for _, mode := range []string{"loaded", "queued", "hidden"} { | |||
| 380 | + | t.Run(mode, func(t *testing.T) { | |||
| 381 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 382 | + | dumbOK(k, tSide*4) | |||
| 383 | + | c := o.cursor | |||
| 384 | + | if _, err := c.setup(k, r); err != nil { | |||
| 385 | + | t.Fatal(err) | |||
| 386 | + | } | |||
| 387 | + | px := []byte{1, 2, 3, 4} | |||
| 388 | + | if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil { | |||
| 389 | + | t.Fatal(err) | |||
| 390 | + | } | |||
| 391 | + | c.screen = 90 | |||
| 392 | + | c.committed(cursorState{on: true, fb: 91, x: 8, y: 9}) | |||
| 393 | + | want := []byte{1, 2, 3, 4} | |||
| 394 | + | if mode == "queued" { | |||
| 395 | + | px = []byte{5, 6, 7, 8} | |||
| 396 | + | want = bytes.Clone(px) | |||
| 397 | + | if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil { | |||
| 398 | + | t.Fatal(err) | |||
| 399 | + | } | |||
| 400 | + | if c.later == nil { | |||
| 401 | + | t.Fatal("image not queued") | |||
| 402 | + | } | |||
| 403 | + | } | |||
| 404 | + | if mode == "hidden" { | |||
| 405 | + | if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil { | |||
| 406 | + | t.Fatal(err) | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | px[0] = 99 // Neither the loaded nor queued image may alias caller memory. | |||
| 410 | + | dumbFree(k) | |||
| 411 | + | rendererOK(t, k, r) | |||
| 412 | + | if mode != "hidden" { | |||
| 413 | + | r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once() | |||
| 414 | + | } | |||
| 415 | + | if !o.testCursor() { | |||
| 416 | + | t.Fatal("swap not reported") | |||
| 417 | + | } | |||
| 418 | + | if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil { | |||
| 419 | + | t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied) | |||
| 420 | + | } | |||
| 421 | + | c.landed() | |||
| 422 | + | if c.screen != 0 { | |||
| 423 | + | t.Fatal("old pending flight resurrected") | |||
| 424 | + | } | |||
| 425 | + | s := c.desired() | |||
| 426 | + | if mode == "hidden" { | |||
| 427 | + | if s.on || c.image { | |||
| 428 | + | t.Fatal("hidden cursor shown") | |||
| 429 | + | } | |||
| 430 | + | } else { | |||
| 431 | + | if !s.on || s.fb != 80 { | |||
| 432 | + | t.Fatalf("desired %+v", s) | |||
| 433 | + | } | |||
| 434 | + | if mode == "queued" && (c.hotX != 4 || c.hotY != 5) { | |||
| 435 | + | t.Fatal("queued hotspot lost") | |||
| 436 | + | } | |||
| 437 | + | } | |||
| 438 | + | req := &atomicReq{} | |||
| 439 | + | c.props(req, o.crtc, s) | |||
| 440 | + | if err := k.commit(req, 0, 0); err != nil { | |||
| 441 | + | t.Fatal(err) | |||
| 442 | + | } | |||
| 443 | + | wantFB := uint64(80) | |||
| 444 | + | if mode == "hidden" { | |||
| 445 | + | wantFB = 0 | |||
| 446 | + | } | |||
| 447 | + | if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB { | |||
| 448 | + | t.Fatalf("next commit fb %d want %d", fb, wantFB) | |||
| 449 | + | } | |||
| 450 | + | c.committed(s) | |||
| 451 | + | c.landed() | |||
| 452 | + | if c.screen != uint32(wantFB) { | |||
| 453 | + | t.Fatalf("screen %d", c.screen) | |||
| 454 | + | } | |||
| 455 | + | }) | |||
| 456 | + | } | |||
| 457 | + | } | |||
| 458 | + | ||||
| 459 | + | func TestCursorFallbackFailureGoesOff(t *testing.T) { | |||
| 460 | + | for _, mode := range []string{"allocation", "rewrite"} { | |||
| 461 | + | t.Run(mode, func(t *testing.T) { | |||
| 462 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 463 | + | dumbOK(k, tSide*4) | |||
| 464 | + | c := o.cursor | |||
| 465 | + | if _, err := c.setup(k, r); err != nil { | |||
| 466 | + | t.Fatal(err) | |||
| 467 | + | } | |||
| 468 | + | px := []byte{1, 2, 3, 4} | |||
| 469 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil { | |||
| 470 | + | t.Fatal(err) | |||
| 471 | + | } | |||
| 472 | + | if mode == "allocation" { | |||
| 473 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once() | |||
| 474 | + | } else { | |||
| 475 | + | dumbFree(k) | |||
| 476 | + | rendererOK(t, k, r) | |||
| 477 | + | r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once() | |||
| 478 | + | } | |||
| 479 | + | if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 { | |||
| 480 | + | t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits)) | |||
| 481 | + | } | |||
| 482 | + | if mode == "allocation" { | |||
| 483 | + | dumbFree(k) | |||
| 484 | + | } else { | |||
| 485 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |||
| 486 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |||
| 487 | + | } | |||
| 488 | + | c.free() | |||
| 489 | + | }) | |||
| 490 | + | } | |||
| 491 | + | } | |||
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 +111 -0
| @@ -0,0 +1,111 @@ | |||
| 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 := func() bool { | |
| 27 | + | defer f.Close() | |
| 28 | + | return dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |
| 29 | + | }() | |
| 30 | + | if done { | |
| 31 | + | return | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | t.Skip("no card node makes dumb buffers for this user") | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // dumbDeviceRoundTrip runs the buffer life cycle on k and reports false | |
| 38 | + | // when the card cannot do it for a reason unrelated to the code under test | |
| 39 | + | // (the permission or the driver). | |
| 40 | + | func dumbDeviceRoundTrip(t *testing.T, path string, k kmsDevice) bool { | |
| 41 | + | size := cursorSize(k.fd) | |
| 42 | + | var imgs [2]dumbBuffer | |
| 43 | + | var mems [2][]byte | |
| 44 | + | var fbs [2]uint32 | |
| 45 | + | cleanup := func() { | |
| 46 | + | for i := range imgs { | |
| 47 | + | if fbs[i] != 0 { | |
| 48 | + | if err := k.rmFB(fbs[i]); err != nil { | |
| 49 | + | t.Errorf("rmfb: %v", err) | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | if mems[i] != nil { | |
| 53 | + | if err := k.unmapDumb(mems[i]); err != nil { | |
| 54 | + | t.Errorf("munmap: %v", err) | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | if imgs[i].handle != 0 { | |
| 58 | + | if err := k.destroyDumb(imgs[i].handle); err != nil { | |
| 59 | + | t.Errorf("destroy dumb: %v", err) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | defer cleanup() | |
| 65 | + | for i := range imgs { | |
| 66 | + | b, err := k.createDumb(size, size) | |
| 67 | + | if err != nil { | |
| 68 | + | 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) { | |
| 69 | + | t.Logf("%s: create dumb: %v", path, err) | |
| 70 | + | return false | |
| 71 | + | } | |
| 72 | + | t.Fatalf("%s: create dumb: %v", path, err) | |
| 73 | + | } | |
| 74 | + | imgs[i] = b | |
| 75 | + | if int(b.pitch) < size*4 || b.size < uint64(int(b.pitch)*size) { | |
| 76 | + | t.Fatalf("%s: dumb %dx%d pitch %d size %d", path, size, size, b.pitch, b.size) | |
| 77 | + | } | |
| 78 | + | if mems[i], err = k.mapDumb(b); err != nil { | |
| 79 | + | t.Fatalf("%s: map dumb: %v", path, err) | |
| 80 | + | } | |
| 81 | + | if fbs[i], err = k.addFBHandle(b, size, size, fourccARGB); err != nil { | |
| 82 | + | t.Fatalf("%s: add fb2 ARGB8888: %v", path, err) | |
| 83 | + | } | |
| 84 | + | t.Logf("%s: dumb buffer %d: %dx%d pitch %d size %d fb %d", path, i, size, size, b.pitch, b.size, fbs[i]) | |
| 85 | + | } | |
| 86 | + | // Write a pattern through the shared mapping; each buffer keeps its own. | |
| 87 | + | for i, mem := range mems { | |
| 88 | + | d := &dumbCursor{k: k, size: size} | |
| 89 | + | d.img[i] = dumbImage{pitch: int(imgs[i].pitch), mem: mem} | |
| 90 | + | px := bytes.Repeat([]byte{byte(0x10 + i), 0x20, 0x30, 0xff}, 3*2) | |
| 91 | + | if err := d.write(i, px, 3, 2, 0); err != nil { | |
| 92 | + | t.Fatal(err) | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | for i, mem := range mems { | |
| 96 | + | pitch := int(imgs[i].pitch) | |
| 97 | + | want := []byte{byte(0x10 + i), 0x20, 0x30, 0xff} | |
| 98 | + | for y := range 2 { | |
| 99 | + | if got := mem[y*pitch : y*pitch+4]; !bytes.Equal(got, want) { | |
| 100 | + | t.Fatalf("%s: buffer %d row %d read back %v, want %v", path, i, y, got, want) | |
| 101 | + | } | |
| 102 | + | if got := mem[y*pitch+3*4]; got != 0 { | |
| 103 | + | t.Fatalf("%s: buffer %d row %d not cleared after the image", path, i, y) | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | if mem[2*pitch] != 0 || mem[(size-1)*pitch+(size-1)*4] != 0 { | |
| 107 | + | t.Fatalf("%s: buffer %d not cleared below the image", path, i) | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | return true | |
| 111 | + | } | |
| @@ -0,0 +1,111 @@ | |||||
| 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 := func() bool { | |||
| 27 | + | defer f.Close() | |||
| 28 | + | return dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |||
| 29 | + | }() | |||
| 30 | + | if done { | |||
| 31 | + | return | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | t.Skip("no card node makes dumb buffers for this user") | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // dumbDeviceRoundTrip runs the buffer life cycle on k and reports false | |||
| 38 | + | // when the card cannot do it for a reason unrelated to the code under test | |||
| 39 | + | // (the permission or the driver). | |||
| 40 | + | func dumbDeviceRoundTrip(t *testing.T, path string, k kmsDevice) bool { | |||
| 41 | + | size := cursorSize(k.fd) | |||
| 42 | + | var imgs [2]dumbBuffer | |||
| 43 | + | var mems [2][]byte | |||
| 44 | + | var fbs [2]uint32 | |||
| 45 | + | cleanup := func() { | |||
| 46 | + | for i := range imgs { | |||
| 47 | + | if fbs[i] != 0 { | |||
| 48 | + | if err := k.rmFB(fbs[i]); err != nil { | |||
| 49 | + | t.Errorf("rmfb: %v", err) | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | if mems[i] != nil { | |||
| 53 | + | if err := k.unmapDumb(mems[i]); err != nil { | |||
| 54 | + | t.Errorf("munmap: %v", err) | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | if imgs[i].handle != 0 { | |||
| 58 | + | if err := k.destroyDumb(imgs[i].handle); err != nil { | |||
| 59 | + | t.Errorf("destroy dumb: %v", err) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | defer cleanup() | |||
| 65 | + | for i := range imgs { | |||
| 66 | + | b, err := k.createDumb(size, size) | |||
| 67 | + | if err != nil { | |||
| 68 | + | 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) { | |||
| 69 | + | t.Logf("%s: create dumb: %v", path, err) | |||
| 70 | + | return false | |||
| 71 | + | } | |||
| 72 | + | t.Fatalf("%s: create dumb: %v", path, err) | |||
| 73 | + | } | |||
| 74 | + | imgs[i] = b | |||
| 75 | + | if int(b.pitch) < size*4 || b.size < uint64(int(b.pitch)*size) { | |||
| 76 | + | t.Fatalf("%s: dumb %dx%d pitch %d size %d", path, size, size, b.pitch, b.size) | |||
| 77 | + | } | |||
| 78 | + | if mems[i], err = k.mapDumb(b); err != nil { | |||
| 79 | + | t.Fatalf("%s: map dumb: %v", path, err) | |||
| 80 | + | } | |||
| 81 | + | if fbs[i], err = k.addFBHandle(b, size, size, fourccARGB); err != nil { | |||
| 82 | + | t.Fatalf("%s: add fb2 ARGB8888: %v", path, err) | |||
| 83 | + | } | |||
| 84 | + | t.Logf("%s: dumb buffer %d: %dx%d pitch %d size %d fb %d", path, i, size, size, b.pitch, b.size, fbs[i]) | |||
| 85 | + | } | |||
| 86 | + | // Write a pattern through the shared mapping; each buffer keeps its own. | |||
| 87 | + | for i, mem := range mems { | |||
| 88 | + | d := &dumbCursor{k: k, size: size} | |||
| 89 | + | d.img[i] = dumbImage{pitch: int(imgs[i].pitch), mem: mem} | |||
| 90 | + | px := bytes.Repeat([]byte{byte(0x10 + i), 0x20, 0x30, 0xff}, 3*2) | |||
| 91 | + | if err := d.write(i, px, 3, 2, 0); err != nil { | |||
| 92 | + | t.Fatal(err) | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | for i, mem := range mems { | |||
| 96 | + | pitch := int(imgs[i].pitch) | |||
| 97 | + | want := []byte{byte(0x10 + i), 0x20, 0x30, 0xff} | |||
| 98 | + | for y := range 2 { | |||
| 99 | + | if got := mem[y*pitch : y*pitch+4]; !bytes.Equal(got, want) { | |||
| 100 | + | t.Fatalf("%s: buffer %d row %d read back %v, want %v", path, i, y, got, want) | |||
| 101 | + | } | |||
| 102 | + | if got := mem[y*pitch+3*4]; got != 0 { | |||
| 103 | + | t.Fatalf("%s: buffer %d row %d not cleared after the image", path, i, y) | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | if mem[2*pitch] != 0 || mem[(size-1)*pitch+(size-1)*4] != 0 { | |||
| 107 | + | t.Fatalf("%s: buffer %d not cleared below the image", path, i) | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | return true | |||
| 111 | + | } | |||
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/convert_test.go +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | package hdrcursor | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestCacheKeyGeometry(t *testing.T) { | |
| 9 | + | px := []byte{10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255} | |
| 10 | + | for _, tc := range []struct { | |
| 11 | + | name string | |
| 12 | + | w, h, side int | |
| 13 | + | }{{"dimensions", 1, 4, 4}, {"max-side", 2, 2, 1}} { | |
| 14 | + | t.Run(tc.name, func(t *testing.T) { | |
| 15 | + | var c Cache | |
| 16 | + | c.Convert(px, 2, 2, 4, 203) | |
| 17 | + | before := c.Key() | |
| 18 | + | got := bytes.Clone(c.Convert(px, tc.w, tc.h, tc.side, 203)) | |
| 19 | + | if c.Key() == before { | |
| 20 | + | t.Fatal("geometry missing from cache key") | |
| 21 | + | } | |
| 22 | + | var fresh Cache | |
| 23 | + | want := fresh.Convert(px, tc.w, tc.h, tc.side, 203) | |
| 24 | + | if !bytes.Equal(got, want) { | |
| 25 | + | t.Fatalf("stale conversion: %v want %v", got, want) | |
| 26 | + | } | |
| 27 | + | }) | |
| 28 | + | } | |
| 29 | + | } | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | package hdrcursor | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestCacheKeyGeometry(t *testing.T) { | |||
| 9 | + | px := []byte{10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255} | |||
| 10 | + | for _, tc := range []struct { | |||
| 11 | + | name string | |||
| 12 | + | w, h, side int | |||
| 13 | + | }{{"dimensions", 1, 4, 4}, {"max-side", 2, 2, 1}} { | |||
| 14 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 15 | + | var c Cache | |||
| 16 | + | c.Convert(px, 2, 2, 4, 203) | |||
| 17 | + | before := c.Key() | |||
| 18 | + | got := bytes.Clone(c.Convert(px, tc.w, tc.h, tc.side, 203)) | |||
| 19 | + | if c.Key() == before { | |||
| 20 | + | t.Fatal("geometry missing from cache key") | |||
| 21 | + | } | |||
| 22 | + | var fresh Cache | |||
| 23 | + | want := fresh.Convert(px, tc.w, tc.h, tc.side, 203) | |||
| 24 | + | if !bytes.Equal(got, want) { | |||
| 25 | + | t.Fatalf("stale conversion: %v want %v", got, want) | |||
| 26 | + | } | |||
| 27 | + | }) | |||
| 28 | + | } | |||
| 29 | + | } | |||
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 +6 -25
| @@ -47,7 +47,7 @@ func TestCursorBuffersWriteCursor(t *testing.T) { | |||
| 47 | 47 | } | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | - | func TestHDRCursorCachesConversion(t *testing.T) { | |
| 50 | + | func TestHDRCursorWritesBothImagesAndInvalidates(t *testing.T) { | |
| 51 | 51 | r, err := New(32, 16) | |
| 52 | 52 | if err != nil { | |
| 53 | 53 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -65,28 +65,6 @@ 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)) | |
| 70 | - | } | |
| 71 | - | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |
| 72 | - | t.Fatal(err) | |
| 73 | - | } | |
| 74 | - | if len(r.cursorCache.pixels) != 4 { | |
| 75 | - | t.Fatalf("cache grew on reselect: %d", len(r.cursorCache.pixels)) | |
| 76 | - | } | |
| 77 | - | first := r.cursorCache.key | |
| 78 | - | for i := 0; i < 12; i++ { | |
| 79 | - | color := byte(i * 18) | |
| 80 | - | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | |
| 81 | - | t.Fatal(err) | |
| 82 | - | } | |
| 83 | - | if len(r.cursorCache.pixels) != 4 { | |
| 84 | - | t.Fatalf("cache grew after image %d: %d", i, len(r.cursorCache.pixels)) | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | if first == r.cursorCache.key { | |
| 88 | - | t.Fatal("cursor conversion not replaced") | |
| 89 | - | } | |
| 90 | 68 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |
| 91 | 69 | t.Fatal(err) | |
| 92 | 70 | } | |
| @@ -97,10 +75,13 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||
| 97 | 75 | } | |
| 98 | 76 | } | |
| 99 | 77 | r.SetHDR(0) | |
| 78 | + | if r.cursorCache.Len() != 0 { | |
| 79 | + | t.Fatal("SetHDR kept conversion") | |
| 80 | + | } | |
| 100 | 81 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | |
| 101 | 82 | t.Fatal(err) | |
| 102 | 83 | } | |
| 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)) | |
| 84 | + | if got := unsafe.Slice((*byte)(r.cursors[0].mapped), 4); got[0] != 255 || r.cursorCache.Len() != 0 { | |
| 85 | + | t.Fatalf("SDR cursor/cache: %v %d", got, r.cursorCache.Len()) | |
| 105 | 86 | } | |
| 106 | 87 | } | |
| @@ -47,7 +47,7 @@ func TestCursorBuffersWriteCursor(t *testing.T) { | |||||
| 47 | } | 47 | } | ||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | - | func TestHDRCursorCachesConversion(t *testing.T) { | 50 | + | func TestHDRCursorWritesBothImagesAndInvalidates(t *testing.T) { |
| 51 | r, err := New(32, 16) | 51 | r, err := New(32, 16) | ||
| 52 | if err != nil { | 52 | if err != nil { | ||
| 53 | t.Skipf("Vulkan unavailable: %v", err) | 53 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -65,28 +65,6 @@ 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 { | |||
| 69 | - | t.Fatalf("cache size %d", len(r.cursorCache.pixels)) | |||
| 70 | - | } | |||
| 71 | - | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |||
| 72 | - | t.Fatal(err) | |||
| 73 | - | } | |||
| 74 | - | if len(r.cursorCache.pixels) != 4 { | |||
| 75 | - | t.Fatalf("cache grew on reselect: %d", len(r.cursorCache.pixels)) | |||
| 76 | - | } | |||
| 77 | - | first := r.cursorCache.key | |||
| 78 | - | for i := 0; i < 12; i++ { | |||
| 79 | - | color := byte(i * 18) | |||
| 80 | - | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | |||
| 81 | - | t.Fatal(err) | |||
| 82 | - | } | |||
| 83 | - | if len(r.cursorCache.pixels) != 4 { | |||
| 84 | - | t.Fatalf("cache grew after image %d: %d", i, len(r.cursorCache.pixels)) | |||
| 85 | - | } | |||
| 86 | - | } | |||
| 87 | - | if first == r.cursorCache.key { | |||
| 88 | - | t.Fatal("cursor conversion not replaced") | |||
| 89 | - | } | |||
| 90 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | 68 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | ||
| 91 | t.Fatal(err) | 69 | t.Fatal(err) | ||
| 92 | } | 70 | } | ||
| @@ -97,10 +75,13 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||||
| 97 | } | 75 | } | ||
| 98 | } | 76 | } | ||
| 99 | r.SetHDR(0) | 77 | r.SetHDR(0) | ||
| 78 | + | if r.cursorCache.Len() != 0 { | |||
| 79 | + | t.Fatal("SetHDR kept conversion") | |||
| 80 | + | } | |||
| 100 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | 81 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | ||
| 101 | t.Fatal(err) | 82 | t.Fatal(err) | ||
| 102 | } | 83 | } | ||
| 103 | - | if got := unsafe.Slice((*byte)(r.cursors[0].mapped), 4); got[0] != 255 || len(r.cursorCache.pixels) != 0 { | 84 | + | 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)) | 85 | + | t.Fatalf("SDR cursor/cache: %v %d", got, r.cursorCache.Len()) |
| 105 | } | 86 | } | ||
| 106 | } | 87 | } | ||
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 | ||