detached 1941387

fix(render): drop exported targets on fallback, validate images on first enable, host-read barrier

commit
19413872d4a3fdd1a99ad13afa46d67edaf32476
author
brice · · 2026-09-25 17:32 UTC
parent
427d358
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +71 -26
M internal/adapters/drm/output.go +50 -22
@@ -14,7 +14,7 @@ import (
1414 "golang.org/x/sys/unix"
1515 )
1616
17-// Output is one KMS connector driven with two dumb buffers. One goroutine
17+// Output is one KMS connector flipping between two images. One goroutine
1818 // runs it (Run) and closes it.
1919 type Output struct {
2020 fd int
@@ -44,13 +44,16 @@ type Output struct {
4444 shown, queued uint64
4545 reason string // why the last frame was composed ("" = scanout)
4646 unsent ports.OutputPresented
47+ // kind is how the images were made; validated once a modeset took them.
48+ kind imageKind
49+ validated bool
4750 }
4851
4952 // CursorLoader returns the image of a cursor at an output scale, at most
5053 // limit pixels on a side; an empty image hides the cursor.
5154 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
5255
53-// newOutput allocates buffers and the cursor for a connector on crtc.
56+// newOutput prepares the cursor and saves the CRTC for a connector on crtc.
5457 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
5558 log := card.log
5659 o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
@@ -180,23 +183,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
180183 case enabled = <-active:
181184 default:
182185 }
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 {
197- return err
198- }
199- kind = got
186+ // Output images, from the best to the fallback (ADR 014). While
187+ // switched away they are validated by the first modeset on enable.
188+ if enabled {
189+ err = o.showImages(r, imagesDriver, nil)
190+ } else {
191+ o.kind, err = o.setupImages(r, imagesDriver, nil)
192+ }
193+ if err != nil {
194+ return err
200195 }
201196 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
202197 var scene ports.Scene
@@ -231,10 +226,17 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
231226 case on := <-active:
232227 // Modeset on every enable: a fast disable+enable can coalesce to one true.
233228 if on {
234- if err := o.modeset(); err != nil {
229+ err := o.modeset()
230+ if err != nil && !o.validated && o.kind < imagesDumb {
231+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images")
232+ o.freeImages()
233+ err = o.showImages(r, o.kind+1, err)
234+ }
235+ if err != nil {
235236 o.log.Error().Err(err).Msg("resume")
236237 return err
237238 }
239+ o.validated = true
238240 dirty = haveScene
239241 if o.cursor != nil {
240242 if err := o.cursor.Reapply(); err != nil {
@@ -370,11 +372,34 @@ const (
370372 imagesDumb // dumb buffers filled by CPU copies
371373 )
372374
375+// showImages sets up images from kind on and modesets them. KMS may refuse
376+// an image only at modeset: then the next kind is tried. cause is why the
377+// previous kind failed, for the log.
378+func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
379+ for {
380+ got, err := o.setupImages(r, kind, cause)
381+ if err != nil {
382+ return err
383+ }
384+ o.kind = got
385+ if err = o.modeset(); err == nil {
386+ o.validated = true
387+ return nil
388+ }
389+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
390+ o.freeImages()
391+ if got == imagesDumb {
392+ return err
393+ }
394+ kind, cause = got+1, err
395+ }
396+}
397+
373398 // setupImages gives the output the two images it flips between: renderer
374399 // targets exported as dmabufs, or dumb buffers as a logged fallback. A
375400 // 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
401+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
402+ err := cause
378403 for ; kind < imagesDumb; kind++ {
379404 mods := []uint64(nil)
380405 if kind == imagesLinear {
@@ -386,6 +411,8 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind) (imageKind, error
386411 o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
387412 }
388413 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
414+ // The renderer draws into its own image again, read back by CopyBGRX.
415+ _, _ = r.ExportTargets(0, nil)
389416 for i := range o.bufs {
390417 if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
391418 o.freeImages()
@@ -419,6 +446,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
419446 }
420447 if err != nil {
421448 o.freeImages()
449+ _, _ = r.ExportTargets(0, nil)
422450 return err
423451 }
424452 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
@@ -14,7 +14,7 @@ import (
14 "golang.org/x/sys/unix"14 "golang.org/x/sys/unix"
15 )15 )
16 16
17-// Output is one KMS connector driven with two dumb buffers. One goroutine17+// Output is one KMS connector flipping between two images. One goroutine
18 // runs it (Run) and closes it.18 // runs it (Run) and closes it.
19 type Output struct {19 type Output struct {
20 fd int20 fd int
@@ -44,13 +44,16 @@ type Output struct {
44 shown, queued uint6444 shown, queued uint64
45 reason string // why the last frame was composed ("" = scanout)45 reason string // why the last frame was composed ("" = scanout)
46 unsent ports.OutputPresented46 unsent ports.OutputPresented
47+ // kind is how the images were made; validated once a modeset took them.
48+ kind imageKind
49+ validated bool
47 }50 }
48 51
49 // CursorLoader returns the image of a cursor at an output scale, at most52 // CursorLoader returns the image of a cursor at an output scale, at most
50 // limit pixels on a side; an empty image hides the cursor.53 // limit pixels on a side; an empty image hides the cursor.
51 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)54 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
52 55
53-// newOutput allocates buffers and the cursor for a connector on crtc.56+// newOutput prepares the cursor and saves the CRTC for a connector on crtc.
54 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {57 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
55 log := card.log58 log := card.log
56 o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}59 o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
@@ -180,23 +183,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
180 case enabled = <-active:183 case enabled = <-active:
181 default:184 default:
182 }185 }
183- // Output images, from the best to the fallback (ADR 014). KMS may186+ // Output images, from the best to the fallback (ADR 014). While
184- // refuse an image only at modeset: then the next kind is tried.187+ // switched away they are validated by the first modeset on enable.
185- for kind := imagesDriver; ; kind++ {188+ if enabled {
186- got, err := o.setupImages(r, kind)189+ err = o.showImages(r, imagesDriver, nil)
187- if err == nil && enabled {190+ } else {
188- if err = o.modeset(); err != nil {191+ o.kind, err = o.setupImages(r, imagesDriver, nil)
189- o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")192+ }
190- o.freeImages()193+ if err != nil {
191- }194+ return err
192- }
193- if err == nil {
194- break
195- }
196- if got == imagesDumb {
197- return err
198- }
199- kind = got
200 }195 }
201 surfaces := make(map[ports.WindowID]ports.SurfaceContent)196 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
202 var scene ports.Scene197 var scene ports.Scene
@@ -231,10 +226,17 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
231 case on := <-active:226 case on := <-active:
232 // Modeset on every enable: a fast disable+enable can coalesce to one true.227 // Modeset on every enable: a fast disable+enable can coalesce to one true.
233 if on {228 if on {
234- if err := o.modeset(); err != nil {229+ err := o.modeset()
230+ if err != nil && !o.validated && o.kind < imagesDumb {
231+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images")
232+ o.freeImages()
233+ err = o.showImages(r, o.kind+1, err)
234+ }
235+ if err != nil {
235 o.log.Error().Err(err).Msg("resume")236 o.log.Error().Err(err).Msg("resume")
236 return err237 return err
237 }238 }
239+ o.validated = true
238 dirty = haveScene240 dirty = haveScene
239 if o.cursor != nil {241 if o.cursor != nil {
240 if err := o.cursor.Reapply(); err != nil {242 if err := o.cursor.Reapply(); err != nil {
@@ -370,11 +372,34 @@ const (
370 imagesDumb // dumb buffers filled by CPU copies372 imagesDumb // dumb buffers filled by CPU copies
371 )373 )
372 374
375+// showImages sets up images from kind on and modesets them. KMS may refuse
376+// an image only at modeset: then the next kind is tried. cause is why the
377+// previous kind failed, for the log.
378+func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
379+ for {
380+ got, err := o.setupImages(r, kind, cause)
381+ if err != nil {
382+ return err
383+ }
384+ o.kind = got
385+ if err = o.modeset(); err == nil {
386+ o.validated = true
387+ return nil
388+ }
389+ o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
390+ o.freeImages()
391+ if got == imagesDumb {
392+ return err
393+ }
394+ kind, cause = got+1, err
395+ }
396+}
397+
373 // setupImages gives the output the two images it flips between: renderer398 // setupImages gives the output the two images it flips between: renderer
374 // targets exported as dmabufs, or dumb buffers as a logged fallback. A399 // targets exported as dmabufs, or dumb buffers as a logged fallback. A
375 // kind that fails falls through to the next one.400 // kind that fails falls through to the next one.
376-func (o *Output) setupImages(r ports.Renderer, kind imageKind) (imageKind, error) {401+func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
377- var err error402+ err := cause
378 for ; kind < imagesDumb; kind++ {403 for ; kind < imagesDumb; kind++ {
379 mods := []uint64(nil)404 mods := []uint64(nil)
380 if kind == imagesLinear {405 if kind == imagesLinear {
@@ -386,6 +411,8 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind) (imageKind, error
386 o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")411 o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
387 }412 }
388 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")413 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
414+ // The renderer draws into its own image again, read back by CopyBGRX.
415+ _, _ = r.ExportTargets(0, nil)
389 for i := range o.bufs {416 for i := range o.bufs {
390 if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {417 if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
391 o.freeImages()418 o.freeImages()
@@ -419,6 +446,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
419 }446 }
420 if err != nil {447 if err != nil {
421 o.freeImages()448 o.freeImages()
449+ _, _ = r.ExportTargets(0, nil)
422 return err450 return err
423 }451 }
424 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")452 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
M internal/adapters/vulkan/renderer.go +9 -0
@@ -746,6 +746,12 @@ func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
746746 }
747747
748748 // readback copies the last frame into the host buffer, once per frame.
749+// Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
750+const (
751+ queueFamilyIgnored = ^uint32(0)
752+ wholeSize = ^vk.DeviceSize(0)
753+)
754+
749755 func (r *Renderer) readback() error {
750756 if r.readBack || r.last == nil || r.last.image == 0 {
751757 return nil
@@ -768,6 +774,9 @@ func (r *Renderer) readback() error {
768774 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
769775 region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
770776 d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
777+ // Make the copy visible to host reads of the mapped buffer.
778+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
779+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
771780 if t.exported {
772781 b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
773782 b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
@@ -746,6 +746,12 @@ func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
746 }746 }
747 747
748 // readback copies the last frame into the host buffer, once per frame.748 // readback copies the last frame into the host buffer, once per frame.
749+// Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
750+const (
751+ queueFamilyIgnored = ^uint32(0)
752+ wholeSize = ^vk.DeviceSize(0)
753+)
754+
749 func (r *Renderer) readback() error {755 func (r *Renderer) readback() error {
750 if r.readBack || r.last == nil || r.last.image == 0 {756 if r.readBack || r.last == nil || r.last.image == 0 {
751 return nil757 return nil
@@ -768,6 +774,9 @@ func (r *Renderer) readback() error {
768 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)774 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}}775 region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
770 d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)776 d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
777+ // Make the copy visible to host reads of the mapped buffer.
778+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
779+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
771 if t.exported {780 if t.exported {
772 b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral781 b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
773 b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0782 b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
M internal/adapters/vulkan/targets.go +9 -2
@@ -32,8 +32,13 @@ type target struct {
3232 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
3333 const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
3434
35-// ExportTargets allocates n exported images of the output size.
35+// ExportTargets allocates n exported images of the output size, replacing
36+// the previous ones; n = 0 only drops them (back to the internal image).
3637 func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
38+ r.dropTargets()
39+ if n == 0 {
40+ return nil, nil
41+ }
3742 if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {
3843 return nil, errors.New("device cannot export dmabufs")
3944 }
@@ -41,7 +46,6 @@ func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, err
4146 if len(mods) == 0 {
4247 return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
4348 }
44- r.dropTargets()
4549 var out []ports.DMABuf
4650 for range n {
4751 t, buf, err := r.exportTarget(mods)
@@ -197,6 +201,9 @@ func (r *Renderer) dropTargets() {
197201 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
198202 }
199203 for _, t := range r.targets {
204+ if r.last == t {
205+ r.last = nil
206+ }
200207 r.freeTarget(t)
201208 }
202209 r.targets, r.current = nil, 0
@@ -32,8 +32,13 @@ type target struct {
32 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.32 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
33 const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<2433 const fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
34 34
35-// ExportTargets allocates n exported images of the output size.35+// ExportTargets allocates n exported images of the output size, replacing
36+// the previous ones; n = 0 only drops them (back to the internal image).
36 func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {37 func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, error) {
38+ r.dropTargets()
39+ if n == 0 {
40+ return nil, nil
41+ }
37 if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {42 if r.dd.GetMemoryFdKHR == nil || r.dd.GetImageDrmFormatModifierPropertiesEXT == nil || len(r.dmabuf.Formats) == 0 {
38 return nil, errors.New("device cannot export dmabufs")43 return nil, errors.New("device cannot export dmabufs")
39 }44 }
@@ -41,7 +46,6 @@ func (r *Renderer) ExportTargets(n int, modifiers []uint64) ([]ports.DMABuf, err
41 if len(mods) == 0 {46 if len(mods) == 0 {
42 return nil, errors.New("no XRGB8888 modifier both the device and the display accept")47 return nil, errors.New("no XRGB8888 modifier both the device and the display accept")
43 }48 }
44- r.dropTargets()
45 var out []ports.DMABuf49 var out []ports.DMABuf
46 for range n {50 for range n {
47 t, buf, err := r.exportTarget(mods)51 t, buf, err := r.exportTarget(mods)
@@ -197,6 +201,9 @@ func (r *Renderer) dropTargets() {
197 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))201 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
198 }202 }
199 for _, t := range r.targets {203 for _, t := range r.targets {
204+ if r.last == t {
205+ r.last = nil
206+ }
200 r.freeTarget(t)207 r.freeTarget(t)
201 }208 }
202 r.targets, r.current = nil, 0209 r.targets, r.current = nil, 0
M internal/core/screens_test.go +2 -2
@@ -494,8 +494,8 @@ func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) {
494494 // An app ID set after map reaches the state.
495495 r.client <- ports.WindowAppID{ID: 1, AppID: "foot"}
496496 receive(t, r.scenes)
497- if st := receive(t, r.state); st.Windows[0].AppID != "foot" {
498- t.Fatalf("%+v", st)
497+ // Snapshots from before the app ID may still be queued.
498+ for st := receive(t, r.state); st.Windows[0].AppID != "foot"; st = receive(t, r.state) {
499499 }
500500 // A hidden workspace on screen: no active number, its name, window 1 off screen.
501501 r.key(t, "n", ports.ModAlt)
@@ -494,8 +494,8 @@ func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) {
494 // An app ID set after map reaches the state.494 // An app ID set after map reaches the state.
495 r.client <- ports.WindowAppID{ID: 1, AppID: "foot"}495 r.client <- ports.WindowAppID{ID: 1, AppID: "foot"}
496 receive(t, r.scenes)496 receive(t, r.scenes)
497- if st := receive(t, r.state); st.Windows[0].AppID != "foot" {497+ // Snapshots from before the app ID may still be queued.
498- t.Fatalf("%+v", st)498+ for st := receive(t, r.state); st.Windows[0].AppID != "foot"; st = receive(t, r.state) {
499 }499 }
500 // A hidden workspace on screen: no active number, its name, window 1 off screen.500 // A hidden workspace on screen: no active number, its name, window 1 off screen.
501 r.key(t, "n", ports.ModAlt)501 r.key(t, "n", ports.ModAlt)
M internal/ports/devices.go +1 -0
@@ -10,6 +10,7 @@ type Renderer interface {
1010 // ExportTargets allocates n images of the renderer's size that the
1111 // display can scan out, with one of the given XRGB8888 modifiers
1212 // (none: any the device exports), and returns them as dmabufs.
13+ // n = 0 drops the targets.
1314 ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
1415 // UseTarget selects the exported image the next Render draws into.
1516 UseTarget(i int)
@@ -10,6 +10,7 @@ type Renderer interface {
10 // ExportTargets allocates n images of the renderer's size that the10 // ExportTargets allocates n images of the renderer's size that the
11 // display can scan out, with one of the given XRGB8888 modifiers11 // display can scan out, with one of the given XRGB8888 modifiers
12 // (none: any the device exports), and returns them as dmabufs.12 // (none: any the device exports), and returns them as dmabufs.
13+ // n = 0 drops the targets.
13 ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)14 ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
14 // UseTarget selects the exported image the next Render draws into.15 // UseTarget selects the exported image the next Render draws into.
15 UseTarget(i int)16 UseTarget(i int)

Keyboard shortcuts

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

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

Branches 23

Tags 10

↑↓ moveenter openesc close