main bcc9a45

render.go RawHistoryFolder

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	// Glyph uploads, a new image texture or the first frame each need
170	// transfers before rendering: an image added without new glyphs must
171	// still be uploaded.
172	if len(uploads) != 0 || len(f.imageCopies) != 0 || !f.device.atlas.initialized || !f.device.images.fallbackReady {
173		var staging vulkan.Buffer
174		if len(uploads) != 0 {
175			staging = f.glyphStaging.Buffer
176		}
177		f.device.atlas.recordUploads(f.Commands, staging, uploads)
178		if !f.device.images.fallbackReady {
179			f.recordFallback()
180		}
181		f.recordImages()
182	}
183	rangeInfo := vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
184	old := vulkan.ImageLayout(vulkan.ImageLayoutGeneral)
185	if initial {
186		old = vulkan.ImageLayoutUndefined
187	}
188	barrier, dependency := &sc.barrier, &sc.dependency
189	*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}
190	*dependency = vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: barrier}
191	d.CmdPipelineBarrier2(f.Commands, dependency)
192	clear := vulkan.ClearValue{}
193	sc.attachment = vulkan.RenderingAttachmentInfo{SType: vulkan.StructureTypeRenderingAttachmentInfo, ImageView: f.View, ImageLayout: vulkan.ImageLayoutColorAttachmentOptimal, LoadOp: vulkan.AttachmentLoadOpClear, StoreOp: vulkan.AttachmentStoreOpStore, ClearValue: clear}
194	area := vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(width), Height: uint32(height)}}
195	sc.rendering = vulkan.RenderingInfo{SType: vulkan.StructureTypeRenderingInfo, RenderArea: area, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &sc.attachment}
196	d.CmdBeginRendering(f.Commands, &sc.rendering)
197	d.CmdBindPipeline(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Handle)
198	// The fragment shader statically references both descriptor sets even
199	// for non-image draws. Bind a valid initialized image set before any
200	// draw; each image batch then replaces set 1 in painter's order.
201	sc.sets = [2]vulkan.DescriptorSet{f.device.atlas.Set, f.device.images.fallback.Texture.Set}
202	d.CmdBindDescriptorSets(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Layout, 0, 2, &sc.sets[0], 0, nil)
203	sc.screen = [2]float32{float32(width), float32(height)}
204	d.CmdPushConstants(f.Commands, pipe.Layout, vulkan.ShaderStageVertexBit|vulkan.ShaderStageFragmentBit, 0, 8, unsafe.Pointer(&sc.screen))
205	if len(instances) > 0 {
206		sc.offset = 0
207		d.CmdBindVertexBuffers(f.Commands, 0, 1, &f.instances.Buffer, &sc.offset)
208		for _, batch := range batches {
209			if batch.Count == 0 || uint64(batch.First)+uint64(batch.Count) > uint64(len(instances)) {
210				return fmt.Errorf("invalid list batch range")
211			}
212			if batch.Source != nil {
213				if batch.Texture == nil {
214					return fmt.Errorf("image batch has no GPU texture")
215				}
216				d.CmdBindDescriptorSets(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Layout, 1, 1, &batch.Texture.Set, 0, nil)
217			}
218			d.CmdDraw(f.Commands, 6, batch.Count, 0, batch.First)
219		}
220	}
221	d.CmdEndRendering(f.Commands)
222	if capture && f.Readback != nil {
223		barrier.SrcStageMask = vulkan.PipelineStage2ColorAttachmentOutputBit
224		barrier.SrcAccessMask = vulkan.Access2ColorAttachmentWriteBit
225		barrier.DstStageMask = vulkan.PipelineStage2TransferBit
226		barrier.DstAccessMask = vulkan.Access2TransferReadBit
227		barrier.OldLayout = vulkan.ImageLayoutColorAttachmentOptimal
228		barrier.NewLayout = vulkan.ImageLayoutTransferSrcOptimal
229		d.CmdPipelineBarrier2(f.Commands, dependency)
230		f.Readback.record(f.Commands, img.Image)
231		barrier.SrcStageMask = vulkan.PipelineStage2TransferBit
232		barrier.SrcAccessMask = vulkan.Access2TransferReadBit
233		barrier.OldLayout = vulkan.ImageLayoutTransferSrcOptimal
234	} else {
235		barrier.SrcStageMask = vulkan.PipelineStage2ColorAttachmentOutputBit
236		barrier.SrcAccessMask = vulkan.Access2ColorAttachmentWriteBit
237		barrier.OldLayout = vulkan.ImageLayoutColorAttachmentOptimal
238	}
239	barrier.DstStageMask = vulkan.PipelineStage2AllCommandsBit
240	barrier.DstAccessMask = vulkan.Access2MemoryReadBit
241	barrier.NewLayout = vulkan.ImageLayoutGeneral
242	d.CmdPipelineBarrier2(f.Commands, dependency)
243	return vulkan.Check(d.EndCommandBuffer(f.Commands))
244}
245
246// Submit has no CPU wait: the next render waits on a temporarily imported
247// release SYNC_FD, and the compositor waits on the exported acquire SYNC_FD.
248func (f *Frame) Submit(wait *BinaryFence) error {
249	sc := &f.scratch
250	sc.cmd = vulkan.CommandBufferSubmitInfo{SType: vulkan.StructureTypeCommandBufferSubmitInfo, CommandBuffer: f.Commands}
251	sc.signal = vulkan.SemaphoreSubmitInfo{SType: vulkan.StructureTypeSemaphoreSubmitInfo, Semaphore: f.Signal.Semaphore, StageMask: vulkan.PipelineStage2AllCommandsBit}
252	sc.submit = vulkan.SubmitInfo2{SType: vulkan.StructureTypeSubmitInfo2, CommandBufferInfoCount: 1, CommandBufferInfos: &sc.cmd, SignalSemaphoreInfoCount: 1, SignalSemaphoreInfos: &sc.signal}
253	if wait != nil {
254		sc.waiting = vulkan.SemaphoreSubmitInfo{SType: vulkan.StructureTypeSemaphoreSubmitInfo, Semaphore: wait.Semaphore, StageMask: vulkan.PipelineStage2AllCommandsBit}
255		sc.submit.WaitSemaphoreInfoCount = 1
256		sc.submit.WaitSemaphoreInfos = &sc.waiting
257	}
258	if err := vulkan.Check(f.device.Dispatch.QueueSubmit2(f.device.Queue, 1, &sc.submit, f.Done)); err != nil {
259		// Only this recording's unsubmitted image state is released; a
260		// previous submission's serial was completed by record.
261		if f.device.images != nil && f.imageSerial != 0 {
262			f.device.images.abort(f)
263		}
264		return err
265	}
266	f.submitted = true
267	f.signalExported = false
268	if f.device.images != nil && f.imageSerial != 0 {
269		f.device.images.inflight[f] = struct{}{}
270	}
271	if f.atlasRecorded {
272		f.device.atlas.initialized = true
273	}
274	if f.fallbackRecorded {
275		f.device.images.fallbackReady = true
276	}
277	return nil
278}
279
280// ExportSignal exports the submitted frame's acquire SYNC_FD. The export
281// resets the semaphore, which lets the next recording of this frame reuse it.
282func (f *Frame) ExportSignal() (int, error) {
283	fd, err := f.Signal.Export()
284	if err == nil {
285		f.signalExported = true
286	}
287	return fd, err
288}
289
290// settleImages runs once the frame is Ready. A submitted frame's serial is
291// completed and its upload list forgotten, so a later failed submission can
292// never abort textures that submitted work used. A recorded but unsubmitted
293// frame is aborted: nothing on the GPU references its new textures.
294func (f *Frame) settleImages() {
295	if f.device.images == nil || f.imageSerial == 0 {
296		return
297	}
298	if f.submitted {
299		delete(f.device.images.inflight, f)
300		f.device.images.complete(f.imageSerial)
301		f.imageSerial = 0
302		f.imageCopies = f.imageCopies[:0]
303		return
304	}
305	f.device.images.abort(f)
306}
307
308func (f *Frame) Wait() error {
309	if !f.submitted {
310		return nil
311	}
312	return vulkan.Check(f.device.Dispatch.WaitForFences(f.device.Logical, 1, &f.Done, 1, math.MaxUint64))
313}
314func (f *Frame) Close() {
315	if f == nil || f.device == nil {
316		return
317	}
318	_ = f.Wait()
319	d := f.device.Dispatch
320	if f.Signal != nil {
321		f.Signal.Close()
322		f.Signal = nil
323	}
324	if f.Done != 0 {
325		d.DestroyFence(f.device.Logical, f.Done, nil)
326		f.Done = 0
327	}
328	if f.View != 0 {
329		d.DestroyImageView(f.device.Logical, f.View, nil)
330		f.View = 0
331	}
332	if f.Pool != 0 {
333		d.DestroyCommandPool(f.device.Logical, f.Pool, nil)
334		f.Pool = 0
335	}
336	if f.instances != nil {
337		f.instances.Close()
338		f.instances = nil
339	}
340	if f.glyphStaging != nil {
341		f.glyphStaging.Close()
342		f.glyphStaging = nil
343	}
344	if f.imageStaging != nil {
345		f.imageStaging.Close()
346		f.imageStaging = nil
347	}
348	// After Wait: complete submitted work, abort a recorded-but-unsubmitted list.
349	f.settleImages()
350	if f.Readback != nil {
351		f.Readback.Close()
352		f.Readback = nil
353	}
354}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close