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 slot.spare.Close()
225 if slot.Wait != nil {
226 slot.Wait.Close()
227 }
228 if slot.Image != nil {
229 slot.Image.Close()
230 }
231 var err error
232 if slot.ReleaseHandle != 0 {
233 err = t.Node.Destroy(slot.ReleaseHandle)
234 }
235 closeFD(&slot.ReleaseExport)
236 return err
237}
238
239func closeFD(fd *int) {
240 if *fd >= 0 {
241 _ = unix.Close(*fd)
242 *fd = -1
243 }
244}
245
246// Draw prepares and presents one frame. It returns false, touching no GPU
247// state, when no image is ready; the caller retries after Released, or after a
248// short delay while Waiting reports GPU progress is needed.
249func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {
250 if t.broken != nil {
251 return false, t.broken
252 }
253 if t.closed {
254 return false, errors.New("session: target closed")
255 }
256 if t.wantW <= 0 || t.wantH <= 0 {
257 return false, nil
258 }
259 for _, r := range t.reported {
260 _ = unix.Close(r.ReleaseFD)
261 }
262 t.reported = t.reported[:0]
263 if t.genPending || t.width != t.wantW || t.height != t.wantH {
264 if !t.genPending && t.width != 0 {
265 if err := t.Pool.Resize(3); err != nil {
266 return false, err
267 }
268 }
269 t.genPending = true
270 if err := t.newGeneration(); err != nil {
271 return false, err
272 }
273 t.genPending = false
274 }
275 if err := t.retire(); err != nil {
276 return false, err
277 }
278 pending, err := t.finishPending()
279 if err != nil {
280 return false, err
281 }
282 b, slot, waiting, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
283 if err != nil {
284 return false, err
285 }
286 t.waiting = pending || waiting
287 if b == nil {
288 return false, nil
289 }
290 ok, err := t.submit(b, slot, display, scale, out)
291 if err != nil {
292 // Before Submit nothing reached the GPU and the buffer is reusable.
293 if t.broken == nil && b.State == buffers.Rendering {
294 b.State = buffers.Available
295 }
296 return false, err
297 }
298 t.waiting = false
299 return ok, nil
300}
301
302// Waiting reports that the last Draw found a frame blocked only on GPU
303// progress (an image still being drawn, or a release wait not yet installed),
304// which no event announces: the caller should retry after a short delay. It is
305// false when every image is owned by the compositor; Released unblocks that.
306func (t *Target) Waiting() bool { return t.waiting }
307
308func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {
309 frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height))
310 if err != nil {
311 return false, err
312 }
313 defer t.list.Release()
314 instances, batches, stats := t.list.List(frame)
315 if len(stats.Skipped) != 0 {
316 return false, fmt.Errorf("unsupported list operations: %v", stats.Skipped)
317 }
318 if slot.Wait != nil && slot.WaitInFlight {
319 // Draw selected this slot only after its frame was Ready, so the
320 // submission that waited on Wait has completed and consumed it.
321 slot.recycle(slot.Wait)
322 slot.Wait = nil
323 slot.WaitInFlight = false
324 }
325 if err = slot.Frame.RecordList(slot.Image, t.pipeline, t.width, t.height, instances, frame.Uploads, batches, slot.First, t.capture); err != nil {
326 return false, err
327 }
328 if err = slot.Frame.Submit(slot.Wait); err != nil {
329 return false, err
330 }
331 t.Preparer.Submitted(frame.Uploads)
332 if slot.Wait != nil {
333 slot.WaitInFlight = true
334 }
335 slot.First = false
336 // From here the buffer is committed to the pipeline; failures are fatal.
337 fd, err := slot.Frame.ExportSignal()
338 if err != nil {
339 return false, t.fail(err)
340 }
341 acquire, err := t.Pool.Acquire(b)
342 if err != nil {
343 _ = unix.Close(fd)
344 return false, t.fail(err)
345 }
346 if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {
347 _ = unix.Close(fd)
348 return false, t.fail(err)
349 }
350 release, err := t.Pool.Commit(b)
351 if err != nil {
352 return false, t.fail(err)
353 }
354 if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {
355 return false, t.fail(err)
356 }
357 if err = t.Pool.Own(b); err != nil {
358 return false, t.fail(err)
359 }
360 t.fill(out, slot, acquire, release)
361 return true, nil
362}
363
364// fail records an error after Submit: the buffer cannot go back to Available
365// and every later Draw returns this error.
366func (t *Target) fail(err error) error {
367 t.broken = fmt.Errorf("session: presentation pipeline broken: %w", err)
368 return t.broken
369}
370
371func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
372 b := slot.Buffer
373 retired := append(out.Retired[:0], t.pending...)
374 t.reported = append(t.reported[:0], t.pending...)
375 t.pending = t.pending[:0]
376 *out = Output{
377 Buffer: b.ID, NewBuffer: !slot.Presented, Retired: retired,
378 Width: t.width, Height: t.height, FourCC: t.selected.DRMFormat, Modifier: slot.Image.Modifier,
379 PlaneCount: 1, Acquire: Timeline{ID: 0, FD: t.acquireFD}, Release: Timeline{ID: b.ID, FD: -1},
380 NewTimelines: !slot.Presented, AcquirePoint: acquire, ReleasePoint: release, ReleaseFD: slot.ReleaseFD,
381 }
382 out.Planes[0] = Plane{FD: slot.Image.FD, Offset: slot.Image.Offset, Stride: slot.Image.Stride}
383 if !slot.Presented {
384 out.Release.FD = slot.ReleaseExport
385 slot.Presented = true
386 }
387}
388
389// Released handles a readable release eventfd of the buffer: the compositor
390// signaled its latest release point, so the image may be reused once the GPU
391// wait is installed.
392func (t *Target) Released(id uint64) error {
393 slot := t.Slots[id]
394 if slot == nil {
395 return nil // retired: its descriptors are gone
396 }
397 // Drain first: a notification that lands after the drain is seen by the
398 // Signaled check below or leaves the eventfd readable for a later no-op call.
399 if err := drainEventFD(slot.ReleaseFD); err != nil {
400 return err
401 }
402 b := slot.Buffer
403 if b.State != buffers.CompositorOwned {
404 return nil
405 }
406 signaled, err := t.Node.Signaled(slot.ReleaseHandle, b.ReleasePoint)
407 if err != nil {
408 return err
409 }
410 if !signaled {
411 return nil // stale notification; the registration for the live point is still armed
412 }
413 return t.release(slot, b.ReleasePoint)
414}
415
416// drainEventFD resets a non-blocking eventfd; an empty one is fine.
417func drainEventFD(fd int) error {
418 var counter [8]byte
419 if _, err := unix.Read(fd, counter[:]); err != nil && err != unix.EAGAIN {
420 return fmt.Errorf("session: read release eventfd: %w", err)
421 }
422 return nil
423}
424
425// acquireWait returns an unsignaled binary semaphore for a release wait,
426// reusing the slot's spare instead of creating one per frame.
427func (t *Target) acquireWait(slot *Slot) (*vkdevice.BinaryFence, error) {
428 if w := slot.spare; w != nil {
429 slot.spare = nil
430 return w, nil
431 }
432 return t.Device.NewBinaryFence(false)
433}
434
435func (t *Target) release(slot *Slot, point uint64) error {
436 b := slot.Buffer
437 // An older completion cannot authorize a newer commit of the same image.
438 if b.ReleasePoint != point || b.State != buffers.CompositorOwned {
439 return nil
440 }
441 if err := t.Pool.Signal(b, point); err != nil {
442 return err
443 }
444 if b.Generation == t.Pool.Generation {
445 fd, err := t.Node.ReleaseFence(slot.ReleaseHandle, point)
446 if err != nil {
447 return err
448 }
449 wait, err := t.acquireWait(slot)
450 if err != nil {
451 _ = unix.Close(fd)
452 return err
453 }
454 if err = wait.ImportTemporary(fd); err != nil {
455 _ = unix.Close(fd)
456 wait.Close()
457 return err
458 }
459 installed, err := installReleaseWait(slot, wait, slot.Frame.Ready)
460 if err != nil {
461 wait.Close()
462 return err
463 }
464 if !installed {
465 return nil
466 }
467 if err = t.Pool.Reuse(b, true); err != nil {
468 return err
469 }
470 }
471 return t.retire()
472}
473
474// finishPending installs release waits deferred because the old submission was
475// still running. It never waits on the GPU.
476func (t *Target) finishPending() (bool, error) {
477 pending := false
478 for _, slot := range t.Slots {
479 // Old generations never return to Available; retire frees them.
480 if slot.PendingWait == nil || slot.Buffer.Generation != t.Pool.Generation {
481 continue
482 }
483 ready, err := slot.Frame.Ready()
484 if err != nil {
485 return false, err
486 }
487 if !ready {
488 pending = true
489 continue
490 }
491 if slot.Wait != nil {
492 if slot.WaitInFlight {
493 slot.recycle(slot.Wait) // submission is Ready: the wait was consumed
494 } else {
495 slot.Wait.Close()
496 }
497 }
498 slot.Wait, slot.PendingWait, slot.WaitInFlight = slot.PendingWait, nil, false
499 if err := t.Pool.Reuse(slot.Buffer, true); err != nil {
500 return false, err
501 }
502 }
503 return pending, nil
504}
505
506// retire frees images of old generations once nothing uses them, and queues
507// the ones the transport imported for the next Output.
508func (t *Target) retire() error {
509 for id, slot := range t.Slots {
510 b := slot.Buffer
511 if !t.Pool.Retirable(b) {
512 continue
513 }
514 ready, err := slot.Frame.Ready()
515 if err != nil {
516 return err
517 }
518 if !ready {
519 continue
520 }
521 eventFD := slot.ReleaseFD
522 slot.ReleaseFD = -1
523 wasPresented := slot.Presented
524 err = t.closeSlot(slot)
525 if wasPresented {
526 t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})
527 } else if eventFD >= 0 {
528 _ = unix.Close(eventFD)
529 }
530 delete(t.Slots, id)
531 if rerr := t.Pool.Remove(b); rerr != nil {
532 return errors.Join(err, rerr)
533 }
534 if err != nil {
535 return err
536 }
537 }
538 for i := 0; i < len(t.retiredPipelines); {
539 old := t.retiredPipelines[i]
540 live := false
541 for _, slot := range t.Slots {
542 if slot.Buffer.Generation == old.generation {
543 live = true
544 break
545 }
546 }
547 if live {
548 i++
549 continue
550 }
551 old.list.Close()
552 t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)
553 }
554 return nil
555}
556
557// Readback returns the last drawn pixels of a buffer in RGBA order. Tests only;
558// call EnableReadback before the first Draw.
559func (t *Target) Readback(id uint64) (*image.NRGBA, error) {
560 slot := t.Slots[id]
561 if slot == nil || slot.Frame.Readback == nil {
562 return nil, errors.New("session: no readback for buffer")
563 }
564 if err := slot.Frame.Wait(); err != nil {
565 return nil, err
566 }
567 img, err := slot.Frame.Readback.Copy()
568 if err != nil {
569 return nil, err
570 }
571 vkdevice.SwizzleBGRA(img)
572 return img, nil
573}
574
575// Close waits for the GPU, then frees every descriptor and image, always
576// completing the teardown. The compositor's imports keep their own kernel
577// references. It returns the joined errors of the steps that failed and any
578// Vulkan validation errors recorded during the Target's life.
579func (t *Target) Close() error {
580 if t == nil || t.closed {
581 return nil
582 }
583 t.closed = true
584 var errs []error
585 if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {
586 errs = append(errs, vulkan.Check(t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)))
587 }
588 for _, r := range t.reported {
589 _ = unix.Close(r.ReleaseFD)
590 }
591 for _, r := range t.pending {
592 _ = unix.Close(r.ReleaseFD)
593 }
594 t.reported, t.pending = nil, nil
595 for id, slot := range t.Slots {
596 closeFD(&slot.ReleaseFD)
597 errs = append(errs, t.closeSlot(slot))
598 delete(t.Slots, id)
599 }
600 for _, old := range t.retiredPipelines {
601 old.list.Close()
602 }
603 t.retiredPipelines = nil
604 t.pipeline.Close()
605 closeFD(&t.acquireFD)
606 if t.Node != nil {
607 if t.acquire != 0 {
608 errs = append(errs, t.Node.Destroy(t.acquire))
609 }
610 errs = append(errs, t.Node.Close())
611 }
612 if t.Device != nil {
613 // Read the messages before Close destroys the messenger.
614 if msgs := t.Device.Validation.Messages(); len(msgs) > 0 {
615 errs = append(errs, fmt.Errorf("%d Vulkan validation errors: %v", len(msgs), msgs))
616 }
617 }
618 t.Device.Close()
619 return errors.Join(errs...)
620}