From c90b0cddabe9fa09a2a8d2ee87be1a56ec8e763a Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 20 Aug 2026 17:33:35 -0400 Subject: [PATCH] perf(screenshot): frame-paced, incremental region selector related #3056 --- core/cmd/dms/commands_screenshot.go | 21 +- core/internal/screenshot/region.go | 66 ++++- core/internal/screenshot/region_input.go | 4 +- core/internal/screenshot/region_render.go | 256 ++++++++++++------ .../internal/screenshot/region_render_test.go | 167 ++++++++++++ core/internal/screenshot/screenshot.go | 17 +- core/internal/screenshot/types.go | 2 + 7 files changed, 419 insertions(+), 114 deletions(-) create mode 100644 core/internal/screenshot/region_render_test.go diff --git a/core/cmd/dms/commands_screenshot.go b/core/cmd/dms/commands_screenshot.go index a24709048..5678bf230 100644 --- a/core/cmd/dms/commands_screenshot.go +++ b/core/cmd/dms/commands_screenshot.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime/debug" "strconv" "strings" @@ -225,7 +226,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config { return config } -// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. Best-effort. +// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. +// Best-effort and not awaited: qs takes ~20ms to come up, longer than the selector needs to show. func setPopoutScreenshotMode(begin bool) { fn := "end" if begin { @@ -244,7 +246,7 @@ func setPopoutScreenshotMode(begin bool) { cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath()) } cmdArgs = append(cmdArgs, "call", "screenshot", fn) - _ = exec.Command("qs", cmdArgs...).Run() + _ = exec.Command("qs", cmdArgs...).Start() } func writeScreenshotJSON(meta screenshotMetadata) { @@ -277,16 +279,13 @@ func runScreenshot(config screenshot.Config) { os.Exit(1) } - // Region select needs the keyboard; drop popout grabs for its duration. - result, err := func() (*screenshot.CaptureResult, error) { - interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion || config.Mode == screenshot.ModeScroll - if interactive { - setPopoutScreenshotMode(true) - defer setPopoutScreenshotMode(false) - } - return screenshot.New(config).Run() - }() + // Short-lived process over a few tens of MB: let the heap grow instead of paying GC cycles mid-capture. + debug.SetGCPercent(-1) + debug.SetMemoryLimit(1 << 30) + // Region select needs the keyboard; drop popout grabs for its duration. + config.SelectorHook = setPopoutScreenshotMode + result, err := screenshot.New(config).Run() if err != nil { exitScreenshotError("", err) } diff --git a/core/internal/screenshot/region.go b/core/internal/screenshot/region.go index 81e888b62..016069cef 100644 --- a/core/internal/screenshot/region.go +++ b/core/internal/screenshot/region.go @@ -34,7 +34,7 @@ type RenderSlot struct { backgroundDragging bool backgroundCursor bool backgroundPhase selectorPhase - dirty *dirtyRect + overlay *overlay } func (s *RenderSlot) cacheValid(src *ShmBuffer, dragging, cursor bool, phase selectorPhase) bool { @@ -61,6 +61,10 @@ type OutputSurface struct { // Triple-buffered render slots slots [3]*RenderSlot slotsReady bool + + shown *overlay + framePending bool + redrawQueued bool } type PreCapture struct { @@ -86,6 +90,8 @@ type RegionSelector struct { screencopy *wlr_screencopy.ZwlrScreencopyManagerV1 viewporter *wp_viewporter.WpViewporter + compositorVersion uint32 + shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1 shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1 @@ -261,6 +267,7 @@ func (r *RegionSelector) handleGlobal(e client.RegistryGlobalEvent) { comp := client.NewCompositor(r.ctx) if err := r.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil { r.compositor = comp + r.compositorVersion = e.Version } case client.ShmInterfaceName: @@ -732,6 +739,9 @@ func (r *RegionSelector) initRenderBuffer(os *OutputSurface) { slotRef := slot wlBuf.SetReleaseHandler(func(e client.BufferReleaseEvent) { slotRef.busy = false + if os.redrawQueued && !os.framePending { + r.renderSurface(os) + } }) os.slots[i] = slot @@ -783,7 +793,16 @@ func (r *RegionSelector) getSourceBuffer(os *OutputSurface) *ShmBuffer { return os.screenBuf } +// redrawSurface coalesces redraws to one per compositor frame. func (r *RegionSelector) redrawSurface(os *OutputSurface) { + os.redrawQueued = true + if os.framePending { + return + } + r.renderSurface(os) +} + +func (r *RegionSelector) renderSurface(os *OutputSurface) { srcBuf := r.getSourceBuffer(os) if srcBuf == nil || !os.slotsReady { return @@ -793,15 +812,20 @@ func (r *RegionSelector) redrawSurface(os *OutputSurface) { if slot == nil { return } + os.redrawQueued = false + fullDamage := true + var damage []dirtyRect switch r.phase { case phaseScroll: r.drawScrollOverlay(os, slot.shm) slot.backgroundInitialized = false slot.backgroundSource = nil - slot.dirty = nil + slot.overlay, os.shown = nil, nil default: - if !slot.cacheValid(srcBuf, r.selection.dragging, r.showCapturedCursor, r.phase) { + cur := r.overlayFor(os, slot.shm) + switch { + case !slot.cacheValid(srcBuf, r.selection.dragging, r.showCapturedCursor, r.phase): slot.shm.CopyFrom(srcBuf) r.dimBackground(slot.shm) r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat) @@ -810,11 +834,16 @@ func (r *RegionSelector) redrawSurface(os *OutputSurface) { slot.backgroundDragging = r.selection.dragging slot.backgroundCursor = r.showCapturedCursor slot.backgroundPhase = r.phase - slot.dirty = nil - } else if slot.dirty != nil { - r.restoreSourceRect(os, slot.shm, *slot.dirty) + slot.overlay = nil + case r.compositorVersion >= 4: + fullDamage = false + damage = overlayDamage(os.shown, cur) + if len(damage) == 0 { + return + } } - slot.dirty = r.drawOverlay(os, slot.shm) + r.drawOverlay(os, slot.shm, slot.overlay, cur) + slot.overlay, os.shown = cur, cur } if os.viewport != nil { @@ -830,7 +859,28 @@ func (r *RegionSelector) redrawSurface(os *OutputSurface) { } _ = os.wlSurface.Attach(slot.wlBuf, 0, 0) - _ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH)) + switch { + case fullDamage: + _ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH)) + default: + for _, d := range damage { + d = d.clampTo(slot.shm.Width, slot.shm.Height) + if d.empty() { + continue + } + _ = os.wlSurface.DamageBuffer(int32(d.x1), int32(d.y1), int32(d.x2-d.x1), int32(d.y2-d.y1)) + } + } + if cb, err := os.wlSurface.Frame(); err == nil { + os.framePending = true + cb.SetDoneHandler(func(client.CallbackDoneEvent) { + _ = cb.Destroy() + os.framePending = false + if os.redrawQueued { + r.renderSurface(os) + } + }) + } _ = os.wlSurface.Commit() // Mark this slot as busy until compositor releases it diff --git a/core/internal/screenshot/region_input.go b/core/internal/screenshot/region_input.go index d75ab4be5..bbd6d8060 100644 --- a/core/internal/screenshot/region_input.go +++ b/core/internal/screenshot/region_input.go @@ -84,8 +84,8 @@ func (r *RegionSelector) setupPointerHandlers() { r.selection.currentX = curX r.selection.currentY = curY - for _, os := range r.surfaces { - r.redrawSurface(os) + if r.selection.surface != nil { + r.redrawSurface(r.selection.surface) } }) diff --git a/core/internal/screenshot/region_render.go b/core/internal/screenshot/region_render.go index 2f19487a5..3b598f30b 100644 --- a/core/internal/screenshot/region_render.go +++ b/core/internal/screenshot/region_render.go @@ -58,6 +58,7 @@ type selectionRenderBounds struct { h int } +// dirtyRect is half-open in buffer pixels. type dirtyRect struct { x1 int y1 int @@ -65,6 +66,10 @@ type dirtyRect struct { y2 int } +func (d dirtyRect) empty() bool { + return d.x2 <= d.x1 || d.y2 <= d.y1 +} + func (d dirtyRect) union(o dirtyRect) dirtyRect { return dirtyRect{ x1: min(d.x1, o.x1), @@ -74,27 +79,127 @@ func (d dirtyRect) union(o dirtyRect) dirtyRect { } } -func (r *RegionSelector) dimBackground(renderBuf *ShmBuffer) { - data := renderBuf.Data() - r.dimRegion(data, renderBuf.Stride, renderBuf.Width, renderBuf.Height, 0, 0, renderBuf.Width, renderBuf.Height) +func (d dirtyRect) intersect(o dirtyRect) dirtyRect { + return dirtyRect{ + x1: max(d.x1, o.x1), + y1: max(d.y1, o.y1), + x2: min(d.x2, o.x2), + y2: min(d.y2, o.y2), + } } -func (r *RegionSelector) dimRegion(data []byte, stride, bufW, bufH, x1, y1, x2, y2 int) { - x1 = clamp(x1, 0, bufW) - y1 = clamp(y1, 0, bufH) - x2 = clamp(x2, 0, bufW) - y2 = clamp(y2, 0, bufH) - for y := y1; y < y2; y++ { - off := y * stride - for x := x1; x < x2; x++ { - i := off + x*4 - if i+3 >= len(data) { - continue - } - data[i+0] = uint8(int(data[i+0]) * 3 / 5) - data[i+1] = uint8(int(data[i+1]) * 3 / 5) - data[i+2] = uint8(int(data[i+2]) * 3 / 5) - data[i+3] = 255 +func (d dirtyRect) grow(n int) dirtyRect { + return dirtyRect{x1: d.x1 - n, y1: d.y1 - n, x2: d.x2 + n, y2: d.y2 + n} +} + +func (d dirtyRect) clampTo(w, h int) dirtyRect { + return d.intersect(dirtyRect{0, 0, w, h}) +} + +// minus returns d with o cut out, as up to four strips. +func (d dirtyRect) minus(o dirtyRect) []dirtyRect { + if d.empty() { + return nil + } + i := d.intersect(o) + if i.empty() { + return []dirtyRect{d} + } + var out []dirtyRect + for _, s := range []dirtyRect{ + {d.x1, d.y1, d.x2, i.y1}, + {d.x1, i.y2, d.x2, d.y2}, + {d.x1, i.y1, i.x1, i.y2}, + {i.x2, i.y1, d.x2, i.y2}, + } { + if !s.empty() { + out = append(out, s) + } + } + return out +} + +// overlay is what a frame draws on top of the dimmed background: the bright +// selection interior with its border ring, and the dimensions label. +type overlay struct { + interior dirtyRect + label dirtyRect + text string +} + +func (o *overlay) full() dirtyRect { + return o.interior.grow(borderThickness - 1) +} + +func (o *overlay) inner() dirtyRect { + return o.interior.grow(-1) +} + +func (o *overlay) ring() []dirtyRect { + return o.full().minus(o.inner()) +} + +// overlayDelta returns the areas to re-dim and to re-brighten when prev is replaced by cur; nil means no overlay. +func overlayDelta(prev, cur *overlay) (dim, bright []dirtyRect) { + var curInterior, prevInner dirtyRect + if cur != nil { + curInterior = cur.interior + } + if prev != nil { + dim = append(prev.full().minus(curInterior), prev.label.minus(curInterior)...) + prevInner = prev.inner() + } + if cur != nil { + bright = cur.interior.minus(prevInner) + } + return dim, bright +} + +var dimLUT = func() (t [256]uint8) { + for i := range t { + t[i] = uint8(i * 3 / 5) + } + return t +}() + +func dimRow(p []byte) { + for i := 0; i+3 < len(p); i += 4 { + p[i], p[i+1], p[i+2], p[i+3] = dimLUT[p[i]], dimLUT[p[i+1]], dimLUT[p[i+2]], 255 + } +} + +func opaqueRow(p []byte) { + for i := 3; i < len(p); i += 4 { + p[i] = 255 + } +} + +func (r *RegionSelector) dimBackground(renderBuf *ShmBuffer) { + data := renderBuf.Data() + for y := range renderBuf.Height { + dimRow(data[y*renderBuf.Stride:][:renderBuf.Width*4]) + } +} + +// paintRect copies area from the capture into dst, dimmed or bright. +func (r *RegionSelector) paintRect(os *OutputSurface, dst *ShmBuffer, area dirtyRect, dim bool) { + src := r.getSourceBuffer(os) + if src == nil { + return + } + area = area.clampTo(min(src.Width, dst.Width), min(src.Height, dst.Height)) + if area.empty() { + return + } + rowLen := (area.x2 - area.x1) * 4 + for y := area.y1; y < area.y2; y++ { + s := src.Data()[y*src.Stride+area.x1*4:][:rowLen] + d := dst.Data()[y*dst.Stride+area.x1*4:][:rowLen] + copy(d, s) + if dim { + dimRow(d) + } else { + opaqueRow(d) } } } @@ -140,72 +245,46 @@ func (r *RegionSelector) selectionRenderBounds(os *OutputSurface) (selectionRend return selectionRenderBounds{x: x1, y: y1, w: w, h: h}, true } -func (r *RegionSelector) restoreSourceRect(os *OutputSurface, renderBuf *ShmBuffer, d dirtyRect) { - srcBuf := r.getSourceBuffer(os) - if srcBuf == nil { - return - } - - maxW := min(srcBuf.Width, renderBuf.Width) - maxH := min(srcBuf.Height, renderBuf.Height) - x1 := clamp(d.x1, 0, maxW) - y1 := clamp(d.y1, 0, maxH) - x2 := clamp(d.x2, 0, maxW) - y2 := clamp(d.y2, 0, maxH) - if x2 <= x1 || y2 <= y1 { - return - } - - width := (x2 - x1) * 4 - for y := y1; y < y2; y++ { - srcStart := y*srcBuf.Stride + x1*4 - dstStart := y*renderBuf.Stride + x1*4 - if srcStart+width > len(srcBuf.Data()) || dstStart+width > len(renderBuf.Data()) { - continue - } - copy(renderBuf.Data()[dstStart:dstStart+width], srcBuf.Data()[srcStart:srcStart+width]) - } - r.dimRegion(renderBuf.Data(), renderBuf.Stride, renderBuf.Width, renderBuf.Height, x1, y1, x2, y2) -} - -func (r *RegionSelector) drawOverlay(os *OutputSurface, renderBuf *ShmBuffer) *dirtyRect { +func (r *RegionSelector) overlayFor(os *OutputSurface, buf *ShmBuffer) *overlay { bounds, ok := r.selectionRenderBounds(os) if !ok { return nil } + label, text := labelRect(bounds, buf.Width, buf.Height) + return &overlay{ + interior: dirtyRect{bounds.x, bounds.y, bounds.x + bounds.w, bounds.y + bounds.h}, + label: label, + text: text, + } +} - data := renderBuf.Data() - stride := renderBuf.Stride - w, h := renderBuf.Width, renderBuf.Height - format := os.screenFormat - - srcBuf := r.getSourceBuffer(os) - srcData := srcBuf.Data() - for y := bounds.y; y < bounds.y+bounds.h; y++ { - rowOff := y * stride - for x := bounds.x; x < bounds.x+bounds.w; x++ { - si := y*srcBuf.Stride + x*4 - di := rowOff + x*4 - if si+3 >= len(srcData) || di+3 >= len(data) { - continue - } - data[di+0] = srcData[si+0] - data[di+1] = srcData[si+1] - data[di+2] = srcData[si+2] - data[di+3] = 255 - } +// drawOverlay brings a buffer holding prev up to cur, touching only what changed. +func (r *RegionSelector) drawOverlay(os *OutputSurface, renderBuf *ShmBuffer, prev, cur *overlay) { + dim, bright := overlayDelta(prev, cur) + for _, d := range dim { + r.paintRect(os, renderBuf, d, true) + } + for _, b := range bright { + r.paintRect(os, renderBuf, b, false) + } + if cur == nil { + return } - r.drawBorder(data, stride, w, h, bounds.x, bounds.y, bounds.w, bounds.h, format) - labelRect := r.drawDimensions(data, stride, w, h, bounds.x, bounds.y, bounds.w, bounds.h, format) + data, stride, w, h := renderBuf.Data(), renderBuf.Stride, renderBuf.Width, renderBuf.Height + in := cur.interior + r.drawBorder(data, stride, w, h, in.x1, in.y1, in.x2-in.x1, in.y2-in.y1, os.screenFormat) + r.drawLabel(data, stride, w, h, cur, os.screenFormat) +} - dirty := dirtyRect{ - x1: bounds.x - borderThickness, - y1: bounds.y - borderThickness, - x2: bounds.x + bounds.w + borderThickness, - y2: bounds.y + bounds.h + borderThickness, - }.union(labelRect) - return &dirty +// overlayDamage lists the buffer areas that differ between a frame showing prev and one showing cur. +func overlayDamage(prev, cur *overlay) []dirtyRect { + dim, bright := overlayDelta(prev, cur) + damage := append(dim, bright...) + if cur == nil { + return damage + } + return append(append(damage, cur.ring()...), cur.label) } func (r *RegionSelector) drawScrollOverlay(os *OutputSurface, renderBuf *ShmBuffer) { @@ -384,24 +463,25 @@ func (r *RegionSelector) drawVLine(data []byte, stride, bufW, bufH, x, y, length } } -func (r *RegionSelector) drawDimensions(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) dirtyRect { - text := fmt.Sprintf("%dx%d", w, h) +func labelRect(b selectionRenderBounds, bufW, bufH int) (dirtyRect, string) { + text := fmt.Sprintf("%dx%d", b.w, b.h) const charW, charH = 8, 12 textW := len(text) * (charW + 1) - textH := charH - tx := x + (w-textW)/2 - ty := y + h + 8 - - if ty+textH > bufH { - ty = y - textH - 8 + tx := b.x + (b.w-textW)/2 + ty := b.y + b.h + 8 + if ty+charH > bufH { + ty = b.y - charH - 8 } tx = clamp(tx, 0, bufW-textW) + return dirtyRect{x1: tx - 4, y1: ty - 2, x2: tx + textW + 4, y2: ty + charH + 2}, text +} - r.fillRect(data, stride, bufW, bufH, tx-4, ty-2, textW+8, textH+4, 0, 0, 0, 200, format) - r.drawText(data, stride, bufW, bufH, tx, ty, text, 255, 255, 255, format) - return dirtyRect{x1: tx - 4, y1: ty - 2, x2: tx + textW + 4, y2: ty + textH + 2} +func (r *RegionSelector) drawLabel(data []byte, stride, bufW, bufH int, o *overlay, format uint32) { + l := o.label + r.fillRect(data, stride, bufW, bufH, l.x1, l.y1, l.x2-l.x1, l.y2-l.y1, 0, 0, 0, 200, format) + r.drawText(data, stride, bufW, bufH, l.x1+4, l.y1+2, o.text, 255, 255, 255, format) } func (r *RegionSelector) fillRect(data []byte, stride, bufW, bufH, x, y, w, h int, cr, cg, cb, ca uint8, format uint32) { diff --git a/core/internal/screenshot/region_render_test.go b/core/internal/screenshot/region_render_test.go new file mode 100644 index 000000000..00804db97 --- /dev/null +++ b/core/internal/screenshot/region_render_test.go @@ -0,0 +1,167 @@ +package screenshot + +import ( + "bytes" + "math/rand" + "testing" +) + +func rectArea(r dirtyRect) int { + if r.empty() { + return 0 + } + return (r.x2 - r.x1) * (r.y2 - r.y1) +} + +func TestDirtyRectMinus(t *testing.T) { + a := dirtyRect{0, 0, 10, 10} + cases := []struct { + name string + b dirtyRect + parts int + area int + }{ + {"disjoint", dirtyRect{20, 20, 30, 30}, 1, 100}, + {"covered", dirtyRect{-1, -1, 11, 11}, 0, 0}, + {"center", dirtyRect{3, 3, 6, 6}, 4, 91}, + {"corner", dirtyRect{5, 5, 20, 20}, 2, 75}, + {"zero", dirtyRect{}, 1, 100}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + parts := a.minus(tc.b) + if len(parts) != tc.parts { + t.Fatalf("parts = %d, want %d", len(parts), tc.parts) + } + area := 0 + for i, p := range parts { + area += rectArea(p) + if !p.intersect(a).equal(p) || !p.intersect(tc.b).empty() { + t.Errorf("part %v outside a or inside b", p) + } + for _, q := range parts[i+1:] { + if !p.intersect(q).empty() { + t.Errorf("parts %v and %v overlap", p, q) + } + } + } + if area != tc.area { + t.Errorf("area = %d, want %d", area, tc.area) + } + }) + } +} + +func (d dirtyRect) equal(o dirtyRect) bool { return d == o } + +func newOverlayFixture(t testing.TB, w, h int) (*RegionSelector, *OutputSurface) { + t.Helper() + src, err := CreateShmBuffer(w, h, w*4+16) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { src.Close() }) + rng := rand.New(rand.NewSource(7)) + rng.Read(src.Data()) + + os := &OutputSurface{screenBuf: src, screenFormat: uint32(FormatXRGB8888), logicalW: w, logicalH: h} + r := &RegionSelector{showCapturedCursor: true} + r.selection.surface = os + return r, os +} + +func newFrameBuffer(t testing.TB, r *RegionSelector, os *OutputSurface) *ShmBuffer { + t.Helper() + buf, err := CreateShmBuffer(os.screenBuf.Width, os.screenBuf.Height, os.screenBuf.Stride) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { buf.Close() }) + buf.CopyFrom(os.screenBuf) + r.dimBackground(buf) + return buf +} + +func (r *RegionSelector) selectRect(x1, y1, x2, y2 float64) { + r.selection.hasSelection = true + r.selection.anchorX, r.selection.anchorY = x1, y1 + r.selection.currentX, r.selection.currentY = x2, y2 +} + +func inRects(x, y int, rects []dirtyRect) bool { + for _, d := range rects { + if x >= d.x1 && x < d.x2 && y >= d.y1 && y < d.y2 { + return true + } + } + return false +} + +func TestDrawOverlayIncremental(t *testing.T) { + const w, h = 160, 120 + r, os := newOverlayFixture(t, w, h) + incr := newFrameBuffer(t, r, os) + shown := newFrameBuffer(t, r, os) + rng := rand.New(rand.NewSource(3)) + + var prev *overlay + for i := range 300 { + var cur *overlay + switch rng.Intn(8) { + case 0: + r.selection.hasSelection = false + default: + r.selectRect(rng.Float64()*w, rng.Float64()*h, rng.Float64()*w, rng.Float64()*h) + cur = r.overlayFor(os, incr) + } + damage := overlayDamage(prev, cur) + r.drawOverlay(os, incr, prev, cur) + + full := newFrameBuffer(t, r, os) + r.drawOverlay(os, full, nil, cur) + if !bytes.Equal(incr.Data(), full.Data()) { + t.Fatalf("step %d: incremental frame differs from full render (%v -> %v)", i, prev, cur) + } + + for y := range h { + for x := range w { + off := y*full.Stride + x*4 + if bytes.Equal(full.Data()[off:off+4], shown.Data()[off:off+4]) { + continue + } + if !inRects(x, y, damage) { + t.Fatalf("step %d: pixel %d,%d changed outside damage %v", i, x, y, damage) + } + } + } + copy(shown.Data(), full.Data()) + prev = cur + } +} + +func BenchmarkDrawOverlayStep(b *testing.B) { + r, os := newOverlayFixture(b, 2560, 1440) + buf := newFrameBuffer(b, r, os) + r.selectRect(10, 10, 2000, 1400) + prev := r.overlayFor(os, buf) + r.drawOverlay(os, buf, nil, prev) + b.ResetTimer() + for i := range b.N { + r.selectRect(10, 10, float64(2000+i%8), float64(1400-i%8)) + cur := r.overlayFor(os, buf) + r.drawOverlay(os, buf, prev, cur) + prev = cur + } +} + +func BenchmarkDrawOverlayFull(b *testing.B) { + r, os := newOverlayFixture(b, 2560, 1440) + buf := newFrameBuffer(b, r, os) + b.ResetTimer() + for i := range b.N { + r.selectRect(10, 10, float64(2000+i%8), float64(1400-i%8)) + cur := r.overlayFor(os, buf) + r.paintRect(os, buf, dirtyRect{0, 0, buf.Width, buf.Height}, true) + r.drawOverlay(os, buf, nil, cur) + } +} diff --git a/core/internal/screenshot/screenshot.go b/core/internal/screenshot/screenshot.go index 4f491f5d0..e92f8bb77 100644 --- a/core/internal/screenshot/screenshot.go +++ b/core/internal/screenshot/screenshot.go @@ -131,6 +131,11 @@ func (s *Screenshoter) captureRegion() (*CaptureResult, error) { } } + if s.config.SelectorHook != nil { + s.config.SelectorHook(true) + defer s.config.SelectorHook(false) + } + selector := NewRegionSelector(s) result, cancelled, err := selector.Run() if err != nil { @@ -579,11 +584,13 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca outX, outY := output.x, output.y scale := float64(output.scale) - if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { - outX, outY = hx, hy - } - if s := GetHyprlandMonitorScale(output.name); s > 0 { - scale = s + if DetectCompositor() == CompositorHyprland { + if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { + outX, outY = hx, hy + } + if s := GetHyprlandMonitorScale(output.name); s > 0 { + scale = s + } } if scale <= 0 { scale = 1.0 diff --git a/core/internal/screenshot/types.go b/core/internal/screenshot/types.go index 54739b12a..4956c44a2 100644 --- a/core/internal/screenshot/types.go +++ b/core/internal/screenshot/types.go @@ -64,6 +64,8 @@ type Config struct { Notify bool Stdout bool IntervalMs int + // SelectorHook runs as the interactive selector starts (true) and ends (false). + SelectorHook func(begin bool) } func DefaultConfig() Config {