main f2d166b

output.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close