fix screenshot mem usage + fix backstage not starting after being stopped once
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user