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}