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