headless.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"path/filepath"
8
9	"github.com/bnema/neferwl/internal/adapters/headless"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/zerowrap"
12)
13
14// headlessOptions are the settings of the virtual outputs.
15type headlessOptions struct {
16	sizes [][2]int
17	shots string
18	hdr   bool
19	raw   bool // also write latest-pq.png (headless.Options.RawHDR)
20}
21
22// runHeadless drives one virtual output per size, named HEADLESS-1, -2, ...
23// With several outputs, screenshots go to a subdirectory per output. Modes
24// are fixed, so apply answers every configuration without backend work.
25//
26// The raw PQ dump (o.raw) needs readable HDR targets, set on these
27// renderers only: the DRM path never reads them back.
28//
29// Virtual outputs have no display to list modifiers, so each renderer is
30// told it drives one (vulkan.Renderer.SetVirtualOutput); without it a real
31// GPU refuses the HDR targets and the output falls back to SDR.
32func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
33	newRenderer = virtualRenderer(newRenderer, o.raw)
34	set := newOutputSet(ctx, ch.captured)
35	set.wireSecurity(ch)
36	inventory := ports.OutputHeads{}
37	for i, size := range o.sizes {
38		name := fmt.Sprintf("HEADLESS-%d", i+1)
39		mode := ports.OutputMode{Width: size[0], Height: size[1], RefreshMilli: 60000, Preferred: true}
40		inventory.Heads = append(inventory.Heads, ports.OutputHead{Info: ports.OutputInfo{Name: name, Width: size[0], Height: size[1], RefreshMilli: 60000}, Modes: []ports.OutputMode{mode}, Current: &mode, Enabled: true})
41		dir := o.shots
42		if dir != "" && len(o.sizes) > 1 {
43			dir = filepath.Join(o.shots, name)
44			if err := os.MkdirAll(dir, 0o755); err != nil {
45				// Outputs already started stop and release their captures.
46				return joinErr(err, set.wait())
47			}
48		}
49		cur := &headless.Cursor{}
50		curs.set(name, cur)
51		opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, HDR: o.hdr, RawHDR: o.raw, Formats: ch.formats, Log: log, NewRenderer: newRenderer, NewCaptureRenderer: newRenderer, Name: name, Presented: ch.presented, Captured: ch.captured}
52		started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error {
53			opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance
54			return headless.Run(octx, opts, sc, cc, cu, cap)
55		}, &opts.SecurityEvents)
56		if !started {
57			return set.wait()
58		}
59		select {
60		case ch.events <- ports.OutputAdded{Info: ports.OutputInfo{Name: name, Width: size[0], Height: size[1]}}:
61		case <-ctx.Done():
62			return set.wait()
63		}
64	}
65	// All virtual lifetimes have registered before this no-lease barrier.
66	if set.state.Protected {
67		set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
68	}
69	apply.heads(inventory)
70	// The pointer starts centred on the first output, like libinput's.
71	curs.move("HEADLESS-1", float64(o.sizes[0][0])/2, float64(o.sizes[0][1])/2, false)
72	for {
73		configs, configNext := apply.configOut(ch.configured), apply.config
74		replies, replyNext := apply.replyOut(ch.replies)
75		heads, headsNext := apply.headsOut(ch.heads)
76		contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents)
77		select {
78		case <-ctx.Done():
79			return set.wait()
80		case state, ok := <-ch.securityChanges:
81			if !ok {
82				ch.securityChanges = nil
83				continue
84			}
85			if set.setSecurity(state) && set.state.Protected {
86				set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
87			}
88		case ev := <-ch.reloads:
89			apply.reload(ev.Config)
90		case req := <-ch.requests:
91			apply.request(req)
92		case configs <- configNext:
93			apply.configSent()
94		case replies <- replyNext:
95			apply.replySent()
96		case heads <- headsNext:
97			apply.headsSent()
98		case s := <-ch.scenes:
99			set.scenes(s)
100		case c := <-contents:
101			set.content(c)
102		case contentOut <- contentNext:
103			contentOwner.contentSent()
104		case q := <-ch.captures:
105			set.routeCapture(q)
106		case c := <-ch.cursorChanges:
107			set.setCursor(c)
108		case stopped := <-set.stopped:
109			if !set.stoppedCurrent(stopped) {
110				continue
111			}
112			// An owner error is not protection or removal evidence.
113			err := set.finish(stopped.name)
114			return joinErr(err, set.wait())
115		}
116	}
117}
118
119// virtualRenderer wraps a renderer factory for headless outputs. Vulkan is
120// the only implementation of these settings; they are not on the port.
121// readback makes the HDR targets readable (HDRPixels).
122func virtualRenderer(newRenderer func(w, h int) (ports.Renderer, error), readback bool) func(w, h int) (ports.Renderer, error) {
123	return func(w, h int) (ports.Renderer, error) {
124		r, err := newRenderer(w, h)
125		if err != nil {
126			return nil, err
127		}
128		if v, ok := r.(interface{ SetVirtualOutput(bool) }); ok {
129			v.SetVirtualOutput(true)
130		}
131		if v, ok := r.(interface{ SetHDRReadback(bool) }); ok && readback {
132			v.SetHDRReadback(true)
133		}
134		return r, nil
135	}
136}
137
138func joinErr(a, b error) error {
139	if a != nil {
140		return a
141	}
142	return b
143}

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 23

Tags 10

↑↓ moveenter openesc close