main 5d6fa26

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

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