fix(render): track windows each target drew and keep overlay refusals apart from scanout
A target records only the windows it drew with their rects, so a window moved to the overlay plane and back, or left out, is redrawn or cleared. Linear filtering pads damage by half the upscale factor. The overlay checks its own plane's IN_FORMATS and caches refusals in a separate field; popups anywhere in the list and bordered windows keep composition; Close turns the overlay off. A logical size change counts as a full change.
- commit
- 94546875409355af178fb7814654c8281032fe04
- author
- brice · · 2026-09-26 12:13 UTC
- parent
- 6f16509
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +185 -45
M internal/adapters/drm/output.go +1 -0
| @@ -608,6 +608,7 @@ func (o *Output) Close() { | |||
| 608 | 608 | if o.cursor != nil { | |
| 609 | 609 | o.cursor.props(req, o.crtc, cursorState{}) | |
| 610 | 610 | } | |
| 611 | + | o.overlayProps(req, overlayWin{}) | |
| 611 | 612 | req.set(o.crtc, o.vrrProp, 0) | |
| 612 | 613 | s := o.saved | |
| 613 | 614 | var blob uint32 | |
| @@ -608,6 +608,7 @@ func (o *Output) Close() { | |||||
| 608 | if o.cursor != nil { | 608 | if o.cursor != nil { | ||
| 609 | o.cursor.props(req, o.crtc, cursorState{}) | 609 | o.cursor.props(req, o.crtc, cursorState{}) | ||
| 610 | } | 610 | } | ||
| 611 | + | o.overlayProps(req, overlayWin{}) | |||
| 611 | req.set(o.crtc, o.vrrProp, 0) | 612 | req.set(o.crtc, o.vrrProp, 0) | ||
| 612 | s := o.saved | 613 | s := o.saved | ||
| 613 | var blob uint32 | 614 | var blob uint32 | ||
M internal/adapters/drm/overlay.go +12 -4
| @@ -65,8 +65,12 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 65 | 65 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 66 | 66 | continue | |
| 67 | 67 | } | |
| 68 | + | if w.Popup { | |
| 69 | + | // Popups draw after every window, wherever they are listed. | |
| 70 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above" | |
| 71 | + | } | |
| 68 | 72 | c := surfaces[w.ID] | |
| 69 | - | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 && !w.Popup { | |
| 73 | + | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 { | |
| 70 | 74 | pick = w | |
| 71 | 75 | continue | |
| 72 | 76 | } | |
| @@ -83,6 +87,10 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 83 | 87 | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | |
| 84 | 88 | } | |
| 85 | 89 | } | |
| 90 | + | if !pick.Fullscreen && !pick.Borderless && s.Border.Width > 0 { | |
| 91 | + | // The overlay shows the buffer alone; its border would go with it. | |
| 92 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "border" | |
| 93 | + | } | |
| 86 | 94 | c := surfaces[pick.ID] | |
| 87 | 95 | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | |
| 88 | 96 | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | |
| @@ -107,7 +115,7 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||
| 107 | 115 | w, c, reason = overlayCandidate(s, surfaces) | |
| 108 | 116 | if reason == "" { | |
| 109 | 117 | var fb uint32 | |
| 110 | - | if fb, reason = o.scanoutFB(c.DMABuf, time.Now()); reason == "" { | |
| 118 | + | if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" { | |
| 111 | 119 | scale := s.Scale | |
| 112 | 120 | if scale <= 0 { | |
| 113 | 121 | scale = 1 | |
| @@ -173,7 +181,7 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |||
| 173 | 181 | return true | |
| 174 | 182 | } | |
| 175 | 183 | if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) { | |
| 176 | - | cfb.failed = "overlay_refused" | |
| 184 | + | cfb.overlayFailed = "overlay_refused" | |
| 177 | 185 | } | |
| 178 | 186 | o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused") | |
| 179 | 187 | return false | |
| @@ -191,7 +199,7 @@ func (o *Output) overlayConflict(err error, buf uint64) bool { | |||
| 191 | 199 | return false | |
| 192 | 200 | } | |
| 193 | 201 | if cfb := o.clientFBs[buf]; cfb != nil { | |
| 194 | - | cfb.failed = "cursor_conflict" | |
| 202 | + | cfb.overlayFailed = "cursor_conflict" | |
| 195 | 203 | } | |
| 196 | 204 | o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay") | |
| 197 | 205 | o.overlayReason = "cursor_conflict" | |
| @@ -65,8 +65,12 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 65 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 65 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | ||
| 66 | continue | 66 | continue | ||
| 67 | } | 67 | } | ||
| 68 | + | if w.Popup { | |||
| 69 | + | // Popups draw after every window, wherever they are listed. | |||
| 70 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above" | |||
| 71 | + | } | |||
| 68 | c := surfaces[w.ID] | 72 | c := surfaces[w.ID] | ||
| 69 | - | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 && !w.Popup { | 73 | + | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 { |
| 70 | pick = w | 74 | pick = w | ||
| 71 | continue | 75 | continue | ||
| 72 | } | 76 | } | ||
| @@ -83,6 +87,10 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 83 | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | 87 | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | ||
| 84 | } | 88 | } | ||
| 85 | } | 89 | } | ||
| 90 | + | if !pick.Fullscreen && !pick.Borderless && s.Border.Width > 0 { | |||
| 91 | + | // The overlay shows the buffer alone; its border would go with it. | |||
| 92 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "border" | |||
| 93 | + | } | |||
| 86 | c := surfaces[pick.ID] | 94 | c := surfaces[pick.ID] | ||
| 87 | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | 95 | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | ||
| 88 | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | 96 | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | ||
| @@ -107,7 +115,7 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||||
| 107 | w, c, reason = overlayCandidate(s, surfaces) | 115 | w, c, reason = overlayCandidate(s, surfaces) | ||
| 108 | if reason == "" { | 116 | if reason == "" { | ||
| 109 | var fb uint32 | 117 | var fb uint32 | ||
| 110 | - | if fb, reason = o.scanoutFB(c.DMABuf, time.Now()); reason == "" { | 118 | + | if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" { |
| 111 | scale := s.Scale | 119 | scale := s.Scale | ||
| 112 | if scale <= 0 { | 120 | if scale <= 0 { | ||
| 113 | scale = 1 | 121 | scale = 1 | ||
| @@ -173,7 +181,7 @@ func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |||||
| 173 | return true | 181 | return true | ||
| 174 | } | 182 | } | ||
| 175 | if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) { | 183 | if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) { | ||
| 176 | - | cfb.failed = "overlay_refused" | 184 | + | cfb.overlayFailed = "overlay_refused" |
| 177 | } | 185 | } | ||
| 178 | o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused") | 186 | o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused") | ||
| 179 | return false | 187 | return false | ||
| @@ -191,7 +199,7 @@ func (o *Output) overlayConflict(err error, buf uint64) bool { | |||||
| 191 | return false | 199 | return false | ||
| 192 | } | 200 | } | ||
| 193 | if cfb := o.clientFBs[buf]; cfb != nil { | 201 | if cfb := o.clientFBs[buf]; cfb != nil { | ||
| 194 | - | cfb.failed = "cursor_conflict" | 202 | + | cfb.overlayFailed = "cursor_conflict" |
| 195 | } | 203 | } | ||
| 196 | o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay") | 204 | o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay") | ||
| 197 | o.overlayReason = "cursor_conflict" | 205 | o.overlayReason = "cursor_conflict" | ||
M internal/adapters/drm/overlay_test.go +25 -3
| @@ -2,6 +2,7 @@ package drm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | + | "time" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/nefertty/internal/ports" | |
| 7 | 8 | "github.com/stretchr/testify/mock" | |
| @@ -19,6 +20,7 @@ func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec | |||
| 19 | 20 | } | |
| 20 | 21 | o.primary.props = props | |
| 21 | 22 | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}} | |
| 23 | + | o.primary.formats = nil // the overlay's own formats decide | |
| 22 | 24 | o.pickOverlay() | |
| 23 | 25 | if o.overlay == nil { | |
| 24 | 26 | t.Fatal("no overlay picked") | |
| @@ -63,7 +65,7 @@ func TestOverlayCandidate(t *testing.T) { | |||
| 63 | 65 | func TestOverlayChosen(t *testing.T) { | |
| 64 | 66 | o, k, commits := overlayOutput(t) | |
| 65 | 67 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 66 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 68 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 67 | 69 | s, c := overlayScene() | |
| 68 | 70 | ov, rest := o.overlayFrame(s, c) | |
| 69 | 71 | if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 { | |
| @@ -91,7 +93,7 @@ func TestOverlayChosen(t *testing.T) { | |||
| 91 | 93 | func TestOverlayTestRefusedFallsBack(t *testing.T) { | |
| 92 | 94 | o, k, _ := overlayOutput(t, unix.EINVAL) | |
| 93 | 95 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 94 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 96 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 95 | 97 | s, c := overlayScene() | |
| 96 | 98 | ov, _ := o.overlayFrame(s, c) | |
| 97 | 99 | if o.testOverlay(70, ov) { | |
| @@ -100,6 +102,26 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||
| 100 | 102 | if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 { | |
| 101 | 103 | t.Fatal("refused buffer tried again") | |
| 102 | 104 | } | |
| 105 | + | // The overlay's refusal does not bar direct scanout of the buffer. | |
| 106 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 107 | + | if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" { | |
| 108 | + | t.Fatalf("scanout after overlay refusal: %d %q", fb, reason) | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // Popups are drawn above every window: one listed first still blocks the | |
| 113 | + | // overlay; a bordered window does too. | |
| 114 | + | func TestOverlayPopupAndBorder(t *testing.T) { | |
| 115 | + | s, c := overlayScene() | |
| 116 | + | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...) | |
| 117 | + | if _, _, reason := overlayCandidate(s, c); reason != "window_above" { | |
| 118 | + | t.Fatalf("popup: %q", reason) | |
| 119 | + | } | |
| 120 | + | s, _ = overlayScene() | |
| 121 | + | s.Border.Width = 2 | |
| 122 | + | if _, _, reason := overlayCandidate(s, c); reason != "border" { | |
| 123 | + | t.Fatalf("border: %q", reason) | |
| 124 | + | } | |
| 103 | 125 | } | |
| 104 | 126 | ||
| 105 | 127 | // A cursor commit refused with the overlay on drops the overlay: the | |
| @@ -107,7 +129,7 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||
| 107 | 129 | func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | |
| 108 | 130 | o, k, _ := overlayOutput(t, nil, unix.EINVAL) | |
| 109 | 131 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 110 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 132 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 111 | 133 | s, c := overlayScene() | |
| 112 | 134 | ov, _ := o.overlayFrame(s, c) | |
| 113 | 135 | if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil { | |
| @@ -2,6 +2,7 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | + | "time" | |||
| 5 | 6 | ||||
| 6 | "github.com/bnema/nefertty/internal/ports" | 7 | "github.com/bnema/nefertty/internal/ports" | ||
| 7 | "github.com/stretchr/testify/mock" | 8 | "github.com/stretchr/testify/mock" | ||
| @@ -19,6 +20,7 @@ func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec | |||||
| 19 | } | 20 | } | ||
| 20 | o.primary.props = props | 21 | o.primary.props = props | ||
| 21 | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}} | 22 | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}} | ||
| 23 | + | o.primary.formats = nil // the overlay's own formats decide | |||
| 22 | o.pickOverlay() | 24 | o.pickOverlay() | ||
| 23 | if o.overlay == nil { | 25 | if o.overlay == nil { | ||
| 24 | t.Fatal("no overlay picked") | 26 | t.Fatal("no overlay picked") | ||
| @@ -63,7 +65,7 @@ func TestOverlayCandidate(t *testing.T) { | |||||
| 63 | func TestOverlayChosen(t *testing.T) { | 65 | func TestOverlayChosen(t *testing.T) { | ||
| 64 | o, k, commits := overlayOutput(t) | 66 | o, k, commits := overlayOutput(t) | ||
| 65 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | 67 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | ||
| 66 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | 68 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} |
| 67 | s, c := overlayScene() | 69 | s, c := overlayScene() | ||
| 68 | ov, rest := o.overlayFrame(s, c) | 70 | ov, rest := o.overlayFrame(s, c) | ||
| 69 | if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 { | 71 | if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 { | ||
| @@ -91,7 +93,7 @@ func TestOverlayChosen(t *testing.T) { | |||||
| 91 | func TestOverlayTestRefusedFallsBack(t *testing.T) { | 93 | func TestOverlayTestRefusedFallsBack(t *testing.T) { | ||
| 92 | o, k, _ := overlayOutput(t, unix.EINVAL) | 94 | o, k, _ := overlayOutput(t, unix.EINVAL) | ||
| 93 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | 95 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | ||
| 94 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | 96 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} |
| 95 | s, c := overlayScene() | 97 | s, c := overlayScene() | ||
| 96 | ov, _ := o.overlayFrame(s, c) | 98 | ov, _ := o.overlayFrame(s, c) | ||
| 97 | if o.testOverlay(70, ov) { | 99 | if o.testOverlay(70, ov) { | ||
| @@ -100,6 +102,26 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||||
| 100 | if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 { | 102 | if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 { | ||
| 101 | t.Fatal("refused buffer tried again") | 103 | t.Fatal("refused buffer tried again") | ||
| 102 | } | 104 | } | ||
| 105 | + | // The overlay's refusal does not bar direct scanout of the buffer. | |||
| 106 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 107 | + | if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" { | |||
| 108 | + | t.Fatalf("scanout after overlay refusal: %d %q", fb, reason) | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // Popups are drawn above every window: one listed first still blocks the | |||
| 113 | + | // overlay; a bordered window does too. | |||
| 114 | + | func TestOverlayPopupAndBorder(t *testing.T) { | |||
| 115 | + | s, c := overlayScene() | |||
| 116 | + | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...) | |||
| 117 | + | if _, _, reason := overlayCandidate(s, c); reason != "window_above" { | |||
| 118 | + | t.Fatalf("popup: %q", reason) | |||
| 119 | + | } | |||
| 120 | + | s, _ = overlayScene() | |||
| 121 | + | s.Border.Width = 2 | |||
| 122 | + | if _, _, reason := overlayCandidate(s, c); reason != "border" { | |||
| 123 | + | t.Fatalf("border: %q", reason) | |||
| 124 | + | } | |||
| 103 | } | 125 | } | ||
| 104 | 126 | ||||
| 105 | // A cursor commit refused with the overlay on drops the overlay: the | 127 | // A cursor commit refused with the overlay on drops the overlay: the | ||
| @@ -107,7 +129,7 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||||
| 107 | func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | 129 | func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | ||
| 108 | o, k, _ := overlayOutput(t, nil, unix.EINVAL) | 130 | o, k, _ := overlayOutput(t, nil, unix.EINVAL) | ||
| 109 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | 131 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | ||
| 110 | - | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | 132 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} |
| 111 | s, c := overlayScene() | 133 | s, c := overlayScene() | ||
| 112 | ov, _ := o.overlayFrame(s, c) | 134 | ov, _ := o.overlayFrame(s, c) | ||
| 113 | if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil { | 135 | if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil { | ||
M internal/adapters/drm/scanout.go +52 -22
| @@ -91,39 +91,69 @@ type clientFB struct { | |||
| 91 | 91 | failed string | |
| 92 | 92 | // noAsync: KMS refused an async flip to it; it flips at vblank. | |
| 93 | 93 | noAsync bool | |
| 94 | + | // overlayFailed is why the overlay plane refused it; kept apart from | |
| 95 | + | // failed, so a refusal on one plane does not bar the other. | |
| 96 | + | overlayFailed string | |
| 97 | + | importErr bool | |
| 94 | 98 | } | |
| 95 | 99 | ||
| 96 | - | // scanoutFB returns the framebuffer of a client buffer, importing it on | |
| 97 | - | // first use. A buffer KMS refuses is remembered with its reason. | |
| 100 | + | // scanoutFB returns the framebuffer of a client buffer for the primary | |
| 101 | + | // plane, importing it on first use. A buffer KMS refuses is remembered | |
| 102 | + | // with its reason. | |
| 98 | 103 | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |
| 99 | - | if fb := o.clientFBs[b.ID]; fb != nil { | |
| 100 | - | fb.last = now | |
| 101 | - | return fb.fbID, fb.failed | |
| 104 | + | return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed }) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // overlayFB is scanoutFB for the overlay plane, with its own formats and | |
| 108 | + | // refusal reason. | |
| 109 | + | func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |
| 110 | + | return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed }) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // planeFB imports b once as a framebuffer; reason is the plane's refusal | |
| 114 | + | // field. Both planes share the framebuffer: they show the same format. | |
| 115 | + | func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) { | |
| 116 | + | fb := o.clientFBs[b.ID] | |
| 117 | + | if fb == nil { | |
| 118 | + | fb = &clientFB{} | |
| 119 | + | o.clientFBs[b.ID] = fb | |
| 120 | + | } | |
| 121 | + | fb.last = now | |
| 122 | + | why := reason(fb) | |
| 123 | + | if *why != "" { | |
| 124 | + | return 0, *why | |
| 102 | 125 | } | |
| 103 | - | fb := &clientFB{last: now} | |
| 104 | - | o.clientFBs[b.ID] = fb | |
| 105 | - | format, ok := o.scanoutFormat(b) | |
| 126 | + | format, ok := planeFormat(p, b) | |
| 106 | 127 | if !ok { | |
| 107 | - | fb.failed = "format" | |
| 108 | - | return 0, fb.failed | |
| 128 | + | *why = "format" | |
| 129 | + | return 0, *why | |
| 109 | 130 | } | |
| 110 | - | id, err := o.k.addFB(b, format) | |
| 111 | - | if err != nil { | |
| 112 | - | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |
| 113 | - | fb.failed = "import_failed" | |
| 114 | - | return 0, fb.failed | |
| 131 | + | if fb.fbID == 0 { | |
| 132 | + | if fb.importErr { | |
| 133 | + | *why = "import_failed" | |
| 134 | + | return 0, *why | |
| 135 | + | } | |
| 136 | + | id, err := o.k.addFB(b, format) | |
| 137 | + | if err != nil { | |
| 138 | + | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |
| 139 | + | fb.importErr, *why = true, "import_failed" | |
| 140 | + | return 0, *why | |
| 141 | + | } | |
| 142 | + | fb.fbID = id | |
| 115 | 143 | } | |
| 116 | - | fb.fbID = id | |
| 117 | - | return id, "" | |
| 144 | + | return fb.fbID, "" | |
| 118 | 145 | } | |
| 119 | 146 | ||
| 120 | 147 | // scanoutFormat is the framebuffer format a client buffer is scanned out | |
| 121 | - | // as, when the primary plane lists it with the buffer's modifier. The | |
| 122 | - | // primary plane shows no alpha: an alpha format goes as its X variant | |
| 123 | - | // when the plane has that one. | |
| 124 | - | func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { | |
| 148 | + | // as on the primary plane (see planeFormat). | |
| 149 | + | func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) } | |
| 150 | + | ||
| 151 | + | // planeFormat is the framebuffer format a client buffer shows as on p, | |
| 152 | + | // when p lists it with the buffer's modifier. Planes here show no alpha: | |
| 153 | + | // an alpha format goes as its X variant when the plane has that one. | |
| 154 | + | func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) { | |
| 125 | 155 | has := func(f uint32) bool { | |
| 126 | - | return slices.Contains(o.primary.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier}) | |
| 156 | + | return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier}) | |
| 127 | 157 | } | |
| 128 | 158 | if x, ok := opaqueVariant[b.Format]; ok && has(x) { | |
| 129 | 159 | return x, true | |
| @@ -91,39 +91,69 @@ type clientFB struct { | |||||
| 91 | failed string | 91 | failed string | ||
| 92 | // noAsync: KMS refused an async flip to it; it flips at vblank. | 92 | // noAsync: KMS refused an async flip to it; it flips at vblank. | ||
| 93 | noAsync bool | 93 | noAsync bool | ||
| 94 | + | // overlayFailed is why the overlay plane refused it; kept apart from | |||
| 95 | + | // failed, so a refusal on one plane does not bar the other. | |||
| 96 | + | overlayFailed string | |||
| 97 | + | importErr bool | |||
| 94 | } | 98 | } | ||
| 95 | 99 | ||||
| 96 | - | // scanoutFB returns the framebuffer of a client buffer, importing it on | 100 | + | // scanoutFB returns the framebuffer of a client buffer for the primary |
| 97 | - | // first use. A buffer KMS refuses is remembered with its reason. | 101 | + | // plane, importing it on first use. A buffer KMS refuses is remembered |
| 102 | + | // with its reason. | |||
| 98 | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | 103 | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | ||
| 99 | - | if fb := o.clientFBs[b.ID]; fb != nil { | 104 | + | return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed }) |
| 100 | - | fb.last = now | 105 | + | } |
| 101 | - | return fb.fbID, fb.failed | 106 | + | |
| 107 | + | // overlayFB is scanoutFB for the overlay plane, with its own formats and | |||
| 108 | + | // refusal reason. | |||
| 109 | + | func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |||
| 110 | + | return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed }) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // planeFB imports b once as a framebuffer; reason is the plane's refusal | |||
| 114 | + | // field. Both planes share the framebuffer: they show the same format. | |||
| 115 | + | func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) { | |||
| 116 | + | fb := o.clientFBs[b.ID] | |||
| 117 | + | if fb == nil { | |||
| 118 | + | fb = &clientFB{} | |||
| 119 | + | o.clientFBs[b.ID] = fb | |||
| 120 | + | } | |||
| 121 | + | fb.last = now | |||
| 122 | + | why := reason(fb) | |||
| 123 | + | if *why != "" { | |||
| 124 | + | return 0, *why | |||
| 102 | } | 125 | } | ||
| 103 | - | fb := &clientFB{last: now} | 126 | + | format, ok := planeFormat(p, b) |
| 104 | - | o.clientFBs[b.ID] = fb | |||
| 105 | - | format, ok := o.scanoutFormat(b) | |||
| 106 | if !ok { | 127 | if !ok { | ||
| 107 | - | fb.failed = "format" | 128 | + | *why = "format" |
| 108 | - | return 0, fb.failed | 129 | + | return 0, *why |
| 109 | } | 130 | } | ||
| 110 | - | id, err := o.k.addFB(b, format) | 131 | + | if fb.fbID == 0 { |
| 111 | - | if err != nil { | 132 | + | if fb.importErr { |
| 112 | - | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | 133 | + | *why = "import_failed" |
| 113 | - | fb.failed = "import_failed" | 134 | + | return 0, *why |
| 114 | - | return 0, fb.failed | 135 | + | } |
| 136 | + | id, err := o.k.addFB(b, format) | |||
| 137 | + | if err != nil { | |||
| 138 | + | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |||
| 139 | + | fb.importErr, *why = true, "import_failed" | |||
| 140 | + | return 0, *why | |||
| 141 | + | } | |||
| 142 | + | fb.fbID = id | |||
| 115 | } | 143 | } | ||
| 116 | - | fb.fbID = id | 144 | + | return fb.fbID, "" |
| 117 | - | return id, "" | |||
| 118 | } | 145 | } | ||
| 119 | 146 | ||||
| 120 | // scanoutFormat is the framebuffer format a client buffer is scanned out | 147 | // scanoutFormat is the framebuffer format a client buffer is scanned out | ||
| 121 | - | // as, when the primary plane lists it with the buffer's modifier. The | 148 | + | // as on the primary plane (see planeFormat). |
| 122 | - | // primary plane shows no alpha: an alpha format goes as its X variant | 149 | + | func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) } |
| 123 | - | // when the plane has that one. | 150 | + | |
| 124 | - | func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { | 151 | + | // planeFormat is the framebuffer format a client buffer shows as on p, |
| 152 | + | // when p lists it with the buffer's modifier. Planes here show no alpha: | |||
| 153 | + | // an alpha format goes as its X variant when the plane has that one. | |||
| 154 | + | func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) { | |||
| 125 | has := func(f uint32) bool { | 155 | has := func(f uint32) bool { | ||
| 126 | - | return slices.Contains(o.primary.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier}) | 156 | + | return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier}) |
| 127 | } | 157 | } | ||
| 128 | if x, ok := opaqueVariant[b.Format]; ok && has(x) { | 158 | if x, ok := opaqueVariant[b.Format]; ok && has(x) { | ||
| 129 | return x, true | 159 | return x, true | ||
M internal/adapters/vulkan/damage.go +42 -13
| @@ -2,6 +2,7 @@ package vulkan | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "image" | |
| 5 | + | "math" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/nefertty/internal/ports" | |
| 7 | 8 | ) | |
| @@ -12,16 +13,25 @@ import ( | |||
| 12 | 13 | // keeps the rest of the image. A changed scene, a target never drawn or | |
| 13 | 14 | // a history too short redraws everything. | |
| 14 | 15 | ||
| 15 | - | // damageRegion collects the region of a target to redraw this frame. | |
| 16 | + | // damageRegion collects the region of a target to redraw this frame and | |
| 17 | + | // what the target will hold after it: the windows drawn, their content | |
| 18 | + | // Seq and rect. | |
| 16 | 19 | type damageRegion struct { | |
| 17 | 20 | held *target // nil: redraw everything | |
| 18 | 21 | area image.Rectangle | |
| 19 | 22 | seen map[ports.WindowID]bool | |
| 20 | 23 | bound image.Rectangle | |
| 24 | + | drawn map[ports.WindowID]heldWindow | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // heldWindow is a window a target holds: its content Seq and rect. | |
| 28 | + | type heldWindow struct { | |
| 29 | + | seq uint64 | |
| 30 | + | rect image.Rectangle | |
| 21 | 31 | } | |
| 22 | 32 | ||
| 23 | 33 | func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion { | |
| 24 | - | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds} | |
| 34 | + | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds, drawn: map[ports.WindowID]heldWindow{}} | |
| 25 | 35 | // Seq 0 is a scene core did not number (setup, tests): redraw all. | |
| 26 | 36 | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | |
| 27 | 37 | d.held = tg | |
| @@ -41,11 +51,16 @@ func (d *damageRegion) add(r image.Rectangle) { | |||
| 41 | 51 | // window records a window drawn in rect (physical): if its content changed | |
| 42 | 52 | // and no finer damage comes (content), the whole rect is redrawn. | |
| 43 | 53 | func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) { | |
| 44 | - | if d.held == nil || d.seen[id] { | |
| 54 | + | if d.seen[id] { | |
| 45 | 55 | return | |
| 46 | 56 | } | |
| 47 | 57 | d.seen[id] = true | |
| 48 | - | held, ok := d.held.seqs[id] | |
| 58 | + | d.drawn[id] = heldWindow{seq: c.Seq, rect: rect} | |
| 59 | + | if d.held == nil { | |
| 60 | + | return | |
| 61 | + | } | |
| 62 | + | hw, ok := d.held.windows[id] | |
| 63 | + | held := hw.seq | |
| 49 | 64 | if ok && held == c.Seq { | |
| 50 | 65 | return | |
| 51 | 66 | } | |
| @@ -69,7 +84,8 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||
| 69 | 84 | if d.held == nil { | |
| 70 | 85 | return | |
| 71 | 86 | } | |
| 72 | - | held, ok := d.held.seqs[id] | |
| 87 | + | hw, ok := d.held.windows[id] | |
| 88 | + | held := hw.seq | |
| 73 | 89 | if !ok || held == c.Seq || len(c.Children) > 0 { | |
| 74 | 90 | return | |
| 75 | 91 | } | |
| @@ -77,9 +93,12 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||
| 77 | 93 | if !known || c.Width <= 0 || c.Height <= 0 { | |
| 78 | 94 | return | |
| 79 | 95 | } | |
| 96 | + | // Linear filtering spreads a texel over half a buffer pixel around | |
| 97 | + | // it: scale/2 target pixels, plus one for rounding. | |
| 80 | 98 | pad := 0 | |
| 81 | 99 | if full.Dx() != c.Width || full.Dy() != c.Height { | |
| 82 | - | pad = 1 | |
| 100 | + | s := max(float64(full.Dx())/float64(c.Width), float64(full.Dy())/float64(c.Height)) | |
| 101 | + | pad = int(math.Ceil(s/2)) + 1 | |
| 83 | 102 | } | |
| 84 | 103 | for _, r := range rects { | |
| 85 | 104 | x0 := full.Min.X + r.X*full.Dx()/c.Width - pad | |
| @@ -90,6 +109,19 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||
| 90 | 109 | } | |
| 91 | 110 | } | |
| 92 | 111 | ||
| 112 | + | // finish adds the rects of windows the target holds that this frame does | |
| 113 | + | // not draw (e.g. a window that moved to an overlay plane and back). | |
| 114 | + | func (d *damageRegion) finish() { | |
| 115 | + | if d.held == nil { | |
| 116 | + | return | |
| 117 | + | } | |
| 118 | + | for id, hw := range d.held.windows { | |
| 119 | + | if _, ok := d.drawn[id]; !ok { | |
| 120 | + | d.add(hw.rect) | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 93 | 125 | // clip limits the draws to the region; draws outside it are dropped. | |
| 94 | 126 | func (d *damageRegion) clip(ds []draw) []draw { | |
| 95 | 127 | if d.held == nil { | |
| @@ -107,11 +139,8 @@ func (d *damageRegion) clip(ds []draw) []draw { | |||
| 107 | 139 | return out | |
| 108 | 140 | } | |
| 109 | 141 | ||
| 110 | - | // hold records what the target holds after this frame. | |
| 111 | - | func (tg *target) hold(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) { | |
| 112 | - | tg.valid, tg.sceneSeq = true, s.Seq | |
| 113 | - | tg.seqs = make(map[ports.WindowID]uint64, len(contents)) | |
| 114 | - | for id, c := range contents { | |
| 115 | - | tg.seqs[id] = c.Seq | |
| 116 | - | } | |
| 142 | + | // hold records what the target holds after this frame: only the windows | |
| 143 | + | // it drew. | |
| 144 | + | func (tg *target) hold(s ports.Scene, d *damageRegion) { | |
| 145 | + | tg.valid, tg.sceneSeq, tg.windows = true, s.Seq, d.drawn | |
| 117 | 146 | } | |
| @@ -2,6 +2,7 @@ package vulkan | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "image" | 4 | "image" | ||
| 5 | + | "math" | |||
| 5 | 6 | ||||
| 6 | "github.com/bnema/nefertty/internal/ports" | 7 | "github.com/bnema/nefertty/internal/ports" | ||
| 7 | ) | 8 | ) | ||
| @@ -12,16 +13,25 @@ import ( | |||||
| 12 | // keeps the rest of the image. A changed scene, a target never drawn or | 13 | // keeps the rest of the image. A changed scene, a target never drawn or | ||
| 13 | // a history too short redraws everything. | 14 | // a history too short redraws everything. | ||
| 14 | 15 | ||||
| 15 | - | // damageRegion collects the region of a target to redraw this frame. | 16 | + | // damageRegion collects the region of a target to redraw this frame and |
| 17 | + | // what the target will hold after it: the windows drawn, their content | |||
| 18 | + | // Seq and rect. | |||
| 16 | type damageRegion struct { | 19 | type damageRegion struct { | ||
| 17 | held *target // nil: redraw everything | 20 | held *target // nil: redraw everything | ||
| 18 | area image.Rectangle | 21 | area image.Rectangle | ||
| 19 | seen map[ports.WindowID]bool | 22 | seen map[ports.WindowID]bool | ||
| 20 | bound image.Rectangle | 23 | bound image.Rectangle | ||
| 24 | + | drawn map[ports.WindowID]heldWindow | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // heldWindow is a window a target holds: its content Seq and rect. | |||
| 28 | + | type heldWindow struct { | |||
| 29 | + | seq uint64 | |||
| 30 | + | rect image.Rectangle | |||
| 21 | } | 31 | } | ||
| 22 | 32 | ||||
| 23 | func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion { | 33 | func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion { | ||
| 24 | - | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds} | 34 | + | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds, drawn: map[ports.WindowID]heldWindow{}} |
| 25 | // Seq 0 is a scene core did not number (setup, tests): redraw all. | 35 | // Seq 0 is a scene core did not number (setup, tests): redraw all. | ||
| 26 | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | 36 | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | ||
| 27 | d.held = tg | 37 | d.held = tg | ||
| @@ -41,11 +51,16 @@ func (d *damageRegion) add(r image.Rectangle) { | |||||
| 41 | // window records a window drawn in rect (physical): if its content changed | 51 | // window records a window drawn in rect (physical): if its content changed | ||
| 42 | // and no finer damage comes (content), the whole rect is redrawn. | 52 | // and no finer damage comes (content), the whole rect is redrawn. | ||
| 43 | func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) { | 53 | func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) { | ||
| 44 | - | if d.held == nil || d.seen[id] { | 54 | + | if d.seen[id] { |
| 45 | return | 55 | return | ||
| 46 | } | 56 | } | ||
| 47 | d.seen[id] = true | 57 | d.seen[id] = true | ||
| 48 | - | held, ok := d.held.seqs[id] | 58 | + | d.drawn[id] = heldWindow{seq: c.Seq, rect: rect} |
| 59 | + | if d.held == nil { | |||
| 60 | + | return | |||
| 61 | + | } | |||
| 62 | + | hw, ok := d.held.windows[id] | |||
| 63 | + | held := hw.seq | |||
| 49 | if ok && held == c.Seq { | 64 | if ok && held == c.Seq { | ||
| 50 | return | 65 | return | ||
| 51 | } | 66 | } | ||
| @@ -69,7 +84,8 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||||
| 69 | if d.held == nil { | 84 | if d.held == nil { | ||
| 70 | return | 85 | return | ||
| 71 | } | 86 | } | ||
| 72 | - | held, ok := d.held.seqs[id] | 87 | + | hw, ok := d.held.windows[id] |
| 88 | + | held := hw.seq | |||
| 73 | if !ok || held == c.Seq || len(c.Children) > 0 { | 89 | if !ok || held == c.Seq || len(c.Children) > 0 { | ||
| 74 | return | 90 | return | ||
| 75 | } | 91 | } | ||
| @@ -77,9 +93,12 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||||
| 77 | if !known || c.Width <= 0 || c.Height <= 0 { | 93 | if !known || c.Width <= 0 || c.Height <= 0 { | ||
| 78 | return | 94 | return | ||
| 79 | } | 95 | } | ||
| 96 | + | // Linear filtering spreads a texel over half a buffer pixel around | |||
| 97 | + | // it: scale/2 target pixels, plus one for rounding. | |||
| 80 | pad := 0 | 98 | pad := 0 | ||
| 81 | if full.Dx() != c.Width || full.Dy() != c.Height { | 99 | if full.Dx() != c.Width || full.Dy() != c.Height { | ||
| 82 | - | pad = 1 | 100 | + | s := max(float64(full.Dx())/float64(c.Width), float64(full.Dy())/float64(c.Height)) |
| 101 | + | pad = int(math.Ceil(s/2)) + 1 | |||
| 83 | } | 102 | } | ||
| 84 | for _, r := range rects { | 103 | for _, r := range rects { | ||
| 85 | x0 := full.Min.X + r.X*full.Dx()/c.Width - pad | 104 | x0 := full.Min.X + r.X*full.Dx()/c.Width - pad | ||
| @@ -90,6 +109,19 @@ func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, | |||||
| 90 | } | 109 | } | ||
| 91 | } | 110 | } | ||
| 92 | 111 | ||||
| 112 | + | // finish adds the rects of windows the target holds that this frame does | |||
| 113 | + | // not draw (e.g. a window that moved to an overlay plane and back). | |||
| 114 | + | func (d *damageRegion) finish() { | |||
| 115 | + | if d.held == nil { | |||
| 116 | + | return | |||
| 117 | + | } | |||
| 118 | + | for id, hw := range d.held.windows { | |||
| 119 | + | if _, ok := d.drawn[id]; !ok { | |||
| 120 | + | d.add(hw.rect) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 93 | // clip limits the draws to the region; draws outside it are dropped. | 125 | // clip limits the draws to the region; draws outside it are dropped. | ||
| 94 | func (d *damageRegion) clip(ds []draw) []draw { | 126 | func (d *damageRegion) clip(ds []draw) []draw { | ||
| 95 | if d.held == nil { | 127 | if d.held == nil { | ||
| @@ -107,11 +139,8 @@ func (d *damageRegion) clip(ds []draw) []draw { | |||||
| 107 | return out | 139 | return out | ||
| 108 | } | 140 | } | ||
| 109 | 141 | ||||
| 110 | - | // hold records what the target holds after this frame. | 142 | + | // hold records what the target holds after this frame: only the windows |
| 111 | - | func (tg *target) hold(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) { | 143 | + | // it drew. |
| 112 | - | tg.valid, tg.sceneSeq = true, s.Seq | 144 | + | func (tg *target) hold(s ports.Scene, d *damageRegion) { |
| 113 | - | tg.seqs = make(map[ports.WindowID]uint64, len(contents)) | 145 | + | tg.valid, tg.sceneSeq, tg.windows = true, s.Seq, d.drawn |
| 114 | - | for id, c := range contents { | |||
| 115 | - | tg.seqs[id] = c.Seq | |||
| 116 | - | } | |||
| 117 | } | 146 | } | ||
M internal/adapters/vulkan/damage_test.go +38 -0
| @@ -96,3 +96,41 @@ func TestRendererDamageMatchesFullRedraw(t *testing.T) { | |||
| 96 | 96 | step(3, c) | |
| 97 | 97 | step(4, c) | |
| 98 | 98 | } | |
| 99 | + | ||
| 100 | + | // A window left out of a frame (on an overlay plane) and drawn again in | |
| 101 | + | // the same scene is redrawn whole; one held but no longer drawn is | |
| 102 | + | // cleared. | |
| 103 | + | func TestRendererDamageWindowLeftOutAndBack(t *testing.T) { | |
| 104 | + | newR := func() *Renderer { | |
| 105 | + | r, err := New(64, 48) | |
| 106 | + | if err != nil { | |
| 107 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 108 | + | } | |
| 109 | + | t.Cleanup(r.Close) | |
| 110 | + | return r | |
| 111 | + | } | |
| 112 | + | damaged, full := newR(), newR() | |
| 113 | + | c := solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}) | |
| 114 | + | c.ID, c.Seq = 1, 1 | |
| 115 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |
| 116 | + | with := ports.Scene{Seq: 9, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 16}, Borderless: true}}} | |
| 117 | + | without := with | |
| 118 | + | without.Windows = nil | |
| 119 | + | check := func(s ports.Scene, what string) { | |
| 120 | + | t.Helper() | |
| 121 | + | if err := render(damaged, s, contents); err != nil { | |
| 122 | + | t.Fatal(err) | |
| 123 | + | } | |
| 124 | + | plain := s | |
| 125 | + | plain.Seq = 0 | |
| 126 | + | if err := render(full, plain, contents); err != nil { | |
| 127 | + | t.Fatal(err) | |
| 128 | + | } | |
| 129 | + | if !bytes.Equal(damaged.Pixels().Pix, full.Pixels().Pix) { | |
| 130 | + | t.Fatalf("%s differs from a full redraw", what) | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | check(without, "overlay frame") | |
| 134 | + | check(with, "overlay refused, window composed") | |
| 135 | + | check(without, "window on the overlay again") | |
| 136 | + | } | |
| @@ -96,3 +96,41 @@ func TestRendererDamageMatchesFullRedraw(t *testing.T) { | |||||
| 96 | step(3, c) | 96 | step(3, c) | ||
| 97 | step(4, c) | 97 | step(4, c) | ||
| 98 | } | 98 | } | ||
| 99 | + | ||||
| 100 | + | // A window left out of a frame (on an overlay plane) and drawn again in | |||
| 101 | + | // the same scene is redrawn whole; one held but no longer drawn is | |||
| 102 | + | // cleared. | |||
| 103 | + | func TestRendererDamageWindowLeftOutAndBack(t *testing.T) { | |||
| 104 | + | newR := func() *Renderer { | |||
| 105 | + | r, err := New(64, 48) | |||
| 106 | + | if err != nil { | |||
| 107 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 108 | + | } | |||
| 109 | + | t.Cleanup(r.Close) | |||
| 110 | + | return r | |||
| 111 | + | } | |||
| 112 | + | damaged, full := newR(), newR() | |||
| 113 | + | c := solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}) | |||
| 114 | + | c.ID, c.Seq = 1, 1 | |||
| 115 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 116 | + | with := ports.Scene{Seq: 9, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 16}, Borderless: true}}} | |||
| 117 | + | without := with | |||
| 118 | + | without.Windows = nil | |||
| 119 | + | check := func(s ports.Scene, what string) { | |||
| 120 | + | t.Helper() | |||
| 121 | + | if err := render(damaged, s, contents); err != nil { | |||
| 122 | + | t.Fatal(err) | |||
| 123 | + | } | |||
| 124 | + | plain := s | |||
| 125 | + | plain.Seq = 0 | |||
| 126 | + | if err := render(full, plain, contents); err != nil { | |||
| 127 | + | t.Fatal(err) | |||
| 128 | + | } | |||
| 129 | + | if !bytes.Equal(damaged.Pixels().Pix, full.Pixels().Pix) { | |||
| 130 | + | t.Fatalf("%s differs from a full redraw", what) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | check(without, "overlay frame") | |||
| 134 | + | check(with, "overlay refused, window composed") | |||
| 135 | + | check(without, "window on the overlay again") | |||
| 136 | + | } | |||
M internal/adapters/vulkan/frame.go +2 -1
| @@ -171,6 +171,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 171 | 171 | tg := r.target() | |
| 172 | 172 | dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height)) | |
| 173 | 173 | ds := r.draws(s, contents, dmg) | |
| 174 | + | dmg.finish() | |
| 174 | 175 | r.dropPools() | |
| 175 | 176 | partial := !dmg.all() | |
| 176 | 177 | if partial { | |
| @@ -268,7 +269,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 268 | 269 | runtime.KeepAlive(waits) | |
| 269 | 270 | slot.frame, slot.busy, r.submitted = frame, true, frame | |
| 270 | 271 | tg.layout = b.NewLayout | |
| 271 | - | tg.hold(s, contents) | |
| 272 | + | tg.hold(s, dmg) | |
| 272 | 273 | r.last, r.readBack = tg, false | |
| 273 | 274 | r.dropUnused() | |
| 274 | 275 | r.dropShm() | |
| @@ -171,6 +171,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 171 | tg := r.target() | 171 | tg := r.target() | ||
| 172 | dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height)) | 172 | dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height)) | ||
| 173 | ds := r.draws(s, contents, dmg) | 173 | ds := r.draws(s, contents, dmg) | ||
| 174 | + | dmg.finish() | |||
| 174 | r.dropPools() | 175 | r.dropPools() | ||
| 175 | partial := !dmg.all() | 176 | partial := !dmg.all() | ||
| 176 | if partial { | 177 | if partial { | ||
| @@ -268,7 +269,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 268 | runtime.KeepAlive(waits) | 269 | runtime.KeepAlive(waits) | ||
| 269 | slot.frame, slot.busy, r.submitted = frame, true, frame | 270 | slot.frame, slot.busy, r.submitted = frame, true, frame | ||
| 270 | tg.layout = b.NewLayout | 271 | tg.layout = b.NewLayout | ||
| 271 | - | tg.hold(s, contents) | 272 | + | tg.hold(s, dmg) |
| 272 | r.last, r.readBack = tg, false | 273 | r.last, r.readBack = tg, false | ||
| 273 | r.dropUnused() | 274 | r.dropUnused() | ||
| 274 | r.dropShm() | 275 | r.dropShm() | ||
M internal/adapters/vulkan/targets.go +1 -1
| @@ -32,7 +32,7 @@ type target struct { | |||
| 32 | 32 | // content Seq; valid once a frame was drawn whole. | |
| 33 | 33 | valid bool | |
| 34 | 34 | sceneSeq uint64 | |
| 35 | - | seqs map[ports.WindowID]uint64 | |
| 35 | + | windows map[ports.WindowID]heldWindow | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | |
| @@ -32,7 +32,7 @@ type target struct { | |||||
| 32 | // content Seq; valid once a frame was drawn whole. | 32 | // content Seq; valid once a frame was drawn whole. | ||
| 33 | valid bool | 33 | valid bool | ||
| 34 | sceneSeq uint64 | 34 | sceneSeq uint64 | ||
| 35 | - | seqs map[ports.WindowID]uint64 | 35 | + | windows map[ports.WindowID]heldWindow |
| 36 | } | 36 | } | ||
| 37 | 37 | ||||
| 38 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | 38 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | ||
M internal/adapters/wayland/damage_test.go +10 -0
| @@ -89,6 +89,16 @@ func TestDamageHistory(t *testing.T) { | |||
| 89 | 89 | if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] { | |
| 90 | 90 | t.Fatalf("damage %v %v", got, ok) | |
| 91 | 91 | } | |
| 92 | + | // Same buffer size, new logical size (buffer scale): full. | |
| 93 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 94 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetBufferScale, int32(2)) | |
| 95 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(1), int32(1)) | |
| 96 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 97 | + | scaled := next() | |
| 98 | + | if _, ok := scaled.DamageSince(second.Seq); ok { | |
| 99 | + | t.Fatal("logical resize kept partial damage") | |
| 100 | + | } | |
| 101 | + | second = scaled | |
| 92 | 102 | // No damage with a new buffer: full. | |
| 93 | 103 | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 94 | 104 | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| @@ -89,6 +89,16 @@ func TestDamageHistory(t *testing.T) { | |||||
| 89 | if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] { | 89 | if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] { | ||
| 90 | t.Fatalf("damage %v %v", got, ok) | 90 | t.Fatalf("damage %v %v", got, ok) | ||
| 91 | } | 91 | } | ||
| 92 | + | // Same buffer size, new logical size (buffer scale): full. | |||
| 93 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 94 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetBufferScale, int32(2)) | |||
| 95 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(1), int32(1)) | |||
| 96 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 97 | + | scaled := next() | |||
| 98 | + | if _, ok := scaled.DamageSince(second.Seq); ok { | |||
| 99 | + | t.Fatal("logical resize kept partial damage") | |||
| 100 | + | } | |||
| 101 | + | second = scaled | |||
| 92 | // No damage with a new buffer: full. | 102 | // No damage with a new buffer: full. | ||
| 93 | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | 103 | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | ||
| 94 | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | 104 | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | ||
M internal/adapters/wayland/surface.go +2 -1
| @@ -303,7 +303,8 @@ func (s *surface) applyCommit() { | |||
| 303 | 303 | if state, ok := s.server.buffers[s.current.Resource]; ok { | |
| 304 | 304 | if c, ok := state.content(0); ok { | |
| 305 | 305 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | |
| 306 | - | s.commitDamage(true, !s.has || c.Width != s.content.Width || c.Height != s.content.Height, c.Width, c.Height) | |
| 306 | + | resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH | |
| 307 | + | s.commitDamage(true, resized, c.Width, c.Height) | |
| 307 | 308 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | |
| 308 | 309 | s.lastW, s.lastH = c.Width, c.Height | |
| 309 | 310 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | |
| @@ -303,7 +303,8 @@ func (s *surface) applyCommit() { | |||||
| 303 | if state, ok := s.server.buffers[s.current.Resource]; ok { | 303 | if state, ok := s.server.buffers[s.current.Resource]; ok { | ||
| 304 | if c, ok := state.content(0); ok { | 304 | if c, ok := state.content(0); ok { | ||
| 305 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | 305 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | ||
| 306 | - | s.commitDamage(true, !s.has || c.Width != s.content.Width || c.Height != s.content.Height, c.Width, c.Height) | 306 | + | resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH |
| 307 | + | s.commitDamage(true, resized, c.Width, c.Height) | |||
| 307 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | 308 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | ||
| 308 | s.lastW, s.lastH = c.Width, c.Height | 309 | s.lastW, s.lastH = c.Width, c.Height | ||
| 309 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | 310 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | ||