feat/hints a471fa4

targets.go RawHistoryFolder

1package vulkan
2
3import (
4	"errors"
5	"fmt"
6	"maps"
7	"os"
8	"slices"
9	"unsafe"
10
11	"github.com/bnema/neferwl/internal/ports"
12	vk "github.com/bnema/purego-vulkan/vulkan"
13)
14
15// Render targets (ADR 014). A DRM output scans out images the renderer
16// draws into: ExportTargets allocates them with a DRM format modifier the
17// display accepts and exports their memory as dmabufs, which the output
18// turns into KMS framebuffers. Render then composes straight into the
19// selected target and the output flips to it; nothing is copied by the
20// CPU. In HDR an internal fp16 linear image keeps full-range composition,
21// sampled by a full-screen GPU pass into the 10-bit target. Capture/Pixels
22// convert to SDR sRGB only when requested.
23
24// target is an image frames are drawn into.
25type target struct {
26	image  vk.Image
27	memory vk.DeviceMemory
28	layout vk.ImageLayout
29	view   vk.ImageView // color attachment for blended draws
30	// exported images are shared with the display: ownership goes to the
31	// foreign queue family after each frame, and KMS reads them.
32	exported bool
33	// What the image holds (damage.go): the scene, its Seq and each
34	// window's content Seq; valid once a frame was drawn whole. Scenes are
35	// immutable for core, but output adapters reuse their slices in place
36	// (DRM overlay frames, capture), so the compared slices of scene point
37	// into the target's own buffers, copied by hold.
38	valid          bool
39	sceneSeq       uint64
40	scene          ports.Scene
41	heldWindows    []ports.SceneWindow
42	heldSeparators []ports.Separator
43	heldLayers     []ports.SceneLayer
44	heldHints      []ports.Rect
45	heldIndicators []ports.CaptureIndicator
46	heldLabels     []ports.HintLabel
47	windows        map[ports.WindowID]heldWindow
48}
49
50// fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
51const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
52
53// fourccXR30 is DRM_FORMAT_XRGB2101010 (A2R10G10B10 packed).
54const fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
55
56// ExportTargets allocates n exported images of the output size, replacing
57// the previous ones; n = 0 only drops them (back to the internal image).
58// singlePlane keeps only one-plane modifiers (no DCC metadata planes), for
59// displays that refuse the multi-plane framebuffers of the others.
60func (r *Renderer) ExportTargets(n int, modifiers []uint64, singlePlane bool) ([]ports.DMABuf, error) {
61	r.dropTargets()
62	if n > 0 && r.hdrNits > 0 && r.hdrReadback && r.physical != 0 {
63		// Readback targets (GPU tests, headless --screenshot-raw) are also
64		// transfer sources, which changes the format requirements.
65		r.hdrMods = r.probeModifiers(r.physical, vk.FormatA2r10g10b10UnormPack32)
66	}
67	if n > 0 && r.hdrNits > 0 && r.hdr.pipeline == 0 {
68		return nil, fmt.Errorf("HDR transform unavailable: %w", r.hdrError)
69	}
70	if n == 0 {
71		return nil, nil
72	}
73	if !r.dd.HasGetMemoryFdKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() || len(r.dmabuf.Formats) == 0 {
74		return nil, errors.New("device cannot export dmabufs")
75	}
76	mods := r.exportModifiers(modifiers, singlePlane)
77	if len(mods) == 0 {
78		if r.hdrNits == 0 {
79			return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
80		}
81		return nil, errors.New("no XRGB2101010 modifier both the device and the display accept")
82	}
83	var out []ports.DMABuf
84	for range n {
85		t, buf, err := r.exportTarget(mods)
86		if err != nil {
87			r.dropTargets()
88			for _, b := range out {
89				for _, p := range b.Planes {
90					p.File.Close()
91				}
92			}
93			return nil, err
94		}
95		r.targets = append(r.targets, t)
96		out = append(out, buf)
97	}
98	r.current = 0
99	return out, nil
100}
101
102// SetVirtualOutput marks the output as having no display (headless): an
103// empty display modifier list then accepts every exportable HDR modifier,
104// as it does for SDR. DRM never sets it. Call before ExportTargets.
105func (r *Renderer) SetVirtualOutput(on bool) { r.virtual = on }
106
107// UseTarget selects the exported image the next frames draw into.
108func (r *Renderer) UseTarget(i int) {
109	if i >= 0 && i < len(r.targets) {
110		r.current = i
111	}
112}
113
114// target is the image the next frame draws into.
115func (r *Renderer) target() *target {
116	if len(r.targets) > 0 {
117		return r.targets[r.current]
118	}
119	return &r.own
120}
121
122// exportModifiers intersects the selected signal format modifiers with
123// those supported by the display. HDR never assumes an unspecified list,
124// except for a virtual output (no display) or a test readback. singlePlane
125// drops the modifiers with more than one memory plane.
126func (r *Renderer) exportModifiers(display []uint64, singlePlane bool) []uint64 {
127	var out []uint64
128	available := r.renderMods
129	if r.hdrNits > 0 {
130		available = r.hdrMods
131	}
132	for _, m := range available {
133		if singlePlane && r.modPlanes[m] > 1 {
134			continue
135		}
136		if (len(display) == 0 && (r.hdrNits == 0 || r.hdrReadback || r.virtual)) || slices.Contains(display, m) {
137			out = append(out, m)
138		}
139	}
140	return out
141}
142
143// probeRenderModifiers lists exportable SDR and HDR targets.
144func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
145	r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm)
146	r.hdrMods = r.probeModifiers(physical, vk.FormatA2r10g10b10UnormPack32)
147}
148
149// probeModifiers lists the exportable modifiers of format (one to four memory
150// planes: DCC adds metadata planes; DCC retile modifiers are left out, see
151// retilesDCC) that support the target usage, and
152// records their plane counts in modPlanes. HDR targets need transfer-src only when
153// hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).
154func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
155	list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
156	fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
157	r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
158	if list.DrmFormatModifierCount == 0 {
159		return nil
160	}
161	mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
162	list.DrmFormatModifierProperties = &mods[0]
163	r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
164	need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
165	if format == vk.FormatB8g8r8a8Unorm {
166		need |= formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBlendBit
167	} else if r.hdrReadback {
168		need |= formatFeatureTransferSrc
169	}
170	// SDR targets are composed and read back; HDR targets are color
171	// attachments, and transfer sources only with hdrReadback.
172	result, planes := filterModifiers(mods[:list.DrmFormatModifierCount], need, func(m uint64) bool { return r.exportable(physical, format, m) })
173	if r.modPlanes == nil {
174		r.modPlanes = make(map[uint64]uint32)
175	}
176	// A modifier has the same plane count for every format (DCC planes
177	// come from the modifier), so SDR and HDR share the map.
178	maps.Copy(r.modPlanes, planes)
179	return result
180}
181
182// maxModifierPlanes is the DRM limit of planes in a framebuffer.
183const maxModifierPlanes = 4
184
185// AMD format modifiers (drm_fourcc.h): the vendor is in bits 56-63, and a DCC
186// modifier with the retile bit keeps a second, display-aligned copy of the
187// DCC metadata.
188const (
189	modVendorShift = 56
190	modVendorAMD   = 0x02
191	amdModRetile   = 1 << 14 // AMD_FMT_MOD_DCC_RETILE
192)
193
194// retilesDCC reports whether m is an AMD DCC modifier with the retile bit.
195// RADV renders such an image into pipe-aligned DCC and runs a compute pass
196// that rewrites the display copy: an extra full-surface GPU pass per frame,
197// so these modifiers are never offered for targets. Plain DCC (pipe-aligned
198// or not) costs no such pass.
199func retilesDCC(m uint64) bool {
200	return m>>modVendorShift == modVendorAMD && m&amdModRetile != 0
201}
202
203// filterModifiers keeps the modifiers with 1..4 memory planes whose tiling
204// features include need, that are not DCC retile modifiers and that pass
205// exportable, and returns each kept modifier's plane count.
206func filterModifiers(mods []vk.DrmFormatModifierPropertiesEXT, need vk.FormatFeatureFlags, exportable func(uint64) bool) ([]uint64, map[uint64]uint32) {
207	var result []uint64
208	planes := make(map[uint64]uint32, len(mods))
209	for _, m := range mods {
210		if m.DrmFormatModifierPlaneCount < 1 || m.DrmFormatModifierPlaneCount > maxModifierPlanes || m.DrmFormatModifierTilingFeatures&need != need || retilesDCC(m.DrmFormatModifier) || !exportable(m.DrmFormatModifier) {
211			continue
212		}
213		result = append(result, m.DrmFormatModifier)
214		planes[m.DrmFormatModifier] = m.DrmFormatModifierPlaneCount
215	}
216	return result, planes
217}
218
219func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
220	mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
221	ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
222	info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)}
223	extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
224	out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
225	if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
226		return false
227	}
228	return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
229}
230
231// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
232// generated bindings.
233const formatFeatureTransferSrc = 1 << 14
234
235// Exported SDR targets compose directly; HDR targets receive the final pass.
236const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit
237
238// Production HDR targets need only a color attachment; test readback opts
239// into transfer source before export and skips if the driver refuses it.
240func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
241	if format == vk.FormatA2r10g10b10UnormPack32 && !readback {
242		return vk.ImageUsageColorAttachmentBit
243	}
244	return targetUsage
245}
246
247// exportTarget creates one exported image with a modifier from mods.
248func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
249	d := r.dd
250	t := &target{exported: true, layout: vk.ImageLayoutUndefined}
251	format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB)
252	if r.hdrNits > 0 {
253		format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30
254	}
255	ok := false
256	defer func() {
257		if !ok {
258			r.freeTarget(t)
259		}
260	}()
261	list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
262	external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
263	ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback), SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
264	if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
265		return nil, ports.DMABuf{}, err
266	}
267	var req vk.MemoryRequirements
268	d.GetImageMemoryRequirements(r.device, t.image, &req)
269	kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
270	if err != nil {
271		return nil, ports.DMABuf{}, err
272	}
273	dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
274	export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
275	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
276	if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
277		return nil, ports.DMABuf{}, err
278	}
279	if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
280		return nil, ports.DMABuf{}, err
281	}
282	props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
283	if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
284		return nil, ports.DMABuf{}, err
285	}
286	n := min(max(r.modPlanes[props.DrmFormatModifier], 1), maxModifierPlanes)
287	var layouts [maxModifierPlanes]vk.SubresourceLayout
288	for i := range n {
289		sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT << i}
290		d.GetImageSubresourceLayout(r.device, t.image, &sub, &layouts[i])
291	}
292	t.view, err = r.imageView(t.image, format)
293	if err != nil {
294		return nil, ports.DMABuf{}, err
295	}
296	var fd int32
297	get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
298	if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
299		return nil, ports.DMABuf{}, err
300	}
301	// Every plane owns its file: the planes share the one memory object,
302	// so planes 1.. are duplicates of the exported fd.
303	planes := make([]ports.DMABufPlane, 0, n)
304	closePlanes := func() {
305		for _, p := range planes {
306			_ = p.File.Close()
307		}
308	}
309	first := os.NewFile(uintptr(fd), "neferwl-target")
310	if first == nil {
311		return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
312	}
313	planes = append(planes, ports.DMABufPlane{File: first, Offset: uint32(layouts[0].Offset), Stride: uint32(layouts[0].RowPitch)})
314	for i := uint32(1); i < n; i++ {
315		dup, err := dupFile(first)
316		if err != nil {
317			closePlanes()
318			return nil, ports.DMABuf{}, fmt.Errorf("duplicate exported fd for plane %d: %w", i, err)
319		}
320		planes = append(planes, ports.DMABufPlane{File: os.NewFile(uintptr(dup), "neferwl-target"), Offset: uint32(layouts[i].Offset), Stride: uint32(layouts[i].RowPitch)})
321	}
322	r.log.Debug().Uint64("modifier", props.DrmFormatModifier).Uint32("planes", n).Msg("render target exported")
323	buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourcc, Modifier: props.DrmFormatModifier, Planes: planes}
324	ok = true
325	return t, buf, nil
326}
327
328func (r *Renderer) freeTarget(t *target) {
329	if t.view != 0 {
330		r.dd.DestroyImageView(r.device, t.view, nil)
331		t.view = 0
332	}
333	if t.image != 0 {
334		r.dd.DestroyImage(r.device, t.image, nil)
335		t.image = 0
336	}
337	if t.memory != 0 {
338		r.dd.FreeMemory(r.device, t.memory, nil)
339		t.memory = 0
340	}
341}
342
343func (r *Renderer) dropTargets() {
344	if len(r.targets) > 0 && r.device != 0 {
345		_ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
346		r.idle()
347	}
348	for _, t := range r.targets {
349		if r.last == t {
350			r.last = nil
351		}
352		r.freeTarget(t)
353	}
354	// New targets hold no HDR frame until the next Render.
355	r.targets, r.current, r.lastHDRTarget = nil, 0, -1
356}

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