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 = cursorConversion{}
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 = cursorConversion{}
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 r.hdrError = r.createGraphicsPipeline(composeVert, composeHDRFrag, vk.FormatR16g16b16a16Sfloat, r.compose.layout, true, &r.compose.hdrPipeline)
63 if r.hdrError != nil {
64 r.dropHDROwn()
65 return
66 }
67 }
68 if r.hdr.pipeline != 0 {
69 return
70 }
71 // The port has no error return: ExportTargets fails if preparation fails.
72 r.hdrError = r.createHDR()
73 if r.hdrError != nil {
74 r.destroyHDR()
75 r.dropHDROwn()
76 }
77}
78
79// createHDROwn allocates the fp16 linear-light composition image once per
80// renderer; toggling HDR reuses it after the previous frames complete.
81func (r *Renderer) createHDROwn() error {
82 // An HDR compositor needs an fp16 blendable color attachment that can
83 // also be sampled by the PQ pass and copied only for capture.
84 if r.physical == 0 {
85 return fmt.Errorf("HDR fp16 device unavailable")
86 }
87 props := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2}
88 r.id.GetPhysicalDeviceFormatProperties2(r.physical, vk.FormatR16g16b16a16Sfloat, &props)
89 need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit | vk.FormatFeatureSampledImageBit | formatFeatureTransferSrc)
90 if props.FormatProperties.OptimalTilingFeatures&need != need {
91 return fmt.Errorf("HDR fp16 composition format lacks attachment/blend/sample/readback features")
92 }
93 d, t := r.dd, &r.hdrOwn
94 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}
95 if err := checked("vkCreateImage(HDR composition)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
96 return err
97 }
98 var req vk.MemoryRequirements
99 d.GetImageMemoryRequirements(r.device, t.image, &req)
100 kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
101 if err != nil {
102 return err
103 }
104 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
105 if err := checked("vkAllocateMemory(HDR composition)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
106 return err
107 }
108 if err := checked("vkBindImageMemory(HDR composition)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
109 return err
110 }
111 t.view, err = r.imageView(t.image, vk.FormatR16g16b16a16Sfloat)
112 return err
113}
114
115func (r *Renderer) createHDR() error {
116 d, h := r.dd, &r.hdr
117 binding := vk.DescriptorSetLayoutBinding{Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}
118 sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &binding}
119 if err := checked("vkCreateDescriptorSetLayout(HDR)", d.CreateDescriptorSetLayout(r.device, &sli, nil, &h.setLayout)); err != nil {
120 return err
121 }
122 push := vk.PushConstantRange{StageFlags: vk.ShaderStageFragmentBit, Size: 4}
123 pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &h.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &push}
124 if err := checked("vkCreatePipelineLayout(HDR)", d.CreatePipelineLayout(r.device, &pli, nil, &h.layout)); err != nil {
125 return err
126 }
127 if err := r.createGraphicsPipeline(hdrVert, hdrFrag, vk.FormatA2r10g10b10UnormPack32, h.layout, false, &h.pipeline); err != nil {
128 return err
129 }
130 si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
131 if err := checked("vkCreateSampler(HDR)", d.CreateSampler(r.device, &si, nil, &h.sampler)); err != nil {
132 return err
133 }
134 size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}
135 dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
136 if err := checked("vkCreateDescriptorPool(HDR)", d.CreateDescriptorPool(r.device, &dpi, nil, &h.pool)); err != nil {
137 return err
138 }
139 dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: h.pool, DescriptorSetCount: 1, SetLayouts: &h.setLayout}
140 if err := checked("vkAllocateDescriptorSets(HDR)", d.AllocateDescriptorSets(r.device, &dai, &h.set)); err != nil {
141 return err
142 }
143 img := vk.DescriptorImageInfo{Sampler: h.sampler, ImageView: r.hdrOwn.view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
144 write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: h.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}
145 d.UpdateDescriptorSets(r.device, 1, &write, 0, nil)
146 return nil
147}
148
149func (r *Renderer) destroyHDR() {
150 if r.dd == nil {
151 return
152 }
153 d, h := r.dd, &r.hdr
154 if h.pool != 0 {
155 d.DestroyDescriptorPool(r.device, h.pool, nil)
156 }
157 if h.sampler != 0 {
158 d.DestroySampler(r.device, h.sampler, nil)
159 }
160 if h.pipeline != 0 {
161 d.DestroyPipeline(r.device, h.pipeline, nil)
162 }
163 if h.layout != 0 {
164 d.DestroyPipelineLayout(r.device, h.layout, nil)
165 }
166 if h.setLayout != 0 {
167 d.DestroyDescriptorSetLayout(r.device, h.setLayout, nil)
168 }
169 *h = hdrPass{}
170}
171
172// recordHDR converts the fully composed linear HDR image in the same command buffer.
173func (r *Renderer) recordHDR(cmd vk.CommandBuffer, out *target) {
174 d, h := r.dd, &r.hdr
175 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}
176 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &in)
177 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}
178 d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
179 attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
180 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}
181 d.CmdBeginRendering(cmd, &info)
182 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, h.pipeline)
183 d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, h.layout, 0, 1, &h.set, 0, nil)
184 nits := float32(r.hdrNits)
185 d.CmdPushConstants(cmd, h.layout, vk.ShaderStageFragmentBit, 0, 4, unsafe.Pointer(&nits))
186 d.CmdDraw(cmd, 3, 1, 0, 0)
187 d.CmdEndRendering(cmd)
188 b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0
189 b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral
190 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
191 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
192 in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit
193 in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal
194 d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &in)
195}
196
197// dropHDROwn frees the HDR composition image with the capture pass that
198// samples it (its descriptor set refers to the view). Pending captures
199// finish first: this runs on SetHDR failures, never per frame.
200func (r *Renderer) dropHDROwn() {
201 if r.capturePass.pipeline != 0 {
202 for _, s := range r.captures {
203 if s.pending {
204 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
205 r.idle()
206 break
207 }
208 }
209 r.destroyCapturePass()
210 }
211 r.freeTarget(&r.hdrOwn)
212}