1package vulkan
2
3import (
4 _ "embed"
5 "fmt"
6 "unsafe"
7
8 vk "github.com/bnema/purego-vulkan/vulkan"
9)
10
11var (
12 //go:embed shaders/hdr.vert.spv
13 hdrVert []byte
14 //go:embed shaders/hdr.frag.spv
15 hdrFrag []byte
16)
17
18type hdrPass struct {
19 setLayout vk.DescriptorSetLayout
20 layout vk.PipelineLayout
21 pipeline vk.Pipeline
22 sampler vk.Sampler
23 pool vk.DescriptorPool
24 set vk.DescriptorSet
25}
26
27// SetHDR prepares the output transform before ExportTargets. The renderer
28// remains owner-goroutine confined; all GPU objects are reused each frame.
29func (r *Renderer) SetHDR(nits float64) {
30 if nits != r.hdrNits {
31 r.cursorCache.Reset()
32 if r.hdrNits > 0 && nits > 0 {
33 r.hdrOwn.valid = false
34 }
35 if (nits > 0) != (r.hdrNits > 0) && r.device != 0 {
36 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
37 r.idle()
38 r.hdrOwn.valid = false
39 r.own.valid = false
40 for _, t := range r.targets {
41 t.valid = false
42 }
43 if r.last == &r.hdrOwn {
44 r.last = nil
45 }
46 }
47 }
48 r.hdrNits = nits
49 if nits <= 0 {
50 r.cursorCache.Reset()
51 r.hdrError = nil
52 return
53 }
54 if r.hdrOwn.image == 0 {
55 r.hdrError = r.createHDROwn()
56 if r.hdrError != nil {
57 r.dropHDROwn()
58 return
59 }
60 }
61 if r.compose.hdrPipeline == 0 {
62 fragment := composeHDRFrag
63 if !r.compose.indexing {
64 fragment = composeHDRSingleFrag
65 }
66 r.hdrError = r.createComposePipeline(fragment, vk.FormatR16g16b16a16Sfloat, &r.compose.hdrPipeline)
67 if r.hdrError != nil {
68 r.dropHDROwn()
69 return
70 }
71 }
72 if r.hdr.pipeline != 0 {
73 return
74 }
75 // The port has no error return: ExportTargets fails if preparation fails.
76 r.hdrError = r.createHDR()
77 if r.hdrError != nil {
78 r.destroyHDR()
79 r.dropHDROwn()
80 }
81}
82
83// createHDROwn allocates the fp16 linear-light composition image once per
84// renderer; toggling HDR reuses it after the previous frames complete.
85func (r *Renderer) createHDROwn() error {
86 // An HDR compositor needs an fp16 blendable color attachment that can
87 // also be sampled by the PQ pass and copied only for capture.
88 if r.physical == 0 {
89 return fmt.Errorf("HDR fp16 device unavailable")
90 }
91 props := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2}
92 r.id.GetPhysicalDeviceFormatProperties2(r.physical, vk.FormatR16g16b16a16Sfloat, &props)
93 need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit | vk.FormatFeatureSampledImageBit | formatFeatureTransferSrc)
94 if props.FormatProperties.OptimalTilingFeatures&need != need {
95 return fmt.Errorf("HDR fp16 composition format lacks attachment/blend/sample/readback features")
96 }
97 d, t := r.dd, &r.hdrOwn
98 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatR16g16b16a16Sfloat, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: targetUsage | vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
99 if err := checked("vkCreateImage(HDR composition)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
100 return err
101 }
102 var req vk.MemoryRequirements
103 d.GetImageMemoryRequirements(r.device, t.image, &req)
104 kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
105 if err != nil {
106 return err
107 }
108 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
109 if err := checked("vkAllocateMemory(HDR composition)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
110 return err
111 }
112 if err := checked("vkBindImageMemory(HDR composition)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
113 return err
114 }
115 t.view, err = r.imageView(t.image, vk.FormatR16g16b16a16Sfloat)
116 return err
117}
118
119func (r *Renderer) createHDR() error {
120 d, h := r.dd, &r.hdr
121 binding := vk.DescriptorSetLayoutBinding{Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}
122 sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &binding}
123 if err := checked("vkCreateDescriptorSetLayout(HDR)", d.CreateDescriptorSetLayout(r.device, &sli, nil, &h.setLayout)); err != nil {
124 return err
125 }
126 push := vk.PushConstantRange{StageFlags: vk.ShaderStageFragmentBit, Size: 4}
127 pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &h.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &push}
128 if err := checked("vkCreatePipelineLayout(HDR)", d.CreatePipelineLayout(r.device, &pli, nil, &h.layout)); err != nil {
129 return err
130 }
131 if err := r.createGraphicsPipeline(hdrVert, hdrFrag, vk.FormatA2r10g10b10UnormPack32, h.layout, false, nil, &h.pipeline); err != nil {
132 return err
133 }
134 si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
135 if err := checked("vkCreateSampler(HDR)", d.CreateSampler(r.device, &si, nil, &h.sampler)); err != nil {
136 return err
137 }
138 size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}
139 dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
140 if err := checked("vkCreateDescriptorPool(HDR)", d.CreateDescriptorPool(r.device, &dpi, nil, &h.pool)); err != nil {
141 return err
142 }
143 dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: h.pool, DescriptorSetCount: 1, SetLayouts: &h.setLayout}
144 if err := checked("vkAllocateDescriptorSets(HDR)", d.AllocateDescriptorSets(r.device, &dai, &h.set)); err != nil {
145 return err
146 }
147 img := vk.DescriptorImageInfo{Sampler: h.sampler, ImageView: r.hdrOwn.view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
148 write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: h.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}
149 d.UpdateDescriptorSets(r.device, 1, &write, 0, nil)
150 return nil
151}
152
153func (r *Renderer) destroyHDR() {
154 if r.dd == nil {
155 return
156 }
157 d, h := r.dd, &r.hdr
158 if h.pool != 0 {
159 d.DestroyDescriptorPool(r.device, h.pool, nil)
160 }
161 if h.sampler != 0 {
162 d.DestroySampler(r.device, h.sampler, nil)
163 }
164 if h.pipeline != 0 {
165 d.DestroyPipeline(r.device, h.pipeline, nil)
166 }
167 if h.layout != 0 {
168 d.DestroyPipelineLayout(r.device, h.layout, nil)
169 }
170 if h.setLayout != 0 {
171 d.DestroyDescriptorSetLayout(r.device, h.setLayout, nil)
172 }
173 *h = hdrPass{}
174}
175
176// recordHDR converts the fully composed linear HDR image in the same command buffer.
177func (r *Renderer) recordHDR(cmd vk.CommandBuffer, out *target) {
178 d, h := r.dd, &r.hdr
179 in, b := &r.call.hdrIn, &r.call.hdrOut
180 attachment, info := &r.call.attach, &r.call.render
181 *in = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessColorAttachmentWriteBit, DstAccessMask: vk.AccessShaderReadBit, OldLayout: vk.ImageLayoutColorAttachmentOptimal, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: r.hdrOwn.image, SubresourceRange: colorRange}
182 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, in)
183 *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: out.image, SubresourceRange: colorRange}
184 d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
185 *attachment = vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
186 *info = vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: attachment}
187 d.CmdBeginRendering(cmd, info)
188 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, h.pipeline)
189 d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, h.layout, 0, 1, &h.set, 0, nil)
190 r.call.hdrNits = float32(r.hdrNits)
191 d.CmdPushConstants(cmd, h.layout, vk.ShaderStageFragmentBit, 0, 4, unsafe.Pointer(&r.call.hdrNits))
192 d.CmdDraw(cmd, 3, 1, 0, 0)
193 d.CmdEndRendering(cmd)
194 b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0
195 b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral
196 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
197 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
198 in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit
199 in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal
200 d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, in)
201}
202
203// dropHDROwn frees the HDR composition image with the capture pass that
204// samples it (its descriptor set refers to the view). Pending captures
205// finish first: this runs on SetHDR failures, never per frame.
206func (r *Renderer) dropHDROwn() {
207 if r.capturePass.pipeline != 0 {
208 for _, s := range r.captures {
209 if s.pending {
210 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
211 r.idle()
212 break
213 }
214 }
215 r.destroyCapturePass()
216 }
217 r.freeTarget(&r.hdrOwn)
218}