v0.4.0 b6be3a8

frame.go RawHistoryFolder

1package vulkan
2
3import (
4	"fmt"
5	"image"
6	"math"
7	"os"
8	"runtime"
9	"slices"
10	"unsafe"
11
12	"github.com/bnema/neferwl/internal/ports"
13	vk "github.com/bnema/purego-vulkan/vulkan"
14)
15
16// Frames are recorded into a ring of slots, one per frame in flight. The
17// CPU never waits for a frame it just submitted: Render returns a sync
18// file the display waits on (KMS IN_FENCE_FD). A slot's fence is waited
19// only when the slot is reused, and objects a frame read (imports, GPU
20// buffers, client semaphores) are freed only once that frame completed.
21
22// frameSlots is how many frames may be in flight: one per output target.
23const frameSlots = 2
24
25// frameSlot is the command buffer and sync objects of one frame in flight.
26type frameSlot struct {
27	cmd   vk.CommandBuffer
28	fence vk.Fence
29	// done is signalled with the frame and exported as its sync file.
30	done  vk.Semaphore
31	frame uint64 // the frame recorded last, 0 before any
32	busy  bool   // submitted, fence not yet seen signalled
33}
34
35// deferredFree runs once frame completed.
36type deferredFree struct {
37	frame uint64
38	free  func()
39}
40
41func (r *Renderer) createSlots() error {
42	d := r.dd
43	cmds := make([]vk.CommandBuffer, frameSlots)
44	ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: frameSlots}
45	if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil {
46		return err
47	}
48	for i := range r.slots {
49		s := &r.slots[i]
50		s.cmd = cmds[i]
51		fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
52		if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
53			return err
54		}
55		if err := r.newDone(s); err != nil {
56			return err
57		}
58	}
59	return nil
60}
61
62// newDone creates a slot's frame semaphore, exportable as a sync file when
63// the device allows it; a device that refuses falls back to waiting for
64// frames on the CPU (syncFD off).
65func (r *Renderer) newDone(s *frameSlot) error {
66	d := r.dd
67	si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
68	export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
69	if r.syncFD {
70		si.Next = unsafe.Pointer(&export)
71		if d.CreateSemaphore(r.device, &si, nil, &s.done) == vk.Success {
72			return nil
73		}
74		r.syncFD = false
75		si.Next = nil
76	}
77	return checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done))
78}
79
80func (r *Renderer) destroySlots() {
81	d := r.dd
82	for i := range r.slots {
83		s := &r.slots[i]
84		if s.fence != 0 {
85			d.DestroyFence(r.device, s.fence, nil)
86		}
87		if s.done != 0 {
88			d.DestroySemaphore(r.device, s.done, nil)
89		}
90		if s.cmd != 0 {
91			d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd)
92		}
93		*s = frameSlot{}
94	}
95}
96
97// waitFrame blocks until frame n completed. Frames not submitted yet and
98// frames known done return at once. Every slot holding a frame up to n
99// is waited and reset, so its fence is unsignalled before reuse.
100func (r *Renderer) waitFrame(n uint64) error {
101	n = min(n, r.submitted)
102	if n <= r.completed {
103		return nil
104	}
105	for i := range r.slots {
106		s := &r.slots[i]
107		if !s.busy || s.frame > n {
108			continue
109		}
110		if err := checked("vkWaitForFences", r.dd.WaitForFences(r.device, 1, &s.fence, 1, math.MaxUint64)); err != nil {
111			return err
112		}
113		if err := checked("vkResetFences", r.dd.ResetFences(r.device, 1, &s.fence)); err != nil {
114			return err
115		}
116		s.busy = false
117	}
118	r.completed = n
119	r.runDeferred()
120	return nil
121}
122
123// retire frees an object once the frame that last used it completed.
124func (r *Renderer) retire(frame uint64, free func()) {
125	if frame <= r.completed {
126		free()
127		return
128	}
129	r.deferred = append(r.deferred, deferredFree{frame: frame, free: free})
130}
131
132func (r *Renderer) runDeferred() {
133	r.deferred = slices.DeleteFunc(r.deferred, func(f deferredFree) bool {
134		if f.frame <= r.completed {
135			f.free()
136			return true
137		}
138		return false
139	})
140}
141
142// idle records that the device finished every submitted frame.
143func (r *Renderer) idle() {
144	for i := range r.slots {
145		s := &r.slots[i]
146		if s.busy {
147			_ = r.dd.ResetFences(r.device, 1, &s.fence)
148			s.busy = false
149		}
150	}
151	// Frames recorded but never submitted have no GPU work either.
152	r.completed = r.frame
153	r.runDeferred()
154}
155
156// Render draws a frame into the current target. done is signalled when
157// the GPU finished it; nil when the device cannot export a sync file,
158// then the frame is finished on return.
159func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
160	// A frame that fails before its submit keeps its number: objects it
161	// touched are freed once a later frame completes (never early).
162	r.frame++
163	slot := &r.slots[r.frame%frameSlots]
164	// The slot's previous frame must be done before its command buffer
165	// is recorded again.
166	if slot.busy {
167		if err := r.waitFrame(slot.frame); err != nil {
168			return nil, err
169		}
170	}
171	tg := r.target()
172	hdr := r.hdrNits > 0 && len(r.targets) > 0
173	if r.hdrNits > 0 && !hdr {
174		return nil, fmt.Errorf("HDR composition requires an exported target")
175	}
176	if hdr {
177		tg = &r.hdrOwn
178	}
179	// Bounds, damage and redrawn pixels are in scene space: the target's
180	// size, swapped for a rotated output. orient maps the draws to the target
181	// below. A transform change always comes with a new scene Seq, so a held
182	// target is never reused across transforms.
183	sw, sh := r.sceneSize(s.Transform)
184	dmg := r.frameDamage(tg, s, image.Rect(0, 0, sw, sh))
185	ds := r.draws(s, contents, dmg)
186	dmg.finish()
187	r.dropPools()
188	partial := !dmg.all()
189	if partial {
190		// The pass keeps the image: repaint the background under the
191		// region, then everything clipped to it.
192		// Keep the background first without allocating a second draw slice.
193		ds = append(ds, draw{})
194		copy(ds[1:], ds[:len(ds)-1])
195		ds[0] = r.fillDraw(dmg.area, parseColor(s.Background))
196		ds = dmg.clip(ds)
197		r.scratchDraws = ds
198		r.redrawn += dmg.area.Dx() * dmg.area.Dy()
199	} else {
200		r.redrawn += sw * sh
201	}
202	r.orient(ds, s.Transform)
203	d := r.dd
204	cmd := slot.cmd
205	if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
206		return nil, err
207	}
208	begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
209	if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
210		return nil, err
211	}
212	// A full redraw clears the target: its old contents never matter
213	// (UNDEFINED). A partial one keeps them.
214	b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
215	if partial {
216		b.OldLayout = tg.layout
217		b.DstAccessMask |= vk.AccessColorAttachmentReadBit
218	}
219	if tg.exported {
220		// Acquire from the display (foreign queue family).
221		b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
222	}
223	d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
224	// Client buffers come from the foreign queue family (their driver)
225	// and go back to it after the frame, so the next frame acquires them.
226	dmas := r.frameDMAs[:0]
227	if r.frameAcquires == nil {
228		r.frameAcquires = make(map[*imported]*os.File)
229	}
230	clear(r.frameAcquires)
231	acquires := r.frameAcquires
232	for _, dr := range ds {
233		if dr.im != nil && !slices.Contains(dmas, dr.im) {
234			dmas = append(dmas, dr.im)
235			dr.im.last = r.frame
236		}
237		if dr.acquire != nil {
238			acquires[dr.im] = dr.acquire
239		}
240	}
241	r.frameDMAs = dmas
242	r.ownership(cmd, dmas, true)
243	r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial)
244	r.ownership(cmd, dmas, false)
245	if hdr {
246		r.recordHDR(cmd, r.targets[r.current])
247	}
248	if !hdr {
249		b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
250		b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored
251		if tg.exported {
252			// Release to the display: KMS scans it out after the fence.
253			b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
254			b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
255			d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
256		} else {
257			b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
258			d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
259		}
260	}
261	if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
262		return nil, err
263	}
264	// Wait for the client's GPU writes to each buffer: its explicit
265	// acquire fence, else the buffer's implicit fences.
266	waits := r.frameWaits[:0]
267	for _, im := range dmas {
268		if f := acquires[im]; f != nil {
269			if sem := r.importFence(f); sem != 0 {
270				waits = append(waits, sem)
271			}
272		} else {
273			waits = r.appendReadFences(waits, im)
274		}
275	}
276	r.frameWaits = waits
277	frame := r.frame
278	for _, sem := range waits {
279		sem := sem
280		r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })
281	}
282	submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
283	if cap(r.frameStages) < len(waits) {
284		r.frameStages = make([]vk.PipelineStageFlags, len(waits))
285	}
286	stages := r.frameStages[:len(waits)]
287	if len(waits) > 0 {
288		for i := range stages {
289			stages[i] = vk.PipelineStageFragmentShaderBit
290		}
291		submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
292	}
293	if r.syncFD {
294		submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
295	}
296	if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil {
297		return nil, err
298	}
299	runtime.KeepAlive(stages)
300	runtime.KeepAlive(waits)
301	slot.frame, slot.busy, r.submitted = frame, true, frame
302	if hdr {
303		// recordHDR left the composed linear image in TRANSFER_SRC for capture.
304		tg.layout = vk.ImageLayoutTransferSrcOptimal
305		r.targets[r.current].layout = vk.ImageLayoutGeneral
306	} else {
307		tg.layout = b.NewLayout
308	}
309	oldWindows := tg.windows
310	tg.hold(s, dmg)
311	r.damageDrawn = oldWindows
312	r.last = tg
313	if hdr {
314		r.lastHDRTarget = r.current
315	}
316	r.dropUnused()
317	r.dropShm()
318	if !r.syncFD {
319		return nil, r.waitFrame(frame)
320	}
321	var fd int32
322	get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
323	if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success {
324		// The frame is drawn: finish it on the CPU. The semaphore stays
325		// signalled: replace it, so the slot's next submit signals a
326		// fresh one.
327		if err := r.waitFrame(frame); err != nil {
328			return nil, err
329		}
330		d.DestroySemaphore(r.device, slot.done, nil)
331		slot.done = 0
332		return nil, r.newDone(slot)
333	}
334	if fd < 0 {
335		// Already signalled: the driver may return -1.
336		return nil, r.waitFrame(frame)
337	}
338	f := os.NewFile(uintptr(fd), "neferwl-frame")
339	if f == nil {
340		return nil, fmt.Errorf("invalid frame sync file %d", fd)
341	}
342	return f, nil
343}
344
345// ownership moves client images between their driver (foreign queue
346// family, GENERAL: their contents survive) and the fragment shader.
347func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
348	for _, im := range dmas {
349		b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
350		if im.yuv {
351			b.SubresourceRange.AspectMask = aspectPlane0 | aspectPlane1
352		}
353		if acquire {
354			b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
355			b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
356			b.DstAccessMask = vk.AccessShaderReadBit
357			// The frame waits on the client's fence at the fragment
358			// stage: the transfer must come after that wait.
359			r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
360		} else {
361			b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
362			b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
363			b.SrcAccessMask = vk.AccessShaderReadBit
364			r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
365		}
366	}
367}
368
369// oneShot records and runs commands outside the frame ring and waits for
370// them (setup and screenshots only, never per frame).
371func (r *Renderer) oneShot(record func(vk.CommandBuffer)) error {
372	d := r.dd
373	var cmd vk.CommandBuffer
374	ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
375	if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmd)); err != nil {
376		return err
377	}
378	defer d.FreeCommandBuffers(r.device, r.pool, 1, &cmd)
379	var fence vk.Fence
380	fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
381	if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &fence)); err != nil {
382		return err
383	}
384	defer d.DestroyFence(r.device, fence, nil)
385	begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
386	if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
387		return err
388	}
389	record(cmd)
390	if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
391		return err
392	}
393	submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
394	if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, fence)); err != nil {
395		return err
396	}
397	return checked("vkWaitForFences", d.WaitForFences(r.device, 1, &fence, 1, math.MaxUint64))
398}
399
400// Clear draws a background-only frame and waits for it.
401func (r *Renderer) Clear(rgb [3]uint8) error {
402	done, err := r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
403	if done != nil {
404		done.Close()
405	}
406	if err != nil {
407		return err
408	}
409	return r.waitFrame(r.frame)
410}

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 23

Tags 10

↑↓ moveenter openesc close