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 // Test-only transfer-source targets have different format requirements.
51 r.hdrMods = r.probeModifiers(r.physical, vk.FormatA2r10g10b10UnormPack32)
52 }
53 if n > 0 && r.hdrNits > 0 && r.hdr.pipeline == 0 {
54 return nil, fmt.Errorf("HDR transform unavailable: %w", r.hdrError)
55 }
56 if n == 0 {
57 return nil, nil
58 }
59 if !r.dd.HasGetMemoryFdKHR() || !r.dd.HasGetImageDrmFormatModifierPropertiesEXT() || len(r.dmabuf.Formats) == 0 {
60 return nil, errors.New("device cannot export dmabufs")
61 }
62 mods := r.exportModifiers(modifiers)
63 if len(mods) == 0 {
64 if r.hdrNits == 0 {
65 return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
66 }
67 return nil, errors.New("no XRGB2101010 modifier both the device and the display accept")
68 }
69 var out []ports.DMABuf
70 for range n {
71 t, buf, err := r.exportTarget(mods)
72 if err != nil {
73 r.dropTargets()
74 for _, b := range out {
75 for _, p := range b.Planes {
76 p.File.Close()
77 }
78 }
79 return nil, err
80 }
81 r.targets = append(r.targets, t)
82 out = append(out, buf)
83 }
84 r.current = 0
85 return out, nil
86}
87
88// UseTarget selects the exported image the next frames draw into.
89func (r *Renderer) UseTarget(i int) {
90 if i >= 0 && i < len(r.targets) {
91 r.current = i
92 }
93}
94
95// target is the image the next frame draws into.
96func (r *Renderer) target() *target {
97 if len(r.targets) > 0 {
98 return r.targets[r.current]
99 }
100 return &r.own
101}
102
103// exportModifiers intersects the selected signal format modifiers with
104// those supported by the display. HDR never assumes an unspecified list.
105func (r *Renderer) exportModifiers(display []uint64) []uint64 {
106 var out []uint64
107 available := r.renderMods
108 if r.hdrNits > 0 {
109 available = r.hdrMods
110 }
111 for _, m := range available {
112 if (len(display) == 0 && (r.hdrNits == 0 || r.hdrReadback)) || slices.Contains(display, m) {
113 out = append(out, m)
114 }
115 }
116 return out
117}
118
119// probeRenderModifiers lists single-plane, exportable SDR and HDR targets.
120func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
121 r.renderMods = r.probeModifiers(physical, vk.FormatB8g8r8a8Unorm)
122 r.hdrMods = r.probeModifiers(physical, vk.FormatA2r10g10b10UnormPack32)
123}
124
125func (r *Renderer) probeModifiers(physical vk.PhysicalDevice, format vk.Format) []uint64 {
126 list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
127 fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
128 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
129 if list.DrmFormatModifierCount == 0 {
130 return nil
131 }
132 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
133 list.DrmFormatModifierProperties = &mods[0]
134 r.id.GetPhysicalDeviceFormatProperties2(physical, format, &fp)
135 need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
136 if format == vk.FormatB8g8r8a8Unorm {
137 need |= formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBlendBit
138 } else if r.hdrReadback {
139 need |= formatFeatureTransferSrc
140 }
141 var result []uint64
142 // SDR targets are composed and read back; HDR targets are color attachments.
143 for _, m := range mods[:list.DrmFormatModifierCount] {
144 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, format, m.DrmFormatModifier) {
145 result = append(result, m.DrmFormatModifier)
146 }
147 }
148 return result
149}
150
151func (r *Renderer) exportable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
152 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
153 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
154 info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: exportUsage(format, r.hdrReadback)}
155 extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
156 out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
157 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
158 return false
159 }
160 return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
161}
162
163// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
164// generated bindings.
165const formatFeatureTransferSrc = 1 << 14
166
167// Exported SDR targets compose directly; HDR targets receive the final pass.
168const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit
169
170// Production HDR targets need only a color attachment; test readback opts
171// into transfer source before export and skips if the driver refuses it.
172func exportUsage(format vk.Format, readback bool) vk.ImageUsageFlags {
173 if format == vk.FormatA2r10g10b10UnormPack32 && !readback {
174 return vk.ImageUsageColorAttachmentBit
175 }
176 return targetUsage
177}
178
179// exportTarget creates one exported image with a modifier from mods.
180func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
181 d := r.dd
182 t := &target{exported: true, layout: vk.ImageLayoutUndefined}
183 format, fourcc := vk.Format(vk.FormatB8g8r8a8Unorm), uint32(fourccXRGB)
184 if r.hdrNits > 0 {
185 format, fourcc = vk.FormatA2r10g10b10UnormPack32, fourccXR30
186 }
187 ok := false
188 defer func() {
189 if !ok {
190 r.freeTarget(t)
191 }
192 }()
193 list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
194 external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
195 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}
196 if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
197 return nil, ports.DMABuf{}, err
198 }
199 var req vk.MemoryRequirements
200 d.GetImageMemoryRequirements(r.device, t.image, &req)
201 kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
202 if err != nil {
203 return nil, ports.DMABuf{}, err
204 }
205 dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
206 export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
207 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
208 if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
209 return nil, ports.DMABuf{}, err
210 }
211 if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
212 return nil, ports.DMABuf{}, err
213 }
214 props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
215 if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
216 return nil, ports.DMABuf{}, err
217 }
218 var layout vk.SubresourceLayout
219 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
220 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
221 t.view, err = r.imageView(t.image, format)
222 if err != nil {
223 return nil, ports.DMABuf{}, err
224 }
225 var fd int32
226 get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
227 if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
228 return nil, ports.DMABuf{}, err
229 }
230 f := os.NewFile(uintptr(fd), "neferwl-target")
231 if f == nil {
232 return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
233 }
234 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)}}}
235 ok = true
236 return t, buf, nil
237}
238
239func (r *Renderer) freeTarget(t *target) {
240 if t.view != 0 {
241 r.dd.DestroyImageView(r.device, t.view, nil)
242 t.view = 0
243 }
244 if t.image != 0 {
245 r.dd.DestroyImage(r.device, t.image, nil)
246 t.image = 0
247 }
248 if t.memory != 0 {
249 r.dd.FreeMemory(r.device, t.memory, nil)
250 t.memory = 0
251 }
252}
253
254func (r *Renderer) dropTargets() {
255 if len(r.targets) > 0 && r.device != 0 {
256 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
257 r.idle()
258 }
259 for _, t := range r.targets {
260 if r.last == t {
261 r.last = nil
262 }
263 r.freeTarget(t)
264 }
265 r.targets, r.current = nil, 0
266}