detached e5cd51b

feat(session): add Wayland-free Target with eventfd release tracking

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close