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// newRenderer is configured for virtual outputs by newVulkanRenderer.
27func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
28 set := newOutputSet(ctx, ch.captured)
29 set.wireSecurity(ch)
30 inventory := ports.OutputHeads{}
31 for i, size := range o.sizes {
32 name := fmt.Sprintf("HEADLESS-%d", i+1)
33 mode := ports.OutputMode{Width: size[0], Height: size[1], RefreshMilli: 60000, Preferred: true}
34 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})
35 dir := o.shots
36 if dir != "" && len(o.sizes) > 1 {
37 dir = filepath.Join(o.shots, name)
38 if err := os.MkdirAll(dir, 0o755); err != nil {
39 // Outputs already started stop and release their captures.
40 return joinErr(err, set.wait())
41 }
42 }
43 cur := &headless.Cursor{}
44 curs.set(name, cur)
45 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}
46 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 {
47 opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance
48 return headless.Run(octx, opts, sc, cc, cu, cap)
49 }, &opts.SecurityEvents)
50 if !started {
51 return set.wait()
52 }
53 select {
54 case ch.events <- ports.OutputAdded{Info: ports.OutputInfo{Name: name, Width: size[0], Height: size[1]}}:
55 case <-ctx.Done():
56 return set.wait()
57 }
58 }
59 // All virtual lifetimes have registered before this no-lease barrier.
60 if set.state.Protected {
61 set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
62 }
63 apply.heads(inventory)
64 // The pointer starts centred on the first output, like libinput's.
65 curs.move("HEADLESS-1", float64(o.sizes[0][0])/2, float64(o.sizes[0][1])/2, false)
66 for {
67 configs, configNext := apply.configOut(ch.configured), apply.config
68 replies, replyNext := apply.replyOut(ch.replies)
69 heads, headsNext := apply.headsOut(ch.heads)
70 contents, contentOut, contentNext, contentOwner := set.contentOut(ch.contents)
71 select {
72 case <-ctx.Done():
73 return set.wait()
74 case state, ok := <-ch.securityChanges:
75 if !ok {
76 ch.securityChanges = nil
77 continue
78 }
79 if set.setSecurity(state) && set.state.Protected {
80 set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
81 }
82 case ev := <-ch.reloads:
83 apply.reload(ev.Config)
84 case req := <-ch.requests:
85 apply.request(req)
86 case configs <- configNext:
87 apply.configSent()
88 case replies <- replyNext:
89 apply.replySent()
90 case heads <- headsNext:
91 apply.headsSent()
92 case s := <-ch.scenes:
93 set.scenes(s)
94 case c := <-contents:
95 set.content(c)
96 case contentOut <- contentNext:
97 contentOwner.contentSent()
98 case q := <-ch.captures:
99 set.routeCapture(q)
100 case c := <-ch.cursorChanges:
101 set.setCursor(c)
102 case stopped := <-set.stopped:
103 if !set.stoppedCurrent(stopped) {
104 continue
105 }
106 // An owner error is not protection or removal evidence.
107 err := set.finish(stopped.name)
108 return joinErr(err, set.wait())
109 }
110 }
111}
112
113func joinErr(a, b error) error {
114 if a != nil {
115 return a
116 }
117 return b
118}