v0.5.1 657f658

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

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