app.go RawHistoryFolder
1//go:build linux
2
3package session
4
5import (
6 "context"
7 "fmt"
8 "os"
9 "time"
10
11 "github.com/bnema/nefergui/internal/platform/wayland"
12 "github.com/bnema/nefergui/internal/render"
13)
14
15// AppHooks runs entirely on the session owner loop. Draw returns dirty=false
16// when there is no new display list; the session retries a dirty frame until
17// committed without waiting for GPU completion on the loop.
18type AppHooks struct {
19 Wake <-chan struct{}
20 Input func([]wayland.Input) error
21 Event func(wayland.Event) error
22 Draw func() (render.Frame, bool, error)
23 Committed func(uint64) error
24}
25
26// RunApp drives a UI until close or cancellation. Wayland input is batched after
27// Apply, and wake tokens, repeat deadlines and release points share one select.
28func (s *Session) RunApp(ctx context.Context, app AppHooks) error {
29 if app.Draw == nil {
30 return fmt.Errorf("session: nil app draw")
31 }
32 if s.waitCancel != nil {
33 return fmt.Errorf("session already running")
34 }
35 s.waitCtx, s.waitCancel = context.WithCancel(ctx)
36 s.releases = make(chan releaseEvent, 128)
37 if s.debugDir != "" {
38 fds, _ := os.ReadDir("/proc/self/fd")
39 s.fdStart, s.fdPeak = len(fds), len(fds)
40 }
41 defer func() { s.waitCancel(); s.waiters.Wait() }()
42 wlEvents := s.Window.StartReader()
43 sched := newFrameSchedule()
44 for !s.Window.Closed {
45 if err := ctx.Err(); err != nil {
46 return err
47 }
48 pending, err := s.finishPending()
49 if err != nil {
50 return err
51 }
52 // Resize or scale change (also set before RunApp, or by the event case
53 // below): the prepared frame is stale until tick clears Window.Dirty.
54 if s.Window.Dirty {
55 sched.Invalidate()
56 }
57 if inputs := s.Window.DrainInput(nil); len(inputs) > 0 {
58 sched.Invalidate()
59 if app.Input != nil {
60 if err := app.Input(inputs); err != nil {
61 return err
62 }
63 }
64 }
65 committed, err := sched.Step(s.Window.Ready(), app.Draw, func(prepared render.Frame) (bool, error) {
66 return s.tickFrame(prepared)
67 })
68 if err != nil {
69 return err
70 }
71 if committed {
72 if app.Committed != nil {
73 if err := app.Committed(s.frame); err != nil {
74 return err
75 }
76 }
77 continue
78 }
79 var retry, repeat <-chan time.Time
80 if sched.NeedsRetry(pending || s.framePending, s.Window.Ready()) {
81 retry = time.After(retryInterval)
82 }
83 if adapter := s.Window.InputAdapter(); adapter != nil {
84 if at, ok := adapter.NextRepeat(); ok {
85 repeat = time.After(time.Until(at))
86 }
87 }
88 select {
89 case <-ctx.Done():
90 return ctx.Err()
91 case <-app.Wake:
92 sched.Invalidate()
93 case <-retry:
94 case <-repeat:
95 if adapter := s.Window.InputAdapter(); adapter != nil {
96 if input, ok := adapter.Tick(time.Now()); ok {
97 sched.Invalidate()
98 if app.Input != nil {
99 if err := app.Input([]wayland.Input{input}); err != nil {
100 return err
101 }
102 }
103 }
104 }
105 case ev := <-wlEvents:
106 if ev.Kind == wayland.BufferRelease {
107 if slot := s.Slots[ev.BufferID]; slot != nil {
108 if err := s.Pool.Release(slot.Buffer); err != nil {
109 return err
110 }
111 s.traceState(slot.Buffer, "wl_buffer.release")
112 }
113 } else if err := s.Window.Apply(ev); err != nil {
114 return err
115 }
116 if app.Event != nil {
117 if err := app.Event(ev); err != nil {
118 return err
119 }
120 }
121 // DrainInput is handled at the start of the next iteration.
122 case ev := <-s.releases:
123 if err := s.release(ev); err != nil {
124 return err
125 }
126 }
127 }
128 if s.Device != nil && s.Device.Validation != nil && len(s.Device.Validation.Messages()) > 0 {
129 return fmt.Errorf("%d Vulkan validation errors: %v", len(s.Device.Validation.Messages()), s.Device.Validation.Messages())
130 }
131 return nil
132}