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}