1package visual
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/bnema/neferclient"
10 "github.com/bnema/nefergui"
11)
12
13// screen is one Wayland surface and the NeferGUI renderer that draws it. It
14// copies plain fields between the two libraries, which never import each
15// other. Everything runs on the owner goroutine that calls Conn.Dispatch.
16type screen struct {
17 conn *neferclient.Conn
18 surf *neferclient.Surface
19 output uint32 // wl_output registry name
20 name string // connector name, for error reports
21 transparent bool // ARGB buffers, presented as non-opaque
22 view func(*nefergui.Frame)
23
24 r *nefergui.Renderer
25 fb *neferclient.Feedback // feedback r was built from
26 out nefergui.Output
27 damage []neferclient.Rect
28 live map[uint64]int // buffer → release eventfd still watched
29
30 configured, canPresent, haveAcquire bool
31 built bool // a renderer was built once: later setups are rebuilds
32}
33
34func newScreen(conn *neferclient.Conn, surf *neferclient.Surface, o neferclient.Output, transparent bool) *screen {
35 return &screen{conn: conn, surf: surf, output: o.Global, name: o.Name, transparent: transparent, live: map[uint64]int{}}
36}
37
38// renderView adapts the per-screen view to Renderer.Render without a
39// per-frame closure.
40func renderView(f *nefergui.Frame, s *screen) { s.view(f) }
41
42// configure applies Handler.Configure. Only the first one lifts the
43// frame-callback gate; later ones must wait for Handler.Frame.
44func (s *screen) configure() error {
45 if !s.configured {
46 s.canPresent = true
47 }
48 s.configured = true
49 s.resize()
50 return s.setup()
51}
52
53// setup builds the renderer once the surface is configured and its dmabuf
54// feedback is complete, and rebuilds it when the compositor changes its
55// preferred device or formats (a repeated round is not a change).
56func (s *screen) setup() error {
57 fb := s.surf.Feedback()
58 if !s.configured || fb == nil || s.r != nil && fb.Equal(s.fb) {
59 return nil
60 }
61 if err := s.dropRenderer(); err != nil {
62 return err
63 }
64 cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Formats: make([]nefergui.Format, len(fb.Formats)), Transparent: s.transparent}
65 for i, f := range fb.Formats {
66 cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
67 }
68 r, err := nefergui.NewRenderer(cfg)
69 if err != nil {
70 return fmt.Errorf("renderer: %w", err)
71 }
72 s.r, s.fb, s.built = r, fb.Clone(), true // Feedback storage is reused by later rounds
73 s.resize()
74 return nil
75}
76
77func (s *screen) resize() {
78 if s.r != nil {
79 w, h, scale := s.surf.Size()
80 s.r.Resize(int(w), int(h), scale)
81 }
82}
83
84func (s *screen) invalidate() {
85 if s.r != nil {
86 s.r.Invalidate()
87 }
88}
89
90// draw renders when something changed and presents the frame. It reports
91// whether a built frame still waits for the GPU.
92func (s *screen) draw() (pending bool, err error) {
93 if s.r == nil {
94 return false, nil
95 }
96 if s.canPresent {
97 ok, err := s.r.Render(&s.out, s, renderView)
98 if err != nil {
99 return false, fmt.Errorf("render: %w", err)
100 }
101 if ok {
102 if err := s.present(); err != nil {
103 return false, err
104 }
105 }
106 }
107 return s.r.Pending(), nil
108}
109
110// watchID packs the surface and a buffer into the Conn.WatchFD id.
111func (s *screen) watchID(buffer uint64) uint64 { return uint64(s.surf.ID())<<32 | buffer }
112
113// present: Renderer output → ImportBuffer, ImportTimeline, Present. The input
114// region is not forwarded: it is fixed when the surface is created.
115func (s *screen) present() error {
116 out := &s.out
117 for _, rt := range out.Retired { // buffers of an older size
118 if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil {
119 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
120 }
121 delete(s.live, rt.Buffer)
122 if err := s.surf.DestroyBuffer(rt.Buffer); err != nil {
123 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
124 }
125 // The release timeline of a buffer is imported under the buffer's id.
126 if err := s.surf.DestroyTimeline(rt.Buffer); err != nil {
127 return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
128 }
129 }
130 if out.NewBuffer {
131 buf := neferclient.Buffer{Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier, PlaneCount: out.PlaneCount}
132 for i, p := range out.Planes {
133 buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
134 }
135 if err := s.surf.ImportBuffer(out.Buffer, &buf); err != nil {
136 return fmt.Errorf("import buffer: %w", err)
137 }
138 // The release eventfd is stable per buffer: watched once, FDReady
139 // hands the buffer back with Renderer.Released.
140 if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil {
141 return fmt.Errorf("watch release fd: %w", err)
142 }
143 s.live[out.Buffer] = out.ReleaseFD
144 }
145 if out.NewTimelines {
146 if !s.haveAcquire { // one acquire timeline shared by every buffer
147 if err := s.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
148 return fmt.Errorf("import acquire timeline: %w", err)
149 }
150 s.haveAcquire = true
151 }
152 if err := s.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
153 return fmt.Errorf("import release timeline: %w", err)
154 }
155 }
156 s.damage = s.damage[:0]
157 for _, d := range out.Damage {
158 s.damage = append(s.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
159 }
160 if err := s.surf.Present(&neferclient.Present{
161 Buffer: out.Buffer, AcquireTimeline: out.Acquire.ID, ReleaseTimeline: out.Release.ID,
162 AcquirePoint: out.AcquirePoint, ReleasePoint: out.ReleasePoint, Damage: s.damage,
163 Opaque: !s.transparent,
164 }); err != nil {
165 return fmt.Errorf("present: %w", err)
166 }
167 s.canPresent = false // until Handler.Frame
168 return nil
169}
170
171// released hands a buffer back after its release eventfd became readable.
172func (s *screen) released(buffer uint64) error {
173 if s.r == nil {
174 return nil
175 }
176 if err := s.r.Released(buffer); err != nil {
177 return fmt.Errorf("release buffer: %w", err)
178 }
179 return nil
180}
181
182// dropRenderer stops watching the release eventfds (they belong to the
183// renderer), destroys the imported buffers and timelines (allowed while a
184// frame is pending), then closes the renderer.
185func (s *screen) dropRenderer() error {
186 if s.r == nil {
187 return nil
188 }
189 var errs []error
190 for buffer, fd := range s.live {
191 if err := s.conn.UnwatchFD(fd); err != nil {
192 errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err))
193 }
194 }
195 clear(s.live)
196 errs = append(errs, s.surf.DestroyImports(), s.r.Close())
197 s.r, s.fb, s.haveAcquire = nil, nil, false
198 return errors.Join(errs...)
199}
200
201// close frees the renderer and the surface.
202func (s *screen) close() error {
203 return errors.Join(s.dropRenderer(), s.surf.Close())
204}
205
206// closeAfterConn frees what is left once the connection is closed: the
207// connection dropped the descriptor watches and every Wayland object, so only
208// the renderer's GPU resources remain.
209func (s *screen) closeAfterConn() error {
210 if s.r == nil {
211 return nil
212 }
213 err := s.r.Close()
214 s.r = nil
215 return err
216}
217
218// screens holds the surfaces of one connection and implements the Handler
219// methods they share. Failures from handler methods are kept in err; the
220// owner loop returns it after Dispatch.
221type screens struct {
222 neferclient.NopHandler
223
224 conn *neferclient.Conn
225 byID map[neferclient.SurfaceID]*screen
226 err error
227 // onSetup reports a screen whose renderer could not be built (rebuild:
228 // after a feedback change); it returns the error that ends the run, or
229 // nil to drop only that screen.
230 onSetup func(s *screen, rebuild bool, err error) error
231 // onError reports an event error (Handler.Error); it returns the error
232 // that ends the run, or nil to go on.
233 onError func(err error) error
234}
235
236func newScreens(conn *neferclient.Conn) *screens {
237 ss := &screens{conn: conn, byID: map[neferclient.SurfaceID]*screen{}}
238 ss.onSetup = func(_ *screen, _ bool, err error) error { return err }
239 ss.onError = func(err error) error { return err }
240 return ss
241}
242
243func (ss *screens) fail(err error) {
244 if ss.err == nil {
245 ss.err = err
246 }
247}
248
249func (ss *screens) add(s *screen) { ss.byID[s.surf.ID()] = s }
250
251// forget drops a screen and frees it.
252func (ss *screens) forget(s *screen) error {
253 delete(ss.byID, s.surf.ID())
254 return s.close()
255}
256
257// Error applies onError to a failure while handling an event. Connection
258// failures are not reported here: Conn.Dispatch returns them and they end
259// the run.
260func (ss *screens) Error(err error) {
261 if err = ss.onError(err); err != nil {
262 ss.fail(err)
263 }
264}
265
266// keyboardError reports whether a neferclient event error leaves the seat
267// without a usable keyboard (seat, keyboard or keymap setup). neferclient has
268// no typed errors for these, so its messages are matched.
269func keyboardError(err error) bool {
270 msg := err.Error()
271 for _, p := range [...]string{"neferclient: keymap", "neferclient: unsupported keymap", "neferclient: keyboard:", "neferclient: bind wl_seat"} {
272 if strings.HasPrefix(msg, p) {
273 return true
274 }
275 }
276 return false
277}
278
279func (ss *screens) setupFailed(s *screen, err error) {
280 if err = ss.onSetup(s, s.built, err); err != nil {
281 ss.fail(err)
282 return
283 }
284 if err := ss.forget(s); err != nil {
285 ss.fail(err)
286 }
287}
288
289func (ss *screens) Configure(id neferclient.SurfaceID, _, _ int32) {
290 if s := ss.byID[id]; s != nil {
291 if err := s.configure(); err != nil {
292 ss.setupFailed(s, err)
293 }
294 }
295}
296
297func (ss *screens) FeedbackDone(id neferclient.SurfaceID) {
298 if s := ss.byID[id]; s != nil {
299 if err := s.setup(); err != nil {
300 ss.setupFailed(s, err)
301 }
302 }
303}
304
305func (ss *screens) Scale(id neferclient.SurfaceID, _ float64) {
306 if s := ss.byID[id]; s != nil {
307 s.resize()
308 }
309}
310
311func (ss *screens) Frame(id neferclient.SurfaceID) {
312 if s := ss.byID[id]; s != nil {
313 s.canPresent = true
314 }
315}
316
317func (ss *screens) FDReady(id uint64) {
318 if s := ss.byID[neferclient.SurfaceID(id>>32)]; s != nil {
319 if err := s.released(id & 0xffffffff); err != nil {
320 ss.fail(err)
321 }
322 }
323}
324
325// OutputRemoved frees the screens of an output that went away.
326func (ss *screens) OutputRemoved(global uint32) {
327 for _, s := range ss.byID {
328 if s.output == global {
329 if err := ss.forget(s); err != nil {
330 ss.fail(fmt.Errorf("output %s removed: %w", s.name, err))
331 }
332 }
333 }
334}
335
336// covers reports whether an output already has a screen.
337func (ss *screens) covers(global uint32) bool {
338 for _, s := range ss.byID {
339 if s.output == global {
340 return true
341 }
342 }
343 return false
344}
345
346func (ss *screens) invalidateAll() {
347 for _, s := range ss.byID {
348 s.invalidate()
349 }
350}
351
352// drawAll draws every screen and reports whether one waits for the GPU.
353func (ss *screens) drawAll() (pending bool, err error) {
354 for _, s := range ss.byID {
355 p, err := s.draw()
356 if err != nil {
357 return false, fmt.Errorf("output %s: %w", s.name, err)
358 }
359 pending = pending || p
360 }
361 return pending, nil
362}
363
364// closeAll frees every screen after the connection was closed.
365func (ss *screens) closeAll() error {
366 var errs []error
367 for id, s := range ss.byID {
368 delete(ss.byID, id)
369 errs = append(errs, s.closeAfterConn())
370 }
371 return errors.Join(errs...)
372}
373
374// gpuRetry is how soon to render again while a frame waits for the GPU; no
375// event announces that the GPU is done.
376const gpuRetry = 2 * time.Millisecond