1package wayland
2
3import (
4 "slices"
5 "sync/atomic"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// batchesPerLink is how many batches circulate between wayland and one
11// output: one wayland fills, one waits in the channel, one the output reads.
12const batchesPerLink = 3
13
14// batchItems is the initial capacity of a batch: windows beyond it grow the
15// batch once, and it keeps the capacity.
16const batchItems = 8
17
18// contentLink is the content path to one running output. The display
19// goroutine owns cur and the sending side of in; the output owns the batch it
20// received until it returns it. free holds the batches nobody uses: it has
21// room for all of them, so Return never blocks.
22//
23// The link is also the ports.ContentReturn handed to the output, and the
24// handle DetachOutput takes: a detach removes this link and no other, even
25// when a later attach of the same name replaced it.
26type contentLink struct {
27 s *Server
28 name string
29 in chan *ports.ContentBatch
30 free chan *ports.ContentBatch
31 cur *ports.ContentBatch
32 // pending: cur holds contents that could not be sent yet (the channel is
33 // full, or every batch is out). Return only wakes the display when it is
34 // set, so a steady stream of frames costs the display no extra wake.
35 pending atomic.Bool
36}
37
38// Return gives a consumed (reset) batch back and, when contents wait for one,
39// has the display goroutine send them. Safe from any goroutine, also after
40// the link was detached: the batch only goes back to its own link.
41func (l *contentLink) Return(b *ports.ContentBatch) {
42 l.free <- b // never full: the link owns batchesPerLink batches
43 // The display stores pending before it looks at free once more, so
44 // either it sees this batch or this load sees pending.
45 if l.pending.Load() && l.s.flushPosted.CompareAndSwap(false, true) {
46 l.s.display.Post(l.s.flushLinksFn)
47 }
48}
49
50// add puts c in the batch being filled, replacing the window's earlier
51// content: the output only needs the newest one (see ports.ContentBatch).
52func (l *contentLink) add(c ports.SurfaceContent) {
53 items := l.cur.Items
54 for i := range items {
55 if items[i].ID == c.ID {
56 items[i] = c
57 return
58 }
59 }
60 l.cur.Items = append(items, c)
61}
62
63// try sends the batch being filled when the channel is empty and a batch is
64// free to fill next. Display goroutine only.
65func (l *contentLink) try() bool {
66 if len(l.in) != 0 {
67 return false
68 }
69 select {
70 case next := <-l.free:
71 l.in <- l.cur // empty, and only this goroutine sends
72 l.cur = next
73 return true
74 default:
75 return false
76 }
77}
78
79// flushLink sends l's batch when the output can take it; otherwise l keeps
80// filling it and asks Return for a wake.
81func (s *Server) flushLink(l *contentLink) {
82 if len(l.cur.Items) == 0 {
83 l.pending.Store(false)
84 return
85 }
86 if l.try() {
87 l.pending.Store(false)
88 return
89 }
90 l.pending.Store(true)
91 if l.try() { // a Return may have slipped in before pending was set
92 l.pending.Store(false)
93 }
94}
95
96// flushLinks retries every link that waits for a free batch. Display
97// goroutine; posted by Return.
98func (s *Server) flushLinks() {
99 s.flushPosted.Store(false)
100 for _, l := range s.links {
101 if l.pending.Load() {
102 s.flushLink(l)
103 }
104 }
105}
106
107// attachOutput starts feeding the output called name; a link of the same name
108// is replaced (dropped from the feed: its handle then detaches nothing). The
109// returned ContentReturn is the link, and the handle for detachOutput. The
110// first batch holds the latest content of every live window,
111// so the output starts with all of them drawn. Display goroutine, or before
112// Run.
113func (s *Server) attachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) {
114 for i, old := range s.links {
115 if old.name == name {
116 s.links = slices.Delete(s.links, i, i+1)
117 break
118 }
119 }
120 l := &contentLink{
121 s: s,
122 name: name,
123 in: make(chan *ports.ContentBatch, 1),
124 free: make(chan *ports.ContentBatch, batchesPerLink),
125 }
126 for range batchesPerLink - 1 {
127 l.free <- &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, batchItems)}
128 }
129 l.cur = &ports.ContentBatch{Items: make([]ports.SurfaceContent, 0, max(batchItems, len(s.latest)))}
130 for _, c := range s.latest {
131 l.add(c)
132 }
133 s.links = append(s.links, l)
134 s.flushLink(l)
135 return l.in, l
136}
137
138// detachOutput stops feeding the link ret, the handle attachOutput returned.
139// A handle whose link was replaced by a later attach of its name, or that is
140// not a link, detaches nothing: a newer link is never removed. Display
141// goroutine, or before Run. The output may still Return its last batch.
142func (s *Server) detachOutput(ret ports.ContentReturn) {
143 l, ok := ret.(*contentLink)
144 if !ok {
145 return
146 }
147 if i := slices.Index(s.links, l); i >= 0 {
148 s.links = slices.Delete(s.links, i, i+1)
149 }
150}
151
152// AttachOutput starts feeding the output called name with the contents of
153// the windows, as batches on the returned channel, starting with the current
154// content of every window. The output hands each batch back, reset, to the
155// returned ContentReturn, which is also the handle for DetachOutput.
156// Attaching a name that already has a feed replaces that feed. Call it from
157// outside the display goroutine. It returns a nil channel and handle when
158// the server is stopped.
159func (s *Server) AttachOutput(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) {
160 var in <-chan *ports.ContentBatch
161 var ret ports.ContentReturn
162 if !s.display.Do(func() { in, ret = s.attachOutput(name) }) {
163 return nil, nil
164 }
165 return in, ret
166}
167
168// DetachOutput stops the feed ret, returned by AttachOutput. Only that feed
169// stops: if a later AttachOutput of the same name replaced it, the newer
170// feed keeps running. Call it from outside the display goroutine, once the
171// output no longer reads its channel. A nil ret does nothing.
172func (s *Server) DetachOutput(ret ports.ContentReturn) {
173 if ret == nil {
174 return
175 }
176 s.display.Do(func() { s.detachOutput(ret) })
177}