v0.1.0 47aaa0f

session.go RawHistoryFolder

1//go:build linux
2
3// Package session coordinates Wayland ownership, Vulkan submissions, DRM
4// release points and the application frame loop behind nefergui.Run.
5package session
6
7import (
8	"bufio"
9	"context"
10	"encoding/json"
11	"errors"
12	"fmt"
13	"image"
14	"image/png"
15	"os"
16	"path/filepath"
17	"runtime"
18	"sync"
19	"time"
20
21	"github.com/bnema/nefergui/internal/platform/wayland"
22	"github.com/bnema/nefergui/internal/presentation/buffers"
23	"github.com/bnema/nefergui/internal/presentation/syncobj"
24	"github.com/bnema/nefergui/internal/presentation/vkdevice"
25	"github.com/bnema/nefergui/internal/render"
26	"github.com/bnema/nefergui/internal/text"
27	"github.com/bnema/wlturbo/protocol/drmsyncobj"
28	"golang.org/x/sys/unix"
29)
30
31type Slot struct {
32	Buffer          *buffers.Buffer
33	Image           *vkdevice.Image
34	Frame           *vkdevice.Frame
35	First           bool
36	Wait            *vkdevice.BinaryFence
37	PendingWait     *vkdevice.BinaryFence
38	WaitInFlight    bool
39	ReadbackFrame   uint64
40	ReadbackPending bool
41	ReleaseHandle   uint32
42	ReleaseTimeline *drmsyncobj.WpLinuxDrmSyncobjTimeline
43}
44type Session struct {
45	Window           *wayland.Window
46	Device           *vkdevice.Device
47	Node             *syncobj.Node
48	Pool             *buffers.Pool
49	Pipeline         *vkdevice.Pipeline
50	ListPipeline     *vkdevice.Pipeline
51	Preparer         *render.Preparer
52	list             render.ListBuffer // owner-loop scratch; see tickFrame
53	Slots            map[uint64]*Slot
54	retiredPipelines []retiredPipelines
55	Acquire          uint32
56	AcquireTimeline  *drmsyncobj.WpLinuxDrmSyncobjTimeline
57	Width, Height    int32
58	Transparent      bool
59	trace            *os.File
60	traceBuffer      *bufio.Writer
61	debugDir         string
62	timings          *os.File
63	timingBuffer     *bufio.Writer
64	frame            uint64
65	debugFrames      uint64
66	debugDropped     uint64
67	readbacks        chan readbackJob
68	readbackDone     chan struct{}
69	debugWritten     chan uint64
70	// Written only by readbackWorker; read after readbackDone.
71	debugWriteErrors  uint64
72	debugLastWriteErr string
73	fdStart, fdPeak   int
74	framePending      bool
75	waitCtx           context.Context
76	waitCancel        context.CancelFunc
77	waiters           sync.WaitGroup
78	releases          chan releaseEvent
79}
80
81type retiredPipelines struct {
82	generation uint64
83	rect, list *vkdevice.Pipeline
84}
85
86type readbackJob struct {
87	frame uint64
88	image *image.NRGBA
89}
90
91const maxDebugPNGs = 16
92const readbackQueueSize = 2
93
94func captureBudget(saved, dropped uint64, pending int, queued int, limit uint64) bool {
95	return saved+dropped+uint64(pending+queued) < limit
96}
97
98// offerReadback never blocks the state owner. The worker owns the copied image.
99func offerReadback(queue chan readbackJob, job readbackJob) bool {
100	select {
101	case queue <- job:
102		return true
103	default:
104		return false
105	}
106}
107
108// readbackWorker owns debugWriteErrors until readbackDone is closed.
109func (s *Session) readbackWorker() {
110	defer close(s.readbackDone)
111	for job := range s.readbacks {
112		path := filepath.Join(s.debugDir, fmt.Sprintf("readback-%06d.png", job.frame))
113		vkdevice.SwizzleBGRA(job.image)
114		if err := writePNG(path, job.image); err != nil {
115			s.debugWriteErrors++
116			s.debugLastWriteErr = err.Error()
117			continue
118		}
119		s.debugWritten <- job.frame
120	}
121}
122
123func writePNG(path string, img image.Image) error {
124	f, err := os.Create(path)
125	if err != nil {
126		return err
127	}
128	err = png.Encode(f, img)
129	if closeErr := f.Close(); err == nil {
130		err = closeErr
131	}
132	return err
133}
134
135type releaseEvent struct {
136	id, point uint64
137	err       error
138}
139
140// traceState is entirely disabled when NEFERGUI_DEBUG_DIR is unset.
141func (s *Session) traceState(b *buffers.Buffer, event string) {
142	if s.trace == nil || b == nil {
143		return
144	}
145	_ = json.NewEncoder(s.traceBuffer).Encode(struct {
146		Time                       time.Time `json:"time"`
147		Frame, Buffer, Generation  uint64
148		State                      buffers.State
149		Event                      string
150		AcquirePoint, ReleasePoint uint64
151		WlReleased, PointSignaled  bool
152	}{time.Now(), s.frame, b.ID, b.Generation, b.State, event, b.AcquirePoint, b.ReleasePoint, b.WlReleased, b.PointSignaled})
153}
154
155func Open(name string, width, height int32, transparent bool) (_ *Session, err error) {
156	w, err := wayland.Connect(name, width, height, transparent)
157	if err != nil {
158		return nil, err
159	}
160	s := &Session{Window: w, Transparent: transparent, Slots: make(map[uint64]*Slot)}
161	if dir := os.Getenv("NEFERGUI_DEBUG_DIR"); dir != "" {
162		s.debugDir = dir
163		if err = os.MkdirAll(dir, 0700); err != nil {
164			_ = w.Close()
165			return nil, err
166		}
167		s.trace, err = os.Create(filepath.Join(dir, "buffer-lifecycle.jsonl"))
168		if err != nil {
169			_ = w.Close()
170			return nil, err
171		}
172		s.timings, err = os.Create(filepath.Join(dir, "frame-timings.jsonl"))
173		if err != nil {
174			_ = s.trace.Close()
175			_ = w.Close()
176			return nil, err
177		}
178		s.traceBuffer = bufio.NewWriterSize(s.trace, 64*1024)
179		s.timingBuffer = bufio.NewWriterSize(s.timings, 64*1024)
180		s.readbacks = make(chan readbackJob, readbackQueueSize)
181		s.readbackDone = make(chan struct{})
182		s.debugWritten = make(chan uint64, maxDebugPNGs)
183		go s.readbackWorker()
184	}
185	defer func() {
186		if err != nil {
187			err = errors.Join(err, s.Close())
188		}
189	}()
190	s.Preparer, err = render.NewPreparer(vkdevice.AtlasSize, vkdevice.AtlasPages)
191	if err != nil {
192		return nil, err
193	}
194	s.Device, err = vkdevice.Open(vkdevice.DeviceIdentity(w.MainDevice))
195	if err != nil {
196		return nil, err
197	}
198	s.Node, err = syncobj.Open(filepath.Join("/dev/dri", fmt.Sprintf("renderD%d", s.Device.Render.Minor)))
199	if err != nil {
200		return nil, err
201	}
202	if s.Acquire, err = s.Node.Create(); err != nil {
203		return nil, err
204	}
205	if s.AcquireTimeline, err = s.importTimeline(s.Acquire); err != nil {
206		return nil, err
207	}
208	s.Pool, err = buffers.New(3)
209	if err != nil {
210		return nil, err
211	}
212	if err = s.newGeneration(); err != nil {
213		return nil, err
214	}
215	return s, nil
216}
217
218func (s *Session) importTimeline(handle uint32) (*drmsyncobj.WpLinuxDrmSyncobjTimeline, error) {
219	fd, err := s.Node.ExportTimeline(handle)
220	if err != nil {
221		return nil, err
222	}
223	// WLTurbo closes the sent descriptor on successful ImportTimeline; on
224	// transport failure its ownership remains with this caller.
225	timeline, err := s.Window.SyncManager.ImportTimeline(fd)
226	if err != nil {
227		_ = unix.Close(fd)
228		return nil, err
229	}
230	return timeline, nil
231}
232
233func (s *Session) newGeneration() error {
234	width, height, err := s.Window.PhysicalSize()
235	if err != nil {
236		return err
237	}
238	mods, err := s.Device.ExportableModifiers()
239	if err != nil {
240		return err
241	}
242	selected, err := vkdevice.ChooseModifier(s.Window.Tranches, s.Transparent, mods)
243	if err != nil {
244		return err
245	}
246	pipeline, err := s.Device.NewRectPipeline(width, height)
247	if err != nil {
248		return err
249	}
250	listPipeline, err := s.Device.NewListPipeline(width, height)
251	if err != nil {
252		pipeline.Close()
253		return err
254	}
255	// A pipeline may still be referenced by submissions for the previous
256	// generation. retire closes it only after all old slots have retired.
257	if s.Pipeline != nil {
258		s.retiredPipelines = append(s.retiredPipelines, retiredPipelines{
259			generation: s.Pool.Generation - 1, rect: s.Pipeline, list: s.ListPipeline,
260		})
261	}
262	s.Pipeline = pipeline
263	s.ListPipeline = listPipeline
264	s.Width, s.Height = width, height
265	for _, b := range s.Pool.Buffers {
266		if b.Generation != s.Pool.Generation {
267			continue
268		}
269		img, e := s.Device.AllocateImage(width, height, selected, s.Window.Dmabuf)
270		if e != nil {
271			return e
272		}
273		frame, e := s.Device.NewFrame(img)
274		if e == nil && s.debugDir != "" {
275			frame.Readback, e = s.Device.NewReadback(width, height)
276		}
277		if e != nil {
278			if frame != nil {
279				frame.Close()
280			}
281			img.Close()
282			return e
283		}
284		slot := &Slot{Buffer: b, Image: img, Frame: frame, First: true}
285		slot.ReleaseHandle, e = s.Node.Create()
286		if e != nil {
287			frame.Close()
288			img.Close()
289			return e
290		}
291		slot.ReleaseTimeline, e = s.importTimeline(slot.ReleaseHandle)
292		if e != nil {
293			_ = s.Node.Destroy(slot.ReleaseHandle)
294			frame.Close()
295			img.Close()
296			return e
297		}
298		s.Slots[b.ID] = slot
299		img.Buffer.OnRelease(func() { s.Window.BufferReleased(b.ID) })
300	}
301	return nil
302}
303
304// beginReady does not change Pool ownership until the previous GPU submission
305// has completed. An unready available buffer is retried on a bounded wakeup.
306func beginReady(pool *buffers.Pool, slots map[uint64]*Slot, ready func(*Slot) (bool, error)) (*buffers.Buffer, *Slot, bool, error) {
307	waiting := false
308	for _, b := range pool.Buffers {
309		if b.Generation != pool.Generation || b.State != buffers.Available {
310			continue
311		}
312		slot := slots[b.ID]
313		if slot == nil {
314			return nil, nil, false, fmt.Errorf("missing slot %d", b.ID)
315		}
316		ok, err := ready(slot)
317		if err != nil {
318			return nil, nil, false, err
319		}
320		if !ok {
321			waiting = true
322			continue
323		}
324		b.State = buffers.Rendering
325		b.WlReleased, b.PointSignaled = false, false
326		return b, slot, false, nil
327	}
328	return nil, nil, waiting, nil
329}
330
331// Tick presents at most one frame. The caller drives the Wayland dispatch
332// loop; a frame callback only schedules Tick, never makes an image reusable.
333func (s *Session) Tick(rect vkdevice.Rect) (bool, error) {
334	return s.tick(&rect, nil, nil, nil)
335}
336
337// TickList presents ordered instanced quads with the same ownership gates as Tick.
338func (s *Session) TickList(instances []vkdevice.Instance, uploads []text.Upload) (bool, error) {
339	var batches []vkdevice.Batch
340	if len(instances) > 0 {
341		batches = []vkdevice.Batch{{Count: uint32(len(instances))}}
342	}
343	return s.tick(nil, instances, uploads, batches)
344}
345
346// tickFrame lists and submits one prepared frame on the owner loop. The list
347// buffer's slices are borrowed only for this call: tick copies instances into
348// mapped GPU memory and resolve copies batches before returning, and no pending
349// GPU work or retained frame refers to them (a blocked frame is re-listed from
350// its render.Frame on retry). Release then drops borrowed image references.
351func (s *Session) tickFrame(frame render.Frame) (bool, error) {
352	defer s.list.Release()
353	instances, batches, stats := s.list.List(frame)
354	if len(stats.Skipped) != 0 {
355		return false, fmt.Errorf("unsupported list operations: %v", stats.Skipped)
356	}
357	return s.tick(nil, instances, frame.Uploads, batches)
358}
359
360func (s *Session) tick(rect *vkdevice.Rect, instances []vkdevice.Instance, uploads []text.Upload, batches []vkdevice.Batch) (bool, error) {
361	started := time.Now()
362	if s.Window.Dirty {
363		w, h, err := s.Window.PhysicalSize()
364		if err != nil {
365			return false, err
366		}
367		if w != s.Width || h != s.Height {
368			if err = s.Pool.Resize(3); err != nil {
369				return false, err
370			}
371			if err = s.newGeneration(); err != nil {
372				return false, err
373			}
374		}
375		s.Window.Dirty = false
376	}
377	if err := s.retire(); err != nil {
378		return false, err
379	}
380	if !s.Window.FrameReady {
381		return false, nil
382	}
383	b, slot, waiting, err := beginReady(s.Pool, s.Slots, func(slot *Slot) (bool, error) { return slot.Frame.Ready() })
384	if err != nil {
385		return false, err
386	}
387	if b == nil {
388		s.framePending = waiting
389		return false, nil
390	}
391	s.framePending = false
392	s.traceState(b, "begin")
393	if err := s.saveReadback(slot); err != nil {
394		return false, err
395	}
396	if slot.Wait != nil && slot.WaitInFlight {
397		slot.Wait.Close()
398		slot.Wait = nil
399		slot.WaitInFlight = false
400	}
401	capture := s.wantReadback()
402	if rect != nil {
403		err = slot.Frame.Record(slot.Image, s.Pipeline, s.Width, s.Height, *rect, slot.First, capture)
404	} else {
405		err = slot.Frame.RecordList(slot.Image, s.ListPipeline, s.Width, s.Height, instances, uploads, batches, slot.First, capture)
406	}
407	if err != nil {
408		return false, err
409	}
410	if err = slot.Frame.Submit(slot.Wait); err != nil {
411		return false, err
412	}
413	// The render module keeps uploads owed until this acknowledgement.
414	s.Preparer.Submitted(uploads)
415	if slot.Wait != nil {
416		slot.WaitInFlight = true
417	}
418	// Keep the imported release-wait semaphore alive until GPU completion.
419	slot.First = false
420	fd, err := slot.Frame.Signal.Export()
421	if err != nil {
422		return false, err
423	}
424	acquire, err := s.Pool.Acquire(b)
425	if err != nil {
426		_ = unix.Close(fd)
427		return false, err
428	}
429	if err = s.Node.AcquirePoint(fd, s.Acquire, acquire); err != nil {
430		_ = unix.Close(fd)
431		return false, err
432	}
433	s.traceState(b, "acquire_point_exported")
434	release, err := s.Pool.Commit(b)
435	if err != nil {
436		return false, err
437	}
438	if err = s.Window.Present(slot.Image.Buffer, s.AcquireTimeline, slot.ReleaseTimeline, acquire, release); err != nil {
439		return false, err
440	}
441	s.traceState(b, "committed")
442	if err = s.Pool.Own(b); err != nil {
443		return false, err
444	}
445	s.traceState(b, "compositor_owns")
446	if capture {
447		slot.ReadbackFrame = s.frame
448		slot.ReadbackPending = true
449	}
450	if s.timings != nil {
451		if len(s.Slots) > 6 {
452			return false, fmt.Errorf("unbounded image generations: %d", len(s.Slots))
453		}
454		fds, _ := os.ReadDir("/proc/self/fd")
455		if len(fds) > s.fdPeak {
456			s.fdPeak = len(fds)
457		}
458		_ = json.NewEncoder(s.timingBuffer).Encode(map[string]any{"frame": s.frame, "buffer": b.ID, "generation": b.Generation, "submit_to_commit_ns": time.Since(started).Nanoseconds(), "fd_count": len(fds), "images": len(s.Slots), "atlas_pages": s.Preparer.Atlas.PagesUsed(), "pipelines": 2 * (1 + len(s.retiredPipelines)), "time": time.Now()})
459	}
460	s.startReleaseWait(slot, release)
461	s.frame++
462	return true, nil
463}
464
465func (s *Session) wantReadback() bool {
466	if s.readbacks == nil {
467		return false
468	}
469	pending := 0
470	for _, slot := range s.Slots {
471		if slot.ReadbackPending {
472			pending++
473		}
474	}
475	return captureBudget(s.debugFrames, s.debugDropped, pending, len(s.readbacks), maxDebugPNGs)
476}
477
478// saveReadback is only called for a completed GPU submission; the state owner
479// copies into owned memory and hands the PNG encoding to the bounded worker.
480func (s *Session) saveReadback(slot *Slot) error {
481	if !slot.ReadbackPending || slot.Frame.Readback == nil {
482		return nil
483	}
484	// Do not map a full-size image while the encoder cannot accept it.
485	if len(s.readbacks) == cap(s.readbacks) {
486		slot.ReadbackPending = false
487		s.debugDropped++
488		return nil
489	}
490	img, err := slot.Frame.Readback.Copy()
491	if err != nil {
492		return err
493	}
494	slot.ReadbackPending = false
495	if offerReadback(s.readbacks, readbackJob{slot.ReadbackFrame, img}) {
496		s.debugFrames++
497	} else {
498		s.debugDropped++
499	}
500	return nil
501}
502
503// startReleaseWait never mutates Pool or Vulkan state from the waiter.
504func (s *Session) startReleaseWait(slot *Slot, point uint64) {
505	if s.waitCancel == nil {
506		return
507	}
508	ctx := s.waitCtx
509	id, handle := slot.Buffer.ID, slot.ReleaseHandle
510	s.waiters.Add(1)
511	go func() {
512		defer s.waiters.Done()
513		for {
514			if ctx.Err() != nil {
515				return
516			}
517			ready, err := s.Node.WaitPoint(handle, point, 20*time.Millisecond)
518			if err != nil || ready {
519				select {
520				case s.releases <- releaseEvent{id: id, point: point, err: err}:
521				case <-ctx.Done():
522				}
523				return
524			}
525		}
526	}()
527}
528
529func (s *Session) release(ev releaseEvent) error {
530	if ev.err != nil {
531		return ev.err
532	}
533	slot := s.Slots[ev.id]
534	if slot == nil {
535		return nil
536	}
537	b := slot.Buffer
538	// An older completion cannot authorize a newer commit of the same image.
539	if b.ReleasePoint != ev.point || b.State != buffers.CompositorOwned {
540		return nil
541	}
542	if err := s.Pool.Signal(b, ev.point); err != nil {
543		return err
544	}
545	s.traceState(b, "release_point_signaled")
546	if b.Generation == s.Pool.Generation {
547		fd, err := s.Node.ReleaseFence(slot.ReleaseHandle, ev.point)
548		if err != nil {
549			return err
550		}
551		wait, err := s.Device.NewBinaryFence(false)
552		if err != nil {
553			_ = unix.Close(fd)
554			return err
555		}
556		if err = wait.ImportTemporary(fd); err != nil {
557			_ = unix.Close(fd)
558			wait.Close()
559			return err
560		}
561		installed, e := installReleaseWait(slot, wait, slot.Frame.Ready)
562		if e != nil {
563			wait.Close()
564			return e
565		}
566		if !installed {
567			return nil
568		}
569		if err = s.Pool.Reuse(b, true); err != nil {
570			return err
571		}
572		s.traceState(b, "available")
573	}
574	return s.retire()
575}
576
577// installReleaseWait holds the new semaphore until the old submission is done.
578// Only the state owner calls it. On an unready fence it leaves Pool Released.
579func installReleaseWait(slot *Slot, next *vkdevice.BinaryFence, ready func() (bool, error)) (bool, error) {
580	if slot.Wait != nil && slot.WaitInFlight {
581		ok, err := ready()
582		if err != nil {
583			return false, err
584		}
585		if !ok {
586			slot.PendingWait = next
587			return false, nil
588		}
589	}
590	if slot.Wait != nil {
591		slot.Wait.Close()
592	}
593	slot.Wait, slot.PendingWait, slot.WaitInFlight = next, nil, false
594	return true, nil
595}
596
597// finishPending runs only on the state-owner loop; never waits on a GPU fence.
598func (s *Session) finishPending() (bool, error) {
599	pending := false
600	for _, slot := range s.Slots {
601		if slot.PendingWait == nil {
602			continue
603		}
604		ready, err := slot.Frame.Ready()
605		if err != nil {
606			return false, err
607		}
608		if !ready {
609			pending = true
610			continue
611		}
612		if slot.Wait != nil {
613			slot.Wait.Close()
614		}
615		slot.Wait, slot.PendingWait, slot.WaitInFlight = slot.PendingWait, nil, false
616		if err := s.Pool.Reuse(slot.Buffer, true); err != nil {
617			return false, err
618		}
619		s.traceState(slot.Buffer, "available")
620	}
621	return pending, nil
622}
623
624func (s *Session) retire() error {
625	for _, slot := range s.Slots {
626		b := slot.Buffer
627		if !s.Pool.Retirable(b) {
628			continue
629		}
630		ready, err := slot.Frame.Ready()
631		if err != nil {
632			return err
633		}
634		if !ready {
635			continue
636		}
637		if err := s.saveReadback(slot); err != nil {
638			return err
639		}
640		slot.Frame.Close()
641		if slot.PendingWait != nil {
642			slot.PendingWait.Close()
643		}
644		if slot.Wait != nil {
645			slot.Wait.Close()
646		}
647		slot.Image.Close()
648		_ = slot.ReleaseTimeline.Destroy()
649		_ = s.Node.Destroy(slot.ReleaseHandle)
650		s.traceState(b, "retired")
651		delete(s.Slots, b.ID)
652		if err = s.Pool.Remove(b); err != nil {
653			return err
654		}
655	}
656	for i := 0; i < len(s.retiredPipelines); {
657		old := s.retiredPipelines[i]
658		live := false
659		for _, slot := range s.Slots {
660			if slot.Buffer.Generation == old.generation {
661				live = true
662				break
663			}
664		}
665		if live {
666			i++
667			continue
668		}
669		old.rect.Close()
670		old.list.Close()
671		s.retiredPipelines = append(s.retiredPipelines[:i], s.retiredPipelines[i+1:]...)
672	}
673	return nil
674}
675
676func (s *Session) Run(ctx context.Context, frames int, draw func(int) vkdevice.Rect) (int, error) {
677	return s.RunWithCommit(ctx, frames, draw, nil)
678}
679
680// RunWithCommit calls committed after each successfully committed frame.
681// The optional hook is for the example's capture request; it never gates reuse.
682func (s *Session) RunWithCommit(ctx context.Context, frames int, draw func(int) vkdevice.Rect, committed func(int) error) (int, error) {
683	if draw == nil {
684		return 0, fmt.Errorf("nil draw function")
685	}
686	return s.runWithCommit(ctx, frames, func(i int) (bool, error) { return s.Tick(draw(i)) }, committed)
687}
688
689// RunPreparedListWithCommit prepares uploads on every attempt. Preparer keeps
690// them owed until a submission, so a skipped attempt is resent by the next one.
691func (s *Session) RunPreparedListWithCommit(ctx context.Context, frames int, draw func(int) (render.Frame, error), committed func(int) error) (int, error) {
692	if draw == nil {
693		return 0, fmt.Errorf("nil draw function")
694	}
695	return s.runWithCommit(ctx, frames, func(i int) (bool, error) {
696		frame, err := draw(i)
697		if err != nil {
698			return false, err
699		}
700		return s.tickFrame(frame)
701	}, committed)
702}
703
704// RunListWithCommit drives the list renderer using the existing frame loop.
705func (s *Session) RunListWithCommit(ctx context.Context, frames int, draw func(int) []vkdevice.Instance, committed func(int) error) (int, error) {
706	if draw == nil {
707		return 0, fmt.Errorf("nil draw function")
708	}
709	return s.runWithCommit(ctx, frames, func(i int) (bool, error) { return s.TickList(draw(i), nil) }, committed)
710}
711
712func (s *Session) runWithCommit(ctx context.Context, frames int, tick func(int) (bool, error), committed func(int) error) (int, error) {
713	if frames < 0 {
714		return 0, fmt.Errorf("invalid frame count")
715	}
716	if s.waitCancel != nil {
717		return 0, fmt.Errorf("session already running")
718	}
719	s.waitCtx, s.waitCancel = context.WithCancel(ctx)
720	s.releases = make(chan releaseEvent, 128)
721	if s.debugDir != "" {
722		fds, _ := os.ReadDir("/proc/self/fd")
723		s.fdStart = len(fds)
724		s.fdPeak = s.fdStart
725	}
726	wlEvents := s.Window.StartReader()
727	count := 0
728	defer func() { s.waitCancel(); s.waiters.Wait() }()
729	for !s.Window.Closed && (frames == 0 || count < frames) {
730		if err := ctx.Err(); err != nil {
731			return count, err
732		}
733		pending, err := s.finishPending()
734		if err != nil {
735			return count, err
736		}
737		if ok, err := tick(count); err != nil {
738			return count, err
739		} else if ok {
740			count++
741			if committed != nil {
742				if err := committed(count); err != nil {
743					return count, err
744				}
745			}
746			continue
747		}
748		var retry <-chan time.Time
749		if pending || s.framePending {
750			retry = time.After(5 * time.Millisecond)
751		}
752		select {
753		case <-ctx.Done():
754			return count, ctx.Err()
755		case <-retry:
756		case ev := <-wlEvents:
757			if ev.Kind == wayland.BufferRelease {
758				if slot := s.Slots[ev.BufferID]; slot != nil {
759					if err := s.Pool.Release(slot.Buffer); err != nil {
760						return count, err
761					}
762					s.traceState(slot.Buffer, "wl_buffer.release")
763				}
764			} else if err := s.Window.Apply(ev); err != nil {
765				return count, err
766			}
767		case ev := <-s.releases:
768			if err := s.release(ev); err != nil {
769				return count, err
770			}
771		}
772	}
773	if s.Device != nil && s.Device.Validation != nil && len(s.Device.Validation.Messages()) > 0 {
774		return count, fmt.Errorf("%d Vulkan validation errors: %v", len(s.Device.Validation.Messages()), s.Device.Validation.Messages())
775	}
776	return count, nil
777}
778
779// Close detaches and destroys the explicit-sync surface, traces release points
780// for at most 200 ms, waits for our GPU, then frees every client allocation.
781// Compositor imports keep independent DMA-BUF kernel references.
782// It returns errors from writing requested debug artifacts; allocations are
783// freed regardless.
784func (s *Session) Close() (err error) {
785	if s == nil {
786		return nil
787	}
788	if s.trace != nil {
789		defer func() { err = errors.Join(err, s.traceBuffer.Flush(), s.trace.Close()) }()
790	}
791	if s.timings != nil {
792		defer func() { err = errors.Join(err, s.timingBuffer.Flush(), s.timings.Close()) }()
793	}
794	if s.readbacks != nil {
795		defer func() {
796			if s.readbacks != nil {
797				close(s.readbacks)
798				<-s.readbackDone
799			}
800		}()
801	}
802	if s.Window != nil && s.Window.Surface != nil {
803		_ = s.Window.Surface.Attach(nil, 0, 0)
804		_ = s.Window.Surface.Commit()
805		_ = s.Window.DestroySyncSurface()
806	}
807	// Drain detach/release points for tracing, not for freeing allocations. The
808	// reader and waiters continue posting events, but the loop owns all state.
809	if s.waitCancel != nil && s.Pool != nil {
810		deadline := time.NewTimer(200 * time.Millisecond)
811		defer deadline.Stop()
812		for {
813			busy := false
814			for _, slot := range s.Slots {
815				if slot.Buffer.State != buffers.Available && slot.Buffer.State != buffers.Released {
816					busy = true
817					break
818				}
819			}
820			if !busy {
821				break
822			}
823			select {
824			case ev := <-s.releases:
825				_ = s.release(ev)
826			case ev := <-s.Window.Events():
827				if ev.Kind == wayland.BufferRelease {
828					if slot := s.Slots[ev.BufferID]; slot != nil {
829						_ = s.Pool.Release(slot.Buffer)
830					}
831				} else if ev.Kind != wayland.TransportError {
832					_ = s.Window.Apply(ev)
833				}
834			case <-deadline.C:
835				goto stopWaiters
836			}
837		}
838	}
839stopWaiters:
840	if s.waitCancel != nil {
841		s.waitCancel()
842		s.waiters.Wait()
843		s.waitCancel = nil
844	}
845	// The client GPU's completion is independent from compositor release.
846	if s.Device != nil && s.Device.Logical != 0 && s.Device.Dispatch != nil {
847		_ = s.Device.Dispatch.DeviceWaitIdle(s.Device.Logical)
848	}
849	if s.Pool != nil {
850		_ = s.retire()
851	}
852	for _, slot := range s.Slots {
853		// GPU work must finish before destroying a semaphore it waits on.
854		if slot.ReadbackPending {
855			_ = s.saveReadback(slot)
856		}
857		if slot.Frame != nil {
858			slot.Frame.Close()
859		}
860		if slot.Wait != nil {
861			slot.Wait.Close()
862		}
863		if slot.PendingWait != nil {
864			slot.PendingWait.Close()
865		}
866		if slot.ReleaseTimeline != nil {
867			_ = slot.ReleaseTimeline.Destroy()
868		}
869		// The exported DMA-BUF remains alive in the compositor independently.
870		if slot.Image != nil {
871			slot.Image.Close()
872		}
873		if s.Node != nil && slot.ReleaseHandle != 0 {
874			_ = s.Node.Destroy(slot.ReleaseHandle)
875		}
876	}
877	if s.AcquireTimeline != nil {
878		_ = s.AcquireTimeline.Destroy()
879	}
880	if s.Window != nil {
881		_ = s.Window.Close()
882	}
883	if s.Node != nil {
884		if s.Acquire != 0 {
885			_ = s.Node.Destroy(s.Acquire)
886		}
887		_ = s.Node.Close()
888	}
889	for _, old := range s.retiredPipelines {
890		old.rect.Close()
891		old.list.Close()
892	}
893	s.retiredPipelines = nil
894	if s.Pipeline != nil {
895		s.Pipeline.Close()
896	}
897	if s.ListPipeline != nil {
898		s.ListPipeline.Close()
899	}
900	if s.Device != nil {
901		s.Device.Close()
902	}
903	if s.debugDir != "" {
904		// Wait for the worker before reporting completed PNGs.
905		if s.readbacks != nil {
906			close(s.readbacks)
907			<-s.readbackDone
908			s.readbacks = nil
909		}
910		runtime.GC()
911		fds, _ := os.ReadDir("/proc/self/fd")
912		data, e := json.MarshalIndent(map[string]any{"fd_start": s.fdStart, "fd_peak": s.fdPeak, "fd_after": len(fds), "frames": s.frame, "images_after": 0, "debug_png_count": len(s.debugWritten), "debug_png_dropped": s.debugDropped, "debug_png_write_errors": s.debugWriteErrors, "debug_png_last_error": s.debugLastWriteErr}, "", "  ")
913		if e == nil {
914			e = os.WriteFile(filepath.Join(s.debugDir, "resource-summary.json"), append(data, '\n'), 0600)
915		}
916		if e != nil {
917			err = errors.Join(err, fmt.Errorf("session: resource summary: %w", e))
918		}
919	}
920	return err
921}

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