detached f2d166b

cursor.go RawHistoryFolder

1package vulkan
2
3import (
4	"errors"
5	"fmt"
6	"os"
7	"unsafe"
8
9	"github.com/bnema/neferwl/internal/ports"
10	vk "github.com/bnema/purego-vulkan/vulkan"
11)
12
13// Cursor images for the display's cursor plane: two linear B8G8R8A8
14// images in host-visible memory, exported as dmabufs. The CPU writes a
15// cursor image only when the cursor changes, never per frame.
16
17type cursorImage struct {
18	image  vk.Image
19	memory vk.DeviceMemory
20	mapped unsafe.Pointer
21	pitch  int
22	size   int
23}
24
25// fourccARGB is DRM_FORMAT_ARGB8888: B8G8R8A8 in memory.
26const fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
27
28// CursorBuffers allocates the two cursor images, replacing earlier ones.
29func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {
30	var out [2]ports.DMABuf
31	r.dropCursors()
32	if !r.dd.HasGetMemoryFdKHR() || len(r.dmabuf.Formats) == 0 || size <= 0 || size > 512 {
33		return out, errors.New("device cannot export cursor images")
34	}
35	for i := range r.cursors {
36		c, buf, err := r.cursorImage(size)
37		if err != nil {
38			r.dropCursors()
39			for _, b := range out[:i] {
40				b.Planes[0].File.Close()
41			}
42			return [2]ports.DMABuf{}, err
43		}
44		r.cursors[i], out[i] = c, buf
45	}
46	return out, nil
47}
48
49func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
50	d := r.dd
51	c := &cursorImage{size: size}
52	ok := false
53	defer func() {
54		if !ok {
55			r.freeCursor(c)
56		}
57	}()
58	// Exported images take an explicit DRM modifier: linear, as cursor
59	// planes scan out. The GPU never touches them (the CPU writes, KMS reads).
60	linear := uint64(0)
61	list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &linear}
62	external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
63	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}
64	if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil {
65		return nil, ports.DMABuf{}, err
66	}
67	var req vk.MemoryRequirements
68	d.GetImageMemoryRequirements(r.device, c.image, &req)
69	kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
70	if err != nil {
71		return nil, ports.DMABuf{}, err
72	}
73	dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: c.image}
74	export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
75	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
76	if err := checked("vkAllocateMemory(cursor)", d.AllocateMemory(r.device, &alloc, nil, &c.memory)); err != nil {
77		return nil, ports.DMABuf{}, err
78	}
79	if err := checked("vkBindImageMemory(cursor)", d.BindImageMemory(r.device, c.image, c.memory, 0)); err != nil {
80		return nil, ports.DMABuf{}, err
81	}
82	var layout vk.SubresourceLayout
83	sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
84	d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout)
85	c.pitch = int(layout.RowPitch)
86	// Cursor planes scan out packed rows from the start of the buffer.
87	if c.pitch != size*4 || layout.Offset != 0 {
88		return nil, ports.DMABuf{}, fmt.Errorf("cursor image layout pitch %d offset %d, want pitch %d offset 0", c.pitch, layout.Offset, size*4)
89	}
90	if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil {
91		return nil, ports.DMABuf{}, err
92	}
93	c.mapped = unsafe.Add(c.mapped, layout.Offset)
94	clear(unsafe.Slice((*byte)(c.mapped), c.pitch*size))
95	var fd int32
96	get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: c.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
97	if err := checked("vkGetMemoryFdKHR(cursor)", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
98		return nil, ports.DMABuf{}, err
99	}
100	f := os.NewFile(uintptr(fd), "neferwl-cursor")
101	if f == nil {
102		return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
103	}
104	ok = true
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
106}
107
108// WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row)
109// into cursor image i at its row pitch, cropped and cleared around.
110func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error {
111	if i < 0 || i >= len(r.cursors) || r.cursors[i] == nil {
112		return errors.New("no cursor image")
113	}
114	if w < 0 || h < 0 || len(pixels) < w*h*4 {
115		return errors.New("short cursor image")
116	}
117	c := r.cursors[i]
118	mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size)
119	clear(mem)
120	cw := min(w, c.size) * 4
121	src, stride := pixels, w*4
122	if r.hdrNits > 0 {
123		// WriteCursor is called only on image changes, never on moves.
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])
128	}
129	return nil
130}
131
132func (r *Renderer) freeCursor(c *cursorImage) {
133	if c.mapped != nil {
134		r.dd.UnmapMemory(r.device, c.memory)
135		c.mapped = nil
136	}
137	if c.image != 0 {
138		r.dd.DestroyImage(r.device, c.image, nil)
139		c.image = 0
140	}
141	if c.memory != 0 {
142		r.dd.FreeMemory(r.device, c.memory, nil)
143		c.memory = 0
144	}
145}
146
147func (r *Renderer) dropCursors() {
148	for i, c := range r.cursors {
149		if c != nil {
150			r.freeCursor(c)
151			r.cursors[i] = nil
152		}
153	}
154}

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 23

Tags 10

↑↓ moveenter openesc close