1package headless
2
3import (
4 "bufio"
5 "context"
6 "fmt"
7 "image"
8 "image/png"
9 "os"
10 "path/filepath"
11 "sync"
12 "time"
13
14 "golang.org/x/sys/unix"
15
16 "github.com/bnema/neferwl/internal/adapters/clock"
17 "github.com/bnema/neferwl/internal/adapters/outputkit/capture"
18 "github.com/bnema/neferwl/internal/adapters/outputkit/corewake"
19 "github.com/bnema/neferwl/internal/adapters/outputkit/presented"
20 "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces"
21 "github.com/bnema/neferwl/internal/adapters/syncfile"
22 "github.com/bnema/neferwl/internal/ports"
23 "github.com/bnema/zerowrap"
24)
25
26type Options struct {
27 // Security is the defensive gate. SecurityChanges wakes an idle owner;
28 // the snapshot, not the queued notification payload, is authoritative.
29 Security ports.SessionSecurity
30 SecurityChanges <-chan ports.SecurityState
31 Instance ports.OutputInstance
32 SecurityEvents chan<- ports.SecurityBackendEvent
33 // Cursor, when set, is drawn into screenshots with the image from
34 // LoadCursor at the scene scale and transform. The cursor is drawn in
35 // target space: the image comes rotated and Move takes target pixels.
36 Cursor *Cursor
37 LoadCursor func(c ports.CursorChange, scale float64, t ports.BufferTransform, limit int) (ports.CursorImage, error)
38 Width, Height int
39 ScreenshotDir string
40 HDR bool // virtual HDR output for protocol testing only
41 // RawHDR also writes latest-pq.png, the raw PQ codes of an HDR output
42 // (16-bit), next to the screenshots. Test-only; needs HDR and ScreenshotDir.
43 RawHDR bool
44 Formats chan<- ports.OutputFormats // confirmed color state; nil disables reporting
45 Log zerowrap.Logger
46 NewRenderer func(w, h int) (ports.Renderer, error)
47 // NewCaptureRenderer makes the child renderer of a hidden workspace
48 // capture session (Scene.CaptureScene). Nil: such captures fail closed.
49 NewCaptureRenderer func(w, h int) (ports.Renderer, error)
50 // Name and Presented report what the output has read after each
51 // frame, so wayland can release client buffers (nil: no reports).
52 Name string
53 Presented ports.PresentationSink
54 // Frames tells core a presented scene is animating: its next frame is
55 // due. Sends never block (nil: nobody is told).
56 Frames chan<- ports.OutputFrame
57 Captured chan<- ports.CaptureDone
58 // Clock paces the renderer trim (nil: system clock).
59 Clock ports.Clock
60}
61
62// trimEvery is how often an output lets its renderer free the buffers of
63// windows it no longer draws.
64const trimEvery = 10 * time.Second
65
66func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan *ports.ContentBatch, returns ports.ContentReturn, cursor <-chan ports.CursorChange, incoming <-chan ports.CaptureRequest) error {
67 r, err := opts.NewRenderer(opts.Width, opts.Height)
68 if err != nil {
69 return fmt.Errorf("create renderer: %w", err)
70 }
71 defer r.Close()
72 var confirmed *ports.OutputHDR
73 if opts.HDR {
74 r.SetHDR(203)
75 if bufs, err := r.ExportTargets(1, nil, false); err != nil {
76 opts.Log.Warn().Err(err).Str("output", opts.Name).Msg("virtual HDR unavailable; falling back to SDR")
77 r.SetHDR(0)
78 _, _ = r.ExportTargets(0, nil, false)
79 } else {
80 for _, b := range bufs {
81 for _, p := range b.Planes {
82 _ = p.File.Close()
83 }
84 }
85 confirmed = &ports.OutputHDR{MaxLuminance: 1000, MaxFrameAverage: 400, MinLuminance: .005}
86 opts.Log.Info().Str("output", opts.Name).Msg("virtual HDR enabled")
87 }
88 if opts.Formats != nil {
89 select {
90 case opts.Formats <- ports.OutputFormats{Output: opts.Name, HDR: confirmed}:
91 case <-ctx.Done():
92 return nil
93 }
94 }
95 }
96 table := surfaces.New()
97 drawn := table.Map()
98 var scene ports.Scene
99 haveScene, dirty := false, false
100 frame := 0
101 var index *os.File
102 var indexWriter *bufio.Writer
103 var lastShot time.Time
104 var lastShotName string
105 defer func() {
106 if index != nil {
107 // A following file makes concat honor the final held interval.
108 if !lastShot.IsZero() {
109 _, _ = fmt.Fprintf(indexWriter, "duration %.6f\nfile '%s'\n", time.Since(lastShot).Seconds(), lastShotName)
110 _ = indexWriter.Flush()
111 }
112 _ = index.Close()
113 }
114 }()
115 var requests capture.Requests
116 ctx, cancelCaptures := context.WithCancel(ctx)
117 requests.Init(ctx, opts.Captured)
118 pipeline := capture.NewPipeline(ctx, opts.Captured)
119 pipeline.Security = opts.Security
120 if opts.NewCaptureRenderer != nil {
121 pipeline.EnableOffscreen(opts.NewCaptureRenderer)
122 }
123 defer func() {
124 // An error path out of a gated frame: what it held is failed.
125 pipeline.EndGate(capture.ErrFrameNotPresented)
126 cancelCaptures()
127 pipeline.Close(r)
128 requests.Stop(incoming)
129 }()
130 var want ports.CursorChange
131 cursorScale := -1.0 // not loaded yet
132 var cursorTransform ports.BufferTransform
133 seen := map[ports.WindowID]uint64{}
134 // reports queues what the output read; every report is pushed to the
135 // sink at once (Push never blocks), so it holds one at most.
136 var reports presented.Queue
137 var shows flipShows
138 var security ports.SecurityState
139 securityKnown := false
140 failRequests := func(reason error) {
141 // Whatever this frame had handed to a worker is failed with it.
142 pipeline.EndGate(capture.GateVerdict(reason))
143 requests.FailPending(reason)
144 }
145 // A transition is its own frame, never a locker-buffer-dependent frame.
146 // Keep its epoch through the fence wait and report only actual completion.
147 checkSecurity := func() error {
148 if opts.Security == nil {
149 return nil
150 }
151 for {
152 next := opts.Security.Snapshot()
153 if securityKnown && next == security {
154 return nil
155 }
156 // No hardware scanout exists: an already applied Off scene is
157 // confirmed software inactivity, independent of its old epoch.
158 inactive := haveScene && scene.Off
159 security, securityKnown = next, true
160 reports.Reset()
161 haveScene, dirty = false, false
162 if !security.Protected {
163 continue
164 }
165 black := ports.Scene{Security: security, Output: opts.Name, OutputWidth: opts.Width, OutputHeight: opts.Height, Scale: 1, Background: "#000000"}
166 black.Off = inactive
167 scene = black
168 kind := ports.ProtectionInactiveOutput
169 if !inactive {
170 kind = ports.ProtectionProtectedFrame
171 done, err := r.Render(black, nil)
172 if err != nil {
173 return fmt.Errorf("render protection: %w", err)
174 }
175 if done != nil {
176 err = syncfile.Wait(ctx, done)
177 _ = done.Close()
178 if err != nil {
179 return fmt.Errorf("protection fence: %w", err)
180 }
181 }
182 }
183 if opts.Security.Snapshot() != security {
184 continue
185 }
186 if opts.SecurityEvents != nil {
187 proof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: security.Generation, Instance: opts.Instance, Kind: kind}}
188 select {
189 case opts.SecurityEvents <- proof:
190 case <-ctx.Done():
191 return ctx.Err()
192 }
193 }
194 // Capture reply backpressure must not postpone the first black
195 // completion/proof. Capture drainage is a separate parent barrier.
196 failRequests(capture.ErrSessionProtected)
197 }
198 }
199 acceptScene := func(s ports.Scene) {
200 if opts.Security != nil && (s.Security != security || s.Security != opts.Security.Snapshot()) {
201 return
202 }
203 // The core keeps the Seq of a scene that draws the same: nothing to do.
204 if haveScene && s.Seq != 0 && s.Seq == scene.Seq && s.Security == scene.Security {
205 return
206 }
207 scene, haveScene, dirty = s, true, true
208 // A closed window's last content goes once no scene draws it.
209 table.Prune(scene)
210 }
211 update := func(b *ports.ContentBatch) {
212 batchDirty, _ := table.Apply(b, scene, seen, returns)
213 dirty = dirty || batchDirty
214 }
215 clk := opts.Clock
216 if clk == nil {
217 clk = clock.System{}
218 }
219 trim := clk.NewTicker(trimEvery)
220 defer trim.Stop()
221 // One timer for the wait of held requests, reset per pass (a time.After
222 // per loop pass would leave a timer behind each).
223 holdTimer := time.NewTimer(time.Hour)
224 holdTimer.Stop()
225 defer holdTimer.Stop()
226 for {
227 // A frame that left its gate open (any path out but its present) fails
228 // what it held: nothing is delivered for a frame that did not present.
229 pipeline.EndGate(capture.ErrFrameNotPresented)
230 if ctx.Err() != nil {
231 return nil
232 }
233 if err := checkSecurity(); err != nil {
234 if ctx.Err() != nil {
235 return nil
236 }
237 return err
238 }
239 // Requests wait for a scene that shows their indicator, at most
240 // capture.HoldFor (see capture.Hold).
241 var holdDue <-chan time.Time
242 if requests.Len() > 0 {
243 var wait time.Duration
244 requests.List, wait = pipeline.Expire(scene, requests.List, time.Now())
245 if wait > 0 {
246 holdTimer.Reset(wait)
247 holdDue = holdTimer.C
248 }
249 }
250 if holdDue == nil {
251 holdTimer.Stop()
252 }
253 select {
254 case <-holdDue:
255 continue
256 case <-ctx.Done():
257 return nil
258 case _, ok := <-opts.SecurityChanges:
259 if !ok {
260 opts.SecurityChanges = nil
261 }
262 if err := checkSecurity(); err != nil {
263 if ctx.Err() != nil {
264 return nil
265 }
266 return err
267 }
268 continue
269 case b := <-pipeline.Completed():
270 // The completed token has left the pipeline's queue. Return its
271 // lease before any transition path can error or block.
272 pipeline.Recycle(b, r)
273 if err := checkSecurity(); err != nil {
274 if ctx.Err() != nil {
275 return nil
276 }
277 return err
278 }
279 continue
280 case b := <-pipeline.HiddenCompleted():
281 pipeline.RecycleHidden(b)
282 if err := checkSecurity(); err != nil {
283 if ctx.Err() != nil {
284 return nil
285 }
286 return err
287 }
288 pipeline.Retire(scene)
289 continue
290 case q, ok := <-incoming:
291 if !ok {
292 incoming = nil
293 continue
294 }
295 if err := checkSecurity(); err != nil {
296 // q has left incoming but is not yet owned by requests/workers.
297 capture.Fail(ctx, q, capture.ErrOutputStopped, opts.Captured)
298 if ctx.Err() != nil {
299 return nil
300 }
301 return err
302 }
303 var refuse error
304 if opts.protected() {
305 refuse = capture.ErrSessionProtected
306 } else if scene.Off {
307 refuse = capture.ErrOutputOff
308 }
309 dirty = requests.Admit(q, refuse, scene, haveScene) || dirty
310 case <-trim.C():
311 if err := checkSecurity(); err != nil {
312 if ctx.Err() != nil {
313 return nil
314 }
315 return err
316 }
317 // An idle output renders nothing: free what windows that left
318 // it held, and the child of an ended session.
319 pipeline.Retire(scene)
320 if err := r.Trim(clk.Now()); err != nil {
321 return fmt.Errorf("trim renderer: %w", err)
322 }
323 opts.Log.Info().Str("output", opts.Name).Int("frames", frame).Msg("stats")
324 continue
325 case s, ok := <-scenes:
326 if !ok {
327 scenes = nil
328 continue
329 }
330 if err := checkSecurity(); err != nil {
331 if ctx.Err() != nil {
332 return nil
333 }
334 return err
335 }
336 acceptScene(s)
337 case b, ok := <-contents:
338 if !ok {
339 contents = nil
340 continue
341 }
342 if err := checkSecurity(); err != nil {
343 if ctx.Err() != nil {
344 return nil
345 }
346 return err
347 }
348 update(b)
349 case c, ok := <-cursor:
350 if !ok {
351 cursor = nil
352 continue
353 }
354 if err := checkSecurity(); err != nil {
355 if ctx.Err() != nil {
356 return nil
357 }
358 return err
359 }
360 // The cursor is only drawn into screenshots: no new frame.
361 want, cursorScale, cursorTransform = c, -1, 0
362 }
363 // Drain all queued updates before presenting one frame.
364 drain:
365 for {
366 select {
367 case s, ok := <-scenes:
368 if !ok {
369 scenes = nil
370 } else {
371 if err := checkSecurity(); err != nil {
372 if ctx.Err() != nil {
373 return nil
374 }
375 return err
376 }
377 acceptScene(s)
378 }
379 case b, ok := <-contents:
380 if !ok {
381 contents = nil
382 } else {
383 if err := checkSecurity(); err != nil {
384 if ctx.Err() != nil {
385 return nil
386 }
387 return err
388 }
389 update(b)
390 }
391 default:
392 break drain
393 }
394 }
395 if err := checkSecurity(); err != nil {
396 if ctx.Err() != nil {
397 return nil
398 }
399 return err
400 }
401 if opts.protected() {
402 failRequests(capture.ErrSessionProtected)
403 }
404 if scene.Off && requests.Len() > 0 {
405 requests.FailPending(capture.ErrOutputOff)
406 }
407 if !haveScene || !dirty || scene.Off {
408 // Contents not drawn are still read: report them. An output
409 // turned off draws nothing.
410 opts.report(&reports, nil, seen)
411 continue
412 }
413 dirty = false
414 if !opts.protected() && opts.Cursor != nil && opts.LoadCursor != nil && (scene.Scale != cursorScale || scene.Transform != cursorTransform) {
415 cursorScale, cursorTransform = scene.Scale, scene.Transform
416 if img, err := opts.LoadCursor(want, scene.Scale, scene.Transform, 256); err == nil {
417 if !opts.protected() {
418 opts.Cursor.set(img)
419 }
420 } else {
421 opts.Log.Warn().Err(err).Msg("cursor")
422 }
423 }
424 if err := checkSecurity(); err != nil {
425 if ctx.Err() != nil {
426 return nil
427 }
428 return err
429 }
430 if !haveScene {
431 continue
432 }
433 start := time.Now()
434 // Requests the scene cannot serve (exclusion of another session, a
435 // workspace that moved) are failed here; see capture.Pipeline.Split.
436 // A request whose indicator the scene does not show yet stays held.
437 ready, _ := pipeline.Hold(scene, requests.List, time.Now())
438 if len(scene.CaptureIndicators) > 0 {
439 // The captures of this frame are delivered only once its frame,
440 // with the indicator, is presented (capture.Pipeline.EndGate).
441 pipeline.BeginGate()
442 }
443 normal, excluded, hidden := pipeline.Split(scene, ready)
444 pipeline.Retire(scene)
445 if len(hidden) > 0 {
446 // A hidden workspace: a child renderer draws it. The display's
447 // fences do not cover the child, so wait for it before reporting.
448 if opts.Security != nil && opts.Security.Snapshot() != scene.Security {
449 failRequests(capture.ErrEpochChanged)
450 continue
451 }
452 pipeline.SubmitHidden(scene, drawn, hidden)
453 clear(hidden)
454 // A slow child may outlive Wayland's stale-report timeout even
455 // though this owner waits. Publish its non-expiring holds first.
456 if opts.Presented != nil {
457 _, reads, _ := pipeline.CapHiddenSeen(seen)
458 if len(reads) > 0 {
459 reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads})
460 reports.Flush(opts.Presented)
461 }
462 }
463 if err := pipeline.WaitHidden(ctx); err != nil {
464 if ctx.Err() != nil {
465 return nil
466 }
467 return fmt.Errorf("workspace frame fence: %w", err)
468 }
469 }
470 // Captures never show the capture indicator or the position hint, and
471 // show hint labels only to clients with hint.capture: their frame goes
472 // first.
473 took, pdone, perr := pipeline.SubmitPlain(r, scene, drawn, normal)
474 if pdone != nil {
475 // The plain render reads client buffers even when its requests
476 // are rejected by a gate transition: own and wait its fence
477 // before any Seen report or protection check can proceed.
478 waitErr := syncfile.Wait(ctx, pdone)
479 _ = pdone.Close()
480 if waitErr != nil {
481 if ctx.Err() != nil {
482 return nil
483 }
484 return fmt.Errorf("plain frame fence: %w", waitErr)
485 }
486 }
487 if perr != nil {
488 return fmt.Errorf("render plain frame: %w", perr)
489 }
490 if took {
491 normal = nil // answered from the frame without the indicator
492 }
493 if len(excluded) > 0 {
494 // Exclude requests: the frame without the excluded surfaces is
495 // drawn first (same renderer, queue order), then the displayed one.
496 // It has no indicator or position hint; it keeps hint labels with
497 // hint.capture (Split refuses internal requests there).
498 if opts.Security != nil && opts.Security.Snapshot() != scene.Security {
499 failRequests(capture.ErrEpochChanged)
500 continue
501 }
502 xdone, renderErr := r.Render(pipeline.ExcludedScene(scene), drawn)
503 if xdone != nil {
504 // The excluded-frame composition reads client buffers even when the displayed
505 // frame is skipped by a gate/off transition. Own and wait its
506 // fence before any Seen report or protection check can proceed.
507 waitErr := syncfile.Wait(ctx, xdone)
508 _ = xdone.Close()
509 if waitErr != nil {
510 if ctx.Err() != nil {
511 return nil
512 }
513 return fmt.Errorf("excluded frame fence: %w", waitErr)
514 }
515 }
516 if renderErr != nil {
517 return fmt.Errorf("render excluded frame: %w", renderErr)
518 }
519 if opts.Security != nil && opts.Security.Snapshot() != scene.Security {
520 failRequests(capture.ErrEpochChanged)
521 continue
522 }
523 pipeline.SubmitScoped(scene.Security, r, excluded)
524 clear(excluded)
525 }
526 if opts.Security != nil && opts.Security.Snapshot() != scene.Security {
527 failRequests(capture.ErrEpochChanged)
528 continue
529 }
530 done, err := r.Render(scene, drawn)
531 if err != nil {
532 return fmt.Errorf("render frame: %w", err)
533 }
534 if done != nil {
535 // No screen paces headless frames: wait for the GPU before
536 // reporting that its buffers were read.
537 err = syncfile.Wait(ctx, done)
538 done.Close()
539 if err != nil {
540 if ctx.Err() != nil {
541 return nil
542 }
543 return fmt.Errorf("frame fence: %w", err)
544 }
545 }
546 if len(normal) > 0 {
547 opts.Log.Debug().Str("output", opts.Name).Int("captures", len(normal)).Msg("capture frame composed")
548 }
549 // Do not capture or report a frame that crossed a transition during
550 // Render/fence wait. The next loop establishes the new black proof.
551 if opts.Security != nil && opts.Security.Snapshot() != scene.Security {
552 failRequests(capture.ErrEpochChanged)
553 continue
554 }
555 pipeline.SubmitScoped(scene.Security, r, normal)
556 // The indicated frame is presented: release what it gated.
557 pipeline.EndGate(nil)
558 requests.List = capture.Waiting(requests.List, scene)
559 frame++
560 opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen)
561 if scene.Animating {
562 corewake.Send(opts.Frames, opts.Name)
563 }
564 if opts.ScreenshotDir != "" && !opts.protected() {
565 shot := r.Pixels()
566 if opts.protected() || opts.Security != nil && opts.Security.Snapshot() != scene.Security {
567 continue
568 }
569 if shot == nil {
570 opts.Log.Debug().Str("output", opts.Name).Int("frame", frame).Msg("screenshot readback unavailable; frame skipped")
571 continue
572 }
573 if opts.Cursor != nil {
574 opts.Cursor.drawSecure(shot, opts.Security)
575 }
576 if opts.protected() || opts.Security != nil && opts.Security.Snapshot() != scene.Security {
577 continue
578 }
579 name := fmt.Sprintf("frame-%06d.png", frame)
580 written, err := writePNGSecureResult(filepath.Join(opts.ScreenshotDir, name), shot, opts.Security, scene.Security)
581 if err != nil {
582 return fmt.Errorf("screenshot: %w", err)
583 }
584 if !written {
585 continue
586 }
587 // latest.png is the frame just written, linked rather than encoded again.
588 written, err = linkPNGSecure(filepath.Join(opts.ScreenshotDir, name), filepath.Join(opts.ScreenshotDir, "latest.png"), shot, opts.Security, scene.Security)
589 if err != nil {
590 return fmt.Errorf("latest screenshot: %w", err)
591 }
592 if !written {
593 continue
594 }
595 now := time.Now()
596 if index == nil {
597 index, err = os.OpenFile(filepath.Join(opts.ScreenshotDir, "frames.ffconcat"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_APPEND, 0o600)
598 if err != nil {
599 return fmt.Errorf("frame index: %w", err)
600 }
601 indexWriter = bufio.NewWriter(index)
602 _, _ = fmt.Fprintln(indexWriter, "ffconcat version 1.0")
603 }
604 // Complete the previous frame's duration before starting this one.
605 // An idle scene leaves that frame displayed and adds no entries.
606 if !lastShot.IsZero() {
607 _, _ = fmt.Fprintf(indexWriter, "duration %.6f\n", now.Sub(lastShot).Seconds())
608 }
609 _, _ = fmt.Fprintf(indexWriter, "file '%s'\n", name)
610 if err := indexWriter.Flush(); err != nil {
611 return fmt.Errorf("frame index: %w", err)
612 }
613 lastShot, lastShotName = now, name
614 if opts.RawHDR && opts.HDR {
615 // No cursor on the raw image. Only a confirmed HDR output
616 // has a readable PQ target; else HDRPixels is nil.
617 if opts.protected() || opts.Security != nil && opts.Security.Snapshot() != scene.Security {
618 continue
619 }
620 raw := r.HDRPixels()
621 if raw == nil {
622 opts.Log.Debug().Str("output", opts.Name).Int("frame", frame).Msg("raw HDR readback unavailable; frame skipped")
623 continue
624 }
625 if err := writePNGSecure(filepath.Join(opts.ScreenshotDir, "latest-pq.png"), raw, opts.Security, scene.Security); err != nil {
626 return fmt.Errorf("raw HDR screenshot: %w", err)
627 }
628 }
629 }
630 if ev := opts.Log.Debug(); ev.Enabled() {
631 ev.Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("ms", time.Since(start)).Int("redrawn", r.TakeRedrawn()).Msg("frame")
632 }
633 }
634}
635
636func (opts Options) protected() bool {
637 return opts.Security != nil && opts.Security.Snapshot().Protected
638}
639
640func writePNGSecure(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) error {
641 _, err := writePNGSecureResult(path, img, security, state)
642 return err
643}
644
645// screenshotPNG encodes the PNGs written by the headless output (frames, the
646// latest.png fallback, raw HDR latest-pq.png): encoding is by far the largest
647// cost of a headless frame, so it trades file size for speed. Its buffers are
648// reused between files.
649var screenshotPNG = png.Encoder{CompressionLevel: png.BestSpeed, BufferPool: new(pngBuffers)}
650
651// pngBuffers is a png.EncoderBufferPool.
652type pngBuffers struct{ pool sync.Pool }
653
654func (p *pngBuffers) Get() *png.EncoderBuffer {
655 b, _ := p.pool.Get().(*png.EncoderBuffer)
656 return b
657}
658
659func (p *pngBuffers) Put(b *png.EncoderBuffer) { p.pool.Put(b) }
660
661// gated reports whether a screenshot must not be published: the session is
662// protected, or its security state is no longer the one the frame was drawn in.
663func gated(security ports.SessionSecurity, state ports.SecurityState) bool {
664 return security != nil && (state.Protected || security.Snapshot() != state)
665}
666
667// writePNGSecureResult distinguishes a security skip from a published PNG.
668func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
669 if gated(security, state) {
670 return false, nil
671 }
672 f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png")
673 if err != nil {
674 return false, err
675 }
676 defer os.Remove(f.Name())
677 if err := screenshotPNG.Encode(f, img); err != nil {
678 _ = f.Close()
679 return false, err
680 }
681 if err := f.Close(); err != nil {
682 return false, err
683 }
684 if gated(security, state) {
685 return false, nil
686 }
687 if err := os.Rename(f.Name(), path); err != nil {
688 return false, err
689 }
690 return true, nil
691}
692
693// linkPNGSecure publishes the PNG already written at src as dst without
694// encoding it again: a hard link under a temporary name, renamed over dst, so
695// readers see the old or the new frame, never a partial one. Frame files are
696// never modified after they are published, which makes the shared file safe.
697// It gates on security like writePNGSecureResult, and falls back to encoding
698// img where the file system cannot link.
699func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
700 if gated(security, state) {
701 return false, nil
702 }
703 tmp := dst + ".tmp"
704 _ = os.Remove(tmp)
705 if err := os.Link(src, tmp); err != nil {
706 return writePNGSecureResult(dst, img, security, state)
707 }
708 defer os.Remove(tmp)
709 if gated(security, state) {
710 return false, nil
711 }
712 if err := os.Rename(tmp, dst); err != nil {
713 return false, err
714 }
715 return true, nil
716}
717
718// report queues what the output read, with flip for a drawn frame, and
719// pushes it to the sink.
720func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
721 if opts.Presented == nil {
722 return
723 }
724 q.Push(ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: seen})
725 q.Flush(opts.Presented)
726}
727
728// flipShows builds the Shows of successive flips. Its owner goroutine is the
729// only user. The Shows snapshot is shared with earlier flips of the same
730// content (see surfaces.ShowsSnapshot): never modify it.
731type flipShows struct {
732 snapshot surfaces.ShowsSnapshot
733}
734
735// of returns the content Seq of the windows scene showed to their client.
736func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
737 // A closed window drawn from its kept content is not shown to its
738 // client.
739 return f.snapshot.FromContents(scene, contents, kept)
740}
741
742// flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC
743// time (software clock, refresh unknown), so presentation feedback and
744// frame callbacks follow headless frames. The FlipInfo itself is new per
745// flip (the sink counts merges on it); only its Shows is shared.
746func (f *flipShows) flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo {
747 var ts unix.Timespec
748 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
749 return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: f.of(scene, contents, kept)}
750}