v0.5.0 5fd5066

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}
34
35func (d *Device) NewFrame(img *Image) (f *Frame, err error) {
36	f = &Frame{device: d}
37	defer func() {
38		if err != nil {
39			f.Close()
40		}
41	}()
42	pool := vulkan.CommandPoolCreateInfo{SType: vulkan.StructureTypeCommandPoolCreateInfo, QueueFamilyIndex: d.QueueFamily, Flags: vulkan.CommandPoolCreateResetCommandBufferBit}
43	if err = vulkan.Check(d.Dispatch.CreateCommandPool(d.Logical, &pool, nil, &f.Pool)); err != nil {
44		return nil, err
45	}
46	allocate := vulkan.CommandBufferAllocateInfo{SType: vulkan.StructureTypeCommandBufferAllocateInfo, CommandPool: f.Pool, Level: vulkan.CommandBufferLevelPrimary, CommandBufferCount: 1}
47	if err = vulkan.Check(d.Dispatch.AllocateCommandBuffers(d.Logical, &allocate, &f.Commands)); err != nil {
48		return nil, err
49	}
50	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}}
51	if err = vulkan.Check(d.Dispatch.CreateImageView(d.Logical, &view, nil, &f.View)); err != nil {
52		return nil, err
53	}
54	fence := vulkan.FenceCreateInfo{SType: vulkan.StructureTypeFenceCreateInfo}
55	if err = vulkan.Check(d.Dispatch.CreateFence(d.Logical, &fence, nil, &f.Done)); err != nil {
56		return nil, err
57	}
58	f.Signal, err = d.NewBinaryFence(true)
59	if err != nil {
60		return nil, err
61	}
62	return f, nil
63}
64
65func (f *Frame) Ready() (bool, error) {
66	if !f.submitted {
67		return true, nil
68	}
69	r := f.device.Dispatch.WaitForFences(f.device.Logical, 1, &f.Done, 1, 0)
70	if r == vulkan.Timeout {
71		return false, nil
72	}
73	if err := vulkan.Check(r); err != nil {
74		return false, err
75	}
76	return true, nil
77}
78
79// RecordList records an ordered list of physical-pixel quads. Reuse is gated
80// by Ready, including any instance-buffer growth.
81func (f *Frame) RecordList(img *Image, pipe *Pipeline, width, height int32, instances []Instance, uploads []text.Upload, batches []Batch, initial, capture bool) error {
82	return f.record(img, pipe, width, height, initial, capture, instances, uploads, batches)
83}
84
85func (f *Frame) record(img *Image, pipe *Pipeline, width, height int32, initial, capture bool, instances []Instance, uploads []text.Upload, batches []Batch) (err error) {
86	if width <= 0 || height <= 0 || pipe == nil {
87		return fmt.Errorf("invalid render dimensions or pipeline")
88	}
89	ready, err := f.Ready()
90	if err != nil || !ready {
91		return fmt.Errorf("command buffer still in use: ready=%v err=%v", ready, err)
92	}
93	f.settleImages()
94	if f.submitted {
95		if err = vulkan.Check(f.device.Dispatch.ResetFences(f.device.Logical, 1, &f.Done)); err != nil {
96			return err
97		}
98		if err = vulkan.Check(f.device.Dispatch.ResetCommandBuffer(f.Commands, 0)); err != nil {
99			return err
100		}
101		f.Signal.Close()
102		f.Signal, err = f.device.NewBinaryFence(true)
103		if err != nil {
104			return err
105		}
106		f.submitted = false
107	}
108	if f.device.atlas == nil {
109		return fmt.Errorf("list atlas not initialized")
110	}
111	if err := f.stageGlyphs(uploads); err != nil {
112		return err
113	}
114	if f.device.images != nil {
115		defer func() {
116			if err != nil {
117				f.device.images.abort(f)
118			}
119		}()
120		batches, err = f.device.images.resolve(f, batches)
121		if err != nil {
122			return err
123		}
124	}
125	if len(instances) > 0 {
126		if err := f.uploadInstances(instances); err != nil {
127			return err
128		}
129	}
130	begin := vulkan.CommandBufferBeginInfo{SType: vulkan.StructureTypeCommandBufferBeginInfo, Flags: vulkan.CommandBufferUsageOneTimeSubmitBit}
131	d := f.device.Dispatch
132	if err = vulkan.Check(d.BeginCommandBuffer(f.Commands, &begin)); err != nil {
133		return err
134	}
135	f.atlasRecorded = !f.device.atlas.initialized
136	var staging vulkan.Buffer
137	if len(uploads) != 0 {
138		staging = f.glyphStaging.Buffer
139		f.device.atlas.recordUploads(f.Commands, staging, uploads)
140		if !f.device.images.fallbackReady {
141			f.recordFallback()
142		}
143		f.recordImages()
144	}
145	rangeInfo := vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
146	old := vulkan.ImageLayout(vulkan.ImageLayoutGeneral)
147	if initial {
148		old = vulkan.ImageLayoutUndefined
149	}
150	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}
151	dependency := vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &barrier}
152	d.CmdPipelineBarrier2(f.Commands, &dependency)
153	clear := vulkan.ClearValue{}
154	attachment := vulkan.RenderingAttachmentInfo{SType: vulkan.StructureTypeRenderingAttachmentInfo, ImageView: f.View, ImageLayout: vulkan.ImageLayoutColorAttachmentOptimal, LoadOp: vulkan.AttachmentLoadOpClear, StoreOp: vulkan.AttachmentStoreOpStore, ClearValue: clear}
155	area := vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(width), Height: uint32(height)}}
156	rendering := vulkan.RenderingInfo{SType: vulkan.StructureTypeRenderingInfo, RenderArea: area, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
157	d.CmdBeginRendering(f.Commands, &rendering)
158	d.CmdBindPipeline(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Handle)
159	// The fragment shader statically references both descriptor sets even
160	// for non-image draws. Bind a valid initialized image set before any
161	// draw; each image batch then replaces set 1 in painter's order.
162	sets := [2]vulkan.DescriptorSet{f.device.atlas.Set, f.device.images.fallback.Texture.Set}
163	d.CmdBindDescriptorSets(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Layout, 0, 2, &sets[0], 0, nil)
164	screen := [2]float32{float32(width), float32(height)}
165	d.CmdPushConstants(f.Commands, pipe.Layout, vulkan.ShaderStageVertexBit|vulkan.ShaderStageFragmentBit, 0, 8, unsafe.Pointer(&screen))
166	if len(instances) > 0 {
167		offset := vulkan.DeviceSize(0)
168		d.CmdBindVertexBuffers(f.Commands, 0, 1, &f.instances.Buffer, &offset)
169		for _, batch := range batches {
170			if batch.Count == 0 || uint64(batch.First)+uint64(batch.Count) > uint64(len(instances)) {
171				return fmt.Errorf("invalid list batch range")
172			}
173			if batch.Source != nil {
174				if batch.Texture == nil {
175					return fmt.Errorf("image batch has no GPU texture")
176				}
177				d.CmdBindDescriptorSets(f.Commands, vulkan.PipelineBindPointGraphics, pipe.Layout, 1, 1, &batch.Texture.Set, 0, nil)
178			}
179			d.CmdDraw(f.Commands, 6, batch.Count, 0, batch.First)
180		}
181	}
182	d.CmdEndRendering(f.Commands)
183	if capture && f.Readback != nil {
184		barrier.SrcStageMask = vulkan.PipelineStage2ColorAttachmentOutputBit
185		barrier.SrcAccessMask = vulkan.Access2ColorAttachmentWriteBit
186		barrier.DstStageMask = vulkan.PipelineStage2TransferBit
187		barrier.DstAccessMask = vulkan.Access2TransferReadBit
188		barrier.OldLayout = vulkan.ImageLayoutColorAttachmentOptimal
189		barrier.NewLayout = vulkan.ImageLayoutTransferSrcOptimal
190		d.CmdPipelineBarrier2(f.Commands, &dependency)
191		f.Readback.record(f.Commands, img.Image)
192		barrier.SrcStageMask = vulkan.PipelineStage2TransferBit
193		barrier.SrcAccessMask = vulkan.Access2TransferReadBit
194		barrier.OldLayout = vulkan.ImageLayoutTransferSrcOptimal
195	} else {
196		barrier.SrcStageMask = vulkan.PipelineStage2ColorAttachmentOutputBit
197		barrier.SrcAccessMask = vulkan.Access2ColorAttachmentWriteBit
198		barrier.OldLayout = vulkan.ImageLayoutColorAttachmentOptimal
199	}
200	barrier.DstStageMask = vulkan.PipelineStage2AllCommandsBit
201	barrier.DstAccessMask = vulkan.Access2MemoryReadBit
202	barrier.NewLayout = vulkan.ImageLayoutGeneral
203	d.CmdPipelineBarrier2(f.Commands, &dependency)
204	return vulkan.Check(d.EndCommandBuffer(f.Commands))
205}
206
207// Submit has no CPU wait: the next render waits on a temporarily imported
208// release SYNC_FD, and the compositor waits on the exported acquire SYNC_FD.
209func (f *Frame) Submit(wait *BinaryFence) error {
210	cmd := vulkan.CommandBufferSubmitInfo{SType: vulkan.StructureTypeCommandBufferSubmitInfo, CommandBuffer: f.Commands}
211	signal := vulkan.SemaphoreSubmitInfo{SType: vulkan.StructureTypeSemaphoreSubmitInfo, Semaphore: f.Signal.Semaphore, StageMask: vulkan.PipelineStage2AllCommandsBit}
212	info := vulkan.SubmitInfo2{SType: vulkan.StructureTypeSubmitInfo2, CommandBufferInfoCount: 1, CommandBufferInfos: &cmd, SignalSemaphoreInfoCount: 1, SignalSemaphoreInfos: &signal}
213	if wait != nil {
214		waiting := vulkan.SemaphoreSubmitInfo{SType: vulkan.StructureTypeSemaphoreSubmitInfo, Semaphore: wait.Semaphore, StageMask: vulkan.PipelineStage2AllCommandsBit}
215		info.WaitSemaphoreInfoCount = 1
216		info.WaitSemaphoreInfos = &waiting
217	}
218	if err := vulkan.Check(f.device.Dispatch.QueueSubmit2(f.device.Queue, 1, &info, f.Done)); err != nil {
219		// Only this recording's unsubmitted image state is released; a
220		// previous submission's serial was completed by record.
221		if f.device.images != nil && f.imageSerial != 0 {
222			f.device.images.abort(f)
223		}
224		return err
225	}
226	f.submitted = true
227	if f.device.images != nil && f.imageSerial != 0 {
228		f.device.images.inflight[f] = struct{}{}
229	}
230	if f.atlasRecorded {
231		f.device.atlas.initialized = true
232	}
233	if f.fallbackRecorded {
234		f.device.images.fallbackReady = true
235	}
236	return nil
237}
238
239// settleImages runs once the frame is Ready. A submitted frame's serial is
240// completed and its upload list forgotten, so a later failed submission can
241// never abort textures that submitted work used. A recorded but unsubmitted
242// frame is aborted: nothing on the GPU references its new textures.
243func (f *Frame) settleImages() {
244	if f.device.images == nil || f.imageSerial == 0 {
245		return
246	}
247	if f.submitted {
248		delete(f.device.images.inflight, f)
249		f.device.images.complete(f.imageSerial)
250		f.imageSerial = 0
251		f.imageCopies = f.imageCopies[:0]
252		return
253	}
254	f.device.images.abort(f)
255}
256
257func (f *Frame) Wait() error {
258	if !f.submitted {
259		return nil
260	}
261	return vulkan.Check(f.device.Dispatch.WaitForFences(f.device.Logical, 1, &f.Done, 1, math.MaxUint64))
262}
263func (f *Frame) Close() {
264	if f == nil || f.device == nil {
265		return
266	}
267	_ = f.Wait()
268	d := f.device.Dispatch
269	if f.Signal != nil {
270		f.Signal.Close()
271		f.Signal = nil
272	}
273	if f.Done != 0 {
274		d.DestroyFence(f.device.Logical, f.Done, nil)
275		f.Done = 0
276	}
277	if f.View != 0 {
278		d.DestroyImageView(f.device.Logical, f.View, nil)
279		f.View = 0
280	}
281	if f.Pool != 0 {
282		d.DestroyCommandPool(f.device.Logical, f.Pool, nil)
283		f.Pool = 0
284	}
285	if f.instances != nil {
286		f.instances.Close()
287		f.instances = nil
288	}
289	if f.glyphStaging != nil {
290		f.glyphStaging.Close()
291		f.glyphStaging = nil
292	}
293	if f.imageStaging != nil {
294		f.imageStaging.Close()
295		f.imageStaging = nil
296	}
297	// After Wait: complete submitted work, abort a recorded-but-unsubmitted list.
298	f.settleImages()
299	if f.Readback != nil {
300		f.Readback.Close()
301		f.Readback = nil
302	}
303}

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