mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 16:54:01 -04:00
perf(screenshot): frame-paced, incremental region selector
related #3056
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user