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