v0.3.0 cf370a0

targets.go RawHistoryFolder

1package vulkan
2
3import (
4	"errors"
5	"fmt"
6	"os"
7	"slices"
8	"unsafe"
9
10	"github.com/bnema/neferwl/internal/ports"
11	vk "github.com/bnema/purego-vulkan/vulkan"
12)
13
14// Render targets (ADR 014). A DRM output scans out images the renderer
15// draws into: ExportTargets allocates them with a DRM format modifier the
16// display accepts and exports their memory as dmabufs, which the output
17// turns into KMS framebuffers. Render then composes straight into the
18// selected target and the output flips to it; nothing is copied by the
19// CPU. In HDR an internal fp16 linear image keeps full-range composition,
20// sampled by a full-screen GPU pass into the 10-bit target. Capture/Pixels
21// convert to SDR sRGB only when requested.
22
23// target is an image frames are drawn into.
24type target struct {
25	image  vk.Image
26	memory vk.DeviceMemory
27	layout vk.ImageLayout
28	view   vk.ImageView // color attachment for blended draws
29	// exported images are shared with the display: ownership goes to the
30	// foreign queue family after each frame, and KMS reads them.
31	exported bool
32	// What the image holds (damage.go): the scene Seq and each window's
33	// content Seq; valid once a frame was drawn whole.
34	valid    bool
35	sceneSeq uint64
36	windows  map[ports.WindowID]heldWindow
37}
38
39// fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
40const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
41
42// fourccXR30 is DRM_FORMAT_XRGB2101010 (A2R10G10B10 packed).
43const fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
44
45// ExportTargets allocates n exported images of the output size, replacing
46// the previous ones; n = 0 only drops them (back to the internal image).
47func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
48	r.dropTargets()
49	if n > 0 && r.hdrNits > 0 && r.hdrReadback && r.physical != 0 {
50		// Readback targets (GPU tests, headless --screenshot-raw) are also
51		// transfer sources, which changes the format requirements.
52		r.hdrMods = r.probeModifiers(r.physical, vk.FormatA2r10g10b10UnormPack32)
53	}
54	if n > 0 && r.hdrNits > 0 && r.hdr.pipeline == 0 {
55		return nil, fmt.Errorf("HDR transform unavailable: %w", r.hdrError)
56	}
57	if n == 0 {
58		return nil, nil
59	}
60	if !r.dd.HasGetMemoryFdKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() || len(r.dmabuf.Formats) == 0 {
61		return nil, errors.New("device cannot export dmabufs")
62	}
63	mods := r.exportModifiers(modifiers)
64	if len(mods) == 0 {
65		if r.hdrNits == 0 {
66			return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
67		}
68		return nil, errors.New("no XRGB2101010 modifier both the device and the display accept")
69	}
70	var out []ports.DMABuf
71	for range n {
72		t, buf, err := r.exportTarget(mods)
73		if err != nil {
74			r.dropTargets()
75			for _, b := range out {
76				for _, p := range b.Planes {
77					p.File.Close()
78				}
79			}
80			return nil, err
81		}
82		r.targets = append(r.targets, t)
83		out = append(out, buf)
84	}
85	r.current = 0
86	return out, nil
87}
88
89// SetVirtualOutput marks the output as having no display (headless): an
90// empty display modifier list then accepts every exportable HDR modifier,
91// as it does for SDR. DRM never sets it. Call before ExportTargets.
92func (r *Renderer) SetVirtualOutput(on bool) { r.virtual = on }
93
94// UseTarget selects the exported image the next frames draw into.
95func (r *Renderer) UseTarget(i int) {
96	if i >= 0 && i < len(r.targets) {
97		r.current = i
98	}
99}
100
101// target is the image the next frame draws into.
102func (r *Renderer) target() *target {
103	if len(r.targets) > 0 {
104		return r.targets[r.current]
105	}
106	return &r.own
107}
108
109// exportModifiers intersects the selected signal format modifiers with
110// those supported by the display. HDR never assumes an unspecified list,
111// except for a virtual output (no display) or a test readback.
112func (r *Renderer) exportModifiers(display []uint64) []uint64 {
113	var out []uint64
114	available := r.renderMods
115	if r.hdrNits > 0 {
116		available = r.hdrMods
117	}
118	for _, m := range available {
119		if (len(display) == 0 && (r.hdrNits == 0 || r.hdrReadback || r.virtual)) || slices.Contains(display, m) {
120			out = append(out, m)
121		}
122	}
123	return out
124}
125
126// probeRenderModifiers lists single-plane, exportable SDR and HDR targets.
127func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
128	r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm)
129	r.hdrMods = r.probeModifiers(physical, vk.FormatA2r10g10b10UnormPack32)
130}
131
132// probeModifiers lists the single-plane, exportable modifiers of format that
133// support the target usage. HDR targets need transfer-src only when
134// hdrReadback is set (GPU tests, headless --screenshot-raw; never DRM).
135func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
136	list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
137	fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
138	r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
139	if list.DrmFormatModifierCount == 0 {
140		return nil
141	}
142	mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
143	list.DrmFormatModifierProperties = &mods[0]
144	r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
145	need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
146	if format == vk.FormatB8g8r8a8Unorm {
147		need |= formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBlendBit
148	} else if r.hdrReadback {
149		need |= formatFeatureTransferSrc
150	}
151	var result []uint64
152	// SDR targets are composed and read back; HDR targets are color
153	// attachments, and transfer sources only with hdrReadback.
154	for _, m := range mods[:list.DrmFormatModifierCount] {
155		if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, format, m.DrmFormatModifier) {
156			result = append(result, m.DrmFormatModifier)
157		}
158	}
159	return result
160}
161
162func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
163	mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
164	ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
165	info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)}
166	extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
167	out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
168	if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
169		return false
170	}
171	return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
172}
173
174// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
175// generated bindings.
176const formatFeatureTransferSrc = 1 << 14
177
178// Exported SDR targets compose directly; HDR targets receive the final pass.
179const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit
180
181// Production HDR targets need only a color attachment; test readback opts
182// into transfer source before export and skips if the driver refuses it.
183func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
184	if format == vk.FormatA2r10g10b10UnormPack32 && !readback {
185		return vk.ImageUsageColorAttachmentBit
186	}
187	return targetUsage
188}
189
190// exportTarget creates one exported image with a modifier from mods.
191func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
192	d := r.dd
193	t := &target{exported: true, layout: vk.ImageLayoutUndefined}
194	format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB)
195	if r.hdrNits > 0 {
196		format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30
197	}
198	ok := false
199	defer func() {
200		if !ok {
201			r.freeTarget(t)
202		}
203	}()
204	list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
205	external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
206	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}
207	if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
208		return nil, ports.DMABuf{}, err
209	}
210	var req vk.MemoryRequirements
211	d.GetImageMemoryRequirements(r.device, t.image, &req)
212	kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
213	if err != nil {
214		return nil, ports.DMABuf{}, err
215	}
216	dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
217	export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
218	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
219	if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
220		return nil, ports.DMABuf{}, err
221	}
222	if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
223		return nil, ports.DMABuf{}, err
224	}
225	props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
226	if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
227		return nil, ports.DMABuf{}, err
228	}
229	var layout vk.SubresourceLayout
230	sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
231	d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
232	t.view, err = r.imageView(t.image, format)
233	if err != nil {
234		return nil, ports.DMABuf{}, err
235	}
236	var fd int32
237	get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
238	if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
239		return nil, ports.DMABuf{}, err
240	}
241	f := os.NewFile(uintptr(fd), "neferwl-target")
242	if f == nil {
243		return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
244	}
245	buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourcc, Modifier: props.DrmFormatModifier, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}
246	ok = true
247	return t, buf, nil
248}
249
250func (r *Renderer) freeTarget(t *target) {
251	if t.view != 0 {
252		r.dd.DestroyImageView(r.device, t.view, nil)
253		t.view = 0
254	}
255	if t.image != 0 {
256		r.dd.DestroyImage(r.device, t.image, nil)
257		t.image = 0
258	}
259	if t.memory != 0 {
260		r.dd.FreeMemory(r.device, t.memory, nil)
261		t.memory = 0
262	}
263}
264
265func (r *Renderer) dropTargets() {
266	if len(r.targets) > 0 && r.device != 0 {
267		_ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
268		r.idle()
269	}
270	for _, t := range r.targets {
271		if r.last == t {
272			r.last = nil
273		}
274		r.freeTarget(t)
275	}
276	// New targets hold no HDR frame until the next Render.
277	r.targets, r.current, r.lastHDRTarget = nil, 0, -1
278}

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