detached 427d358

feat(render): compose into exported GPU images flipped by KMS (zero copy)

commit
427d358af1aa7183e1e344c1fa8b031ec691fb58
author
brice · · 2026-09-25 17:14 UTC
parent
5a76c9e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +607 -64
M internal/adapters/drm/output.go +115 -26
@@ -7,6 +7,7 @@ import (
77 "maps"
88 "slices"
99 "time"
10+ "unsafe"
1011
1112 "github.com/bnema/nefertty/internal/ports"
1213 "github.com/bnema/zerowrap"
@@ -16,13 +17,18 @@ import (
1617 // Output is one KMS connector driven with two dumb buffers. 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
@@ -52,12 +58,6 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er
5258 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
5359 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
5460 }
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- }
6161 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {
6262 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
6363 }
@@ -86,19 +86,20 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) }
8686 func (o *Output) modeset() error {
8787 o.pending = false
8888 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 {
89+ if err := setCrtc(o.fd, o.crtc, o.conn.id, o.fbs[1-o.back], &o.mode); err != nil {
9190 return fmt.Errorf("set crtc: %w", err)
9291 }
9392 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
9493 return nil
9594 }
9695
97-// present copies img into the back buffer and queues a page flip.
96+// present queues a page flip to the frame just drawn into the back image
97+// (copied into it by the CPU on the dumb-buffer fallback).
9898 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 {
99+ if b := o.bufs[o.back]; b != nil {
100+ r.CopyBGRX(b.mem, int(b.pitch))
101+ }
102+ if err := flip(o.fd, o.crtc, o.fbs[o.back]); err != nil {
102103 return err
103104 }
104105 o.pending, o.flipStart = true, time.Now()
@@ -159,11 +160,7 @@ func (o *Output) Close() {
159160 _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)
160161 }
161162 }
162- for _, b := range o.bufs {
163- if b != nil {
164- b.destroy(o.fd)
165- }
166- }
163+ o.freeImages()
167164 o.shown, o.queued = 0, 0
168165 o.dropClientFBs(time.Now(), true)
169166 }
@@ -183,10 +180,23 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
183180 case enabled = <-active:
184181 default:
185182 }
186- if enabled {
187- if err := o.modeset(); err != nil {
183+ // Output images, from the best to the fallback (ADR 014). KMS may
184+ // refuse an image only at modeset: then the next kind is tried.
185+ for kind := imagesDriver; ; kind++ {
186+ got, err := o.setupImages(r, kind)
187+ if err == nil && enabled {
188+ if err = o.modeset(); err != nil {
189+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
190+ o.freeImages()
191+ }
192+ }
193+ if err == nil {
194+ break
195+ }
196+ if got == imagesDumb {
188197 return err
189198 }
199+ kind = got
190200 }
191201 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
192202 var scene ports.Scene
@@ -286,6 +296,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
286296 start := time.Now()
287297 direct, err := o.tryScanout(scene, surfaces)
288298 if !direct {
299+ r.UseTarget(o.back)
289300 if err := r.Render(scene, surfaces); err != nil {
290301 return fmt.Errorf("render frame: %w", err)
291302 }
@@ -349,3 +360,81 @@ func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
349360 default:
350361 }
351362 }
363+
364+// imageKind is how output images are made, best first.
365+type imageKind int
366+
367+const (
368+ imagesDriver imageKind = iota // exported, modifier chosen by the driver
369+ imagesLinear // exported, linear
370+ imagesDumb // dumb buffers filled by CPU copies
371+)
372+
373+// setupImages gives the output the two images it flips between: renderer
374+// targets exported as dmabufs, or dumb buffers as a logged fallback. A
375+// kind that fails falls through to the next one.
376+func (o *Output) setupImages(r ports.Renderer, kind imageKind) (imageKind, error) {
377+ var err error
378+ for ; kind < imagesDumb; kind++ {
379+ mods := []uint64(nil)
380+ if kind == imagesLinear {
381+ mods = []uint64{0}
382+ }
383+ if err = o.exportImages(r, mods); err == nil {
384+ return kind, nil
385+ }
386+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
387+ }
388+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
389+ for i := range o.bufs {
390+ if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
391+ o.freeImages()
392+ return imagesDumb, err
393+ }
394+ o.fbs[i] = o.bufs[i].fbID
395+ }
396+ return imagesDumb, nil
397+}
398+
399+// exportImages makes the renderer's exported targets the output images.
400+func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
401+ bufs, err := r.ExportTargets(len(o.fbs), mods)
402+ if err != nil {
403+ return err
404+ }
405+ for i := range bufs {
406+ if err == nil {
407+ o.fbs[i], err = o.card.addFB(&bufs[i])
408+ }
409+ bufs[i].Planes[0].File.Close()
410+ }
411+ // GPU memory starts undefined (old VRAM contents): clear both images
412+ // before the modeset shows one.
413+ for i := range o.fbs {
414+ if err != nil {
415+ break
416+ }
417+ r.UseTarget(i)
418+ err = r.Render(ports.Scene{Background: "#000000"}, nil)
419+ }
420+ if err != nil {
421+ o.freeImages()
422+ return err
423+ }
424+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
425+ return nil
426+}
427+
428+// freeImages removes the output images; dumb buffers own their fb.
429+func (o *Output) freeImages() {
430+ for i, fb := range o.fbs {
431+ if b := o.bufs[i]; b != nil {
432+ b.destroy(o.fd)
433+ o.bufs[i] = nil
434+ } else if fb != 0 {
435+ v := fb
436+ _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v))
437+ }
438+ o.fbs[i] = 0
439+ }
440+}
@@ -7,6 +7,7 @@ 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"
@@ -16,13 +17,18 @@ import (
16 // Output is one KMS connector driven with two dumb buffers. One goroutine17 // Output is one KMS connector driven with two dumb buffers. 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
@@ -52,12 +58,6 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er
52 if o.saved, err = getCrtc(card.fd, crtc); err != nil {58 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")59 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
54 }60 }
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 {61 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")62 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
63 }63 }
@@ -86,19 +86,20 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) }
86 func (o *Output) modeset() error {86 func (o *Output) modeset() error {
87 o.pending = false87 o.pending = false
88 o.shown, o.queued = 0, 088 o.shown, o.queued = 0, 0
89- front := o.bufs[1-o.back]89+ 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)90 return fmt.Errorf("set crtc: %w", err)
92 }91 }
93 o.log.Info().Str("connector", o.conn.name).Msg("modeset")92 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
94 return nil93 return nil
95 }94 }
96 95
97-// present copies img into the back buffer and queues a page flip.96+// present queues a page flip to the frame just drawn into the back image
97+// (copied into it by the CPU on the dumb-buffer fallback).
98 func (o *Output) present(r ports.Renderer) error {98 func (o *Output) present(r ports.Renderer) error {
99- b := o.bufs[o.back]99+ if b := o.bufs[o.back]; b != nil {
100- r.CopyBGRX(b.mem, int(b.pitch))100+ r.CopyBGRX(b.mem, int(b.pitch))
101- if err := flip(o.fd, o.crtc, b.fbID); err != nil {101+ }
102+ if err := flip(o.fd, o.crtc, o.fbs[o.back]); err != nil {
102 return err103 return err
103 }104 }
104 o.pending, o.flipStart = true, time.Now()105 o.pending, o.flipStart = true, time.Now()
@@ -159,11 +160,7 @@ func (o *Output) Close() {
159 _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)160 _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)
160 }161 }
161 }162 }
162- for _, b := range o.bufs {163+ o.freeImages()
163- if b != nil {
164- b.destroy(o.fd)
165- }
166- }
167 o.shown, o.queued = 0, 0164 o.shown, o.queued = 0, 0
168 o.dropClientFBs(time.Now(), true)165 o.dropClientFBs(time.Now(), true)
169 }166 }
@@ -183,10 +180,23 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
183 case enabled = <-active:180 case enabled = <-active:
184 default:181 default:
185 }182 }
186- if enabled {183+ // Output images, from the best to the fallback (ADR 014). KMS may
187- if err := o.modeset(); err != nil {184+ // refuse an image only at modeset: then the next kind is tried.
185+ for kind := imagesDriver; ; kind++ {
186+ got, err := o.setupImages(r, kind)
187+ if err == nil && enabled {
188+ if err = o.modeset(); err != nil {
189+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
190+ o.freeImages()
191+ }
192+ }
193+ if err == nil {
194+ break
195+ }
196+ if got == imagesDumb {
188 return err197 return err
189 }198 }
199+ kind = got
190 }200 }
191 surfaces := make(map[ports.WindowID]ports.SurfaceContent)201 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
192 var scene ports.Scene202 var scene ports.Scene
@@ -286,6 +296,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
286 start := time.Now()296 start := time.Now()
287 direct, err := o.tryScanout(scene, surfaces)297 direct, err := o.tryScanout(scene, surfaces)
288 if !direct {298 if !direct {
299+ r.UseTarget(o.back)
289 if err := r.Render(scene, surfaces); err != nil {300 if err := r.Render(scene, surfaces); err != nil {
290 return fmt.Errorf("render frame: %w", err)301 return fmt.Errorf("render frame: %w", err)
291 }302 }
@@ -349,3 +360,81 @@ func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
349 default:360 default:
350 }361 }
351 }362 }
363+
364+// imageKind is how output images are made, best first.
365+type imageKind int
366+
367+const (
368+ imagesDriver imageKind = iota // exported, modifier chosen by the driver
369+ imagesLinear // exported, linear
370+ imagesDumb // dumb buffers filled by CPU copies
371+)
372+
373+// setupImages gives the output the two images it flips between: renderer
374+// targets exported as dmabufs, or dumb buffers as a logged fallback. A
375+// kind that fails falls through to the next one.
376+func (o *Output) setupImages(r ports.Renderer, kind imageKind) (imageKind, error) {
377+ var err error
378+ for ; kind < imagesDumb; kind++ {
379+ mods := []uint64(nil)
380+ if kind == imagesLinear {
381+ mods = []uint64{0}
382+ }
383+ if err = o.exportImages(r, mods); err == nil {
384+ return kind, nil
385+ }
386+ o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
387+ }
388+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
389+ for i := range o.bufs {
390+ if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
391+ o.freeImages()
392+ return imagesDumb, err
393+ }
394+ o.fbs[i] = o.bufs[i].fbID
395+ }
396+ return imagesDumb, nil
397+}
398+
399+// exportImages makes the renderer's exported targets the output images.
400+func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
401+ bufs, err := r.ExportTargets(len(o.fbs), mods)
402+ if err != nil {
403+ return err
404+ }
405+ for i := range bufs {
406+ if err == nil {
407+ o.fbs[i], err = o.card.addFB(&bufs[i])
408+ }
409+ bufs[i].Planes[0].File.Close()
410+ }
411+ // GPU memory starts undefined (old VRAM contents): clear both images
412+ // before the modeset shows one.
413+ for i := range o.fbs {
414+ if err != nil {
415+ break
416+ }
417+ r.UseTarget(i)
418+ err = r.Render(ports.Scene{Background: "#000000"}, nil)
419+ }
420+ if err != nil {
421+ o.freeImages()
422+ return err
423+ }
424+ o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
425+ return nil
426+}
427+
428+// freeImages removes the output images; dumb buffers own their fb.
429+func (o *Output) freeImages() {
430+ for i, fb := range o.fbs {
431+ if b := o.bufs[i]; b != nil {
432+ b.destroy(o.fd)
433+ o.bufs[i] = nil
434+ } else if fb != 0 {
435+ v := fb
436+ _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v))
437+ }
438+ o.fbs[i] = 0
439+ }
440+}
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 +94 -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,51 @@ func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
719745 }
720746 }
721747
748+// readback copies the last frame into the host buffer, once per frame.
749+func (r *Renderer) readback() error {
750+ if r.readBack || r.last == nil || r.last.image == 0 {
751+ return nil
752+ }
753+ d := r.dd
754+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
755+ return err
756+ }
757+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
758+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
759+ return err
760+ }
761+ t := r.last
762+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
763+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}
764+ if t.exported {
765+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
766+ b.SrcAccessMask = 0
767+ }
768+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
769+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
770+ d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
771+ if t.exported {
772+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
773+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
774+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
775+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
776+ } else {
777+ t.layout = vk.ImageLayoutTransferSrcOptimal
778+ }
779+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
780+ return err
781+ }
782+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
783+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
784+ return err
785+ }
786+ if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
787+ return err
788+ }
789+ r.readBack = true
790+ return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
791+}
792+
722793 func (r *Renderer) Close() {
723794 if r == nil {
724795 return
@@ -728,6 +799,7 @@ func (r *Renderer) Close() {
728799 if r.device != 0 {
729800 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
730801 }
802+ r.dropTargets()
731803 for id, im := range r.imports {
732804 r.release(im)
733805 delete(r.imports, id)
@@ -770,14 +842,7 @@ func (r *Renderer) Close() {
770842 d.FreeMemory(r.device, r.bufferMemory, nil)
771843 r.bufferMemory = 0
772844 }
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- }
845+ r.freeTarget(&r.own)
781846 d.DestroyDevice(r.device, nil)
782847 r.device = 0
783848 } 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,51 @@ func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
719 }745 }
720 }746 }
721 747
748+// readback copies the last frame into the host buffer, once per frame.
749+func (r *Renderer) readback() error {
750+ if r.readBack || r.last == nil || r.last.image == 0 {
751+ return nil
752+ }
753+ d := r.dd
754+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
755+ return err
756+ }
757+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
758+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
759+ return err
760+ }
761+ t := r.last
762+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
763+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}
764+ if t.exported {
765+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
766+ b.SrcAccessMask = 0
767+ }
768+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
769+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
770+ d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
771+ if t.exported {
772+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
773+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
774+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
775+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
776+ } else {
777+ t.layout = vk.ImageLayoutTransferSrcOptimal
778+ }
779+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
780+ return err
781+ }
782+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
783+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
784+ return err
785+ }
786+ if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
787+ return err
788+ }
789+ r.readBack = true
790+ return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
791+}
792+
722 func (r *Renderer) Close() {793 func (r *Renderer) Close() {
723 if r == nil {794 if r == nil {
724 return795 return
@@ -728,6 +799,7 @@ func (r *Renderer) Close() {
728 if r.device != 0 {799 if r.device != 0 {
729 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))800 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
730 }801 }
802+ r.dropTargets()
731 for id, im := range r.imports {803 for id, im := range r.imports {
732 r.release(im)804 r.release(im)
733 delete(r.imports, id)805 delete(r.imports, id)
@@ -770,14 +842,7 @@ func (r *Renderer) Close() {
770 d.FreeMemory(r.device, r.bufferMemory, nil)842 d.FreeMemory(r.device, r.bufferMemory, nil)
771 r.bufferMemory = 0843 r.bufferMemory = 0
772 }844 }
773- if r.image != 0 {845+ 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)846 d.DestroyDevice(r.device, nil)
782 r.device = 0847 r.device = 0
783 } else if r.device != 0 && r.id != nil {848 } else if r.device != 0 && r.id != nil {
A internal/adapters/vulkan/targets.go +203 -0
@@ -0,0 +1,203 @@
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.
36+func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
37+ if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {
38+ return nil, errors.New("device cannot export dmabufs")
39+ }
40+ mods := r.exportModifiers(modifiers)
41+ if len(mods) == 0 {
42+ return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
43+ }
44+ r.dropTargets()
45+ var out []ports.DMABuf
46+ for range n {
47+ t, buf, err := r.exportTarget(mods)
48+ if err != nil {
49+ r.dropTargets()
50+ for _, b := range out {
51+ for _, p := range b.Planes {
52+ p.File.Close()
53+ }
54+ }
55+ return nil, err
56+ }
57+ r.targets = append(r.targets, t)
58+ out = append(out, buf)
59+ }
60+ r.current = 0
61+ return out, nil
62+}
63+
64+// UseTarget selects the exported image the next frames draw into.
65+func (r *Renderer) UseTarget(i int) {
66+ if i >= 0 && i < len(r.targets) {
67+ r.current = i
68+ }
69+}
70+
71+// target is the image the next frame draws into.
72+func (r *Renderer) target() *target {
73+ if len(r.targets) > 0 {
74+ return r.targets[r.current]
75+ }
76+ return &r.own
77+}
78+
79+// exportModifiers are the XRGB8888 modifiers the device renders to and
80+// exports with one plane, restricted to the display's when given.
81+func (r *Renderer) exportModifiers(display []uint64) []uint64 {
82+ var out []uint64
83+ for _, m := range r.renderMods {
84+ if len(display) == 0 || slices.Contains(display, m) {
85+ out = append(out, m)
86+ }
87+ }
88+ return out
89+}
90+
91+// probeRenderModifiers lists the B8G8R8A8 modifiers usable as a transfer
92+// destination with a single plane that the device can export.
93+func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
94+ list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
95+ fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
96+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
97+ if list.DrmFormatModifierCount == 0 {
98+ return
99+ }
100+ mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
101+ list.DrmFormatModifierProperties = &mods[0]
102+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
103+ need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit)
104+ for _, m := range mods[:list.DrmFormatModifierCount] {
105+ if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
106+ r.renderMods = append(r.renderMods, m.DrmFormatModifier)
107+ }
108+ }
109+}
110+
111+func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool {
112+ mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
113+ ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
114+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: vk.FormatB8g8r8a8Unorm, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: targetUsage}
115+ extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
116+ out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
117+ if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
118+ return false
119+ }
120+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
121+}
122+
123+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from
124+// the generated bindings.
125+const (
126+ formatFeatureTransferSrc = 1 << 14
127+ formatFeatureTransferDst = 1 << 15
128+)
129+
130+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit
131+
132+// exportTarget creates one exported image with a modifier from mods.
133+func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
134+ d := r.dd
135+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
136+ ok := false
137+ defer func() {
138+ if !ok {
139+ r.freeTarget(t)
140+ }
141+ }()
142+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
143+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
144+ 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}
145+ if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
146+ return nil, ports.DMABuf{}, err
147+ }
148+ var req vk.MemoryRequirements
149+ d.GetImageMemoryRequirements(r.device, t.image, &req)
150+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
151+ if err != nil {
152+ return nil, ports.DMABuf{}, err
153+ }
154+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
155+ export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
156+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
157+ if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
158+ return nil, ports.DMABuf{}, err
159+ }
160+ if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
161+ return nil, ports.DMABuf{}, err
162+ }
163+ props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
164+ if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
165+ return nil, ports.DMABuf{}, err
166+ }
167+ var layout vk.SubresourceLayout
168+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
169+ d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
170+ var fd int32
171+ get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
172+ if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
173+ return nil, ports.DMABuf{}, err
174+ }
175+ f := os.NewFile(uintptr(fd), "nefertty-target")
176+ if f == nil {
177+ return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
178+ }
179+ 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)}}}
180+ ok = true
181+ return t, buf, nil
182+}
183+
184+func (r *Renderer) freeTarget(t *target) {
185+ if t.image != 0 {
186+ r.dd.DestroyImage(r.device, t.image, nil)
187+ t.image = 0
188+ }
189+ if t.memory != 0 {
190+ r.dd.FreeMemory(r.device, t.memory, nil)
191+ t.memory = 0
192+ }
193+}
194+
195+func (r *Renderer) dropTargets() {
196+ if len(r.targets) > 0 && r.device != 0 {
197+ _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
198+ }
199+ for _, t := range r.targets {
200+ r.freeTarget(t)
201+ }
202+ r.targets, r.current = nil, 0
203+}
@@ -0,0 +1,203 @@
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.
36+func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
37+ if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {
38+ return nil, errors.New("device cannot export dmabufs")
39+ }
40+ mods := r.exportModifiers(modifiers)
41+ if len(mods) == 0 {
42+ return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
43+ }
44+ r.dropTargets()
45+ var out []ports.DMABuf
46+ for range n {
47+ t, buf, err := r.exportTarget(mods)
48+ if err != nil {
49+ r.dropTargets()
50+ for _, b := range out {
51+ for _, p := range b.Planes {
52+ p.File.Close()
53+ }
54+ }
55+ return nil, err
56+ }
57+ r.targets = append(r.targets, t)
58+ out = append(out, buf)
59+ }
60+ r.current = 0
61+ return out, nil
62+}
63+
64+// UseTarget selects the exported image the next frames draw into.
65+func (r *Renderer) UseTarget(i int) {
66+ if i >= 0 && i < len(r.targets) {
67+ r.current = i
68+ }
69+}
70+
71+// target is the image the next frame draws into.
72+func (r *Renderer) target() *target {
73+ if len(r.targets) > 0 {
74+ return r.targets[r.current]
75+ }
76+ return &r.own
77+}
78+
79+// exportModifiers are the XRGB8888 modifiers the device renders to and
80+// exports with one plane, restricted to the display's when given.
81+func (r *Renderer) exportModifiers(display []uint64) []uint64 {
82+ var out []uint64
83+ for _, m := range r.renderMods {
84+ if len(display) == 0 || slices.Contains(display, m) {
85+ out = append(out, m)
86+ }
87+ }
88+ return out
89+}
90+
91+// probeRenderModifiers lists the B8G8R8A8 modifiers usable as a transfer
92+// destination with a single plane that the device can export.
93+func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
94+ list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
95+ fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
96+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
97+ if list.DrmFormatModifierCount == 0 {
98+ return
99+ }
100+ mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
101+ list.DrmFormatModifierProperties = &mods[0]
102+ r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
103+ need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit)
104+ for _, m := range mods[:list.DrmFormatModifierCount] {
105+ if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
106+ r.renderMods = append(r.renderMods, m.DrmFormatModifier)
107+ }
108+ }
109+}
110+
111+func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool {
112+ mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
113+ ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
114+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: vk.FormatB8g8r8a8Unorm, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: targetUsage}
115+ extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
116+ out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
117+ if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
118+ return false
119+ }
120+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
121+}
122+
123+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from
124+// the generated bindings.
125+const (
126+ formatFeatureTransferSrc = 1 << 14
127+ formatFeatureTransferDst = 1 << 15
128+)
129+
130+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit
131+
132+// exportTarget creates one exported image with a modifier from mods.
133+func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
134+ d := r.dd
135+ t := &target{exported: true, layout: vk.ImageLayoutUndefined}
136+ ok := false
137+ defer func() {
138+ if !ok {
139+ r.freeTarget(t)
140+ }
141+ }()
142+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: uint32(len(mods)), DrmFormatModifiers: &mods[0]}
143+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
144+ 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}
145+ if err := checked("vkCreateImage(target)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
146+ return nil, ports.DMABuf{}, err
147+ }
148+ var req vk.MemoryRequirements
149+ d.GetImageMemoryRequirements(r.device, t.image, &req)
150+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
151+ if err != nil {
152+ return nil, ports.DMABuf{}, err
153+ }
154+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: t.image}
155+ export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
156+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
157+ if err := checked("vkAllocateMemory(target)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
158+ return nil, ports.DMABuf{}, err
159+ }
160+ if err := checked("vkBindImageMemory(target)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
161+ return nil, ports.DMABuf{}, err
162+ }
163+ props := vk.ImageDrmFormatModifierPropertiesEXT{SType: vk.StructureTypeImageDRMFormatModifierPropertiesEXT}
164+ if err := checked("vkGetImageDrmFormatModifierPropertiesEXT", d.GetImageDrmFormatModifierPropertiesEXT(r.device, t.image, &props)); err != nil {
165+ return nil, ports.DMABuf{}, err
166+ }
167+ var layout vk.SubresourceLayout
168+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
169+ d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
170+ var fd int32
171+ get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
172+ if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
173+ return nil, ports.DMABuf{}, err
174+ }
175+ f := os.NewFile(uintptr(fd), "nefertty-target")
176+ if f == nil {
177+ return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
178+ }
179+ 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)}}}
180+ ok = true
181+ return t, buf, nil
182+}
183+
184+func (r *Renderer) freeTarget(t *target) {
185+ if t.image != 0 {
186+ r.dd.DestroyImage(r.device, t.image, nil)
187+ t.image = 0
188+ }
189+ if t.memory != 0 {
190+ r.dd.FreeMemory(r.device, t.memory, nil)
191+ t.memory = 0
192+ }
193+}
194+
195+func (r *Renderer) dropTargets() {
196+ if len(r.targets) > 0 && r.device != 0 {
197+ _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
198+ }
199+ for _, t := range r.targets {
200+ r.freeTarget(t)
201+ }
202+ r.targets, r.current = nil, 0
203+}
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 +4 -0
@@ -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}},
@@ -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}},
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 +12 -3
@@ -2,12 +2,21 @@ 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+ ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
14+ // UseTarget selects the exported image the next Render draws into.
15+ UseTarget(i int)
16+ // Pixels reads the last frame back (headless screenshots, tests).
917 Pixels() *image.RGBA
10- // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch (DRM scanout).
18+ // CopyBGRX reads the last frame back as XRGB8888 rows of the given
19+ // pitch: the logged fallback when no image can be exported.
1120 CopyBGRX(dst []byte, pitch int)
1221 Close()
1322 }
@@ -2,12 +2,21 @@ 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+ ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
14+ // UseTarget selects the exported image the next Render draws into.
15+ UseTarget(i int)
16+ // Pixels reads the last frame back (headless screenshots, tests).
9 Pixels() *image.RGBA17 Pixels() *image.RGBA
10- // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch (DRM scanout).18+ // CopyBGRX reads the last frame back as XRGB8888 rows of the given
19+ // pitch: the logged fallback when no image can be exported.
11 CopyBGRX(dst []byte, pitch int)20 CopyBGRX(dst []byte, pitch int)
12 Close()21 Close()
13 }22 }

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 25

Tags 10

↑↓ moveenter openesc close