1//go:build linux
2
3package vkdevice
4
5import (
6 "fmt"
7 "math"
8 "unsafe"
9
10 "github.com/bnema/nefergui/internal/text"
11 "github.com/bnema/purego-vulkan/vulkan"
12)
13
14// Frame records into its own command buffer and signals a binary semaphore.
15// Do not reset the command buffer or destroy the semaphore until its GPU fence
16// completes. The compositor's release point is an independent ownership gate.
17type Frame struct {
18 device *Device
19 Pool vulkan.CommandPool
20 Commands vulkan.CommandBuffer
21 Done vulkan.Fence
22 Signal *BinaryFence
23 View vulkan.ImageView
24 submitted bool
25 Readback *Readback
26 instances *instanceBuffer
27 glyphStaging *instanceBuffer
28 imageStaging *instanceBuffer
29 imageCopies []imageCopy
30 imageSerial uint64
31 atlasRecorded bool
32 fallbackRecorded bool
33 // signalExported is set once the acquire SYNC_FD was exported. Exporting
34 // with copy transference resets the binary semaphore, so Signal may be
35 // signaled again after the frame's fence completes.
36 signalExported bool
37 scratch frameScratch
38}
39
40// frameScratch holds the Vulkan info structs and out-parameters of one
41// recording and submission. The purego bindings force every pointer argument
42// to the heap, so building these per call allocated each frame. A Frame is
43// used by a single owner and its structs are only read by the driver during
44// the call that receives them (the command buffer copies recorded state), so
45// they are rebuilt and reused on every call.
46type frameScratch struct {
47 begin vulkan.CommandBufferBeginInfo
48 barrier vulkan.ImageMemoryBarrier2
49 dependency vulkan.DependencyInfo
50 attachment vulkan.RenderingAttachmentInfo
51 rendering vulkan.RenderingInfo
52 sets [2]vulkan.DescriptorSet
53 screen [2]float32
54 offset vulkan.DeviceSize
55 cmd vulkan.CommandBufferSubmitInfo
56 signal vulkan.SemaphoreSubmitInfo
57 waiting vulkan.SemaphoreSubmitInfo
58 submit vulkan.SubmitInfo2
59 mapped unsafe.Pointer
60}
61
62func (d *Device) NewFrame(img *Image) (f *Frame, err error) {
63 f = &Frame{device: d}
64 defer func() {
65 if err != nil {
66 f.Close()
67 }
68 }()
69 pool := vulkan.CommandPoolCreateInfo{SType: vulkan.StructureTypeCommandPoolCreateInfo, QueueFamilyIndex: d.QueueFamily, Flags: vulkan.CommandPoolCreateResetCommandBufferBit}
70 if err = vulkan.Check(d.Dispatch.CreateCommandPool(d.Logical, &pool, nil, &f.Pool)); err != nil {
71 return nil, err
72 }
73 allocate := vulkan.CommandBufferAllocateInfo{SType: vulkan.StructureTypeCommandBufferAllocateInfo, CommandPool: f.Pool, Level: vulkan.CommandBufferLevelPrimary, CommandBufferCount: 1}
74 if err = vulkan.Check(d.Dispatch.AllocateCommandBuffers(d.Logical, &allocate, &f.Commands)); err != nil {
75 return nil, err
76 }
77 view := vulkan.ImageViewCreateInfo{SType: vulkan.StructureTypeImageViewCreateInfo, Image: img.Image, ViewType: vulkan.ImageViewType2d, Format: vulkan.FormatB8g8r8a8Unorm, SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}}
78 if err = vulkan.Check(d.Dispatch.CreateImageView(d.Logical, &view, nil, &f.View)); err != nil {
79 return nil, err
80 }
81 fence := vulkan.FenceCreateInfo{SType: vulkan.StructureTypeFenceCreateInfo}
82 if err = vulkan.Check(d.Dispatch.CreateFence(d.Logical, &fence, nil, &f.Done)); err != nil {
83 return nil, err
84 }
85 f.Signal, err = d.NewBinaryFence(true)
86 if err != nil {
87 return nil, err
88 }
89 return f, nil
90}
91
92func (f *Frame) Ready() (bool, error) {
93 if !f.submitted {
94 return true, nil
95 }
96 r := f.device.Dispatch.WaitForFences(f.device.Logical, 1, &f.Done, 1, 0)
97 if r == vulkan.Timeout {
98 return false, nil
99 }
100 if err := vulkan.Check(r); err != nil {
101 return false, err
102 }
103 return true, nil
104}
105
106// RecordList records an ordered list of physical-pixel quads. Reuse is gated
107// by Ready, including any instance-buffer growth.
108func (f *Frame) RecordList(img *Image, pipe *Pipeline, width, height int32, instances []Instance, uploads []text.Upload, batches []Batch, initial, capture bool) error {
109 return f.record(img, pipe, width, height, initial, capture, instances, uploads, batches)
110}
111
112func (f *Frame) record(img *Image, pipe *Pipeline, width, height int32, initial, capture bool, instances []Instance, uploads []text.Upload, batches []Batch) (err error) {
113 if width <= 0 || height <= 0 || pipe == nil {
114 return fmt.Errorf("invalid render dimensions or pipeline")
115 }
116 ready, err := f.Ready()
117 if err != nil || !ready {
118 return fmt.Errorf("command buffer still in use: ready=%v err=%v", ready, err)
119 }
120 f.settleImages()
121 if f.submitted {
122 if err = vulkan.Check(f.device.Dispatch.ResetFences(f.device.Logical, 1, &f.Done)); err != nil {
123 return err
124 }
125 if err = vulkan.Check(f.device.Dispatch.ResetCommandBuffer(f.Commands, 0)); err != nil {
126 return err
127 }
128 // The fence is signaled, so the previous submission finished. Its
129 // semaphore is reusable only if the SYNC_FD export consumed the signal;
130 // otherwise it is still signaled and must be replaced.
131 if !f.signalExported {
132 f.Signal.Close()
133 f.Signal, err = f.device.NewBinaryFence(true)
134 if err != nil {
135 return err
136 }
137 }
138 f.submitted = false
139 }
140 if f.device.atlas == nil {
141 return fmt.Errorf("list atlas not initialized")
142 }
143 if err := f.stageGlyphs(uploads); err != nil {
144 return err
145 }
146 if f.device.images != nil {
147 defer func() {
148 if err != nil {
149 f.device.images.abort(f)
150 }
151 }()
152 batches, err = f.device.images.resolve(f, batches)
153 if err != nil {
154 return err
155 }
156 }
157 if len(instances) > 0 {
158 if err := f.uploadInstances(instances); err != nil {
159 return err
160 }
161 }
162 sc := &f.scratch
163 sc.begin = vulkan.CommandBufferBeginInfo{SType: vulkan.StructureTypeCommandBufferBeginInfo, Flags: vulkan.CommandBufferUsageOneTimeSubmitBit}
164 d := f.device.Dispatch
165 if err = vulkan.Check(d.BeginCommandBuffer(f.Commands, &sc.begin)); err != nil {
166 return err
167 }
168 f.atlasRecorded = !f.device.atlas.initialized
169 var staging vulkan.Buffer
170 if len(uploads) != 0 {
171 staging = f.glyphStaging.Buffer
172 f.device.atlas.recordUploads(f.Commands, staging, uploads)
173 if !f.device.images.fallbackReady {
174 f.recordFallback()
175 }
176 f.recordImages()
177 }
178 rangeInfo := vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
179 old := vulkan.ImageLayout(vulkan.ImageLayoutGeneral)
180 if initial {
181 old = vulkan.ImageLayoutUndefined
182 }
183 barrier, dependency := &sc.barrier, &sc.dependency
184 *barrier = vulkan.ImageMemoryBarrier2{SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: vulkan.PipelineStage2None, DstStageMask: vulkan.PipelineStage2ColorAttachmentOutputBit, DstAccessMask: vulkan.Access2ColorAttachmentWriteBit, OldLayout: old, NewLayout: vulkan.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: img.Image, SubresourceRange: rangeInfo}
185 *dependency = vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: barrier}
186 d.CmdPipelineBarrier2(f.Commands, dependency)
187 clear := vulkan.ClearValue{}
188 sc.attachment = vulkan.RenderingAttachmentInfo{SType: vulkan.StructureTypeRenderingAttachmentInfo, ImageView: f.View, ImageLayout: vulkan.ImageLayoutColorAttachmentOptimal, LoadOp: vulkan.AttachmentLoadOpClear, StoreOp: vulkan.AttachmentStoreOpStore, ClearValue: clear}
189 area := vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(width), Height: uint32(height)}}
190 sc.rendering = vulkan.RenderingInfo{SType: vulkan.StructureTypeRenderingInfo, RenderArea: area, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &sc.attachment}
191 d.CmdBeginRendering(f.Commands, &sc.rendering)
192 d.CmdBindPipeline(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Handle)
193 // The fragment shader statically references both descriptor sets even
194 // for non-image draws. Bind a valid initialized image set before any
195 // draw; each image batch then replaces set 1 in painter's order.
196 sc.sets = [2]vulkan.DescriptorSet{f.device.atlas.Set, f.device.images.fallback.Texture.Set}
197 d.CmdBindDescriptorSets(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Layout, 0, 2, &sc.sets[0], 0, nil)
198 sc.screen = [2]float32{float32(width), float32(height)}
199 d.CmdPushConstants(f.Commands, pipe.Layout, vulkan.ShaderStageVertexBit|vulkan.ShaderStageFragmentBit, 0, 8, unsafe.Pointer(&sc.screen))
200 if len(instances) > 0 {
201 sc.offset = 0
202 d.CmdBindVertexBuffers(f.Commands, 0, 1, &f.instances.Buffer, &sc.offset)
203 for _, batch := range batches {
204 if batch.Count == 0 || uint64(batch.First)+uint64(batch.Count) > uint64(len(instances)) {
205 return fmt.Errorf("invalid list batch range")
206 }
207 if batch.Source != nil {
208 if batch.Texture == nil {
209 return fmt.Errorf("image batch has no GPU texture")
210 }
211 d.CmdBindDescriptorSets(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Layout, 1, 1, &batch.Texture.Set, 0, nil)
212 }
213 d.CmdDraw(f.Commands, 6, batch.Count, 0, batch.First)
214 }
215 }
216 d.CmdEndRendering(f.Commands)
217 if capture && f.Readback != nil {
218 barrier.SrcStageMask = vulkan.PipelineStage2ColorAttachmentOutputBit
219 barrier.SrcAccessMask = vulkan.Access2ColorAttachmentWriteBit
220 barrier.DstStageMask = vulkan.PipelineStage2TransferBit
221 barrier.DstAccessMask = vulkan.Access2TransferReadBit
222 barrier.OldLayout = vulkan.ImageLayoutColorAttachmentOptimal
223 barrier.NewLayout = vulkan.ImageLayoutTransferSrcOptimal
224 d.CmdPipelineBarrier2(f.Commands, dependency)
225 f.Readback.record(f.Commands, img.Image)
226 barrier.SrcStageMask = vulkan.PipelineStage2TransferBit
227 barrier.SrcAccessMask = vulkan.Access2TransferReadBit
228 barrier.OldLayout = vulkan.ImageLayoutTransferSrcOptimal
229 } else {
230 barrier.SrcStageMask = vulkan.PipelineStage2ColorAttachmentOutputBit
231 barrier.SrcAccessMask = vulkan.Access2ColorAttachmentWriteBit
232 barrier.OldLayout = vulkan.ImageLayoutColorAttachmentOptimal
233 }
234 barrier.DstStageMask = vulkan.PipelineStage2AllCommandsBit
235 barrier.DstAccessMask = vulkan.Access2MemoryReadBit
236 barrier.NewLayout = vulkan.ImageLayoutGeneral
237 d.CmdPipelineBarrier2(f.Commands, dependency)
238 return vulkan.Check(d.EndCommandBuffer(f.Commands))
239}
240
241// Submit has no CPU wait: the next render waits on a temporarily imported
242// release SYNC_FD, and the compositor waits on the exported acquire SYNC_FD.
243func (f *Frame) Submit(wait *BinaryFence) error {
244 sc := &f.scratch
245 sc.cmd = vulkan.CommandBufferSubmitInfo{SType: vulkan.StructureTypeCommandBufferSubmitInfo, CommandBuffer: f.Commands}
246 sc.signal = vulkan.SemaphoreSubmitInfo{SType: vulkan.StructureTypeSemaphoreSubmitInfo, Semaphore: f.Signal.Semaphore, StageMask: vulkan.PipelineStage2AllCommandsBit}
247 sc.submit = vulkan.SubmitInfo2{SType: vulkan.StructureTypeSubmitInfo2, CommandBufferInfoCount: 1, CommandBufferInfos: &sc.cmd, SignalSemaphoreInfoCount: 1, SignalSemaphoreInfos: &sc.signal}
248 if wait != nil {
249 sc.waiting = vulkan.SemaphoreSubmitInfo{SType: vulkan.StructureTypeSemaphoreSubmitInfo, Semaphore: wait.Semaphore, StageMask: vulkan.PipelineStage2AllCommandsBit}
250 sc.submit.WaitSemaphoreInfoCount = 1
251 sc.submit.WaitSemaphoreInfos = &sc.waiting
252 }
253 if err := vulkan.Check(f.device.Dispatch.QueueSubmit2(f.device.Queue, 1, &sc.submit, f.Done)); err != nil {
254 // Only this recording's unsubmitted image state is released; a
255 // previous submission's serial was completed by record.
256 if f.device.images != nil && f.imageSerial != 0 {
257 f.device.images.abort(f)
258 }
259 return err
260 }
261 f.submitted = true
262 f.signalExported = false
263 if f.device.images != nil && f.imageSerial != 0 {
264 f.device.images.inflight[f] = struct{}{}
265 }
266 if f.atlasRecorded {
267 f.device.atlas.initialized = true
268 }
269 if f.fallbackRecorded {
270 f.device.images.fallbackReady = true
271 }
272 return nil
273}
274
275// ExportSignal exports the submitted frame's acquire SYNC_FD. The export
276// resets the semaphore, which lets the next recording of this frame reuse it.
277func (f *Frame) ExportSignal() (int, error) {
278 fd, err := f.Signal.Export()
279 if err == nil {
280 f.signalExported = true
281 }
282 return fd, err
283}
284
285// settleImages runs once the frame is Ready. A submitted frame's serial is
286// completed and its upload list forgotten, so a later failed submission can
287// never abort textures that submitted work used. A recorded but unsubmitted
288// frame is aborted: nothing on the GPU references its new textures.
289func (f *Frame) settleImages() {
290 if f.device.images == nil || f.imageSerial == 0 {
291 return
292 }
293 if f.submitted {
294 delete(f.device.images.inflight, f)
295 f.device.images.complete(f.imageSerial)
296 f.imageSerial = 0
297 f.imageCopies = f.imageCopies[:0]
298 return
299 }
300 f.device.images.abort(f)
301}
302
303func (f *Frame) Wait() error {
304 if !f.submitted {
305 return nil
306 }
307 return vulkan.Check(f.device.Dispatch.WaitForFences(f.device.Logical, 1, &f.Done, 1, math.MaxUint64))
308}
309func (f *Frame) Close() {
310 if f == nil || f.device == nil {
311 return
312 }
313 _ = f.Wait()
314 d := f.device.Dispatch
315 if f.Signal != nil {
316 f.Signal.Close()
317 f.Signal = nil
318 }
319 if f.Done != 0 {
320 d.DestroyFence(f.device.Logical, f.Done, nil)
321 f.Done = 0
322 }
323 if f.View != 0 {
324 d.DestroyImageView(f.device.Logical, f.View, nil)
325 f.View = 0
326 }
327 if f.Pool != 0 {
328 d.DestroyCommandPool(f.device.Logical, f.Pool, nil)
329 f.Pool = 0
330 }
331 if f.instances != nil {
332 f.instances.Close()
333 f.instances = nil
334 }
335 if f.glyphStaging != nil {
336 f.glyphStaging.Close()
337 f.glyphStaging = nil
338 }
339 if f.imageStaging != nil {
340 f.imageStaging.Close()
341 f.imageStaging = nil
342 }
343 // After Wait: complete submitted work, abort a recorded-but-unsubmitted list.
344 f.settleImages()
345 if f.Readback != nil {
346 f.Readback.Close()
347 f.Readback = nil
348 }
349}