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