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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +607 -64
M internal/adapters/drm/output.go +115 -26
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "maps" | |
| 8 | 8 | "slices" | |
| 9 | 9 | "time" | |
| 10 | + | "unsafe" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/nefertty/internal/ports" | |
| 12 | 13 | "github.com/bnema/zerowrap" | |
| @@ -16,13 +17,18 @@ import ( | |||
| 16 | 17 | // Output is one KMS connector driven with two dumb buffers. One goroutine | |
| 17 | 18 | // runs it (Run) and closes it. | |
| 18 | 19 | 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 | |
| 26 | 32 | back int | |
| 27 | 33 | pending bool | |
| 28 | 34 | flipStart time.Time | |
| @@ -52,12 +58,6 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er | |||
| 52 | 58 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 53 | 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 | 61 | if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil { | |
| 62 | 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 | 86 | func (o *Output) modeset() error { | |
| 87 | 87 | o.pending = false | |
| 88 | 88 | 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 { | |
| 91 | 90 | return fmt.Errorf("set crtc: %w", err) | |
| 92 | 91 | } | |
| 93 | 92 | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |
| 94 | 93 | 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 | 98 | 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 { | |
| 102 | 103 | return err | |
| 103 | 104 | } | |
| 104 | 105 | o.pending, o.flipStart = true, time.Now() | |
| @@ -159,11 +160,7 @@ func (o *Output) Close() { | |||
| 159 | 160 | _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode) | |
| 160 | 161 | } | |
| 161 | 162 | } | |
| 162 | - | for _, b := range o.bufs { | |
| 163 | - | if b != nil { | |
| 164 | - | b.destroy(o.fd) | |
| 165 | - | } | |
| 166 | - | } | |
| 163 | + | o.freeImages() | |
| 167 | 164 | o.shown, o.queued = 0, 0 | |
| 168 | 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 | 180 | case enabled = <-active: | |
| 184 | 181 | default: | |
| 185 | 182 | } | |
| 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 { | |
| 188 | 197 | return err | |
| 189 | 198 | } | |
| 199 | + | kind = got | |
| 190 | 200 | } | |
| 191 | 201 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | |
| 192 | 202 | var scene ports.Scene | |
| @@ -286,6 +296,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 286 | 296 | start := time.Now() | |
| 287 | 297 | direct, err := o.tryScanout(scene, surfaces) | |
| 288 | 298 | if !direct { | |
| 299 | + | r.UseTarget(o.back) | |
| 289 | 300 | if err := r.Render(scene, surfaces); err != nil { | |
| 290 | 301 | return fmt.Errorf("render frame: %w", err) | |
| 291 | 302 | } | |
| @@ -349,3 +360,81 @@ func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |||
| 349 | 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 | + | } | |
| @@ -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 goroutine | 17 | // 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 int | 20 | + | fd int |
| 20 | - | flipped <-chan int // page flips of this CRTC, from Card.ReadEvents | 21 | + | flipped <-chan int // page flips of this CRTC, from Card.ReadEvents |
| 21 | - | crtc uint32 | 22 | + | crtc uint32 |
| 22 | - | conn connector | 23 | + | conn connector |
| 23 | - | mode modeInfo | 24 | + | mode modeInfo |
| 24 | - | saved modeCrtc | 25 | + | saved modeCrtc |
| 25 | - | bufs [2]*dumbBuffer | 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 | |||
| 26 | back int | 32 | back int | ||
| 27 | pending bool | 33 | pending bool | ||
| 28 | flipStart time.Time | 34 | 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 = false | 87 | o.pending = false | ||
| 88 | o.shown, o.queued = 0, 0 | 88 | 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 nil | 93 | 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 err | 103 | 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, 0 | 164 | 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 err | 197 | 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.Scene | 202 | 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) { | |||
| 100 | 100 | } | |
| 101 | 101 | fb := &clientFB{last: now} | |
| 102 | 102 | o.clientFBs[b.ID] = fb | |
| 103 | - | id, err := o.card.addClientFB(b) | |
| 103 | + | id, err := o.card.addFB(b) | |
| 104 | 104 | if err != nil { | |
| 105 | 105 | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |
| 106 | 106 | fb.failed = "import_failed" | |
| @@ -127,11 +127,12 @@ const ( | |||
| 127 | 127 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 128 | 128 | ) | |
| 129 | 129 | ||
| 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) { | |
| 135 | 136 | format, ok := scanoutFormat(b.Format) | |
| 136 | 137 | if !ok { | |
| 137 | 138 | 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] = fb | 102 | 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'<<24 | 127 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | ||
| 128 | ) | 128 | ) | ||
| 129 | 129 | ||||
| 130 | - | // addClientFB imports a dmabuf as a framebuffer. The GEM handle belongs to | 130 | + | // 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 right | 132 | + | // 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 { | |||
| 23 | 23 | device vk.Device | |
| 24 | 24 | dd *vk.DeviceDispatch | |
| 25 | 25 | 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 | |
| 28 | 36 | buffer vk.Buffer | |
| 29 | 37 | bufferMemory vk.DeviceMemory | |
| 30 | 38 | staging vk.Buffer | |
| @@ -35,7 +43,6 @@ type Renderer struct { | |||
| 35 | 43 | pool vk.CommandPool | |
| 36 | 44 | command vk.CommandBuffer | |
| 37 | 45 | fence vk.Fence | |
| 38 | - | layout vk.ImageLayout | |
| 39 | 46 | memory vk.PhysicalDeviceMemoryProperties | |
| 40 | 47 | family uint32 | |
| 41 | 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 | 72 | if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) { | |
| 66 | 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 | 76 | defer func() { | |
| 70 | 77 | if err != nil { | |
| 71 | 78 | r.Close() | |
| @@ -169,26 +176,27 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 169 | 176 | // Clients need the render node to allocate on the right GPU. | |
| 170 | 177 | if sup := r.probeDMABuf(physical); sup.Device != 0 { | |
| 171 | 178 | r.dmabuf = sup | |
| 179 | + | r.probeRenderModifiers(physical) | |
| 172 | 180 | } | |
| 173 | 181 | } | |
| 174 | 182 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | |
| 175 | 183 | extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1} | |
| 176 | 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 | 186 | return | |
| 179 | 187 | } | |
| 180 | 188 | var req vk.MemoryRequirements | |
| 181 | - | r.dd.GetImageMemoryRequirements(r.device, r.image, &req) | |
| 189 | + | r.dd.GetImageMemoryRequirements(r.device, r.own.image, &req) | |
| 182 | 190 | var kind uint32 | |
| 183 | 191 | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit) | |
| 184 | 192 | if err != nil { | |
| 185 | 193 | return | |
| 186 | 194 | } | |
| 187 | 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 | 197 | 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 | 200 | return | |
| 193 | 201 | } | |
| 194 | 202 | size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4 | |
| @@ -197,7 +205,7 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 197 | 205 | return | |
| 198 | 206 | } | |
| 199 | 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 | 209 | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit) | |
| 202 | 210 | if err != nil { | |
| 203 | 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 | 583 | return err | |
| 576 | 584 | } | |
| 577 | 585 | rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | |
| 586 | + | tg := r.target() | |
| 578 | 587 | srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit) | |
| 579 | 588 | srcAccess := vk.AccessFlags(0) | |
| 580 | - | if r.layout == vk.ImageLayoutTransferSrcOptimal { | |
| 589 | + | if tg.layout == vk.ImageLayoutTransferSrcOptimal { | |
| 581 | 590 | srcStage = vk.PipelineStageTransferBit | |
| 582 | 591 | 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 | 601 | d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 602 | + | barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0 | |
| 586 | 603 | // VkClearColorValue is a union; UNORM formats read it as float32. | |
| 587 | 604 | unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) } | |
| 588 | 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 | 607 | barrier.SrcAccessMask = vk.AccessTransferWriteBit | |
| 591 | 608 | barrier.DstAccessMask = vk.AccessTransferWriteBit | |
| 592 | 609 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| @@ -633,7 +650,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 633 | 650 | DstSubresource: layers, | |
| 634 | 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 | 654 | continue | |
| 638 | 655 | } | |
| 639 | 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 | 661 | h := min(fillRows, rect.Max.Y-y) | |
| 645 | 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 | 665 | continue | |
| 649 | 666 | } | |
| 650 | 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, ®ion) | |
| 668 | + | d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |
| 652 | 669 | } | |
| 653 | 670 | ownership(false) | |
| 671 | + | barrier.SrcAccessMask = vk.AccessTransferWriteBit | |
| 654 | 672 | barrier.DstAccessMask = vk.AccessTransferReadBit | |
| 655 | 673 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 656 | 674 | 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, ©Region) | |
| 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 | + | } | |
| 660 | 684 | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil { | |
| 661 | 685 | return err | |
| 662 | 686 | } | |
| @@ -684,7 +708,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 684 | 708 | return err | |
| 685 | 709 | } | |
| 686 | 710 | 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. | |
| 688 | 714 | if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil { | |
| 689 | 715 | return err | |
| 690 | 716 | } | |
| @@ -695,7 +721,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 695 | 721 | ||
| 696 | 722 | func (r *Renderer) Pixels() *image.RGBA { | |
| 697 | 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 | 725 | return out | |
| 700 | 726 | } | |
| 701 | 727 | src := unsafe.Slice((*byte)(r.mapped), len(out.Pix)) | |
| @@ -709,7 +735,7 @@ func (r *Renderer) Pixels() *image.RGBA { | |||
| 709 | 735 | // CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch. | |
| 710 | 736 | // The image is B8G8R8A8, which is already XRGB8888 in memory. | |
| 711 | 737 | func (r *Renderer) CopyBGRX(dst []byte, pitch int) { | |
| 712 | - | if r.mapped == nil { | |
| 738 | + | if r.mapped == nil || r.readback() != nil { | |
| 713 | 739 | return | |
| 714 | 740 | } | |
| 715 | 741 | 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, ®ion) | |
| 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 | 793 | func (r *Renderer) Close() { | |
| 723 | 794 | if r == nil { | |
| 724 | 795 | return | |
| @@ -728,6 +799,7 @@ func (r *Renderer) Close() { | |||
| 728 | 799 | if r.device != 0 { | |
| 729 | 800 | _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device)) | |
| 730 | 801 | } | |
| 802 | + | r.dropTargets() | |
| 731 | 803 | for id, im := range r.imports { | |
| 732 | 804 | r.release(im) | |
| 733 | 805 | delete(r.imports, id) | |
| @@ -770,14 +842,7 @@ func (r *Renderer) Close() { | |||
| 770 | 842 | d.FreeMemory(r.device, r.bufferMemory, nil) | |
| 771 | 843 | r.bufferMemory = 0 | |
| 772 | 844 | } | |
| 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) | |
| 781 | 846 | d.DestroyDevice(r.device, nil) | |
| 782 | 847 | r.device = 0 | |
| 783 | 848 | } else if r.device != 0 && r.id != nil { | |
| @@ -23,8 +23,16 @@ type Renderer struct { | |||||
| 23 | device vk.Device | 23 | device vk.Device | ||
| 24 | dd *vk.DeviceDispatch | 24 | dd *vk.DeviceDispatch | ||
| 25 | queue vk.Queue | 25 | queue vk.Queue | ||
| 26 | - | image vk.Image | 26 | + | // own is the target until ExportTargets; targets are exported images |
| 27 | - | imageMemory vk.DeviceMemory | 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 | |||
| 28 | buffer vk.Buffer | 36 | buffer vk.Buffer | ||
| 29 | bufferMemory vk.DeviceMemory | 37 | bufferMemory vk.DeviceMemory | ||
| 30 | staging vk.Buffer | 38 | staging vk.Buffer | ||
| @@ -35,7 +43,6 @@ type Renderer struct { | |||||
| 35 | pool vk.CommandPool | 43 | pool vk.CommandPool | ||
| 36 | command vk.CommandBuffer | 44 | command vk.CommandBuffer | ||
| 37 | fence vk.Fence | 45 | fence vk.Fence | ||
| 38 | - | layout vk.ImageLayout | |||
| 39 | memory vk.PhysicalDeviceMemoryProperties | 46 | memory vk.PhysicalDeviceMemoryProperties | ||
| 40 | family uint32 | 47 | 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 = sup | 178 | 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 | return | 186 | return | ||
| 179 | } | 187 | } | ||
| 180 | var req vk.MemoryRequirements | 188 | 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 uint32 | 190 | 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 | return | 193 | 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 | return | 197 | 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 | return | 200 | return | ||
| 193 | } | 201 | } | ||
| 194 | size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4 | 202 | size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4 | ||
| @@ -197,7 +205,7 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 197 | return | 205 | 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 err | 583 | 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.PipelineStageTransferBit | 590 | srcStage = vk.PipelineStageTransferBit | ||
| 582 | srcAccess = vk.AccessTransferReadBit | 591 | 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.AccessTransferWriteBit | 607 | barrier.SrcAccessMask = vk.AccessTransferWriteBit | ||
| 591 | barrier.DstAccessMask = vk.AccessTransferWriteBit | 608 | barrier.DstAccessMask = vk.AccessTransferWriteBit | ||
| 592 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | 609 | 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 | continue | 654 | 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 | continue | 665 | 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, ®ion) | 668 | + | d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) |
| 652 | } | 669 | } | ||
| 653 | ownership(false) | 670 | ownership(false) | ||
| 671 | + | barrier.SrcAccessMask = vk.AccessTransferWriteBit | |||
| 654 | barrier.DstAccessMask = vk.AccessTransferReadBit | 672 | barrier.DstAccessMask = vk.AccessTransferReadBit | ||
| 655 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | 673 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | ||
| 656 | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | 674 | 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, ©Region) | 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 err | 685 | return err | ||
| 662 | } | 686 | } | ||
| @@ -684,7 +708,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 684 | return err | 708 | return err | ||
| 685 | } | 709 | } | ||
| 686 | runtime.KeepAlive(stages) | 710 | 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. | |||
| 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 err | 715 | 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 out | 725 | 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 | return | 739 | return | ||
| 714 | } | 740 | } | ||
| 715 | row := r.width * 4 | 741 | 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, ®ion) | |||
| 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 | return | 795 | 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 = 0 | 843 | 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 = 0 | 847 | 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) { | |||
| 457 | 457 | <-r.state // the channel holds the latest snapshot only | |
| 458 | 458 | r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200} | |
| 459 | 459 | receive(t, r.scenes) | |
| 460 | + | // Snapshots from before window 2 may still be queued: wait for it. | |
| 460 | 461 | st := receive(t, r.state) | |
| 462 | + | for len(st.Windows) < 2 { | |
| 463 | + | st = receive(t, r.state) | |
| 464 | + | } | |
| 461 | 465 | want := ports.State{ | |
| 462 | 466 | Output: "DP-2", | |
| 463 | 467 | 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 only | 457 | <-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 | |||
| 126 | 126 | 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 | 197 | // Pixels provides a mock function for the type MockRenderer | |
| 130 | 198 | func (_mock *MockRenderer) Pixels() *image.RGBA { | |
| 131 | 199 | ret := _mock.Called() | |
| @@ -229,6 +297,46 @@ func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, win | |||
| 229 | 297 | 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 | 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 | 341 | // The first argument is typically a *testing.T value. | |
| 234 | 342 | func NewMockSeat(t interface { | |
| @@ -126,6 +126,74 @@ func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch in | |||||
| 126 | return _c | 126 | 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 MockRenderer | 197 | // 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 _c | 297 | 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 | |||
| 2 | 2 | ||
| 3 | 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 | 8 | type Renderer interface { | |
| 7 | 9 | 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 | 17 | 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 | 20 | CopyBGRX(dst []byte, pitch int) | |
| 12 | 21 | Close() | |
| 13 | 22 | } | |
| @@ -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) error | 9 | 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.RGBA | 17 | 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 | } | ||