detached 2102161

merge: zero-copy (compose into exported GPU images flipped by KMS)

commit
21021618f6b2741cb2a19913da00976b8ddd7918
author
brice · · 2026-09-25 17:34 UTC
merge of
5a76c9e + 6c2aaa2 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +676 -70
M internal/adapters/drm/output.go +165 -30
@@ -7,22 +7,28 @@ import (
77 "maps"
88 "slices"
99 "time"
10+ "unsafe"
1011
1112 "github.com/bnema/nefertty/internal/ports"
1213 "github.com/bnema/zerowrap"
1314 "golang.org/x/sys/unix"
1415 )
1516
16-// Output is one KMS connector driven with two dumb buffers. One goroutine
17+// Output is one KMS connector flipping between two images. One goroutine
1718 // runs it (Run) and closes it.
1819 type Output struct {
19- fd int
20- flipped <-chan int // page flips of this CRTC, from Card.ReadEvents
21- crtc uint32
22- conn connector
23- mode modeInfo
24- saved modeCrtc
25- bufs [2]*dumbBuffer
20+ fd int
21+ flipped <-chan int // page flips of this CRTC, from Card.ReadEvents
22+ crtc uint32
23+ conn connector
24+ mode modeInfo
25+ saved modeCrtc
26+ // fbs are the two images frames alternate between, back the one the
27+ // next frame draws into. They are renderer images exported as dmabufs
28+ // (ADR 014: zero copy), or dumb buffers filled by the CPU when the GPU
29+ // cannot export a scanout-capable image (logged fallback).
30+ fbs [2]uint32
31+ bufs [2]*dumbBuffer // fallback only
2632 back int
2733 pending bool
2834 flipStart time.Time
@@ -38,13 +44,16 @@ type Output struct {
3844 shown, queued uint64
3945 reason string // why the last frame was composed ("" = scanout)
4046 unsent ports.OutputPresented
47+ // kind is how the images were made; validated once a modeset took them.
48+ kind imageKind
49+ validated bool
4150 }
4251
4352 // CursorLoader returns the image of a cursor at an output scale, at most
4453 // limit pixels on a side; an empty image hides the cursor.
4554 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
4655
47-// newOutput allocates buffers and the cursor for a connector on crtc.
56+// newOutput prepares the cursor and saves the CRTC for a connector on crtc.
4857 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
4958 log := card.log
5059 o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
@@ -52,12 +61,6 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er
5261 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
5362 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
5463 }
55- for i := range o.bufs {
56- if o.bufs[i], err = newDumb(card.fd, o.Width(), o.Height()); err != nil {
57- o.Close()
58- return nil, err
59- }
60- }
6164 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {
6265 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
6366 }
@@ -86,19 +89,20 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) }
8689 func (o *Output) modeset() error {
8790 o.pending = false
8891 o.shown, o.queued = 0, 0
89- front := o.bufs[1-o.back]
90- if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil {
92+ if err := setCrtc(o.fd, o.crtc, o.conn.id, o.fbs[1-o.back], &o.mode); err != nil {
9193 return fmt.Errorf("set crtc: %w", err)
9294 }
9395 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
9496 return nil
9597 }
9698
97-// present copies img into the back buffer and queues a page flip.
99+// present queues a page flip to the frame just drawn into the back image
100+// (copied into it by the CPU on the dumb-buffer fallback).
98101 func (o *Output) present(r ports.Renderer) error {
99- b := o.bufs[o.back]
100- r.CopyBGRX(b.mem, int(b.pitch))
101- if err := flip(o.fd, o.crtc, b.fbID); err != nil {
102+ if b := o.bufs[o.back]; b != nil {
103+ r.CopyBGRX(b.mem, int(b.pitch))
104+ }
105+ if err := flip(o.fd, o.crtc, o.fbs[o.back]); err != nil {
102106 return err
103107 }
104108 o.pending, o.flipStart = true, time.Now()
@@ -159,11 +163,7 @@ func (o *Output) Close() {
159163 _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)
160164 }
161165 }
162- for _, b := range o.bufs {
163- if b != nil {
164- b.destroy(o.fd)
165- }
166- }
166+ o.freeImages()
167167 o.shown, o.queued = 0, 0
168168 o.dropClientFBs(time.Now(), true)
169169 }
@@ -183,10 +183,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
183183 case enabled = <-active:
184184 default:
185185 }
186+ // Output images, from the best to the fallback (ADR 014). While
187+ // switched away they are validated by the first modeset on enable.
186188 if enabled {
187- if err := o.modeset(); err != nil {
188- return err
189- }
189+ err = o.showImages(r, imagesDriver, nil)
190+ } else {
191+ o.kind, err = o.setupImages(r, imagesDriver, nil)
192+ }
193+ if err != nil {
194+ return err
190195 }
191196 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
192197 var scene ports.Scene
@@ -221,10 +226,22 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
221226 case on := <-active:
222227 // Modeset on every enable: a fast disable+enable can coalesce to one true.
223228 if on {
224- if err := o.modeset(); err != nil {
229+ err := o.modeset()
230+ if lostMaster(err) {
231+ // The seat took DRM master back: wait for the next enable.
232+ o.log.Warn().Err(err).Msg("resume")
233+ continue
234+ }
235+ if refused(err) && !o.validated && o.kind < imagesDumb {
236+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images")
237+ o.freeImages()
238+ err = o.showImages(r, o.kind+1, err)
239+ }
240+ if err != nil {
225241 o.log.Error().Err(err).Msg("resume")
226242 return err
227243 }
244+ o.validated = true
228245 dirty = haveScene
229246 if o.cursor != nil {
230247 if err := o.cursor.Reapply(); err != nil {
@@ -286,6 +303,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
286303 start := time.Now()
287304 direct, err := o.tryScanout(scene, surfaces)
288305 if !direct {
306+ r.UseTarget(o.back)
289307 if err := r.Render(scene, surfaces); err != nil {
290308 return fmt.Errorf("render frame: %w", err)
291309 }
@@ -349,3 +367,120 @@ func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
349367 default:
350368 }
351369 }
370+
371+// imageKind is how output images are made, best first.
372+type imageKind int
373+
374+const (
375+ imagesDriver imageKind = iota // exported, modifier chosen by the driver
376+ imagesLinear // exported, linear
377+ imagesDumb // dumb buffers filled by CPU copies
378+)
379+
380+// showImages sets up images from kind on and modesets them. KMS may refuse
381+// an image only at modeset: then the next kind is tried. cause is why the
382+// previous kind failed, for the log.
383+func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
384+ for {
385+ got, err := o.setupImages(r, kind, cause)
386+ if err != nil {
387+ return err
388+ }
389+ o.kind = got
390+ if err = o.modeset(); err == nil {
391+ o.validated = true
392+ return nil
393+ }
394+ if !refused(err) {
395+ return err
396+ }
397+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
398+ o.freeImages()
399+ if got == imagesDumb {
400+ return err
401+ }
402+ kind, cause = got+1, err
403+ }
404+}
405+
406+// refused reports a modeset error meaning KMS rejects the images.
407+func refused(err error) bool {
408+ return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)
409+}
410+
411+// lostMaster reports a modeset error meaning the seat is switched away.
412+func lostMaster(err error) bool {
413+ return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM)
414+}
415+
416+// setupImages gives the output the two images it flips between: renderer
417+// targets exported as dmabufs, or dumb buffers as a logged fallback. A
418+// kind that fails falls through to the next one.
419+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
420+ err := cause
421+ for ; kind < imagesDumb; kind++ {
422+ mods := []uint64(nil)
423+ if kind == imagesLinear {
424+ mods = []uint64{0}
425+ }
426+ if err = o.exportImages(r, mods); err == nil {
427+ return kind, nil
428+ }
429+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
430+ }
431+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
432+ // The renderer draws into its own image again, read back by CopyBGRX.
433+ _, _ = r.ExportTargets(0, nil)
434+ for i := range o.bufs {
435+ if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
436+ o.freeImages()
437+ return imagesDumb, err
438+ }
439+ o.fbs[i] = o.bufs[i].fbID
440+ }
441+ return imagesDumb, nil
442+}
443+
444+// exportImages makes the renderer's exported targets the output images.
445+func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
446+ bufs, err := r.ExportTargets(len(o.fbs), mods)
447+ if err != nil {
448+ return err
449+ }
450+ for i := range bufs {
451+ if err == nil {
452+ o.fbs[i], err = o.card.addFB(&bufs[i])
453+ }
454+ bufs[i].Planes[0].File.Close()
455+ }
456+ // GPU memory starts undefined (old VRAM contents): clear both images
457+ // before the modeset shows one.
458+ for i := range o.fbs {
459+ if err != nil {
460+ break
461+ }
462+ r.UseTarget(i)
463+ err = r.Render(ports.Scene{Background: "#000000"}, nil)
464+ }
465+ if err != nil {
466+ o.freeImages()
467+ _, _ = r.ExportTargets(0, nil)
468+ return err
469+ }
470+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
471+ return nil
472+}
473+
474+// freeImages removes the output images; dumb buffers own their fb.
475+func (o *Output) freeImages() {
476+ for i, fb := range o.fbs {
477+ if b := o.bufs[i]; b != nil {
478+ b.destroy(o.fd)
479+ o.bufs[i] = nil
480+ } else if fb != 0 {
481+ v := fb
482+ _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v))
483+ }
484+ o.fbs[i] = 0
485+ }
486+}
@@ -7,22 +7,28 @@ import (
7 "maps"7 "maps"
8 "slices"8 "slices"
9 "time"9 "time"
10+ "unsafe"
10 11
11 "github.com/bnema/nefertty/internal/ports"12 "github.com/bnema/nefertty/internal/ports"
12 "github.com/bnema/zerowrap"13 "github.com/bnema/zerowrap"
13 "golang.org/x/sys/unix"14 "golang.org/x/sys/unix"
14 )15 )
15 16
16-// Output is one KMS connector driven with two dumb buffers. One goroutine17+// Output is one KMS connector flipping between two images. One goroutine
17 // runs it (Run) and closes it.18 // runs it (Run) and closes it.
18 type Output struct {19 type Output struct {
19- fd int20+ fd int
20- flipped <-chan int // page flips of this CRTC, from Card.ReadEvents21+ flipped <-chan int // page flips of this CRTC, from Card.ReadEvents
21- crtc uint3222+ crtc uint32
22- conn connector23+ conn connector
23- mode modeInfo24+ mode modeInfo
24- saved modeCrtc25+ saved modeCrtc
25- bufs [2]*dumbBuffer26+ // fbs are the two images frames alternate between, back the one the
27+ // next frame draws into. They are renderer images exported as dmabufs
28+ // (ADR 014: zero copy), or dumb buffers filled by the CPU when the GPU
29+ // cannot export a scanout-capable image (logged fallback).
30+ fbs [2]uint32
31+ bufs [2]*dumbBuffer // fallback only
26 back int32 back int
27 pending bool33 pending bool
28 flipStart time.Time34 flipStart time.Time
@@ -38,13 +44,16 @@ type Output struct {
38 shown, queued uint6444 shown, queued uint64
39 reason string // why the last frame was composed ("" = scanout)45 reason string // why the last frame was composed ("" = scanout)
40 unsent ports.OutputPresented46 unsent ports.OutputPresented
47+ // kind is how the images were made; validated once a modeset took them.
48+ kind imageKind
49+ validated bool
41 }50 }
42 51
43 // CursorLoader returns the image of a cursor at an output scale, at most52 // CursorLoader returns the image of a cursor at an output scale, at most
44 // limit pixels on a side; an empty image hides the cursor.53 // limit pixels on a side; an empty image hides the cursor.
45 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)54 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
46 55
47-// newOutput allocates buffers and the cursor for a connector on crtc.56+// newOutput prepares the cursor and saves the CRTC for a connector on crtc.
48 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {57 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
49 log := card.log58 log := card.log
50 o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}59 o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
@@ -52,12 +61,6 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er
52 if o.saved, err = getCrtc(card.fd, crtc); err != nil {61 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
53 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")62 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
54 }63 }
55- for i := range o.bufs {
56- if o.bufs[i], err = newDumb(card.fd, o.Width(), o.Height()); err != nil {
57- o.Close()
58- return nil, err
59- }
60- }
61 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {64 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {
62 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")65 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
63 }66 }
@@ -86,19 +89,20 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) }
86 func (o *Output) modeset() error {89 func (o *Output) modeset() error {
87 o.pending = false90 o.pending = false
88 o.shown, o.queued = 0, 091 o.shown, o.queued = 0, 0
89- front := o.bufs[1-o.back]92+ if err := setCrtc(o.fd, o.crtc, o.conn.id, o.fbs[1-o.back], &o.mode); err != nil {
90- if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil {
91 return fmt.Errorf("set crtc: %w", err)93 return fmt.Errorf("set crtc: %w", err)
92 }94 }
93 o.log.Info().Str("connector", o.conn.name).Msg("modeset")95 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
94 return nil96 return nil
95 }97 }
96 98
97-// present copies img into the back buffer and queues a page flip.99+// present queues a page flip to the frame just drawn into the back image
100+// (copied into it by the CPU on the dumb-buffer fallback).
98 func (o *Output) present(r ports.Renderer) error {101 func (o *Output) present(r ports.Renderer) error {
99- b := o.bufs[o.back]102+ if b := o.bufs[o.back]; b != nil {
100- r.CopyBGRX(b.mem, int(b.pitch))103+ r.CopyBGRX(b.mem, int(b.pitch))
101- if err := flip(o.fd, o.crtc, b.fbID); err != nil {104+ }
105+ if err := flip(o.fd, o.crtc, o.fbs[o.back]); err != nil {
102 return err106 return err
103 }107 }
104 o.pending, o.flipStart = true, time.Now()108 o.pending, o.flipStart = true, time.Now()
@@ -159,11 +163,7 @@ func (o *Output) Close() {
159 _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)163 _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)
160 }164 }
161 }165 }
162- for _, b := range o.bufs {166+ o.freeImages()
163- if b != nil {
164- b.destroy(o.fd)
165- }
166- }
167 o.shown, o.queued = 0, 0167 o.shown, o.queued = 0, 0
168 o.dropClientFBs(time.Now(), true)168 o.dropClientFBs(time.Now(), true)
169 }169 }
@@ -183,10 +183,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
183 case enabled = <-active:183 case enabled = <-active:
184 default:184 default:
185 }185 }
186+ // Output images, from the best to the fallback (ADR 014). While
187+ // switched away they are validated by the first modeset on enable.
186 if enabled {188 if enabled {
187- if err := o.modeset(); err != nil {189+ err = o.showImages(r, imagesDriver, nil)
188- return err190+ } else {
189- }191+ o.kind, err = o.setupImages(r, imagesDriver, nil)
192+ }
193+ if err != nil {
194+ return err
190 }195 }
191 surfaces := make(map[ports.WindowID]ports.SurfaceContent)196 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
192 var scene ports.Scene197 var scene ports.Scene
@@ -221,10 +226,22 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
221 case on := <-active:226 case on := <-active:
222 // Modeset on every enable: a fast disable+enable can coalesce to one true.227 // Modeset on every enable: a fast disable+enable can coalesce to one true.
223 if on {228 if on {
224- if err := o.modeset(); err != nil {229+ err := o.modeset()
230+ if lostMaster(err) {
231+ // The seat took DRM master back: wait for the next enable.
232+ o.log.Warn().Err(err).Msg("resume")
233+ continue
234+ }
235+ if refused(err) && !o.validated && o.kind < imagesDumb {
236+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images")
237+ o.freeImages()
238+ err = o.showImages(r, o.kind+1, err)
239+ }
240+ if err != nil {
225 o.log.Error().Err(err).Msg("resume")241 o.log.Error().Err(err).Msg("resume")
226 return err242 return err
227 }243 }
244+ o.validated = true
228 dirty = haveScene245 dirty = haveScene
229 if o.cursor != nil {246 if o.cursor != nil {
230 if err := o.cursor.Reapply(); err != nil {247 if err := o.cursor.Reapply(); err != nil {
@@ -286,6 +303,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
286 start := time.Now()303 start := time.Now()
287 direct, err := o.tryScanout(scene, surfaces)304 direct, err := o.tryScanout(scene, surfaces)
288 if !direct {305 if !direct {
306+ r.UseTarget(o.back)
289 if err := r.Render(scene, surfaces); err != nil {307 if err := r.Render(scene, surfaces); err != nil {
290 return fmt.Errorf("render frame: %w", err)308 return fmt.Errorf("render frame: %w", err)
291 }309 }
@@ -349,3 +367,120 @@ func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
349 default:367 default:
350 }368 }
351 }369 }
370+
371+// imageKind is how output images are made, best first.
372+type imageKind int
373+
374+const (
375+ imagesDriver imageKind = iota // exported, modifier chosen by the driver
376+ imagesLinear // exported, linear
377+ imagesDumb // dumb buffers filled by CPU copies
378+)
379+
380+// showImages sets up images from kind on and modesets them. KMS may refuse
381+// an image only at modeset: then the next kind is tried. cause is why the
382+// previous kind failed, for the log.
383+func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
384+ for {
385+ got, err := o.setupImages(r, kind, cause)
386+ if err != nil {
387+ return err
388+ }
389+ o.kind = got
390+ if err = o.modeset(); err == nil {
391+ o.validated = true
392+ return nil
393+ }
394+ if !refused(err) {
395+ return err
396+ }
397+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
398+ o.freeImages()
399+ if got == imagesDumb {
400+ return err
401+ }
402+ kind, cause = got+1, err
403+ }
404+}
405+
406+// refused reports a modeset error meaning KMS rejects the images.
407+func refused(err error) bool {
408+ return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)
409+}
410+
411+// lostMaster reports a modeset error meaning the seat is switched away.
412+func lostMaster(err error) bool {
413+ return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM)
414+}
415+
416+// setupImages gives the output the two images it flips between: renderer
417+// targets exported as dmabufs, or dumb buffers as a logged fallback. A
418+// kind that fails falls through to the next one.
419+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
420+ err := cause
421+ for ; kind < imagesDumb; kind++ {
422+ mods := []uint64(nil)
423+ if kind == imagesLinear {
424+ mods = []uint64{0}
425+ }
426+ if err = o.exportImages(r, mods); err == nil {
427+ return kind, nil
428+ }
429+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
430+ }
431+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
432+ // The renderer draws into its own image again, read back by CopyBGRX.
433+ _, _ = r.ExportTargets(0, nil)
434+ for i := range o.bufs {
435+ if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
436+ o.freeImages()
437+ return imagesDumb, err
438+ }
439+ o.fbs[i] = o.bufs[i].fbID
440+ }
441+ return imagesDumb, nil
442+}
443+
444+// exportImages makes the renderer's exported targets the output images.
445+func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
446+ bufs, err := r.ExportTargets(len(o.fbs), mods)
447+ if err != nil {
448+ return err
449+ }
450+ for i := range bufs {
451+ if err == nil {
452+ o.fbs[i], err = o.card.addFB(&bufs[i])
453+ }
454+ bufs[i].Planes[0].File.Close()
455+ }
456+ // GPU memory starts undefined (old VRAM contents): clear both images
457+ // before the modeset shows one.
458+ for i := range o.fbs {
459+ if err != nil {
460+ break
461+ }
462+ r.UseTarget(i)
463+ err = r.Render(ports.Scene{Background: "#000000"}, nil)
464+ }
465+ if err != nil {
466+ o.freeImages()
467+ _, _ = r.ExportTargets(0, nil)
468+ return err
469+ }
470+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
471+ return nil
472+}
473+
474+// freeImages removes the output images; dumb buffers own their fb.
475+func (o *Output) freeImages() {
476+ for i, fb := range o.fbs {
477+ if b := o.bufs[i]; b != nil {
478+ b.destroy(o.fd)
479+ o.bufs[i] = nil
480+ } else if fb != 0 {
481+ v := fb
482+ _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v))
483+ }
484+ o.fbs[i] = 0
485+ }
486+}
M internal/adapters/drm/scanout.go +7 -6
@@ -100,7 +100,7 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
100100 }
101101 fb := &clientFB{last: now}
102102 o.clientFBs[b.ID] = fb
103- id, err := o.card.addClientFB(b)
103+ id, err := o.card.addFB(b)
104104 if err != nil {
105105 o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
106106 fb.failed = "import_failed"
@@ -127,11 +127,12 @@ const (
127127 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
128128 )
129129
130-// addClientFB imports a dmabuf as a framebuffer. The GEM handle belongs to
131-// the card fd and is shared by every import of the same buffer, so import,
132-// ADDFB2 and close run under the card lock and the handle is closed right
133-// away: the framebuffer keeps its own reference to the buffer.
134-func (c *Card) addClientFB(b *ports.DMABuf) (uint32, error) {
130+// addFB imports a dmabuf as a framebuffer: client buffers and the
131+// renderer's exported output images. The GEM handle belongs to the card fd
132+// and is shared by every import of the same buffer, so import, ADDFB2 and
133+// close run under the card lock and the handle is closed right away: the
134+// framebuffer keeps its own reference to the buffer.
135+func (c *Card) addFB(b *ports.DMABuf) (uint32, error) {
135136 format, ok := scanoutFormat(b.Format)
136137 if !ok {
137138 return 0, errors.New("format not scanned out")
@@ -100,7 +100,7 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
100 }100 }
101 fb := &clientFB{last: now}101 fb := &clientFB{last: now}
102 o.clientFBs[b.ID] = fb102 o.clientFBs[b.ID] = fb
103- id, err := o.card.addClientFB(b)103+ id, err := o.card.addFB(b)
104 if err != nil {104 if err != nil {
105 o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")105 o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
106 fb.failed = "import_failed"106 fb.failed = "import_failed"
@@ -127,11 +127,12 @@ const (
127 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24127 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
128 )128 )
129 129
130-// addClientFB imports a dmabuf as a framebuffer. The GEM handle belongs to130+// addFB imports a dmabuf as a framebuffer: client buffers and the
131-// the card fd and is shared by every import of the same buffer, so import,131+// renderer's exported output images. The GEM handle belongs to the card fd
132-// ADDFB2 and close run under the card lock and the handle is closed right132+// and is shared by every import of the same buffer, so import, ADDFB2 and
133-// away: the framebuffer keeps its own reference to the buffer.133+// close run under the card lock and the handle is closed right away: the
134-func (c *Card) addClientFB(b *ports.DMABuf) (uint32, error) {134+// framebuffer keeps its own reference to the buffer.
135+func (c *Card) addFB(b *ports.DMABuf) (uint32, error) {
135 format, ok := scanoutFormat(b.Format)136 format, ok := scanoutFormat(b.Format)
136 if !ok {137 if !ok {
137 return 0, errors.New("format not scanned out")138 return 0, errors.New("format not scanned out")
M internal/adapters/vulkan/renderer.go +103 -29
@@ -23,8 +23,16 @@ type Renderer struct {
2323 device vk.Device
2424 dd *vk.DeviceDispatch
2525 queue vk.Queue
26- image vk.Image
27- imageMemory vk.DeviceMemory
26+ // own is the target until ExportTargets; targets are exported images
27+ // (targets.go), current the one the next frame draws into.
28+ own target
29+ targets []*target
30+ current int
31+ renderMods []uint64
32+ // last is the target of the last frame; readback copies it into
33+ // buffer only when Pixels or CopyBGRX asks (stale until then).
34+ last *target
35+ readBack bool
2836 buffer vk.Buffer
2937 bufferMemory vk.DeviceMemory
3038 staging vk.Buffer
@@ -35,7 +43,6 @@ type Renderer struct {
3543 pool vk.CommandPool
3644 command vk.CommandBuffer
3745 fence vk.Fence
38- layout vk.ImageLayout
3946 memory vk.PhysicalDeviceMemoryProperties
4047 family uint32
4148 // dmabuf is what clients may send; imports are their buffers by ID.
@@ -65,7 +72,7 @@ func New(width, height int) (r *Renderer, err error) {
6572 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
6673 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
6774 }
68- r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
75+ r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
6976 defer func() {
7077 if err != nil {
7178 r.Close()
@@ -169,26 +176,27 @@ func New(width, height int) (r *Renderer, err error) {
169176 // Clients need the render node to allocate on the right GPU.
170177 if sup := r.probeDMABuf(physical); sup.Device != 0 {
171178 r.dmabuf = sup
179+ r.probeRenderModifiers(physical)
172180 }
173181 }
174182 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
175183 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
176184 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
177- if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.image)); err != nil {
185+ if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {
178186 return
179187 }
180188 var req vk.MemoryRequirements
181- r.dd.GetImageMemoryRequirements(r.device, r.image, &req)
189+ r.dd.GetImageMemoryRequirements(r.device, r.own.image, &req)
182190 var kind uint32
183191 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
184192 if err != nil {
185193 return
186194 }
187195 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
188- if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.imageMemory)); err != nil {
196+ if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.own.memory)); err != nil {
189197 return
190198 }
191- if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.image, r.imageMemory, 0)); err != nil {
199+ if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.own.image, r.own.memory, 0)); err != nil {
192200 return
193201 }
194202 size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4
@@ -197,7 +205,7 @@ func New(width, height int) (r *Renderer, err error) {
197205 return
198206 }
199207 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
200- // The CPU reads this buffer every frame: uncached memory makes that ~100x slower.
208+ // Readback (screenshots, fallback) reads this buffer: uncached memory is ~100x slower.
201209 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
202210 if err != nil {
203211 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
@@ -575,18 +583,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
575583 return err
576584 }
577585 rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
586+ tg := r.target()
578587 srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
579588 srcAccess := vk.AccessFlags(0)
580- if r.layout == vk.ImageLayoutTransferSrcOptimal {
589+ if tg.layout == vk.ImageLayoutTransferSrcOptimal {
581590 srcStage = vk.PipelineStageTransferBit
582591 srcAccess = vk.AccessTransferReadBit
583592 }
584- barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: r.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: r.image, SubresourceRange: rangeInfo}
593+ // The whole target is cleared: its old contents never matter.
594+ barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: tg.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: tg.image, SubresourceRange: rangeInfo}
595+ if tg.exported {
596+ // Acquire from the display (foreign queue family).
597+ barrier.OldLayout = vk.ImageLayoutUndefined
598+ barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
599+ srcStage, barrier.SrcAccessMask = vk.PipelineStageTopOfPipeBit, 0
600+ }
585601 d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
602+ barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0
586603 // VkClearColorValue is a union; UNORM formats read it as float32.
587604 unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
588605 color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
589- d.CmdClearColorImage(r.command, r.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
606+ d.CmdClearColorImage(r.command, tg.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
590607 barrier.SrcAccessMask = vk.AccessTransferWriteBit
591608 barrier.DstAccessMask = vk.AccessTransferWriteBit
592609 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
@@ -633,7 +650,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
633650 DstSubresource: layers,
634651 DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
635652 }
636- d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
653+ d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
637654 continue
638655 }
639656 layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
@@ -644,19 +661,26 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
644661 h := min(fillRows, rect.Max.Y-y)
645662 r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
646663 }
647- d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
664+ d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
648665 continue
649666 }
650667 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
651- d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
668+ d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
652669 }
653670 ownership(false)
671+ barrier.SrcAccessMask = vk.AccessTransferWriteBit
654672 barrier.DstAccessMask = vk.AccessTransferReadBit
655673 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
656674 barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
657- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
658- copyRegion := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
659- d.CmdCopyImageToBuffer(r.command, r.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &copyRegion)
675+ if tg.exported {
676+ // Release to the display: KMS scans it out after the fence.
677+ barrier.NewLayout = vk.ImageLayoutGeneral
678+ barrier.DstAccessMask = 0
679+ barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
680+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &barrier)
681+ } else {
682+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
683+ }
660684 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
661685 return err
662686 }
@@ -684,7 +708,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
684708 return err
685709 }
686710 runtime.KeepAlive(stages)
687- r.layout = vk.ImageLayoutTransferSrcOptimal
711+ tg.layout = barrier.NewLayout
712+ r.last, r.readBack = tg, false
713+ // The fence wait is the GPU completion ADR 014 requires before a flip.
688714 if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
689715 return err
690716 }
@@ -695,7 +721,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
695721
696722 func (r *Renderer) Pixels() *image.RGBA {
697723 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
698- if r.mapped == nil {
724+ if r.mapped == nil || r.readback() != nil {
699725 return out
700726 }
701727 src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))
@@ -709,7 +735,7 @@ func (r *Renderer) Pixels() *image.RGBA {
709735 // CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch.
710736 // The image is B8G8R8A8, which is already XRGB8888 in memory.
711737 func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
712- if r.mapped == nil {
738+ if r.mapped == nil || r.readback() != nil {
713739 return
714740 }
715741 row := r.width * 4
@@ -719,6 +745,60 @@ func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
719745 }
720746 }
721747
748+// Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
749+const (
750+ queueFamilyIgnored = ^uint32(0)
751+ wholeSize = ^vk.DeviceSize(0)
752+)
753+
754+// readback copies the last frame into the host buffer, once per frame.
755+func (r *Renderer) readback() error {
756+ if r.readBack || r.last == nil || r.last.image == 0 {
757+ return nil
758+ }
759+ d := r.dd
760+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
761+ return err
762+ }
763+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
764+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
765+ return err
766+ }
767+ t := r.last
768+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
769+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}
770+ if t.exported {
771+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
772+ b.SrcAccessMask = 0
773+ }
774+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
775+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
776+ d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
777+ // Make the copy visible to host reads of the mapped buffer.
778+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
779+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
780+ if t.exported {
781+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
782+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
783+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
784+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
785+ } else {
786+ t.layout = vk.ImageLayoutTransferSrcOptimal
787+ }
788+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
789+ return err
790+ }
791+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
792+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
793+ return err
794+ }
795+ if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
796+ return err
797+ }
798+ r.readBack = true
799+ return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
800+}
801+
722802 func (r *Renderer) Close() {
723803 if r == nil {
724804 return
@@ -728,6 +808,7 @@ func (r *Renderer) Close() {
728808 if r.device != 0 {
729809 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
730810 }
811+ r.dropTargets()
731812 for id, im := range r.imports {
732813 r.release(im)
733814 delete(r.imports, id)
@@ -770,14 +851,7 @@ func (r *Renderer) Close() {
770851 d.FreeMemory(r.device, r.bufferMemory, nil)
771852 r.bufferMemory = 0
772853 }
773- if r.image != 0 {
774- d.DestroyImage(r.device, r.image, nil)
775- r.image = 0
776- }
777- if r.imageMemory != 0 {
778- d.FreeMemory(r.device, r.imageMemory, nil)
779- r.imageMemory = 0
780- }
854+ r.freeTarget(&r.own)
781855 d.DestroyDevice(r.device, nil)
782856 r.device = 0
783857 } else if r.device != 0 && r.id != nil {
@@ -23,8 +23,16 @@ type Renderer struct {
23 device vk.Device23 device vk.Device
24 dd *vk.DeviceDispatch24 dd *vk.DeviceDispatch
25 queue vk.Queue25 queue vk.Queue
26- image vk.Image26+ // own is the target until ExportTargets; targets are exported images
27- imageMemory vk.DeviceMemory27+ // (targets.go), current the one the next frame draws into.
28+ own target
29+ targets []*target
30+ current int
31+ renderMods []uint64
32+ // last is the target of the last frame; readback copies it into
33+ // buffer only when Pixels or CopyBGRX asks (stale until then).
34+ last *target
35+ readBack bool
28 buffer vk.Buffer36 buffer vk.Buffer
29 bufferMemory vk.DeviceMemory37 bufferMemory vk.DeviceMemory
30 staging vk.Buffer38 staging vk.Buffer
@@ -35,7 +43,6 @@ type Renderer struct {
35 pool vk.CommandPool43 pool vk.CommandPool
36 command vk.CommandBuffer44 command vk.CommandBuffer
37 fence vk.Fence45 fence vk.Fence
38- layout vk.ImageLayout
39 memory vk.PhysicalDeviceMemoryProperties46 memory vk.PhysicalDeviceMemoryProperties
40 family uint3247 family uint32
41 // dmabuf is what clients may send; imports are their buffers by ID.48 // dmabuf is what clients may send; imports are their buffers by ID.
@@ -65,7 +72,7 @@ func New(width, height int) (r *Renderer, err error) {
65 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {72 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
66 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)73 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
67 }74 }
68- r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}75+ r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
69 defer func() {76 defer func() {
70 if err != nil {77 if err != nil {
71 r.Close()78 r.Close()
@@ -169,26 +176,27 @@ func New(width, height int) (r *Renderer, err error) {
169 // Clients need the render node to allocate on the right GPU.176 // Clients need the render node to allocate on the right GPU.
170 if sup := r.probeDMABuf(physical); sup.Device != 0 {177 if sup := r.probeDMABuf(physical); sup.Device != 0 {
171 r.dmabuf = sup178 r.dmabuf = sup
179+ r.probeRenderModifiers(physical)
172 }180 }
173 }181 }
174 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)182 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
175 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}183 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
176 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}184 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
177- if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.image)); err != nil {185+ if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {
178 return186 return
179 }187 }
180 var req vk.MemoryRequirements188 var req vk.MemoryRequirements
181- r.dd.GetImageMemoryRequirements(r.device, r.image, &req)189+ r.dd.GetImageMemoryRequirements(r.device, r.own.image, &req)
182 var kind uint32190 var kind uint32
183 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)191 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
184 if err != nil {192 if err != nil {
185 return193 return
186 }194 }
187 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}195 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
188- if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.imageMemory)); err != nil {196+ if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.own.memory)); err != nil {
189 return197 return
190 }198 }
191- if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.image, r.imageMemory, 0)); err != nil {199+ if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.own.image, r.own.memory, 0)); err != nil {
192 return200 return
193 }201 }
194 size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4202 size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4
@@ -197,7 +205,7 @@ func New(width, height int) (r *Renderer, err error) {
197 return205 return
198 }206 }
199 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)207 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
200- // The CPU reads this buffer every frame: uncached memory makes that ~100x slower.208+ // Readback (screenshots, fallback) reads this buffer: uncached memory is ~100x slower.
201 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)209 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
202 if err != nil {210 if err != nil {
203 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)211 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
@@ -575,18 +583,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
575 return err583 return err
576 }584 }
577 rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}585 rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
586+ tg := r.target()
578 srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)587 srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
579 srcAccess := vk.AccessFlags(0)588 srcAccess := vk.AccessFlags(0)
580- if r.layout == vk.ImageLayoutTransferSrcOptimal {589+ if tg.layout == vk.ImageLayoutTransferSrcOptimal {
581 srcStage = vk.PipelineStageTransferBit590 srcStage = vk.PipelineStageTransferBit
582 srcAccess = vk.AccessTransferReadBit591 srcAccess = vk.AccessTransferReadBit
583 }592 }
584- barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: r.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: r.image, SubresourceRange: rangeInfo}593+ // The whole target is cleared: its old contents never matter.
594+ barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: tg.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: tg.image, SubresourceRange: rangeInfo}
595+ if tg.exported {
596+ // Acquire from the display (foreign queue family).
597+ barrier.OldLayout = vk.ImageLayoutUndefined
598+ barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
599+ srcStage, barrier.SrcAccessMask = vk.PipelineStageTopOfPipeBit, 0
600+ }
585 d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)601 d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
602+ barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0
586 // VkClearColorValue is a union; UNORM formats read it as float32.603 // VkClearColorValue is a union; UNORM formats read it as float32.
587 unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }604 unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
588 color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}605 color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
589- d.CmdClearColorImage(r.command, r.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)606+ d.CmdClearColorImage(r.command, tg.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
590 barrier.SrcAccessMask = vk.AccessTransferWriteBit607 barrier.SrcAccessMask = vk.AccessTransferWriteBit
591 barrier.DstAccessMask = vk.AccessTransferWriteBit608 barrier.DstAccessMask = vk.AccessTransferWriteBit
592 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal609 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
@@ -633,7 +650,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
633 DstSubresource: layers,650 DstSubresource: layers,
634 DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},651 DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
635 }652 }
636- d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)653+ d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
637 continue654 continue
638 }655 }
639 layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}656 layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
@@ -644,19 +661,26 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
644 h := min(fillRows, rect.Max.Y-y)661 h := min(fillRows, rect.Max.Y-y)
645 r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})662 r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
646 }663 }
647- d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])664+ d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
648 continue665 continue
649 }666 }
650 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}667 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
651- d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)668+ d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
652 }669 }
653 ownership(false)670 ownership(false)
671+ barrier.SrcAccessMask = vk.AccessTransferWriteBit
654 barrier.DstAccessMask = vk.AccessTransferReadBit672 barrier.DstAccessMask = vk.AccessTransferReadBit
655 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal673 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
656 barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal674 barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
657- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)675+ if tg.exported {
658- copyRegion := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}676+ // Release to the display: KMS scans it out after the fence.
659- d.CmdCopyImageToBuffer(r.command, r.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &copyRegion)677+ barrier.NewLayout = vk.ImageLayoutGeneral
678+ barrier.DstAccessMask = 0
679+ barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
680+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &barrier)
681+ } else {
682+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
683+ }
660 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {684 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
661 return err685 return err
662 }686 }
@@ -684,7 +708,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
684 return err708 return err
685 }709 }
686 runtime.KeepAlive(stages)710 runtime.KeepAlive(stages)
687- r.layout = vk.ImageLayoutTransferSrcOptimal711+ tg.layout = barrier.NewLayout
712+ r.last, r.readBack = tg, false
713+ // The fence wait is the GPU completion ADR 014 requires before a flip.
688 if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {714 if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
689 return err715 return err
690 }716 }
@@ -695,7 +721,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
695 721
696 func (r *Renderer) Pixels() *image.RGBA {722 func (r *Renderer) Pixels() *image.RGBA {
697 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))723 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
698- if r.mapped == nil {724+ if r.mapped == nil || r.readback() != nil {
699 return out725 return out
700 }726 }
701 src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))727 src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))
@@ -709,7 +735,7 @@ func (r *Renderer) Pixels() *image.RGBA {
709 // CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch.735 // CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch.
710 // The image is B8G8R8A8, which is already XRGB8888 in memory.736 // The image is B8G8R8A8, which is already XRGB8888 in memory.
711 func (r *Renderer) CopyBGRX(dst []byte, pitch int) {737 func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
712- if r.mapped == nil {738+ if r.mapped == nil || r.readback() != nil {
713 return739 return
714 }740 }
715 row := r.width * 4741 row := r.width * 4
@@ -719,6 +745,60 @@ func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
719 }745 }
720 }746 }
721 747
748+// Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
749+const (
750+ queueFamilyIgnored = ^uint32(0)
751+ wholeSize = ^vk.DeviceSize(0)
752+)
753+
754+// readback copies the last frame into the host buffer, once per frame.
755+func (r *Renderer) readback() error {
756+ if r.readBack || r.last == nil || r.last.image == 0 {
757+ return nil
758+ }
759+ d := r.dd
760+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
761+ return err
762+ }
763+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
764+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
765+ return err
766+ }
767+ t := r.last
768+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
769+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}
770+ if t.exported {
771+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
772+ b.SrcAccessMask = 0
773+ }
774+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
775+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
776+ d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
777+ // Make the copy visible to host reads of the mapped buffer.
778+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
779+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
780+ if t.exported {
781+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
782+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
783+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
784+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
785+ } else {
786+ t.layout = vk.ImageLayoutTransferSrcOptimal
787+ }
788+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
789+ return err
790+ }
791+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
792+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
793+ return err
794+ }
795+ if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
796+ return err
797+ }
798+ r.readBack = true
799+ return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
800+}
801+
722 func (r *Renderer) Close() {802 func (r *Renderer) Close() {
723 if r == nil {803 if r == nil {
724 return804 return
@@ -728,6 +808,7 @@ func (r *Renderer) Close() {
728 if r.device != 0 {808 if r.device != 0 {
729 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))809 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
730 }810 }
811+ r.dropTargets()
731 for id, im := range r.imports {812 for id, im := range r.imports {
732 r.release(im)813 r.release(im)
733 delete(r.imports, id)814 delete(r.imports, id)
@@ -770,14 +851,7 @@ func (r *Renderer) Close() {
770 d.FreeMemory(r.device, r.bufferMemory, nil)851 d.FreeMemory(r.device, r.bufferMemory, nil)
771 r.bufferMemory = 0852 r.bufferMemory = 0
772 }853 }
773- if r.image != 0 {854+ r.freeTarget(&r.own)
774- d.DestroyImage(r.device, r.image, nil)
775- r.image = 0
776- }
777- if r.imageMemory != 0 {
778- d.FreeMemory(r.device, r.imageMemory, nil)
779- r.imageMemory = 0
780- }
781 d.DestroyDevice(r.device, nil)855 d.DestroyDevice(r.device, nil)
782 r.device = 0856 r.device = 0
783 } else if r.device != 0 && r.id != nil {857 } else if r.device != 0 && r.id != nil {
A internal/adapters/vulkan/targets.go +210 -0
@@ -0,0 +1,210 @@
1+package vulkan
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "slices"
8+ "unsafe"
9+
10+ "github.com/bnema/nefertty/internal/ports"
11+ vk "github.com/bnema/purego-vulkan/vulkan"
12+)
13+
14+// Render targets (ADR 014). A DRM output scans out images the renderer
15+// draws into: ExportTargets allocates them with a DRM format modifier the
16+// display accepts and exports their memory as dmabufs, which the output
17+// turns into KMS framebuffers. Render then composes straight into the
18+// selected target and the output flips to it; nothing is copied by the
19+// CPU. The internal image is the target until then (headless, tests).
20+// Readback into host memory happens only for Pixels and CopyBGRX.
21+
22+// target is an image frames are drawn into.
23+type target struct {
24+ image vk.Image
25+ memory vk.DeviceMemory
26+ layout vk.ImageLayout
27+ // exported images are shared with the display: ownership goes to the
28+ // foreign queue family after each frame, and KMS reads them.
29+ exported bool
30+}
31+
32+// fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
33+const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
34+
35+// ExportTargets allocates n exported images of the output size, replacing
36+// the previous ones; n = 0 only drops them (back to the internal image).
37+func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
38+ r.dropTargets()
39+ if n == 0 {
40+ return nil, nil
41+ }
42+ if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {
43+ return nil, errors.New("device cannot export dmabufs")
44+ }
45+ mods := r.exportModifiers(modifiers)
46+ if len(mods) == 0 {
47+ return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
48+ }
49+ var out []ports.DMABuf
50+ for range n {
51+ t, buf, err := r.exportTarget(mods)
52+ if err != nil {
53+ r.dropTargets()
54+ for _, b := range out {
55+ for _, p := range b.Planes {
56+ p.File.Close()
57+ }
58+ }
59+ return nil, err
60+ }
61+ r.targets = append(r.targets, t)
62+ out = append(out, buf)
63+ }
64+ r.current = 0
65+ return out, nil
66+}
67+
68+// UseTarget selects the exported image the next frames draw into.
69+func (r *Renderer) UseTarget(i int) {
70+ if i >= 0 && i < len(r.targets) {
71+ r.current = i
72+ }
73+}
74+
75+// target is the image the next frame draws into.
76+func (r *Renderer) target() *target {
77+ if len(r.targets) > 0 {
78+ return r.targets[r.current]
79+ }
80+ return &r.own
81+}
82+
83+// exportModifiers are the XRGB8888 modifiers the device renders to and
84+// exports with one plane, restricted to the display's when given.
85+func (r *Renderer) exportModifiers(display []uint64) []uint64 {
86+ var out []uint64
87+ for _, m := range r.renderMods {
88+ if len(display) == 0 || slices.Contains(display, m) {
89+ out = append(out, m)
90+ }
91+ }
92+ return out
93+}
94+
95+// probeRenderModifiers lists the B8G8R8A8 modifiers usable as a transfer
96+// destination with a single plane that the device can export.
97+func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
98+ list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
99+ fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
100+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
101+ if list.DrmFormatModifierCount == 0 {
102+ return
103+ }
104+ mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
105+ list.DrmFormatModifierProperties = &mods[0]
106+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
107+ need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit)
108+ for _, m := range mods[:list.DrmFormatModifierCount] {
109+ if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
110+ r.renderMods = append(r.renderMods, m.DrmFormatModifier)
111+ }
112+ }
113+}
114+
115+func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool {
116+ mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
117+ ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
118+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: vk.FormatB8g8r8a8Unorm, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: targetUsage}
119+ extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
120+ out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
121+ if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
122+ return false
123+ }
124+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
125+}
126+
127+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from
128+// the generated bindings.
129+const (
130+ formatFeatureTransferSrc = 1 << 14
131+ formatFeatureTransferDst = 1 << 15
132+)
133+
134+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit
135+
136+// exportTarget creates one exported image with a modifier from mods.
137+func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
138+ d := r.dd
139+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
140+ ok := false
141+ defer func() {
142+ if !ok {
143+ r.freeTarget(t)
144+ }
145+ }()
146+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
147+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
148+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: targetUsage, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
149+ if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
150+ return nil, ports.DMABuf{}, err
151+ }
152+ var req vk.MemoryRequirements
153+ d.GetImageMemoryRequirements(r.device, t.image, &req)
154+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
155+ if err != nil {
156+ return nil, ports.DMABuf{}, err
157+ }
158+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
159+ export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
160+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
161+ if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
162+ return nil, ports.DMABuf{}, err
163+ }
164+ if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
165+ return nil, ports.DMABuf{}, err
166+ }
167+ props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
168+ if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
169+ return nil, ports.DMABuf{}, err
170+ }
171+ var layout vk.SubresourceLayout
172+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
173+ d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
174+ var fd int32
175+ get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
176+ if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
177+ return nil, ports.DMABuf{}, err
178+ }
179+ f := os.NewFile(uintptr(fd), "nefertty-target")
180+ if f == nil {
181+ return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
182+ }
183+ buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourccXRGB, Modifier: props.DrmFormatModifier, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}
184+ ok = true
185+ return t, buf, nil
186+}
187+
188+func (r *Renderer) freeTarget(t *target) {
189+ if t.image != 0 {
190+ r.dd.DestroyImage(r.device, t.image, nil)
191+ t.image = 0
192+ }
193+ if t.memory != 0 {
194+ r.dd.FreeMemory(r.device, t.memory, nil)
195+ t.memory = 0
196+ }
197+}
198+
199+func (r *Renderer) dropTargets() {
200+ if len(r.targets) > 0 && r.device != 0 {
201+ _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
202+ }
203+ for _, t := range r.targets {
204+ if r.last == t {
205+ r.last = nil
206+ }
207+ r.freeTarget(t)
208+ }
209+ r.targets, r.current = nil, 0
210+}
@@ -0,0 +1,210 @@
1+package vulkan
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "slices"
8+ "unsafe"
9+
10+ "github.com/bnema/nefertty/internal/ports"
11+ vk "github.com/bnema/purego-vulkan/vulkan"
12+)
13+
14+// Render targets (ADR 014). A DRM output scans out images the renderer
15+// draws into: ExportTargets allocates them with a DRM format modifier the
16+// display accepts and exports their memory as dmabufs, which the output
17+// turns into KMS framebuffers. Render then composes straight into the
18+// selected target and the output flips to it; nothing is copied by the
19+// CPU. The internal image is the target until then (headless, tests).
20+// Readback into host memory happens only for Pixels and CopyBGRX.
21+
22+// target is an image frames are drawn into.
23+type target struct {
24+ image vk.Image
25+ memory vk.DeviceMemory
26+ layout vk.ImageLayout
27+ // exported images are shared with the display: ownership goes to the
28+ // foreign queue family after each frame, and KMS reads them.
29+ exported bool
30+}
31+
32+// fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
33+const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
34+
35+// ExportTargets allocates n exported images of the output size, replacing
36+// the previous ones; n = 0 only drops them (back to the internal image).
37+func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
38+ r.dropTargets()
39+ if n == 0 {
40+ return nil, nil
41+ }
42+ if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {
43+ return nil, errors.New("device cannot export dmabufs")
44+ }
45+ mods := r.exportModifiers(modifiers)
46+ if len(mods) == 0 {
47+ return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
48+ }
49+ var out []ports.DMABuf
50+ for range n {
51+ t, buf, err := r.exportTarget(mods)
52+ if err != nil {
53+ r.dropTargets()
54+ for _, b := range out {
55+ for _, p := range b.Planes {
56+ p.File.Close()
57+ }
58+ }
59+ return nil, err
60+ }
61+ r.targets = append(r.targets, t)
62+ out = append(out, buf)
63+ }
64+ r.current = 0
65+ return out, nil
66+}
67+
68+// UseTarget selects the exported image the next frames draw into.
69+func (r *Renderer) UseTarget(i int) {
70+ if i >= 0 && i < len(r.targets) {
71+ r.current = i
72+ }
73+}
74+
75+// target is the image the next frame draws into.
76+func (r *Renderer) target() *target {
77+ if len(r.targets) > 0 {
78+ return r.targets[r.current]
79+ }
80+ return &r.own
81+}
82+
83+// exportModifiers are the XRGB8888 modifiers the device renders to and
84+// exports with one plane, restricted to the display's when given.
85+func (r *Renderer) exportModifiers(display []uint64) []uint64 {
86+ var out []uint64
87+ for _, m := range r.renderMods {
88+ if len(display) == 0 || slices.Contains(display, m) {
89+ out = append(out, m)
90+ }
91+ }
92+ return out
93+}
94+
95+// probeRenderModifiers lists the B8G8R8A8 modifiers usable as a transfer
96+// destination with a single plane that the device can export.
97+func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
98+ list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
99+ fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
100+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
101+ if list.DrmFormatModifierCount == 0 {
102+ return
103+ }
104+ mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
105+ list.DrmFormatModifierProperties = &mods[0]
106+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
107+ need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit)
108+ for _, m := range mods[:list.DrmFormatModifierCount] {
109+ if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
110+ r.renderMods = append(r.renderMods, m.DrmFormatModifier)
111+ }
112+ }
113+}
114+
115+func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool {
116+ mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
117+ ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
118+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: vk.FormatB8g8r8a8Unorm, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: targetUsage}
119+ extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
120+ out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
121+ if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
122+ return false
123+ }
124+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
125+}
126+
127+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from
128+// the generated bindings.
129+const (
130+ formatFeatureTransferSrc = 1 << 14
131+ formatFeatureTransferDst = 1 << 15
132+)
133+
134+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit
135+
136+// exportTarget creates one exported image with a modifier from mods.
137+func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
138+ d := r.dd
139+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
140+ ok := false
141+ defer func() {
142+ if !ok {
143+ r.freeTarget(t)
144+ }
145+ }()
146+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
147+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
148+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: targetUsage, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
149+ if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
150+ return nil, ports.DMABuf{}, err
151+ }
152+ var req vk.MemoryRequirements
153+ d.GetImageMemoryRequirements(r.device, t.image, &req)
154+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
155+ if err != nil {
156+ return nil, ports.DMABuf{}, err
157+ }
158+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
159+ export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
160+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
161+ if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
162+ return nil, ports.DMABuf{}, err
163+ }
164+ if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
165+ return nil, ports.DMABuf{}, err
166+ }
167+ props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
168+ if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
169+ return nil, ports.DMABuf{}, err
170+ }
171+ var layout vk.SubresourceLayout
172+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
173+ d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
174+ var fd int32
175+ get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
176+ if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
177+ return nil, ports.DMABuf{}, err
178+ }
179+ f := os.NewFile(uintptr(fd), "nefertty-target")
180+ if f == nil {
181+ return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
182+ }
183+ buf := ports.DMABuf{Width: r.width, Height: r.height, Format: fourccXRGB, Modifier: props.DrmFormatModifier, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}
184+ ok = true
185+ return t, buf, nil
186+}
187+
188+func (r *Renderer) freeTarget(t *target) {
189+ if t.image != 0 {
190+ r.dd.DestroyImage(r.device, t.image, nil)
191+ t.image = 0
192+ }
193+ if t.memory != 0 {
194+ r.dd.FreeMemory(r.device, t.memory, nil)
195+ t.memory = 0
196+ }
197+}
198+
199+func (r *Renderer) dropTargets() {
200+ if len(r.targets) > 0 && r.device != 0 {
201+ _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
202+ }
203+ for _, t := range r.targets {
204+ if r.last == t {
205+ r.last = nil
206+ }
207+ r.freeTarget(t)
208+ }
209+ r.targets, r.current = nil, 0
210+}
A internal/adapters/vulkan/targets_test.go +64 -0
@@ -0,0 +1,64 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "testing"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+)
9+
10+// Frames drawn into exported targets reach their dmabufs: another renderer
11+// importing a target (as KMS scans it out) sees the frame, and readback
12+// reads the target drawn last.
13+func TestExportTargets(t *testing.T) {
14+ r, err := New(64, 32)
15+ if err != nil {
16+ t.Skipf("Vulkan unavailable: %v", err)
17+ }
18+ defer r.Close()
19+ bufs, err := r.ExportTargets(2, nil)
20+ if err != nil {
21+ t.Skipf("no exportable targets: %v", err)
22+ }
23+ defer func() {
24+ for _, b := range bufs {
25+ b.Planes[0].File.Close()
26+ }
27+ }()
28+ if len(bufs) != 2 || bufs[0].Width != 64 || bufs[0].Format != fourccXRGB || bufs[0].Planes[0].Stride < 64*4 {
29+ t.Fatalf("targets %+v", bufs)
30+ }
31+ colors := []string{"#ff0000", "#0000ff"}
32+ for i, c := range colors {
33+ r.UseTarget(i)
34+ if err := r.Render(ports.Scene{Background: c}, nil); err != nil {
35+ t.Fatal(err)
36+ }
37+ }
38+ if got := r.Pixels().At(3, 3); got != (color.RGBA{0, 0, 255, 255}) {
39+ t.Fatalf("readback of the last target %v", got)
40+ }
41+ // Import target 0 as a client buffer, like the display would.
42+ viewer, err := New(64, 32)
43+ if err != nil {
44+ t.Fatal(err)
45+ }
46+ defer viewer.Close()
47+ b := bufs[0]
48+ b.ID = 1
49+ importable := false
50+ for _, f := range viewer.DMABuf().Formats {
51+ importable = importable || f.Format == b.Format && f.Modifier == b.Modifier
52+ }
53+ if !importable {
54+ t.Skipf("modifier %#x not importable", b.Modifier)
55+ }
56+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
57+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
58+ if err := viewer.Render(scene, contents); err != nil {
59+ t.Fatal(err)
60+ }
61+ if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {
62+ t.Fatalf("target 0 as seen by the display %v", got)
63+ }
64+}
@@ -0,0 +1,64 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "testing"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+)
9+
10+// Frames drawn into exported targets reach their dmabufs: another renderer
11+// importing a target (as KMS scans it out) sees the frame, and readback
12+// reads the target drawn last.
13+func TestExportTargets(t *testing.T) {
14+ r, err := New(64, 32)
15+ if err != nil {
16+ t.Skipf("Vulkan unavailable: %v", err)
17+ }
18+ defer r.Close()
19+ bufs, err := r.ExportTargets(2, nil)
20+ if err != nil {
21+ t.Skipf("no exportable targets: %v", err)
22+ }
23+ defer func() {
24+ for _, b := range bufs {
25+ b.Planes[0].File.Close()
26+ }
27+ }()
28+ if len(bufs) != 2 || bufs[0].Width != 64 || bufs[0].Format != fourccXRGB || bufs[0].Planes[0].Stride < 64*4 {
29+ t.Fatalf("targets %+v", bufs)
30+ }
31+ colors := []string{"#ff0000", "#0000ff"}
32+ for i, c := range colors {
33+ r.UseTarget(i)
34+ if err := r.Render(ports.Scene{Background: c}, nil); err != nil {
35+ t.Fatal(err)
36+ }
37+ }
38+ if got := r.Pixels().At(3, 3); got != (color.RGBA{0, 0, 255, 255}) {
39+ t.Fatalf("readback of the last target %v", got)
40+ }
41+ // Import target 0 as a client buffer, like the display would.
42+ viewer, err := New(64, 32)
43+ if err != nil {
44+ t.Fatal(err)
45+ }
46+ defer viewer.Close()
47+ b := bufs[0]
48+ b.ID = 1
49+ importable := false
50+ for _, f := range viewer.DMABuf().Formats {
51+ importable = importable || f.Format == b.Format && f.Modifier == b.Modifier
52+ }
53+ if !importable {
54+ t.Skipf("modifier %#x not importable", b.Modifier)
55+ }
56+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
57+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
58+ if err := viewer.Render(scene, contents); err != nil {
59+ t.Fatal(err)
60+ }
61+ if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {
62+ t.Fatalf("target 0 as seen by the display %v", got)
63+ }
64+}
M internal/core/screens_test.go +6 -2
@@ -457,7 +457,11 @@ func TestStateSnapshot(t *testing.T) {
457457 <-r.state // the channel holds the latest snapshot only
458458 r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200}
459459 receive(t, r.scenes)
460+ // Snapshots from before window 2 may still be queued: wait for it.
460461 st := receive(t, r.state)
462+ for len(st.Windows) < 2 {
463+ st = receive(t, r.state)
464+ }
461465 want := ports.State{
462466 Output: "DP-2",
463467 Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1}, {Name: "DP-2", Active: 1, Count: 1}},
@@ -490,8 +494,8 @@ func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) {
490494 // An app ID set after map reaches the state.
491495 r.client <- ports.WindowAppID{ID: 1, AppID: "foot"}
492496 receive(t, r.scenes)
493- if st := receive(t, r.state); st.Windows[0].AppID != "foot" {
494- t.Fatalf("%+v", st)
497+ // Snapshots from before the app ID may still be queued.
498+ for st := receive(t, r.state); st.Windows[0].AppID != "foot"; st = receive(t, r.state) {
495499 }
496500 // A hidden workspace on screen: no active number, its name, window 1 off screen.
497501 r.key(t, "n", ports.ModAlt)
@@ -457,7 +457,11 @@ func TestStateSnapshot(t *testing.T) {
457 <-r.state // the channel holds the latest snapshot only457 <-r.state // the channel holds the latest snapshot only
458 r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200}458 r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200}
459 receive(t, r.scenes)459 receive(t, r.scenes)
460+ // Snapshots from before window 2 may still be queued: wait for it.
460 st := receive(t, r.state)461 st := receive(t, r.state)
462+ for len(st.Windows) < 2 {
463+ st = receive(t, r.state)
464+ }
461 want := ports.State{465 want := ports.State{
462 Output: "DP-2",466 Output: "DP-2",
463 Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1}, {Name: "DP-2", Active: 1, Count: 1}},467 Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1}, {Name: "DP-2", Active: 1, Count: 1}},
@@ -490,8 +494,8 @@ func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) {
490 // An app ID set after map reaches the state.494 // An app ID set after map reaches the state.
491 r.client <- ports.WindowAppID{ID: 1, AppID: "foot"}495 r.client <- ports.WindowAppID{ID: 1, AppID: "foot"}
492 receive(t, r.scenes)496 receive(t, r.scenes)
493- if st := receive(t, r.state); st.Windows[0].AppID != "foot" {497+ // Snapshots from before the app ID may still be queued.
494- t.Fatalf("%+v", st)498+ for st := receive(t, r.state); st.Windows[0].AppID != "foot"; st = receive(t, r.state) {
495 }499 }
496 // A hidden workspace on screen: no active number, its name, window 1 off screen.500 // A hidden workspace on screen: no active number, its name, window 1 off screen.
497 r.key(t, "n", ports.ModAlt)501 r.key(t, "n", ports.ModAlt)
M internal/mocks/ports/devices_mock.go +108 -0
@@ -126,6 +126,74 @@ func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch in
126126 return _c
127127 }
128128
129+// ExportTargets provides a mock function for the type MockRenderer
130+func (_mock *MockRenderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
131+ ret := _mock.Called(n, modifiers)
132+
133+ if len(ret) == 0 {
134+ panic("no return value specified for ExportTargets")
135+ }
136+
137+ var r0 []ports.DMABuf
138+ var r1 error
139+ if returnFunc, ok := ret.Get(0).(func(int, []uint64) ([]ports.DMABuf, error)); ok {
140+ return returnFunc(n, modifiers)
141+ }
142+ if returnFunc, ok := ret.Get(0).(func(int, []uint64) []ports.DMABuf); ok {
143+ r0 = returnFunc(n, modifiers)
144+ } else {
145+ if ret.Get(0) != nil {
146+ r0 = ret.Get(0).([]ports.DMABuf)
147+ }
148+ }
149+ if returnFunc, ok := ret.Get(1).(func(int, []uint64) error); ok {
150+ r1 = returnFunc(n, modifiers)
151+ } else {
152+ r1 = ret.Error(1)
153+ }
154+ return r0, r1
155+}
156+
157+// MockRenderer_ExportTargets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExportTargets'
158+type MockRenderer_ExportTargets_Call struct {
159+ *mock.Call
160+}
161+
162+// ExportTargets is a helper method to define mock.On call
163+// - n int
164+// - modifiers []uint64
165+func (_e *MockRenderer_Expecter) ExportTargets(n any, modifiers any) *MockRenderer_ExportTargets_Call {
166+ return &MockRenderer_ExportTargets_Call{Call: _e.mock.On("ExportTargets", n, modifiers)}
167+}
168+
169+func (_c *MockRenderer_ExportTargets_Call) Run(run func(n int, modifiers []uint64)) *MockRenderer_ExportTargets_Call {
170+ _c.Call.Run(func(args mock.Arguments) {
171+ var arg0 int
172+ if args[0] != nil {
173+ arg0 = args[0].(int)
174+ }
175+ var arg1 []uint64
176+ if args[1] != nil {
177+ arg1 = args[1].([]uint64)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ )
183+ })
184+ return _c
185+}
186+
187+func (_c *MockRenderer_ExportTargets_Call) Return(dMABufs []ports.DMABuf, err error) *MockRenderer_ExportTargets_Call {
188+ _c.Call.Return(dMABufs, err)
189+ return _c
190+}
191+
192+func (_c *MockRenderer_ExportTargets_Call) RunAndReturn(run func(n int, modifiers []uint64) ([]ports.DMABuf, error)) *MockRenderer_ExportTargets_Call {
193+ _c.Call.Return(run)
194+ return _c
195+}
196+
129197 // Pixels provides a mock function for the type MockRenderer
130198 func (_mock *MockRenderer) Pixels() *image.RGBA {
131199 ret := _mock.Called()
@@ -229,6 +297,46 @@ func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, win
229297 return _c
230298 }
231299
300+// UseTarget provides a mock function for the type MockRenderer
301+func (_mock *MockRenderer) UseTarget(i int) {
302+ _mock.Called(i)
303+ return
304+}
305+
306+// MockRenderer_UseTarget_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseTarget'
307+type MockRenderer_UseTarget_Call struct {
308+ *mock.Call
309+}
310+
311+// UseTarget is a helper method to define mock.On call
312+// - i int
313+func (_e *MockRenderer_Expecter) UseTarget(i any) *MockRenderer_UseTarget_Call {
314+ return &MockRenderer_UseTarget_Call{Call: _e.mock.On("UseTarget", i)}
315+}
316+
317+func (_c *MockRenderer_UseTarget_Call) Run(run func(i int)) *MockRenderer_UseTarget_Call {
318+ _c.Call.Run(func(args mock.Arguments) {
319+ var arg0 int
320+ if args[0] != nil {
321+ arg0 = args[0].(int)
322+ }
323+ run(
324+ arg0,
325+ )
326+ })
327+ return _c
328+}
329+
330+func (_c *MockRenderer_UseTarget_Call) Return() *MockRenderer_UseTarget_Call {
331+ _c.Call.Return()
332+ return _c
333+}
334+
335+func (_c *MockRenderer_UseTarget_Call) RunAndReturn(run func(i int)) *MockRenderer_UseTarget_Call {
336+ _c.Run(run)
337+ return _c
338+}
339+
232340 // NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
233341 // The first argument is typically a *testing.T value.
234342 func NewMockSeat(t interface {
@@ -126,6 +126,74 @@ func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch in
126 return _c126 return _c
127 }127 }
128 128
129+// ExportTargets provides a mock function for the type MockRenderer
130+func (_mock *MockRenderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
131+ ret := _mock.Called(n, modifiers)
132+
133+ if len(ret) == 0 {
134+ panic("no return value specified for ExportTargets")
135+ }
136+
137+ var r0 []ports.DMABuf
138+ var r1 error
139+ if returnFunc, ok := ret.Get(0).(func(int, []uint64) ([]ports.DMABuf, error)); ok {
140+ return returnFunc(n, modifiers)
141+ }
142+ if returnFunc, ok := ret.Get(0).(func(int, []uint64) []ports.DMABuf); ok {
143+ r0 = returnFunc(n, modifiers)
144+ } else {
145+ if ret.Get(0) != nil {
146+ r0 = ret.Get(0).([]ports.DMABuf)
147+ }
148+ }
149+ if returnFunc, ok := ret.Get(1).(func(int, []uint64) error); ok {
150+ r1 = returnFunc(n, modifiers)
151+ } else {
152+ r1 = ret.Error(1)
153+ }
154+ return r0, r1
155+}
156+
157+// MockRenderer_ExportTargets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExportTargets'
158+type MockRenderer_ExportTargets_Call struct {
159+ *mock.Call
160+}
161+
162+// ExportTargets is a helper method to define mock.On call
163+// - n int
164+// - modifiers []uint64
165+func (_e *MockRenderer_Expecter) ExportTargets(n any, modifiers any) *MockRenderer_ExportTargets_Call {
166+ return &MockRenderer_ExportTargets_Call{Call: _e.mock.On("ExportTargets", n, modifiers)}
167+}
168+
169+func (_c *MockRenderer_ExportTargets_Call) Run(run func(n int, modifiers []uint64)) *MockRenderer_ExportTargets_Call {
170+ _c.Call.Run(func(args mock.Arguments) {
171+ var arg0 int
172+ if args[0] != nil {
173+ arg0 = args[0].(int)
174+ }
175+ var arg1 []uint64
176+ if args[1] != nil {
177+ arg1 = args[1].([]uint64)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ )
183+ })
184+ return _c
185+}
186+
187+func (_c *MockRenderer_ExportTargets_Call) Return(dMABufs []ports.DMABuf, err error) *MockRenderer_ExportTargets_Call {
188+ _c.Call.Return(dMABufs, err)
189+ return _c
190+}
191+
192+func (_c *MockRenderer_ExportTargets_Call) RunAndReturn(run func(n int, modifiers []uint64) ([]ports.DMABuf, error)) *MockRenderer_ExportTargets_Call {
193+ _c.Call.Return(run)
194+ return _c
195+}
196+
129 // Pixels provides a mock function for the type MockRenderer197 // Pixels provides a mock function for the type MockRenderer
130 func (_mock *MockRenderer) Pixels() *image.RGBA {198 func (_mock *MockRenderer) Pixels() *image.RGBA {
131 ret := _mock.Called()199 ret := _mock.Called()
@@ -229,6 +297,46 @@ func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, win
229 return _c297 return _c
230 }298 }
231 299
300+// UseTarget provides a mock function for the type MockRenderer
301+func (_mock *MockRenderer) UseTarget(i int) {
302+ _mock.Called(i)
303+ return
304+}
305+
306+// MockRenderer_UseTarget_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseTarget'
307+type MockRenderer_UseTarget_Call struct {
308+ *mock.Call
309+}
310+
311+// UseTarget is a helper method to define mock.On call
312+// - i int
313+func (_e *MockRenderer_Expecter) UseTarget(i any) *MockRenderer_UseTarget_Call {
314+ return &MockRenderer_UseTarget_Call{Call: _e.mock.On("UseTarget", i)}
315+}
316+
317+func (_c *MockRenderer_UseTarget_Call) Run(run func(i int)) *MockRenderer_UseTarget_Call {
318+ _c.Call.Run(func(args mock.Arguments) {
319+ var arg0 int
320+ if args[0] != nil {
321+ arg0 = args[0].(int)
322+ }
323+ run(
324+ arg0,
325+ )
326+ })
327+ return _c
328+}
329+
330+func (_c *MockRenderer_UseTarget_Call) Return() *MockRenderer_UseTarget_Call {
331+ _c.Call.Return()
332+ return _c
333+}
334+
335+func (_c *MockRenderer_UseTarget_Call) RunAndReturn(run func(i int)) *MockRenderer_UseTarget_Call {
336+ _c.Run(run)
337+ return _c
338+}
339+
232 // NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.340 // NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
233 // The first argument is typically a *testing.T value.341 // The first argument is typically a *testing.T value.
234 func NewMockSeat(t interface {342 func NewMockSeat(t interface {
M internal/ports/devices.go +13 -3
@@ -2,12 +2,22 @@ package ports
22
33 import "image"
44
5-// Renderer draws scenes for an output. One goroutine owns it.
5+// Renderer draws scenes for an output. One goroutine owns it. Frames go
6+// to its own image until ExportTargets gives it scanout images; then each
7+// Render draws into the target chosen by UseTarget (ADR 014: no CPU copy).
68 type Renderer interface {
79 Render(Scene, map[WindowID]SurfaceContent) error
8- // Pixels returns the last frame (headless screenshots).
10+ // ExportTargets allocates n images of the renderer's size that the
11+ // display can scan out, with one of the given XRGB8888 modifiers
12+ // (none: any the device exports), and returns them as dmabufs.
13+ // n = 0 drops the targets.
14+ ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
15+ // UseTarget selects the exported image the next Render draws into.
16+ UseTarget(i int)
17+ // Pixels reads the last frame back (headless screenshots, tests).
918 Pixels() *image.RGBA
10- // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch (DRM scanout).
19+ // CopyBGRX reads the last frame back as XRGB8888 rows of the given
20+ // pitch: the logged fallback when no image can be exported.
1121 CopyBGRX(dst []byte, pitch int)
1222 Close()
1323 }
@@ -2,12 +2,22 @@ package ports
2 2
3 import "image"3 import "image"
4 4
5-// Renderer draws scenes for an output. One goroutine owns it.5+// Renderer draws scenes for an output. One goroutine owns it. Frames go
6+// to its own image until ExportTargets gives it scanout images; then each
7+// Render draws into the target chosen by UseTarget (ADR 014: no CPU copy).
6 type Renderer interface {8 type Renderer interface {
7 Render(Scene, map[WindowID]SurfaceContent) error9 Render(Scene, map[WindowID]SurfaceContent) error
8- // Pixels returns the last frame (headless screenshots).10+ // ExportTargets allocates n images of the renderer's size that the
11+ // display can scan out, with one of the given XRGB8888 modifiers
12+ // (none: any the device exports), and returns them as dmabufs.
13+ // n = 0 drops the targets.
14+ ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
15+ // UseTarget selects the exported image the next Render draws into.
16+ UseTarget(i int)
17+ // Pixels reads the last frame back (headless screenshots, tests).
9 Pixels() *image.RGBA18 Pixels() *image.RGBA
10- // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch (DRM scanout).19+ // CopyBGRX reads the last frame back as XRGB8888 rows of the given
20+ // pitch: the logged fallback when no image can be exported.
11 CopyBGRX(dst []byte, pitch int)21 CopyBGRX(dst []byte, pitch int)
12 Close()22 Close()
13 }23 }

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 23

Tags 10

↑↓ moveenter openesc close