feat/data-dnd e05cb15

cursor.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close