1//go:build linux
2
3package session
4
5import (
6 "errors"
7 "fmt"
8 "image"
9 "path/filepath"
10
11 "github.com/bnema/purego-vulkan/vulkan"
12
13 "github.com/bnema/nefergui/internal/layout"
14 "github.com/bnema/nefergui/internal/presentation/buffers"
15 "github.com/bnema/nefergui/internal/presentation/syncobj"
16 "github.com/bnema/nefergui/internal/presentation/vkdevice"
17 "github.com/bnema/nefergui/internal/render"
18 "golang.org/x/sys/unix"
19)
20
21// Plane is one DMA-BUF plane. FD stays owned by the Target.
22type Plane struct {
23 FD int
24 Offset uint32
25 Stride uint32
26}
27
28// Timeline is a DRM syncobj timeline the transport must import. FD stays owned
29// by the Target and is valid only when the Output says the timeline is new.
30type Timeline struct {
31 ID uint64
32 FD int
33}
34
35// Retired names a buffer the transport must stop watching and destroy before
36// the next Draw. ReleaseFD closes at that Draw.
37type Retired struct {
38 Buffer uint64
39 ReleaseFD int
40}
41
42// Output describes one presented frame in physical pixels. Slices are reused;
43// they are valid until the next Draw.
44type Output struct {
45 Buffer uint64
46 NewBuffer bool // import Planes as a new wl_buffer
47 Retired []Retired
48 Width int32
49 Height int32
50 FourCC uint32
51 Modifier uint64
52 Planes [4]Plane
53 PlaneCount int
54 Acquire Timeline // ID 0; FD always valid (persistent)
55 Release Timeline // ID is the buffer; FD valid only with NewTimelines
56 NewTimelines bool
57 AcquirePoint uint64
58 ReleasePoint uint64
59 ReleaseFD int // eventfd readable when ReleasePoint signals; stable per buffer
60}
61
62// TargetConfig selects the GPU and the compositor-acceptable formats.
63type TargetConfig struct {
64 MainDevice uint64
65 Formats []vkdevice.Format
66 Transparent bool
67}
68
69// Target is the Wayland-free presentation core: it renders prepared frames
70// into DMA-BUF images, tracks buffer ownership with explicit-sync timelines
71// and reports release through per-buffer eventfds. It starts no goroutines and
72// is used from one owner goroutine.
73type Target struct {
74 Device *vkdevice.Device
75 Node *syncobj.Node
76 Pool *buffers.Pool
77 Preparer *render.Preparer
78 Slots map[uint64]*Slot
79
80 list render.ListBuffer
81 pipeline *vkdevice.Pipeline
82 pipelineGen uint64 // pool generation the current pipeline belongs to
83 retiredPipelines []retiredPipelines
84 genPending bool // Pool.Resize done, images of the new generation not yet built
85 waiting bool // the last Draw found work that only GPU progress unblocks
86 broken error // sticky: a failure after Submit left the pipeline inconsistent
87 acquire uint32
88 acquireFD int
89 formats []vkdevice.Format
90 transparent bool
91 selected vkdevice.Modifier
92 wantW, wantH int32
93 width, height int32
94 pending []Retired // retired since the last Draw output
95 reported []Retired // given to the caller; fds close at the next Draw
96 capture bool
97 closed bool
98}
99
100// NewTarget opens the GPU and the render node. It allocates no images.
101func NewTarget(cfg TargetConfig) (_ *Target, err error) {
102 t := &Target{Slots: make(map[uint64]*Slot), acquireFD: -1, formats: append([]vkdevice.Format(nil), cfg.Formats...), transparent: cfg.Transparent}
103 defer func() {
104 if err != nil {
105 err = errors.Join(err, t.Close())
106 }
107 }()
108 if t.Preparer, err = render.NewPreparer(vkdevice.AtlasSize, vkdevice.AtlasPages); err != nil {
109 return nil, err
110 }
111 if t.Device, err = vkdevice.Open(vkdevice.DeviceIdentity(cfg.MainDevice)); err != nil {
112 return nil, err
113 }
114 if t.Node, err = syncobj.Open(filepath.Join("/dev/dri", fmt.Sprintf("renderD%d", t.Device.Render.Minor))); err != nil {
115 return nil, err
116 }
117 if t.acquire, err = t.Node.Create(); err != nil {
118 return nil, err
119 }
120 if t.acquireFD, err = t.Node.ExportTimeline(t.acquire); err != nil {
121 return nil, err
122 }
123 if t.Pool, err = buffers.New(3); err != nil {
124 return nil, err
125 }
126 // The pool starts with generation 1 buffers but no images yet; the first
127 // Draw allocates them for the requested size.
128 return t, nil
129}
130
131// EnableReadback makes later images capture their output for Readback. Tests only.
132func (t *Target) EnableReadback() { t.capture = true }
133
134// Resize sets the physical surface size; images follow lazily at the next Draw.
135func (t *Target) Resize(width, height int32) { t.wantW, t.wantH = width, height }
136
137// Size reports the physical size of the current image generation.
138func (t *Target) Size() (int32, int32) { return t.width, t.height }
139
140// newGeneration builds the pipeline and every image of the current pool
141// generation into locals and commits them only when all succeeded, so a
142// failure leaves the previous size, pipeline and slots intact.
143func (t *Target) newGeneration() (err error) {
144 mods, err := t.Device.ExportableModifiers()
145 if err != nil {
146 return err
147 }
148 selected, err := vkdevice.ChooseModifier(t.formats, t.transparent, mods)
149 if err != nil {
150 return err
151 }
152 pipeline, err := t.Device.NewListPipeline(t.wantW, t.wantH)
153 if err != nil {
154 return err
155 }
156 fresh := make(map[uint64]*Slot)
157 defer func() {
158 if err != nil {
159 for _, slot := range fresh {
160 _ = t.closeSlot(slot)
161 }
162 pipeline.Close()
163 }
164 }()
165 for _, b := range t.Pool.Buffers {
166 if b.Generation != t.Pool.Generation {
167 continue
168 }
169 slot, err := t.newSlot(b, t.wantW, t.wantH, selected)
170 if err != nil {
171 return err
172 }
173 fresh[b.ID] = slot
174 }
175 if t.pipeline != nil {
176 t.retiredPipelines = append(t.retiredPipelines, retiredPipelines{generation: t.pipelineGen, list: t.pipeline})
177 }
178 t.pipeline, t.pipelineGen, t.selected = pipeline, t.Pool.Generation, selected
179 t.width, t.height = t.wantW, t.wantH
180 for id, slot := range fresh {
181 t.Slots[id] = slot
182 }
183 return nil
184}
185
186func (t *Target) newSlot(b *buffers.Buffer, width, height int32, selected vkdevice.Modifier) (_ *Slot, err error) {
187 slot := &Slot{Buffer: b, First: true, ReleaseFD: -1, ReleaseExport: -1}
188 defer func() {
189 if err != nil {
190 _ = t.closeSlot(slot)
191 }
192 }()
193 if slot.Image, err = t.Device.AllocateImage(width, height, selected); err != nil {
194 return nil, err
195 }
196 if slot.Frame, err = t.Device.NewFrame(slot.Image); err != nil {
197 return nil, err
198 }
199 if t.capture {
200 if slot.Frame.Readback, err = t.Device.NewReadback(width, height); err != nil {
201 return nil, err
202 }
203 }
204 if slot.ReleaseHandle, err = t.Node.Create(); err != nil {
205 return nil, err
206 }
207 if slot.ReleaseExport, err = t.Node.ExportTimeline(slot.ReleaseHandle); err != nil {
208 return nil, err
209 }
210 if slot.ReleaseFD, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
211 return nil, err
212 }
213 return slot, nil
214}
215
216// closeSlot frees a slot whose GPU work has completed (or never started).
217func (t *Target) closeSlot(slot *Slot) error {
218 if slot.Frame != nil {
219 slot.Frame.Close()
220 }
221 if slot.PendingWait != nil {
222 slot.PendingWait.Close()
223 }
224 if slot.Wait != nil {
225 slot.Wait.Close()
226 }
227 if slot.Image != nil {
228 slot.Image.Close()
229 }
230 var err error
231 if slot.ReleaseHandle != 0 {
232 err = t.Node.Destroy(slot.ReleaseHandle)
233 }
234 closeFD(&slot.ReleaseExport)
235 return err
236}
237
238func closeFD(fd *int) {
239 if *fd >= 0 {
240 _ = unix.Close(*fd)
241 *fd = -1
242 }
243}
244
245// Draw prepares and presents one frame. It returns false, touching no GPU
246// state, when no image is ready; the caller retries after Released, or after a
247// short delay while Waiting reports GPU progress is needed.
248func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {
249 if t.broken != nil {
250 return false, t.broken
251 }
252 if t.closed {
253 return false, errors.New("session: target closed")
254 }
255 if t.wantW <= 0 || t.wantH <= 0 {
256 return false, nil
257 }
258 for _, r := range t.reported {
259 _ = unix.Close(r.ReleaseFD)
260 }
261 t.reported = t.reported[:0]
262 if t.genPending || t.width != t.wantW || t.height != t.wantH {
263 if !t.genPending && t.width != 0 {
264 if err := t.Pool.Resize(3); err != nil {
265 return false, err
266 }
267 }
268 t.genPending = true
269 if err := t.newGeneration(); err != nil {
270 return false, err
271 }
272 t.genPending = false
273 }
274 if err := t.retire(); err != nil {
275 return false, err
276 }
277 pending, err := t.finishPending()
278 if err != nil {
279 return false, err
280 }
281 b, slot, waiting, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
282 if err != nil {
283 return false, err
284 }
285 t.waiting = pending || waiting
286 if b == nil {
287 return false, nil
288 }
289 ok, err := t.submit(b, slot, display, scale, out)
290 if err != nil {
291 // Before Submit nothing reached the GPU and the buffer is reusable.
292 if t.broken == nil && b.State == buffers.Rendering {
293 b.State = buffers.Available
294 }
295 return false, err
296 }
297 t.waiting = false
298 return ok, nil
299}
300
301// Waiting reports that the last Draw found a frame blocked only on GPU
302// progress (an image still being drawn, or a release wait not yet installed),
303// which no event announces: the caller should retry after a short delay. It is
304// false when every image is owned by the compositor; Released unblocks that.
305func (t *Target) Waiting() bool { return t.waiting }
306
307func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {
308 frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height))
309 if err != nil {
310 return false, err
311 }
312 defer t.list.Release()
313 instances, batches, stats := t.list.List(frame)
314 if len(stats.Skipped) != 0 {
315 return false, fmt.Errorf("unsupported list operations: %v", stats.Skipped)
316 }
317 if slot.Wait != nil && slot.WaitInFlight {
318 slot.Wait.Close()
319 slot.Wait = nil
320 slot.WaitInFlight = false
321 }
322 if err = slot.Frame.RecordList(slot.Image, t.pipeline, t.width, t.height, instances, frame.Uploads, batches, slot.First, t.capture); err != nil {
323 return false, err
324 }
325 if err = slot.Frame.Submit(slot.Wait); err != nil {
326 return false, err
327 }
328 t.Preparer.Submitted(frame.Uploads)
329 if slot.Wait != nil {
330 slot.WaitInFlight = true
331 }
332 slot.First = false
333 // From here the buffer is committed to the pipeline; failures are fatal.
334 fd, err := slot.Frame.Signal.Export()
335 if err != nil {
336 return false, t.fail(err)
337 }
338 acquire, err := t.Pool.Acquire(b)
339 if err != nil {
340 _ = unix.Close(fd)
341 return false, t.fail(err)
342 }
343 if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {
344 _ = unix.Close(fd)
345 return false, t.fail(err)
346 }
347 release, err := t.Pool.Commit(b)
348 if err != nil {
349 return false, t.fail(err)
350 }
351 if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {
352 return false, t.fail(err)
353 }
354 if err = t.Pool.Own(b); err != nil {
355 return false, t.fail(err)
356 }
357 t.fill(out, slot, acquire, release)
358 return true, nil
359}
360
361// fail records an error after Submit: the buffer cannot go back to Available
362// and every later Draw returns this error.
363func (t *Target) fail(err error) error {
364 t.broken = fmt.Errorf("session: presentation pipeline broken: %w", err)
365 return t.broken
366}
367
368func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
369 b := slot.Buffer
370 retired := append(out.Retired[:0], t.pending...)
371 t.reported = append(t.reported[:0], t.pending...)
372 t.pending = t.pending[:0]
373 *out = Output{
374 Buffer: b.ID, NewBuffer: !slot.Presented, Retired: retired,
375 Width: t.width, Height: t.height, FourCC: t.selected.DRMFormat, Modifier: slot.Image.Modifier,
376 PlaneCount: 1, Acquire: Timeline{ID: 0, FD: t.acquireFD}, Release: Timeline{ID: b.ID, FD: -1},
377 NewTimelines: !slot.Presented, AcquirePoint: acquire, ReleasePoint: release, ReleaseFD: slot.ReleaseFD,
378 }
379 out.Planes[0] = Plane{FD: slot.Image.FD, Offset: slot.Image.Offset, Stride: slot.Image.Stride}
380 if !slot.Presented {
381 out.Release.FD = slot.ReleaseExport
382 slot.Presented = true
383 }
384}
385
386// Released handles a readable release eventfd of the buffer: the compositor
387// signaled its latest release point, so the image may be reused once the GPU
388// wait is installed.
389func (t *Target) Released(id uint64) error {
390 slot := t.Slots[id]
391 if slot == nil {
392 return nil // retired: its descriptors are gone
393 }
394 // Drain first: a notification that lands after the drain is seen by the
395 // Signaled check below or leaves the eventfd readable for a later no-op call.
396 if err := drainEventFD(slot.ReleaseFD); err != nil {
397 return err
398 }
399 b := slot.Buffer
400 if b.State != buffers.CompositorOwned {
401 return nil
402 }
403 signaled, err := t.Node.Signaled(slot.ReleaseHandle, b.ReleasePoint)
404 if err != nil {
405 return err
406 }
407 if !signaled {
408 return nil // stale notification; the registration for the live point is still armed
409 }
410 return t.release(slot, b.ReleasePoint)
411}
412
413// drainEventFD resets a non-blocking eventfd; an empty one is fine.
414func drainEventFD(fd int) error {
415 var counter [8]byte
416 if _, err := unix.Read(fd, counter[:]); err != nil && err != unix.EAGAIN {
417 return fmt.Errorf("session: read release eventfd: %w", err)
418 }
419 return nil
420}
421
422func (t *Target) release(slot *Slot, point uint64) error {
423 b := slot.Buffer
424 // An older completion cannot authorize a newer commit of the same image.
425 if b.ReleasePoint != point || b.State != buffers.CompositorOwned {
426 return nil
427 }
428 if err := t.Pool.Signal(b, point); err != nil {
429 return err
430 }
431 if b.Generation == t.Pool.Generation {
432 fd, err := t.Node.ReleaseFence(slot.ReleaseHandle, point)
433 if err != nil {
434 return err
435 }
436 wait, err := t.Device.NewBinaryFence(false)
437 if err != nil {
438 _ = unix.Close(fd)
439 return err
440 }
441 if err = wait.ImportTemporary(fd); err != nil {
442 _ = unix.Close(fd)
443 wait.Close()
444 return err
445 }
446 installed, err := installReleaseWait(slot, wait, slot.Frame.Ready)
447 if err != nil {
448 wait.Close()
449 return err
450 }
451 if !installed {
452 return nil
453 }
454 if err = t.Pool.Reuse(b, true); err != nil {
455 return err
456 }
457 }
458 return t.retire()
459}
460
461// finishPending installs release waits deferred because the old submission was
462// still running. It never waits on the GPU.
463func (t *Target) finishPending() (bool, error) {
464 pending := false
465 for _, slot := range t.Slots {
466 // Old generations never return to Available; retire frees them.
467 if slot.PendingWait == nil || slot.Buffer.Generation != t.Pool.Generation {
468 continue
469 }
470 ready, err := slot.Frame.Ready()
471 if err != nil {
472 return false, err
473 }
474 if !ready {
475 pending = true
476 continue
477 }
478 if slot.Wait != nil {
479 slot.Wait.Close()
480 }
481 slot.Wait, slot.PendingWait, slot.WaitInFlight = slot.PendingWait, nil, false
482 if err := t.Pool.Reuse(slot.Buffer, true); err != nil {
483 return false, err
484 }
485 }
486 return pending, nil
487}
488
489// retire frees images of old generations once nothing uses them, and queues
490// the ones the transport imported for the next Output.
491func (t *Target) retire() error {
492 for id, slot := range t.Slots {
493 b := slot.Buffer
494 if !t.Pool.Retirable(b) {
495 continue
496 }
497 ready, err := slot.Frame.Ready()
498 if err != nil {
499 return err
500 }
501 if !ready {
502 continue
503 }
504 eventFD := slot.ReleaseFD
505 slot.ReleaseFD = -1
506 wasPresented := slot.Presented
507 err = t.closeSlot(slot)
508 if wasPresented {
509 t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})
510 } else if eventFD >= 0 {
511 _ = unix.Close(eventFD)
512 }
513 delete(t.Slots, id)
514 if rerr := t.Pool.Remove(b); rerr != nil {
515 return errors.Join(err, rerr)
516 }
517 if err != nil {
518 return err
519 }
520 }
521 for i := 0; i < len(t.retiredPipelines); {
522 old := t.retiredPipelines[i]
523 live := false
524 for _, slot := range t.Slots {
525 if slot.Buffer.Generation == old.generation {
526 live = true
527 break
528 }
529 }
530 if live {
531 i++
532 continue
533 }
534 old.list.Close()
535 t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)
536 }
537 return nil
538}
539
540// Readback returns the last drawn pixels of a buffer in RGBA order. Tests only;
541// call EnableReadback before the first Draw.
542func (t *Target) Readback(id uint64) (*image.NRGBA, error) {
543 slot := t.Slots[id]
544 if slot == nil || slot.Frame.Readback == nil {
545 return nil, errors.New("session: no readback for buffer")
546 }
547 if err := slot.Frame.Wait(); err != nil {
548 return nil, err
549 }
550 img, err := slot.Frame.Readback.Copy()
551 if err != nil {
552 return nil, err
553 }
554 vkdevice.SwizzleBGRA(img)
555 return img, nil
556}
557
558// Close waits for the GPU, then frees every descriptor and image, always
559// completing the teardown. The compositor's imports keep their own kernel
560// references. It returns the joined errors of the steps that failed and any
561// Vulkan validation errors recorded during the Target's life.
562func (t *Target) Close() error {
563 if t == nil || t.closed {
564 return nil
565 }
566 t.closed = true
567 var errs []error
568 if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {
569 errs = append(errs, vulkan.Check(t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)))
570 }
571 for _, r := range t.reported {
572 _ = unix.Close(r.ReleaseFD)
573 }
574 for _, r := range t.pending {
575 _ = unix.Close(r.ReleaseFD)
576 }
577 t.reported, t.pending = nil, nil
578 for id, slot := range t.Slots {
579 closeFD(&slot.ReleaseFD)
580 errs = append(errs, t.closeSlot(slot))
581 delete(t.Slots, id)
582 }
583 for _, old := range t.retiredPipelines {
584 old.list.Close()
585 }
586 t.retiredPipelines = nil
587 t.pipeline.Close()
588 closeFD(&t.acquireFD)
589 if t.Node != nil {
590 if t.acquire != 0 {
591 errs = append(errs, t.Node.Destroy(t.acquire))
592 }
593 errs = append(errs, t.Node.Close())
594 }
595 if t.Device != nil {
596 // Read the messages before Close destroys the messenger.
597 if msgs := t.Device.Validation.Messages(); len(msgs) > 0 {
598 errs = append(errs, fmt.Errorf("%d Vulkan validation errors: %v", len(msgs), msgs))
599 }
600 }
601 t.Device.Close()
602 return errors.Join(errs...)
603}