1package headless
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sys/unix"
7 "image"
8 "image/png"
9 "maps"
10 "os"
11 "path/filepath"
12 "time"
13
14 "github.com/bnema/neferwl/internal/adapters/syncfile"
15 "github.com/bnema/neferwl/internal/ports"
16 "github.com/bnema/zerowrap"
17)
18
19type Options struct {
20 // Cursor, when set, is drawn into screenshots with the image from
21 // LoadCursor at the scene scale.
22 Cursor *Cursor
23 LoadCursor func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
24 Width, Height int
25 ScreenshotDir string
26 Log zerowrap.Logger
27 NewRenderer func(w, h int) (ports.Renderer, error)
28 // Name and Presented report what the output has read after each
29 // frame, so wayland can release client buffers (nil: no reports).
30 Name string
31 Presented chan<- ports.OutputPresented
32}
33
34func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error {
35 r, err := opts.NewRenderer(opts.Width, opts.Height)
36 if err != nil {
37 return fmt.Errorf("create renderer: %w", err)
38 }
39 defer r.Close()
40 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
41 var scene ports.Scene
42 haveScene, dirty := false, false
43 frame := 0
44 var want ports.CursorChange
45 cursorScale := -1.0 // not loaded yet
46 seen := map[ports.WindowID]uint64{}
47 // pending is a report the channel could not take, retried soon.
48 var pending *ports.OutputPresented
49 update := func(c ports.SurfaceContent) {
50 seen[c.ID] = max(seen[c.ID], c.Seq)
51 dirty = dirty || scene.Shows(c.ID)
52 if c.Empty() {
53 delete(surfaces, c.ID)
54 } else {
55 surfaces[c.ID] = c
56 }
57 }
58 for {
59 if ctx.Err() != nil {
60 return nil
61 }
62 var retry <-chan time.Time
63 if pending != nil {
64 retry = time.After(time.Millisecond)
65 }
66 select {
67 case <-ctx.Done():
68 return nil
69 case <-retry:
70 pending = opts.send(pending)
71 continue
72 case s, ok := <-scenes:
73 if !ok {
74 scenes = nil
75 continue
76 }
77 scene, haveScene, dirty = s, true, true
78 case c, ok := <-contents:
79 if !ok {
80 contents = nil
81 continue
82 }
83 update(c)
84 case c := <-cursor:
85 // The cursor is only drawn into screenshots: no new frame.
86 want, cursorScale = c, -1
87 }
88 // Drain all queued updates before presenting one frame.
89 drain:
90 for {
91 select {
92 case s, ok := <-scenes:
93 if !ok {
94 scenes = nil
95 } else {
96 scene, haveScene, dirty = s, true, true
97 }
98 case c, ok := <-contents:
99 if !ok {
100 contents = nil
101 } else {
102 update(c)
103 }
104 default:
105 break drain
106 }
107 }
108 if !haveScene || !dirty {
109 // Contents not drawn are still read: report them.
110 pending = opts.report(pending, seen)
111 continue
112 }
113 dirty = false
114 if opts.Cursor != nil && opts.LoadCursor != nil && scene.Scale != cursorScale {
115 cursorScale = scene.Scale
116 if img, err := opts.LoadCursor(want, scene.Scale, 256); err == nil {
117 opts.Cursor.set(img)
118 } else {
119 opts.Log.Warn().Err(err).Msg("cursor")
120 }
121 }
122 start := time.Now()
123 done, err := r.Render(scene, surfaces)
124 if err != nil {
125 return fmt.Errorf("render frame: %w", err)
126 }
127 if done != nil {
128 // No screen paces headless frames: wait for the GPU before
129 // reporting that its buffers were read.
130 err = syncfile.Wait(ctx, done)
131 done.Close()
132 if err != nil {
133 if ctx.Err() != nil {
134 return nil
135 }
136 return fmt.Errorf("frame fence: %w", err)
137 }
138 }
139 frame++
140 pending = opts.flipped(pending, seen, scene, surfaces)
141 if opts.ScreenshotDir != "" {
142 shot := r.Pixels()
143 if opts.Cursor != nil {
144 opts.Cursor.draw(shot)
145 }
146 if err := writePNG(filepath.Join(opts.ScreenshotDir, fmt.Sprintf("frame-%06d.png", frame)), shot); err != nil {
147 return fmt.Errorf("screenshot: %w", err)
148 }
149 if err := writePNG(filepath.Join(opts.ScreenshotDir, "latest.png"), shot); err != nil {
150 return fmt.Errorf("latest screenshot: %w", err)
151 }
152 }
153 opts.Log.Debug().Str("component", "render").Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("ms", time.Since(start)).Msg("frame")
154 }
155}
156
157func writePNG(path string, img *image.RGBA) error {
158 f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png")
159 if err != nil {
160 return err
161 }
162 defer os.Remove(f.Name())
163 if err := png.Encode(f, img); err != nil {
164 _ = f.Close()
165 return err
166 }
167 if err := f.Close(); err != nil {
168 return err
169 }
170 return os.Rename(f.Name(), path)
171}
172
173// report sends what the output read, or returns it to retry when the
174// channel is full. Frames that were not drawn carry no flip.
175func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented {
176 if opts.Presented == nil {
177 return nil
178 }
179 return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)})
180}
181
182// flipped reports a drawn frame as a flip at the current CLOCK_MONOTONIC
183// time (software clock, refresh unknown), so presentation feedback and
184// frame callbacks follow headless frames. A report still waiting is
185// sent first.
186func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.WindowID]uint64, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) *ports.OutputPresented {
187 if opts.Presented == nil {
188 return nil
189 }
190 var merged int
191 if pending != nil && opts.send(pending) != nil {
192 // The reader is behind: this frame's report replaces the waiting
193 // one, and its flip counts as merged (its feedback is discarded).
194 if pending.Flip != nil {
195 merged = 1 + pending.Flip.Merged
196 }
197 }
198 var ts unix.Timespec
199 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
200 shows := map[ports.WindowID]uint64{}
201 for id, c := range surfaces {
202 if scene.Shows(id) {
203 shows[id] = c.Seq
204 }
205 }
206 return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows, Merged: merged}})
207}
208
209func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented {
210 select {
211 case opts.Presented <- *r:
212 return nil
213 default:
214 return r
215 }
216}