From e6a5fae190756ddf6f535296e2bc1f9a8b96c237 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 20 Aug 2026 19:17:25 -0400 Subject: [PATCH] perf(colorpicker): incremental magnifier redraws, frame-paced commits --- core/cmd/dms/commands_colorpicker.go | 5 + core/internal/colorpicker/picker.go | 58 ++++++++--- core/internal/colorpicker/state.go | 132 ++++++++++++++++++------ core/internal/colorpicker/state_test.go | 100 +++++++++++++++++- 4 files changed, 244 insertions(+), 51 deletions(-) diff --git a/core/cmd/dms/commands_colorpicker.go b/core/cmd/dms/commands_colorpicker.go index 36ed9d746..fc62e56dd 100644 --- a/core/cmd/dms/commands_colorpicker.go +++ b/core/cmd/dms/commands_colorpicker.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "runtime/debug" "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker" @@ -96,6 +97,10 @@ func runColorPick(cmd *cobra.Command, args []string) { Notify: colorNotify, } + // One-shot process holding a few screen-sized buffers: skip GC cycles while it runs. + debug.SetGCPercent(-1) + debug.SetMemoryLimit(1 << 30) + picker := colorpicker.New(config) color, err := picker.Run() if err != nil { diff --git a/core/internal/colorpicker/picker.go b/core/internal/colorpicker/picker.go index 6d2a977f9..ef53a2e1c 100644 --- a/core/internal/colorpicker/picker.go +++ b/core/internal/colorpicker/picker.go @@ -35,18 +35,19 @@ type Output struct { } type LayerSurface struct { - output *Output - state *SurfaceState - wlSurface *client.Surface - layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1 - viewport *wp_viewporter.WpViewport - wlPools [2]*client.ShmPool - wlBuffers [2]*client.Buffer - slotBusy [2]bool - needsRedraw bool - scopyBuffer *client.Buffer - configured bool - hidden bool + output *Output + state *SurfaceState + wlSurface *client.Surface + layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1 + viewport *wp_viewporter.WpViewport + wlPools [2]*client.ShmPool + wlBuffers [2]*client.Buffer + slotBusy [2]bool + needsRedraw bool + framePending bool + scopyBuffer *client.Buffer + configured bool + hidden bool } type Picker struct { @@ -66,6 +67,8 @@ type Picker struct { viewporter *wp_viewporter.WpViewporter colorMgr *wp_color_management.WpColorManagerV1 + compositorVersion uint32 + shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1 shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1 @@ -166,9 +169,10 @@ func (p *Picker) checkDone() { } } +// flushRedraws paints queued surfaces, at most once per compositor frame each. func (p *Picker) flushRedraws() { for _, ls := range p.surfaces { - if !ls.needsRedraw { + if !ls.needsRedraw || ls.framePending { continue } p.redrawSurface(ls) @@ -215,6 +219,7 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) { compositor := client.NewCompositor(p.ctx) if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil { p.compositor = compositor + p.compositorVersion = e.Version } case client.ShmInterfaceName: @@ -547,17 +552,18 @@ func (p *Picker) captureForSurface(ls *LayerSurface) { func (p *Picker) redrawSurface(ls *LayerSurface) { slot := ls.state.FrontIndex() - if ls.slotBusy[slot] { + if ls.slotBusy[slot] || ls.framePending { ls.needsRedraw = true return } var renderBuf *ShmBuffer + var damage []rect switch { case ls.hidden: - renderBuf = ls.state.RedrawScreenOnly() + renderBuf, damage = ls.state.RedrawScreenOnly() default: - renderBuf = ls.state.Redraw() + renderBuf, damage = ls.state.Redraw() } if renderBuf == nil { return @@ -604,7 +610,25 @@ func (p *Picker) redrawSurface(ls *LayerSurface) { _ = ls.wlSurface.SetBufferScale(bufferScale) } _ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0) - _ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH)) + switch { + case p.compositorVersion < 4: + _ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH)) + default: + for _, r := range damage { + r = r.clip(renderBuf.Width, renderBuf.Height) + if r.w <= 0 || r.h <= 0 { + continue + } + _ = ls.wlSurface.DamageBuffer(int32(r.x), int32(r.y), int32(r.w), int32(r.h)) + } + } + if cb, err := ls.wlSurface.Frame(); err == nil { + ls.framePending = true + cb.SetDoneHandler(func(client.CallbackDoneEvent) { + _ = cb.Destroy() + ls.framePending = false + }) + } _ = ls.wlSurface.Commit() ls.state.SwapBuffers() diff --git a/core/internal/colorpicker/state.go b/core/internal/colorpicker/state.go index a0efd9db5..ee3ebd091 100644 --- a/core/internal/colorpicker/state.go +++ b/core/internal/colorpicker/state.go @@ -34,6 +34,10 @@ type SurfaceState struct { renderBufs [2]*ShmBuffer front int + primed [2]bool + drawn [2][]rect + shown []rect + fullDamage bool scale int32 scaleX float64 @@ -212,6 +216,10 @@ func (s *SurfaceState) ensureRenderBuffers() { if s.screenBuf == nil { return } + s.primed = [2]bool{} + s.drawn = [2][]rect{} + s.shown = nil + s.fullDamage = true width := s.screenBuf.Width height := s.screenBuf.Height @@ -306,21 +314,70 @@ func (s *SurfaceState) SwapBuffers() { s.mu.Unlock() } -func (s *SurfaceState) Redraw() *ShmBuffer { +// rect is a buffer-pixel area, clipped on use. +type rect struct{ x, y, w, h int } + +func (r rect) clip(w, h int) rect { + x1, y1 := max(r.x, 0), max(r.y, 0) + x2, y2 := min(r.x+r.w, w), min(r.y+r.h, h) + return rect{x1, y1, max(x2-x1, 0), max(y2-y1, 0)} +} + +func magnifierRect(cx, cy int) rect { + const reach = magnifierRadius + 2 + return rect{cx - reach, cy - reach, 2*reach + 1, 2*reach + 1} +} + +// restore puts screen pixels back under the front buffer's previous overlay and +// records cur as its new footprint. It returns what differs from the frame on screen. +func (s *SurfaceState) restore(dst *ShmBuffer, cur []rect) []rect { + i := s.front + switch { + case !s.primed[i]: + dst.CopyFrom(s.screenBuf) + s.primed[i] = true + default: + for _, r := range s.drawn[i] { + s.copyRect(dst, r) + } + } + s.drawn[i] = cur + + damage := append(append([]rect(nil), s.shown...), cur...) + if s.fullDamage { + s.fullDamage = false + damage = []rect{{0, 0, dst.Width, dst.Height}} + } + s.shown = cur + return damage +} + +func (s *SurfaceState) copyRect(dst *ShmBuffer, r rect) { + r = r.clip(min(dst.Width, s.screenBuf.Width), min(dst.Height, s.screenBuf.Height)) + if r.w <= 0 || r.h <= 0 { + return + } + src := s.screenBuf + for y := r.y; y < r.y+r.h; y++ { + copy(dst.Data()[y*dst.Stride+r.x*4:][:r.w*4], src.Data()[y*src.Stride+r.x*4:][:r.w*4]) + } +} + +// Redraw draws the magnifier and preview at the pointer into the front buffer, +// touching only the previous and new overlay areas; the rects are the damage. +func (s *SurfaceState) Redraw() (*ShmBuffer, []rect) { s.mu.Lock() defer s.mu.Unlock() if !s.readyForDisplay || s.screenBuf == nil { - return nil + return nil, nil } dst := s.renderBufs[s.front] if dst == nil { - return nil + return nil, nil } - dst.CopyFrom(s.screenBuf) - px := int(math.Round(float64(s.pointerX) * s.scaleX)) py := int(math.Round(float64(s.pointerY) * s.scaleY)) @@ -333,6 +390,8 @@ func (s *SurfaceState) Redraw() *ShmBuffer { } picked := GetPixelColorWithFormat(s.screenBuf, px, sampleY, s.screenFormat) + text := formatColorForPreview(picked, s.displayFormat, s.lowercase) + damage := s.restore(dst, []rect{magnifierRect(px, py), previewRect(dst.Width, dst.Height, px, py, text)}) drawMagnifierWithInversion( dst.Data(), dst.Stride, dst.Width, dst.Height, @@ -342,26 +401,25 @@ func (s *SurfaceState) Redraw() *ShmBuffer { drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat) - return dst + return dst, damage } // RedrawScreenOnly renders just the screenshot without any overlay (magnifier, preview). // Used for when pointer leaves the surface. -func (s *SurfaceState) RedrawScreenOnly() *ShmBuffer { +func (s *SurfaceState) RedrawScreenOnly() (*ShmBuffer, []rect) { s.mu.Lock() defer s.mu.Unlock() if !s.readyForDisplay || s.screenBuf == nil { - return nil + return nil, nil } dst := s.renderBufs[s.front] if dst == nil { - return nil + return nil, nil } - dst.CopyFrom(s.screenBuf) - return dst + return dst, s.restore(dst, nil) } func (s *SurfaceState) PickColor() (Color, bool) { @@ -442,6 +500,8 @@ func blendColors(bg, fg Color, alpha float64) Color { } } +const magnifierRadius = 80 + func drawMagnifierWithInversion( dst []byte, dstStride, dstW, dstH int, src []byte, srcStride, srcW, srcH int, @@ -455,7 +515,7 @@ func drawMagnifierWithInversion( } const ( - outerRadius = 80 + outerRadius = magnifierRadius borderThickness = 4 aaWidth = 1.5 zoom = 8.0 @@ -1065,30 +1125,28 @@ var fontGlyphs = map[rune][fontH]uint8{ }, } -func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) { - text := formatColorForPreview(c, format, lowercase) +const ( + previewPaddingX = 8 + previewPaddingY = 4 + previewSpace = 2 + previewOffset = 88 +) + +// previewRect places the color label beside the magnifier, flipping to the left +// and clamping so it stays on the buffer. +func previewRect(width, height, cx, cy int, text string) rect { if len(text) == 0 { - return + return rect{} } + textW := len(text)*(fontW+previewSpace) - previewSpace + boxW := textW + previewPaddingX*2 + boxH := fontH + previewPaddingY*2 - const ( - paddingX = 8 - paddingY = 4 - space = 2 - offset = 88 - ) - - textW := len(text)*(fontW+space) - space - textH := fontH - - boxW := textW + paddingX*2 - boxH := textH + paddingY*2 - - x := cx + offset + x := cx + previewOffset y := cy - boxH/2 if x+boxW >= width { - x = cx - boxW - offset + x = cx - boxW - previewOffset } if x < 0 { x = 0 @@ -1099,8 +1157,18 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo if y+boxH >= height { y = height - boxH } + return rect{x, y, boxW, boxH} +} - drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat) +func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) { + text := formatColorForPreview(c, format, lowercase) + if len(text) == 0 { + return + } + + box := previewRect(width, height, cx, cy, text) + x, y := box.x, box.y + drawFilledRect(data, stride, width, height, x, y, box.w, box.h, c, pixelFormat) lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B) var fg Color @@ -1109,7 +1177,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo } else { fg = Color{R: 255, G: 255, B: 255, A: 255} } - drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat) + drawText(data, stride, width, height, x+previewPaddingX, y+previewPaddingY, text, fg, pixelFormat) } func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string { diff --git a/core/internal/colorpicker/state_test.go b/core/internal/colorpicker/state_test.go index 045cc74d7..a6ba27a60 100644 --- a/core/internal/colorpicker/state_test.go +++ b/core/internal/colorpicker/state_test.go @@ -196,13 +196,13 @@ func TestSurfaceState_PickColorNilBuffer(t *testing.T) { func TestSurfaceState_RedrawNilBuffer(t *testing.T) { s := NewSurfaceState(FormatHex, false) - buf := s.Redraw() + buf, _ := s.Redraw() assert.Nil(t, buf) } func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) { s := NewSurfaceState(FormatHex, false) - buf := s.RedrawScreenOnly() + buf, _ := s.RedrawScreenOnly() assert.Nil(t, buf) } @@ -298,3 +298,99 @@ func TestBlendColors(t *testing.T) { result = blendColors(bg, fg, 2.0) assert.Equal(t, fg.R, result.R) } + +func newReadyState(t *testing.T, w, h int) *SurfaceState { + t.Helper() + s := NewSurfaceState(FormatHex, false) + if err := s.OnScreencopyBuffer(FormatXRGB8888, w, h, w*4+16); err != nil { + t.Fatal(err) + } + t.Cleanup(s.Destroy) + data := s.ScreenBuffer().Data() + for i := range data { + data[i] = byte(i*31 + i/7) + } + if err := s.OnLayerConfigure(w, h); err != nil { + t.Fatal(err) + } + s.OnScreencopyReady() + return s +} + +func TestSurfaceState_RedrawIncremental(t *testing.T) { + const w, h = 400, 300 + s := newReadyState(t, w, h) + screen := s.ScreenBuffer() + + reference := func(px, py int, overlay bool) []byte { + ref, err := CreateShmBuffer(w, h, screen.Stride) + if err != nil { + t.Fatal(err) + } + defer ref.Close() + ref.CopyFrom(screen) + if overlay { + c := GetPixelColorWithFormat(screen, px, py, s.screenFormat) + drawMagnifierWithInversion(ref.Data(), ref.Stride, w, h, screen.Data(), screen.Stride, w, h, px, py, c, false, s.screenFormat) + drawColorPreview(ref.Data(), ref.Stride, w, h, px, py, c, FormatHex, false, s.screenFormat) + } + return append([]byte(nil), ref.Data()...) + } + + inDamage := func(x, y int, damage []rect) bool { + for _, r := range damage { + if x >= r.x && x < r.x+r.w && y >= r.y && y < r.y+r.h { + return true + } + } + return false + } + + var onScreen []byte + positions := [][2]int{{10, 10}, {200, 150}, {205, 152}, {399, 299}, {0, 299}, {-1, -1}, {120, 40}, {125, 40}, {300, 290}} + for i, pos := range positions { + hidden := pos[0] < 0 + var buf *ShmBuffer + var damage []rect + px, py := pos[0], pos[1] + switch { + case hidden: + buf, damage = s.RedrawScreenOnly() + default: + s.OnPointerMotion(float64(px), float64(py)) + buf, damage = s.Redraw() + } + if buf == nil { + t.Fatalf("step %d: no buffer", i) + } + want := reference(px, py, !hidden) + if !bytesEqual(buf.Data(), want) { + t.Fatalf("step %d: incremental frame differs from full render", i) + } + if onScreen != nil { + for y := range h { + for x := range w { + off := y*buf.Stride + x*4 + if bytesEqual(want[off:off+4], onScreen[off:off+4]) || inDamage(x, y, damage) { + continue + } + t.Fatalf("step %d: pixel %d,%d changed outside damage %v", i, x, y, damage) + } + } + } + onScreen = want + s.SwapBuffers() + } +} + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +}