main bcc9a45

target.go RawHistoryFolder

1//go:build linux
2
3package session
4
5import (
6	"errors"
7	"fmt"
8	"image"
9	"path/filepath"
10
11	"github.com/bnema/purego-vulkan/vulkan"
12
13	"github.com/bnema/nefergui/internal/layout"
14	"github.com/bnema/nefergui/internal/presentation/buffers"
15	"github.com/bnema/nefergui/internal/presentation/syncobj"
16	"github.com/bnema/nefergui/internal/presentation/vkdevice"
17	"github.com/bnema/nefergui/internal/render"
18	"golang.org/x/sys/unix"
19)
20
21// Plane is one DMA-BUF plane. FD stays owned by the Target.
22type Plane struct {
23	FD     int
24	Offset uint32
25	Stride uint32
26}
27
28// Timeline is a DRM syncobj timeline the transport must import. FD stays owned
29// by the Target and is valid only when the Output says the timeline is new.
30type Timeline struct {
31	ID uint64
32	FD int
33}
34
35// Retired names a buffer the transport must stop watching and destroy before
36// the next Draw. ReleaseFD closes at that Draw.
37type Retired struct {
38	Buffer    uint64
39	ReleaseFD int
40}
41
42// Output describes one presented frame in physical pixels. Slices are reused;
43// they are valid until the next Draw.
44type Output struct {
45	Buffer       uint64
46	NewBuffer    bool // import Planes as a new wl_buffer
47	Retired      []Retired
48	Width        int32
49	Height       int32
50	FourCC       uint32
51	Modifier     uint64
52	Planes       [4]Plane
53	PlaneCount   int
54	Acquire      Timeline // ID 0; FD always valid (persistent)
55	Release      Timeline // ID is the buffer; FD valid only with NewTimelines
56	NewTimelines bool
57	AcquirePoint uint64
58	ReleasePoint uint64
59	ReleaseFD    int // eventfd readable when ReleasePoint signals; stable per buffer
60}
61
62// TargetConfig selects the GPU and the compositor-acceptable formats.
63type TargetConfig struct {
64	MainDevice  uint64
65	Formats     []vkdevice.Format
66	Transparent bool
67}
68
69// Target is the Wayland-free presentation core: it renders prepared frames
70// into DMA-BUF images, tracks buffer ownership with explicit-sync timelines
71// and reports release through per-buffer eventfds. It starts no goroutines and
72// is used from one owner goroutine.
73type Target struct {
74	Device   *vkdevice.Device
75	Node     *syncobj.Node
76	Pool     *buffers.Pool
77	Preparer *render.Preparer
78	Slots    map[uint64]*Slot
79
80	list             render.ListBuffer
81	quads            []render.Quad // reused by submit; the instance upload copies them
82	pipeline         *vkdevice.Pipeline
83	pipelineGen      uint64 // pool generation the current pipeline belongs to
84	retiredPipelines []retiredPipelines
85	genPending       bool  // Pool.Resize done, images of the new generation not yet built
86	waiting          bool  // the last Draw found work that only GPU progress unblocks
87	broken           error // sticky: a failure after Submit left the pipeline inconsistent
88	acquire          uint32
89	acquireFD        int
90	formats          []vkdevice.Format
91	transparent      bool
92	selected         vkdevice.Modifier
93	wantW, wantH     int32
94	width, height    int32
95	pending          []Retired // retired since the last Draw output
96	reported         []Retired // given to the caller; fds close at the next Draw
97	capture          bool
98	closed           bool
99}
100
101// NewTarget opens the GPU and the render node. It allocates no images.
102func NewTarget(cfg TargetConfig) (_ *Target, err error) {
103	t := &Target{Slots: make(map[uint64]*Slot), acquireFD: -1, formats: append([]vkdevice.Format(nil), cfg.Formats...), transparent: cfg.Transparent}
104	defer func() {
105		if err != nil {
106			err = errors.Join(err, t.Close())
107		}
108	}()
109	if t.Preparer, err = render.NewPreparer(vkdevice.AtlasSize, vkdevice.AtlasPages); err != nil {
110		return nil, err
111	}
112	if t.Device, err = vkdevice.Open(vkdevice.DeviceIdentity(cfg.MainDevice)); err != nil {
113		return nil, err
114	}
115	if t.Node, err = syncobj.Open(filepath.Join("/dev/dri", fmt.Sprintf("renderD%d", t.Device.Render.Minor))); err != nil {
116		return nil, err
117	}
118	if t.acquire, err = t.Node.Create(); err != nil {
119		return nil, err
120	}
121	if t.acquireFD, err = t.Node.ExportTimeline(t.acquire); err != nil {
122		return nil, err
123	}
124	if t.Pool, err = buffers.New(3); err != nil {
125		return nil, err
126	}
127	// The pool starts with generation 1 buffers but no images yet; the first
128	// Draw allocates them for the requested size.
129	return t, nil
130}
131
132// EnableReadback makes later images capture their output for Readback. Tests only.
133func (t *Target) EnableReadback() { t.capture = true }
134
135// Resize sets the physical surface size; images follow lazily at the next Draw.
136func (t *Target) Resize(width, height int32) { t.wantW, t.wantH = width, height }
137
138// Size reports the physical size of the current image generation.
139func (t *Target) Size() (int32, int32) { return t.width, t.height }
140
141// newGeneration builds the pipeline and every image of the current pool
142// generation into locals and commits them only when all succeeded, so a
143// failure leaves the previous size, pipeline and slots intact.
144func (t *Target) newGeneration() (err error) {
145	mods, err := t.Device.ExportableModifiers()
146	if err != nil {
147		return err
148	}
149	selected, err := vkdevice.ChooseModifier(t.formats, t.transparent, mods)
150	if err != nil {
151		return err
152	}
153	pipeline, err := t.Device.NewListPipeline(t.wantW, t.wantH)
154	if err != nil {
155		return err
156	}
157	fresh := make(map[uint64]*Slot)
158	defer func() {
159		if err != nil {
160			for _, slot := range fresh {
161				_ = t.closeSlot(slot)
162			}
163			pipeline.Close()
164		}
165	}()
166	for _, b := range t.Pool.Buffers {
167		if b.Generation != t.Pool.Generation {
168			continue
169		}
170		slot, err := t.newSlot(b, t.wantW, t.wantH, selected)
171		if err != nil {
172			return err
173		}
174		fresh[b.ID] = slot
175	}
176	if t.pipeline != nil {
177		t.retiredPipelines = append(t.retiredPipelines, retiredPipelines{generation: t.pipelineGen, list: t.pipeline})
178	}
179	t.pipeline, t.pipelineGen, t.selected = pipeline, t.Pool.Generation, selected
180	t.width, t.height = t.wantW, t.wantH
181	for id, slot := range fresh {
182		t.Slots[id] = slot
183	}
184	return nil
185}
186
187func (t *Target) newSlot(b *buffers.Buffer, width, height int32, selected vkdevice.Modifier) (_ *Slot, err error) {
188	slot := &Slot{Buffer: b, First: true, ReleaseFD: -1, ReleaseExport: -1}
189	defer func() {
190		if err != nil {
191			_ = t.closeSlot(slot)
192		}
193	}()
194	if slot.Image, err = t.Device.AllocateImage(width, height, selected); err != nil {
195		return nil, err
196	}
197	if slot.Frame, err = t.Device.NewFrame(slot.Image); err != nil {
198		return nil, err
199	}
200	if t.capture {
201		if slot.Frame.Readback, err = t.Device.NewReadback(width, height); err != nil {
202			return nil, err
203		}
204	}
205	if slot.ReleaseHandle, err = t.Node.Create(); err != nil {
206		return nil, err
207	}
208	if slot.ReleaseExport, err = t.Node.ExportTimeline(slot.ReleaseHandle); err != nil {
209		return nil, err
210	}
211	if slot.ReleaseFD, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
212		return nil, err
213	}
214	return slot, nil
215}
216
217// closeSlot frees a slot whose GPU work has completed (or never started).
218func (t *Target) closeSlot(slot *Slot) error {
219	if slot.Frame != nil {
220		slot.Frame.Close()
221	}
222	if slot.PendingWait != nil {
223		slot.PendingWait.Close()
224	}
225	slot.spare.Close()
226	if slot.Wait != nil {
227		slot.Wait.Close()
228	}
229	if slot.Image != nil {
230		slot.Image.Close()
231	}
232	var err error
233	if slot.ReleaseHandle != 0 {
234		err = t.Node.Destroy(slot.ReleaseHandle)
235	}
236	closeFD(&slot.ReleaseExport)
237	return err
238}
239
240func closeFD(fd *int) {
241	if *fd >= 0 {
242		_ = unix.Close(*fd)
243		*fd = -1
244	}
245}
246
247// Draw prepares and presents one frame. It returns false, touching no GPU
248// state, when no image is ready; the caller retries after Released, or after a
249// short delay while Waiting reports GPU progress is needed.
250func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {
251	if t.broken != nil {
252		return false, t.broken
253	}
254	if t.closed {
255		return false, errors.New("session: target closed")
256	}
257	if t.wantW <= 0 || t.wantH <= 0 {
258		return false, nil
259	}
260	for _, r := range t.reported {
261		_ = unix.Close(r.ReleaseFD)
262	}
263	t.reported = t.reported[:0]
264	if t.genPending || t.width != t.wantW || t.height != t.wantH {
265		if !t.genPending && t.width != 0 {
266			if err := t.Pool.Resize(3); err != nil {
267				return false, err
268			}
269		}
270		t.genPending = true
271		if err := t.newGeneration(); err != nil {
272			return false, err
273		}
274		t.genPending = false
275	}
276	if err := t.retire(); err != nil {
277		return false, err
278	}
279	pending, err := t.finishPending()
280	if err != nil {
281		return false, err
282	}
283	b, slot, waiting, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
284	if err != nil {
285		return false, err
286	}
287	t.waiting = pending || waiting
288	if b == nil {
289		return false, nil
290	}
291	ok, err := t.submit(b, slot, display, scale, out)
292	if err != nil {
293		// Before Submit nothing reached the GPU and the buffer is reusable.
294		if t.broken == nil && b.State == buffers.Rendering {
295			b.State = buffers.Available
296		}
297		return false, err
298	}
299	t.waiting = false
300	return ok, nil
301}
302
303// Waiting reports that the last Draw found a frame blocked only on GPU
304// progress (an image still being drawn, or a release wait not yet installed),
305// which no event announces: the caller should retry after a short delay. It is
306// false when every image is owned by the compositor; Released unblocks that.
307func (t *Target) Waiting() bool { return t.waiting }
308
309func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {
310	frame, err := t.Preparer.PrepareInto(t.quads, display, scale, int(t.width), int(t.height))
311	if err != nil {
312		return false, err
313	}
314	t.quads = frame.Quads
315	defer t.list.Release()
316	instances, batches, stats := t.list.List(frame)
317	if len(stats.Skipped) != 0 {
318		return false, fmt.Errorf("unsupported list operations: %v", stats.Skipped)
319	}
320	if slot.Wait != nil && slot.WaitInFlight {
321		// Draw selected this slot only after its frame was Ready, so the
322		// submission that waited on Wait has completed and consumed it.
323		slot.recycle(slot.Wait)
324		slot.Wait = nil
325		slot.WaitInFlight = false
326	}
327	if err = slot.Frame.RecordList(slot.Image, t.pipeline, t.width, t.height, instances, frame.Uploads, batches, slot.First, t.capture); err != nil {
328		return false, err
329	}
330	if err = slot.Frame.Submit(slot.Wait); err != nil {
331		return false, err
332	}
333	t.Preparer.Submitted(frame.Uploads)
334	if slot.Wait != nil {
335		slot.WaitInFlight = true
336	}
337	slot.First = false
338	// From here the buffer is committed to the pipeline; failures are fatal.
339	fd, err := slot.Frame.ExportSignal()
340	if err != nil {
341		return false, t.fail(err)
342	}
343	acquire, err := t.Pool.Acquire(b)
344	if err != nil {
345		_ = unix.Close(fd)
346		return false, t.fail(err)
347	}
348	if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {
349		_ = unix.Close(fd)
350		return false, t.fail(err)
351	}
352	release, err := t.Pool.Commit(b)
353	if err != nil {
354		return false, t.fail(err)
355	}
356	if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {
357		return false, t.fail(err)
358	}
359	if err = t.Pool.Own(b); err != nil {
360		return false, t.fail(err)
361	}
362	t.fill(out, slot, acquire, release)
363	return true, nil
364}
365
366// fail records an error after Submit: the buffer cannot go back to Available
367// and every later Draw returns this error.
368func (t *Target) fail(err error) error {
369	t.broken = fmt.Errorf("session: presentation pipeline broken: %w", err)
370	return t.broken
371}
372
373func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
374	b := slot.Buffer
375	retired := append(out.Retired[:0], t.pending...)
376	t.reported = append(t.reported[:0], t.pending...)
377	t.pending = t.pending[:0]
378	*out = Output{
379		Buffer: b.ID, NewBuffer: !slot.Presented, Retired: retired,
380		Width: t.width, Height: t.height, FourCC: t.selected.DRMFormat, Modifier: slot.Image.Modifier,
381		PlaneCount: 1, Acquire: Timeline{ID: 0, FD: t.acquireFD}, Release: Timeline{ID: b.ID, FD: -1},
382		NewTimelines: !slot.Presented, AcquirePoint: acquire, ReleasePoint: release, ReleaseFD: slot.ReleaseFD,
383	}
384	out.Planes[0] = Plane{FD: slot.Image.FD, Offset: slot.Image.Offset, Stride: slot.Image.Stride}
385	if !slot.Presented {
386		out.Release.FD = slot.ReleaseExport
387		slot.Presented = true
388	}
389}
390
391// Released handles a readable release eventfd of the buffer: the compositor
392// signaled its latest release point, so the image may be reused once the GPU
393// wait is installed.
394func (t *Target) Released(id uint64) error {
395	slot := t.Slots[id]
396	if slot == nil {
397		return nil // retired: its descriptors are gone
398	}
399	// Drain first: a notification that lands after the drain is seen by the
400	// Signaled check below or leaves the eventfd readable for a later no-op call.
401	if err := drainEventFD(slot.ReleaseFD); err != nil {
402		return err
403	}
404	b := slot.Buffer
405	if b.State != buffers.CompositorOwned {
406		return nil
407	}
408	signaled, err := t.Node.Signaled(slot.ReleaseHandle, b.ReleasePoint)
409	if err != nil {
410		return err
411	}
412	if !signaled {
413		return nil // stale notification; the registration for the live point is still armed
414	}
415	return t.release(slot, b.ReleasePoint)
416}
417
418// drainEventFD resets a non-blocking eventfd; an empty one is fine.
419func drainEventFD(fd int) error {
420	var counter [8]byte
421	if _, err := unix.Read(fd, counter[:]); err != nil && err != unix.EAGAIN {
422		return fmt.Errorf("session: read release eventfd: %w", err)
423	}
424	return nil
425}
426
427// acquireWait returns an unsignaled binary semaphore for a release wait,
428// reusing the slot's spare instead of creating one per frame.
429func (t *Target) acquireWait(slot *Slot) (*vkdevice.BinaryFence, error) {
430	if w := slot.spare; w != nil {
431		slot.spare = nil
432		return w, nil
433	}
434	return t.Device.NewBinaryFence(false)
435}
436
437func (t *Target) release(slot *Slot, point uint64) error {
438	b := slot.Buffer
439	// An older completion cannot authorize a newer commit of the same image.
440	if b.ReleasePoint != point || b.State != buffers.CompositorOwned {
441		return nil
442	}
443	if err := t.Pool.Signal(b, point); err != nil {
444		return err
445	}
446	if b.Generation == t.Pool.Generation {
447		fd, err := t.Node.ReleaseFence(slot.ReleaseHandle, point)
448		if err != nil {
449			return err
450		}
451		wait, err := t.acquireWait(slot)
452		if err != nil {
453			_ = unix.Close(fd)
454			return err
455		}
456		if err = wait.ImportTemporary(fd); err != nil {
457			_ = unix.Close(fd)
458			wait.Close()
459			return err
460		}
461		installed, err := installReleaseWait(slot, wait, slot.Frame.Ready)
462		if err != nil {
463			wait.Close()
464			return err
465		}
466		if !installed {
467			return nil
468		}
469		if err = t.Pool.Reuse(b, true); err != nil {
470			return err
471		}
472	}
473	return t.retire()
474}
475
476// finishPending installs release waits deferred because the old submission was
477// still running. It never waits on the GPU.
478func (t *Target) finishPending() (bool, error) {
479	pending := false
480	for _, slot := range t.Slots {
481		// Old generations never return to Available; retire frees them.
482		if slot.PendingWait == nil || slot.Buffer.Generation != t.Pool.Generation {
483			continue
484		}
485		ready, err := slot.Frame.Ready()
486		if err != nil {
487			return false, err
488		}
489		if !ready {
490			pending = true
491			continue
492		}
493		if slot.Wait != nil {
494			if slot.WaitInFlight {
495				slot.recycle(slot.Wait) // submission is Ready: the wait was consumed
496			} else {
497				slot.Wait.Close()
498			}
499		}
500		slot.Wait, slot.PendingWait, slot.WaitInFlight = slot.PendingWait, nil, false
501		if err := t.Pool.Reuse(slot.Buffer, true); err != nil {
502			return false, err
503		}
504	}
505	return pending, nil
506}
507
508// retire frees images of old generations once nothing uses them, and queues
509// the ones the transport imported for the next Output.
510func (t *Target) retire() error {
511	for id, slot := range t.Slots {
512		b := slot.Buffer
513		if !t.Pool.Retirable(b) {
514			continue
515		}
516		ready, err := slot.Frame.Ready()
517		if err != nil {
518			return err
519		}
520		if !ready {
521			continue
522		}
523		eventFD := slot.ReleaseFD
524		slot.ReleaseFD = -1
525		wasPresented := slot.Presented
526		err = t.closeSlot(slot)
527		if wasPresented {
528			t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})
529		} else if eventFD >= 0 {
530			_ = unix.Close(eventFD)
531		}
532		delete(t.Slots, id)
533		if rerr := t.Pool.Remove(b); rerr != nil {
534			return errors.Join(err, rerr)
535		}
536		if err != nil {
537			return err
538		}
539	}
540	for i := 0; i < len(t.retiredPipelines); {
541		old := t.retiredPipelines[i]
542		live := false
543		for _, slot := range t.Slots {
544			if slot.Buffer.Generation == old.generation {
545				live = true
546				break
547			}
548		}
549		if live {
550			i++
551			continue
552		}
553		old.list.Close()
554		t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)
555	}
556	return nil
557}
558
559// Readback returns the last drawn pixels of a buffer in RGBA order. Tests only;
560// call EnableReadback before the first Draw.
561func (t *Target) Readback(id uint64) (*image.NRGBA, error) {
562	slot := t.Slots[id]
563	if slot == nil || slot.Frame.Readback == nil {
564		return nil, errors.New("session: no readback for buffer")
565	}
566	if err := slot.Frame.Wait(); err != nil {
567		return nil, err
568	}
569	img, err := slot.Frame.Readback.Copy()
570	if err != nil {
571		return nil, err
572	}
573	vkdevice.SwizzleBGRA(img)
574	return img, nil
575}
576
577// Close waits for the GPU, then frees every descriptor and image, always
578// completing the teardown. The compositor's imports keep their own kernel
579// references. It returns the joined errors of the steps that failed and any
580// Vulkan validation errors recorded during the Target's life.
581func (t *Target) Close() error {
582	if t == nil || t.closed {
583		return nil
584	}
585	t.closed = true
586	var errs []error
587	if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {
588		errs = append(errs, vulkan.Check(t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)))
589	}
590	for _, r := range t.reported {
591		_ = unix.Close(r.ReleaseFD)
592	}
593	for _, r := range t.pending {
594		_ = unix.Close(r.ReleaseFD)
595	}
596	t.reported, t.pending = nil, nil
597	for id, slot := range t.Slots {
598		closeFD(&slot.ReleaseFD)
599		errs = append(errs, t.closeSlot(slot))
600		delete(t.Slots, id)
601	}
602	for _, old := range t.retiredPipelines {
603		old.list.Close()
604	}
605	t.retiredPipelines = nil
606	t.pipeline.Close()
607	closeFD(&t.acquireFD)
608	if t.Node != nil {
609		if t.acquire != 0 {
610			errs = append(errs, t.Node.Destroy(t.acquire))
611		}
612		errs = append(errs, t.Node.Close())
613	}
614	if t.Device != nil {
615		// Read the messages before Close destroys the messenger.
616		if msgs := t.Device.Validation.Messages(); len(msgs) > 0 {
617			errs = append(errs, fmt.Errorf("%d Vulkan validation errors: %v", len(msgs), msgs))
618		}
619	}
620	t.Device.Close()
621	return errors.Join(errs...)
622}

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