1package app
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9
10 "github.com/bnema/neferwl/internal/adapters/headless"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/bnema/zerowrap"
13)
14
15// headlessOptions are the settings of the virtual outputs.
16type headlessOptions struct {
17 sizes [][2]int
18 shots string
19 hdr bool
20 raw bool // also write latest-pq.png (headless.Options.RawHDR)
21}
22
23// runHeadless drives one virtual output per size, named HEADLESS-1, -2, ...
24// With several outputs, screenshots go to a subdirectory per output. Modes
25// are fixed, so apply answers every configuration without backend work.
26//
27// newRenderer is configured for virtual outputs by newVulkanRenderer.
28func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
29 set := newOutputSet(ctx, ch.captured)
30 set.attach = ch.content
31 set.wireSecurity(ch)
32 inventory := ports.OutputHeads{}
33 for i, size := range o.sizes {
34 name := fmt.Sprintf("HEADLESS-%d", i+1)
35 mode := ports.OutputMode{Width: size[0], Height: size[1], RefreshMilli: 60000, Preferred: true}
36 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})
37 dir := o.shots
38 if dir != "" && len(o.sizes) > 1 {
39 dir = filepath.Join(o.shots, name)
40 if err := os.MkdirAll(dir, 0o755); err != nil {
41 // Outputs already started stop and release their captures.
42 return errors.Join(err, set.wait())
43 }
44 }
45 cur := &headless.Cursor{}
46 curs.set(name, cur)
47 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, Frames: ch.frames, Captured: ch.captured}
48 started := set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan *ports.ContentBatch, rc ports.ContentReturn, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest, secure <-chan ports.SecurityState, instance ports.OutputInstance) error {
49 opts.Security, opts.SecurityChanges, opts.Instance = ch.security, secure, instance
50 return headless.Run(octx, opts, sc, cc, rc, cu, cap)
51 }, &opts.SecurityEvents)
52 if !started {
53 return set.wait()
54 }
55 select {
56 case ch.events <- ports.OutputAdded{Info: ports.OutputInfo{Name: name, Width: size[0], Height: size[1]}}:
57 case <-ctx.Done():
58 return set.wait()
59 }
60 }
61 // All virtual lifetimes have registered before this no-lease barrier.
62 if set.state.Protected {
63 set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
64 }
65 apply.heads(inventory)
66 // The pointer starts centred on the first output, like libinput's.
67 curs.move("HEADLESS-1", float64(o.sizes[0][0])/2, float64(o.sizes[0][1])/2, false)
68 for {
69 configs, configNext := apply.configOut(ch.configured), apply.config
70 replies, replyNext := apply.replyOut(ch.replies)
71 heads, headsNext := apply.headsOut(ch.heads)
72 select {
73 case <-ctx.Done():
74 return set.wait()
75 case state, ok := <-ch.securityChanges:
76 if !ok {
77 ch.securityChanges = nil
78 continue
79 }
80 if set.setSecurity(state) && set.state.Protected {
81 set.securityEvent(ports.SecurityBackendBarrier{Generation: set.state.Generation})
82 }
83 case ev := <-ch.reloads:
84 apply.reload(ev.Config)
85 case req := <-ch.requests:
86 apply.request(req)
87 case configs <- configNext:
88 apply.configSent()
89 case replies <- replyNext:
90 apply.replySent()
91 case heads <- headsNext:
92 apply.headsSent()
93 case s := <-ch.scenes:
94 set.scenes(s)
95 case q := <-ch.captures:
96 set.routeCapture(q)
97 case c := <-ch.cursorChanges:
98 set.setCursor(c)
99 case stopped := <-set.stopped:
100 if !set.stoppedCurrent(stopped) {
101 continue
102 }
103 // An owner error is not protection or removal evidence.
104 err := set.finish(stopped.name)
105 return errors.Join(err, set.wait())
106 }
107 }
108}