1package vulkan
2
3import (
4 "crypto/sha256"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "math"
9 "os"
10 "unsafe"
11
12 "github.com/bnema/neferwl/internal/ports"
13 vk "github.com/bnema/purego-vulkan/vulkan"
14)
15
16// Cursor images for the display's cursor plane: two linear B8G8R8A8
17// images in host-visible memory, exported as dmabufs. The CPU writes a
18// cursor image only when the cursor changes, never per frame.
19
20type cursorImage struct {
21 image vk.Image
22 memory vk.DeviceMemory
23 mapped unsafe.Pointer
24 pitch int
25 size int
26}
27
28// fourccARGB is DRM_FORMAT_ARGB8888: B8G8R8A8 in memory.
29const fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
30
31// CursorBuffers allocates the two cursor images, replacing earlier ones.
32func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {
33 var out [2]ports.DMABuf
34 r.dropCursors()
35 if !r.dd.HasGetMemoryFdKHR() || len(r.dmabuf.Formats) == 0 || size <= 0 || size > 512 {
36 return out, errors.New("device cannot export cursor images")
37 }
38 for i := range r.cursors {
39 c, buf, err := r.cursorImage(size)
40 if err != nil {
41 r.dropCursors()
42 for _, b := range out[:i] {
43 b.Planes[0].File.Close()
44 }
45 return [2]ports.DMABuf{}, err
46 }
47 r.cursors[i], out[i] = c, buf
48 }
49 return out, nil
50}
51
52func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
53 d := r.dd
54 c := &cursorImage{size: size}
55 ok := false
56 defer func() {
57 if !ok {
58 r.freeCursor(c)
59 }
60 }()
61 // Exported images take an explicit DRM modifier: linear, as cursor
62 // planes scan out. The GPU never touches them (the CPU writes, KMS reads).
63 linear := uint64(0)
64 list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &linear}
65 external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
66 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
67 if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil {
68 return nil, ports.DMABuf{}, err
69 }
70 var req vk.MemoryRequirements
71 d.GetImageMemoryRequirements(r.device, c.image, &req)
72 kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
73 if err != nil {
74 return nil, ports.DMABuf{}, err
75 }
76 dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: c.image}
77 export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
78 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
79 if err := checked("vkAllocateMemory(cursor)", d.AllocateMemory(r.device, &alloc, nil, &c.memory)); err != nil {
80 return nil, ports.DMABuf{}, err
81 }
82 if err := checked("vkBindImageMemory(cursor)", d.BindImageMemory(r.device, c.image, c.memory, 0)); err != nil {
83 return nil, ports.DMABuf{}, err
84 }
85 var layout vk.SubresourceLayout
86 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
87 d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout)
88 c.pitch = int(layout.RowPitch)
89 // Cursor planes scan out packed rows from the start of the buffer.
90 if c.pitch != size*4 || layout.Offset != 0 {
91 return nil, ports.DMABuf{}, fmt.Errorf("cursor image layout pitch %d offset %d, want pitch %d offset 0", c.pitch, layout.Offset, size*4)
92 }
93 if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil {
94 return nil, ports.DMABuf{}, err
95 }
96 c.mapped = unsafe.Add(c.mapped, layout.Offset)
97 clear(unsafe.Slice((*byte)(c.mapped), c.pitch*size))
98 var fd int32
99 get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: c.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
100 if err := checked("vkGetMemoryFdKHR(cursor)", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
101 return nil, ports.DMABuf{}, err
102 }
103 f := os.NewFile(uintptr(fd), "neferwl-cursor")
104 if f == nil {
105 return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
106 }
107 ok = true
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
109}
110
111// cursorConversion keeps only the last converted image, bounded by the
112// cursor plane's maximum dimensions, regardless of cursor theme size.
113type cursorConversion struct {
114 key [32]byte
115 pixels []byte
116}
117
118func 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)
132// into cursor image i at its row pitch, cropped and cleared around.
133func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error {
134 if i < 0 || i >= len(r.cursors) || r.cursors[i] == nil {
135 return errors.New("no cursor image")
136 }
137 if w < 0 || h < 0 || len(pixels) < w*h*4 {
138 return errors.New("short cursor image")
139 }
140 c := r.cursors[i]
141 mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size)
142 clear(mem)
143 cw := min(w, c.size) * 4
144 if r.hdrNits > 0 {
145 // 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 }
165 }
166 return nil
167}
168
169func (r *Renderer) freeCursor(c *cursorImage) {
170 if c.mapped != nil {
171 r.dd.UnmapMemory(r.device, c.memory)
172 c.mapped = nil
173 }
174 if c.image != 0 {
175 r.dd.DestroyImage(r.device, c.image, nil)
176 c.image = 0
177 }
178 if c.memory != 0 {
179 r.dd.FreeMemory(r.device, c.memory, nil)
180 c.memory = 0
181 }
182}
183
184func (r *Renderer) dropCursors() {
185 for i, c := range r.cursors {
186 if c != nil {
187 r.freeCursor(c)
188 r.cursors[i] = nil
189 }
190 }
191}