v0.5.0 5fd5066

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	if slot.Wait != nil {
225		slot.Wait.Close()
226	}
227	if slot.Image != nil {
228		slot.Image.Close()
229	}
230	var err error
231	if slot.ReleaseHandle != 0 {
232		err = t.Node.Destroy(slot.ReleaseHandle)
233	}
234	closeFD(&slot.ReleaseExport)
235	return err
236}
237
238func closeFD(fd *int) {
239	if *fd >= 0 {
240		_ = unix.Close(*fd)
241		*fd = -1
242	}
243}
244
245// Draw prepares and presents one frame. It returns false, touching no GPU
246// state, when no image is ready; the caller retries after Released, or after a
247// short delay while Waiting reports GPU progress is needed.
248func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {
249	if t.broken != nil {
250		return false, t.broken
251	}
252	if t.closed {
253		return false, errors.New("session: target closed")
254	}
255	if t.wantW <= 0 || t.wantH <= 0 {
256		return false, nil
257	}
258	for _, r := range t.reported {
259		_ = unix.Close(r.ReleaseFD)
260	}
261	t.reported = t.reported[:0]
262	if t.genPending || t.width != t.wantW || t.height != t.wantH {
263		if !t.genPending && t.width != 0 {
264			if err := t.Pool.Resize(3); err != nil {
265				return false, err
266			}
267		}
268		t.genPending = true
269		if err := t.newGeneration(); err != nil {
270			return false, err
271		}
272		t.genPending = false
273	}
274	if err := t.retire(); err != nil {
275		return false, err
276	}
277	pending, err := t.finishPending()
278	if err != nil {
279		return false, err
280	}
281	b, slot, waiting, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
282	if err != nil {
283		return false, err
284	}
285	t.waiting = pending || waiting
286	if b == nil {
287		return false, nil
288	}
289	ok, err := t.submit(b, slot, display, scale, out)
290	if err != nil {
291		// Before Submit nothing reached the GPU and the buffer is reusable.
292		if t.broken == nil && b.State == buffers.Rendering {
293			b.State = buffers.Available
294		}
295		return false, err
296	}
297	t.waiting = false
298	return ok, nil
299}
300
301// Waiting reports that the last Draw found a frame blocked only on GPU
302// progress (an image still being drawn, or a release wait not yet installed),
303// which no event announces: the caller should retry after a short delay. It is
304// false when every image is owned by the compositor; Released unblocks that.
305func (t *Target) Waiting() bool { return t.waiting }
306
307func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {
308	frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height))
309	if err != nil {
310		return false, err
311	}
312	defer t.list.Release()
313	instances, batches, stats := t.list.List(frame)
314	if len(stats.Skipped) != 0 {
315		return false, fmt.Errorf("unsupported list operations: %v", stats.Skipped)
316	}
317	if slot.Wait != nil && slot.WaitInFlight {
318		slot.Wait.Close()
319		slot.Wait = nil
320		slot.WaitInFlight = false
321	}
322	if err = slot.Frame.RecordList(slot.Image, t.pipeline, t.width, t.height, instances, frame.Uploads, batches, slot.First, t.capture); err != nil {
323		return false, err
324	}
325	if err = slot.Frame.Submit(slot.Wait); err != nil {
326		return false, err
327	}
328	t.Preparer.Submitted(frame.Uploads)
329	if slot.Wait != nil {
330		slot.WaitInFlight = true
331	}
332	slot.First = false
333	// From here the buffer is committed to the pipeline; failures are fatal.
334	fd, err := slot.Frame.Signal.Export()
335	if err != nil {
336		return false, t.fail(err)
337	}
338	acquire, err := t.Pool.Acquire(b)
339	if err != nil {
340		_ = unix.Close(fd)
341		return false, t.fail(err)
342	}
343	if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {
344		_ = unix.Close(fd)
345		return false, t.fail(err)
346	}
347	release, err := t.Pool.Commit(b)
348	if err != nil {
349		return false, t.fail(err)
350	}
351	if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {
352		return false, t.fail(err)
353	}
354	if err = t.Pool.Own(b); err != nil {
355		return false, t.fail(err)
356	}
357	t.fill(out, slot, acquire, release)
358	return true, nil
359}
360
361// fail records an error after Submit: the buffer cannot go back to Available
362// and every later Draw returns this error.
363func (t *Target) fail(err error) error {
364	t.broken = fmt.Errorf("session: presentation pipeline broken: %w", err)
365	return t.broken
366}
367
368func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
369	b := slot.Buffer
370	retired := append(out.Retired[:0], t.pending...)
371	t.reported = append(t.reported[:0], t.pending...)
372	t.pending = t.pending[:0]
373	*out = Output{
374		Buffer: b.ID, NewBuffer: !slot.Presented, Retired: retired,
375		Width: t.width, Height: t.height, FourCC: t.selected.DRMFormat, Modifier: slot.Image.Modifier,
376		PlaneCount: 1, Acquire: Timeline{ID: 0, FD: t.acquireFD}, Release: Timeline{ID: b.ID, FD: -1},
377		NewTimelines: !slot.Presented, AcquirePoint: acquire, ReleasePoint: release, ReleaseFD: slot.ReleaseFD,
378	}
379	out.Planes[0] = Plane{FD: slot.Image.FD, Offset: slot.Image.Offset, Stride: slot.Image.Stride}
380	if !slot.Presented {
381		out.Release.FD = slot.ReleaseExport
382		slot.Presented = true
383	}
384}
385
386// Released handles a readable release eventfd of the buffer: the compositor
387// signaled its latest release point, so the image may be reused once the GPU
388// wait is installed.
389func (t *Target) Released(id uint64) error {
390	slot := t.Slots[id]
391	if slot == nil {
392		return nil // retired: its descriptors are gone
393	}
394	// Drain first: a notification that lands after the drain is seen by the
395	// Signaled check below or leaves the eventfd readable for a later no-op call.
396	if err := drainEventFD(slot.ReleaseFD); err != nil {
397		return err
398	}
399	b := slot.Buffer
400	if b.State != buffers.CompositorOwned {
401		return nil
402	}
403	signaled, err := t.Node.Signaled(slot.ReleaseHandle, b.ReleasePoint)
404	if err != nil {
405		return err
406	}
407	if !signaled {
408		return nil // stale notification; the registration for the live point is still armed
409	}
410	return t.release(slot, b.ReleasePoint)
411}
412
413// drainEventFD resets a non-blocking eventfd; an empty one is fine.
414func drainEventFD(fd int) error {
415	var counter [8]byte
416	if _, err := unix.Read(fd, counter[:]); err != nil && err != unix.EAGAIN {
417		return fmt.Errorf("session: read release eventfd: %w", err)
418	}
419	return nil
420}
421
422func (t *Target) release(slot *Slot, point uint64) error {
423	b := slot.Buffer
424	// An older completion cannot authorize a newer commit of the same image.
425	if b.ReleasePoint != point || b.State != buffers.CompositorOwned {
426		return nil
427	}
428	if err := t.Pool.Signal(b, point); err != nil {
429		return err
430	}
431	if b.Generation == t.Pool.Generation {
432		fd, err := t.Node.ReleaseFence(slot.ReleaseHandle, point)
433		if err != nil {
434			return err
435		}
436		wait, err := t.Device.NewBinaryFence(false)
437		if err != nil {
438			_ = unix.Close(fd)
439			return err
440		}
441		if err = wait.ImportTemporary(fd); err != nil {
442			_ = unix.Close(fd)
443			wait.Close()
444			return err
445		}
446		installed, err := installReleaseWait(slot, wait, slot.Frame.Ready)
447		if err != nil {
448			wait.Close()
449			return err
450		}
451		if !installed {
452			return nil
453		}
454		if err = t.Pool.Reuse(b, true); err != nil {
455			return err
456		}
457	}
458	return t.retire()
459}
460
461// finishPending installs release waits deferred because the old submission was
462// still running. It never waits on the GPU.
463func (t *Target) finishPending() (bool, error) {
464	pending := false
465	for _, slot := range t.Slots {
466		// Old generations never return to Available; retire frees them.
467		if slot.PendingWait == nil || slot.Buffer.Generation != t.Pool.Generation {
468			continue
469		}
470		ready, err := slot.Frame.Ready()
471		if err != nil {
472			return false, err
473		}
474		if !ready {
475			pending = true
476			continue
477		}
478		if slot.Wait != nil {
479			slot.Wait.Close()
480		}
481		slot.Wait, slot.PendingWait, slot.WaitInFlight = slot.PendingWait, nil, false
482		if err := t.Pool.Reuse(slot.Buffer, true); err != nil {
483			return false, err
484		}
485	}
486	return pending, nil
487}
488
489// retire frees images of old generations once nothing uses them, and queues
490// the ones the transport imported for the next Output.
491func (t *Target) retire() error {
492	for id, slot := range t.Slots {
493		b := slot.Buffer
494		if !t.Pool.Retirable(b) {
495			continue
496		}
497		ready, err := slot.Frame.Ready()
498		if err != nil {
499			return err
500		}
501		if !ready {
502			continue
503		}
504		eventFD := slot.ReleaseFD
505		slot.ReleaseFD = -1
506		wasPresented := slot.Presented
507		err = t.closeSlot(slot)
508		if wasPresented {
509			t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})
510		} else if eventFD >= 0 {
511			_ = unix.Close(eventFD)
512		}
513		delete(t.Slots, id)
514		if rerr := t.Pool.Remove(b); rerr != nil {
515			return errors.Join(err, rerr)
516		}
517		if err != nil {
518			return err
519		}
520	}
521	for i := 0; i < len(t.retiredPipelines); {
522		old := t.retiredPipelines[i]
523		live := false
524		for _, slot := range t.Slots {
525			if slot.Buffer.Generation == old.generation {
526				live = true
527				break
528			}
529		}
530		if live {
531			i++
532			continue
533		}
534		old.list.Close()
535		t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)
536	}
537	return nil
538}
539
540// Readback returns the last drawn pixels of a buffer in RGBA order. Tests only;
541// call EnableReadback before the first Draw.
542func (t *Target) Readback(id uint64) (*image.NRGBA, error) {
543	slot := t.Slots[id]
544	if slot == nil || slot.Frame.Readback == nil {
545		return nil, errors.New("session: no readback for buffer")
546	}
547	if err := slot.Frame.Wait(); err != nil {
548		return nil, err
549	}
550	img, err := slot.Frame.Readback.Copy()
551	if err != nil {
552		return nil, err
553	}
554	vkdevice.SwizzleBGRA(img)
555	return img, nil
556}
557
558// Close waits for the GPU, then frees every descriptor and image, always
559// completing the teardown. The compositor's imports keep their own kernel
560// references. It returns the joined errors of the steps that failed and any
561// Vulkan validation errors recorded during the Target's life.
562func (t *Target) Close() error {
563	if t == nil || t.closed {
564		return nil
565	}
566	t.closed = true
567	var errs []error
568	if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {
569		errs = append(errs, vulkan.Check(t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)))
570	}
571	for _, r := range t.reported {
572		_ = unix.Close(r.ReleaseFD)
573	}
574	for _, r := range t.pending {
575		_ = unix.Close(r.ReleaseFD)
576	}
577	t.reported, t.pending = nil, nil
578	for id, slot := range t.Slots {
579		closeFD(&slot.ReleaseFD)
580		errs = append(errs, t.closeSlot(slot))
581		delete(t.Slots, id)
582	}
583	for _, old := range t.retiredPipelines {
584		old.list.Close()
585	}
586	t.retiredPipelines = nil
587	t.pipeline.Close()
588	closeFD(&t.acquireFD)
589	if t.Node != nil {
590		if t.acquire != 0 {
591			errs = append(errs, t.Node.Destroy(t.acquire))
592		}
593		errs = append(errs, t.Node.Close())
594	}
595	if t.Device != nil {
596		// Read the messages before Close destroys the messenger.
597		if msgs := t.Device.Validation.Messages(); len(msgs) > 0 {
598			errs = append(errs, fmt.Errorf("%d Vulkan validation errors: %v", len(msgs), msgs))
599		}
600	}
601	t.Device.Close()
602	return errors.Join(errs...)
603}

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