mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 23:17:51 -04:00
perf(colorpicker): incremental magnifier redraws, frame-paced commits
This commit is contained in:
1 parent
52244c01f7
commit
e6a5fae190
4 files changed
+244
-51
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user