diff --git a/Overlord-Client/cmd/agent/capture/capture.go b/Overlord-Client/cmd/agent/capture/capture.go index 47471a0..903db4d 100644 --- a/Overlord-Client/cmd/agent/capture/capture.go +++ b/Overlord-Client/cmd/agent/capture/capture.go @@ -35,6 +35,8 @@ func Loop(ctx context.Context, env *rt.Env) { done := make(chan struct{}) goSafe("capture initial frame", env.Cancel, func() { defer close(done) + restore := BypassResolutionCap() + defer restore() var err error if goruntime.GOOS == "windows" && activeDisplays() > 1 { err = captureAllDisplaysAndSend(ctx, env) @@ -197,12 +199,53 @@ func captureAllDisplaysAndSend(ctx context.Context, env *rt.Env) error { } } - canvas := image.NewRGBA(image.Rect(0, 0, maxX-minX, maxY-minY)) - for _, part := range parts { - offX := part.bounds.Min.X - minX - offY := part.bounds.Min.Y - minY - dst := image.Rect(offX, offY, offX+part.img.Rect.Dx(), offY+part.img.Rect.Dy()) - draw.Draw(canvas, dst, part.img, part.img.Rect.Min, draw.Src) + // Compute per-monitor scale factors to handle resolution-cap or DPI + // mismatches between displayBounds (virtual coords) and captured pixels. + type scaledPart struct { + img *image.RGBA + scaleX float64 + scaleY float64 + offX int + offY int + } + scaled := make([]scaledPart, len(parts)) + for i, part := range parts { + sx, sy := 1.0, 1.0 + bw := part.bounds.Dx() + bh := part.bounds.Dy() + iw := part.img.Rect.Dx() + ih := part.img.Rect.Dy() + if bw > 0 && iw > 0 { + sx = float64(iw) / float64(bw) + } + if bh > 0 && ih > 0 { + sy = float64(ih) / float64(bh) + } + scaled[i] = scaledPart{ + img: part.img, + scaleX: sx, + scaleY: sy, + offX: int(float64(part.bounds.Min.X-minX) * sx), + offY: int(float64(part.bounds.Min.Y-minY) * sy), + } + } + + // Canvas sized from scaled image placements. + cW, cH := 0, 0 + for _, sp := range scaled { + rx := sp.offX + sp.img.Rect.Dx() + ry := sp.offY + sp.img.Rect.Dy() + if rx > cW { + cW = rx + } + if ry > cH { + cH = ry + } + } + canvas := image.NewRGBA(image.Rect(0, 0, cW, cH)) + for _, sp := range scaled { + dst := image.Rect(sp.offX, sp.offY, sp.offX+sp.img.Rect.Dx(), sp.offY+sp.img.Rect.Dy()) + draw.Draw(canvas, dst, sp.img, sp.img.Rect.Min, draw.Src) } quality := jpegQuality() diff --git a/Overlord-Client/cmd/agent/capture/cursor_windows.go b/Overlord-Client/cmd/agent/capture/cursor_windows.go index 9b4f304..cc682ec 100644 --- a/Overlord-Client/cmd/agent/capture/cursor_windows.go +++ b/Overlord-Client/cmd/agent/capture/cursor_windows.go @@ -196,6 +196,10 @@ func drawCursor(img *image.RGBA, cursorX, cursorY int32, bounds image.Rectangle) } func DrawCursorOnDC(hdc uintptr, captureBounds image.Rectangle) bool { + return DrawCursorOnDCScaled(hdc, captureBounds, 1.0, 1.0) +} + +func DrawCursorOnDCScaled(hdc uintptr, captureBounds image.Rectangle, scaleX, scaleY float64) bool { if !cursorCaptureEnabled.Load() || hdc == 0 { return false } @@ -217,8 +221,9 @@ func DrawCursorOnDC(hdc uintptr, captureBounds image.Rectangle) bool { 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) + // Map from screen space into the (possibly scaled) DC space. + x := int32(float64(ci.ptScreenPos.x-int32(captureBounds.Min.X))*scaleX) - int32(icon.xHotspot) + y := int32(float64(ci.ptScreenPos.y-int32(captureBounds.Min.Y))*scaleY) - int32(icon.yHotspot) ret, _, _ = procDrawIconEx.Call( hdc, uintptr(x), diff --git a/Overlord-Client/cmd/agent/capture/resolution_default.go b/Overlord-Client/cmd/agent/capture/resolution_default.go new file mode 100644 index 0000000..f26c72d --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/resolution_default.go @@ -0,0 +1,13 @@ +//go:build !windows + +package capture + +func SetMaxResolution(maxH int) {} + +func BypassResolutionCap() (restore func()) { + return func() {} +} + +func EffectiveScaleForInput() float64 { + return 1.0 +} diff --git a/Overlord-Client/cmd/agent/capture/win_bitblt.go b/Overlord-Client/cmd/agent/capture/win_bitblt.go index c953189..a522522 100644 --- a/Overlord-Client/cmd/agent/capture/win_bitblt.go +++ b/Overlord-Client/cmd/agent/capture/win_bitblt.go @@ -227,7 +227,7 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) { dstH = srcH } - useStretch := dstW > 0 && dstH > 0 && (dstW < srcW || dstH < srcH) && !cursorCaptureEnabled.Load() + useStretch := dstW > 0 && dstH > 0 && (dstW < srcW || dstH < srcH) capW := srcW capH := srcH if useStretch { @@ -290,7 +290,11 @@ func captureDisplayBitBlt(display int) (*image.RGBA, error) { } bitDur := time.Since(bitStart) cursorDrawn := false - if !useStretch { + if useStretch { + sx := float64(capW) / float64(srcW) + sy := float64(capH) / float64(srcH) + cursorDrawn = DrawCursorOnDCScaled(hdcMem, bounds, sx, sy) + } else { cursorDrawn = DrawCursorOnDC(hdcMem, bounds) } @@ -544,6 +548,11 @@ func SetMaxResolution(maxH int) { log.Printf("capture: max resolution set to %d", maxH) } +func BypassResolutionCap() (restore func()) { + old := maxResHeight.Swap(-1) + return func() { maxResHeight.Store(old) } +} + func EffectiveScaleForInput() float64 { bits := lastCapScale.Load() if bits == 0 { diff --git a/Overlord-Client/cmd/agent/handlers/screenshot.go b/Overlord-Client/cmd/agent/handlers/screenshot.go index 40d735d..83d59ac 100644 --- a/Overlord-Client/cmd/agent/handlers/screenshot.go +++ b/Overlord-Client/cmd/agent/handlers/screenshot.go @@ -24,6 +24,9 @@ var ( ) func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplays bool) error { + restore := capture.BypassResolutionCap() + defer restore() + if allDisplays { log.Printf("screenshot: capturing all displays") } else { @@ -202,13 +205,41 @@ func captureScreenshotImageWindows(allDisplays bool) (*image.RGBA, int, image.Re } } - virtualBounds := image.Rect(minX, minY, maxX, maxY) - canvas := image.NewRGBA(image.Rect(0, 0, virtualBounds.Dx(), virtualBounds.Dy())) - for _, part := range parts { - offX := part.bounds.Min.X - minX - offY := part.bounds.Min.Y - minY - dst := image.Rect(offX, offY, offX+part.img.Rect.Dx(), offY+part.img.Rect.Dy()) - draw.Draw(canvas, dst, part.img, part.img.Rect.Min, draw.Src) + type scaledPart struct { + img *image.RGBA + offX int + offY int + } + sp := make([]scaledPart, len(parts)) + for i, part := range parts { + sx, sy := 1.0, 1.0 + if bw := part.bounds.Dx(); bw > 0 { + sx = float64(part.img.Rect.Dx()) / float64(bw) + } + if bh := part.bounds.Dy(); bh > 0 { + sy = float64(part.img.Rect.Dy()) / float64(bh) + } + sp[i] = scaledPart{ + img: part.img, + offX: int(float64(part.bounds.Min.X-minX) * sx), + offY: int(float64(part.bounds.Min.Y-minY) * sy), + } + } + + cW, cH := 0, 0 + for _, s := range sp { + if rx := s.offX + s.img.Rect.Dx(); rx > cW { + cW = rx + } + if ry := s.offY + s.img.Rect.Dy(); ry > cH { + cH = ry + } + } + + canvas := image.NewRGBA(image.Rect(0, 0, cW, cH)) + for _, s := range sp { + dst := image.Rect(s.offX, s.offY, s.offX+s.img.Rect.Dx(), s.offY+s.img.Rect.Dy()) + draw.Draw(canvas, dst, s.img, s.img.Rect.Min, draw.Src) } return canvas, 0, canvas.Rect, nil diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index fe61b99..57f7a92 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -220,7 +220,7 @@ export function createRenderer({ wrapper.className = "flex-shrink-0"; const img = document.createElement("img"); img.className = - "w-40 h-24 rounded-lg border border-slate-800 object-cover cursor-pointer thumb-img"; + "w-40 h-24 rounded-lg object-contain cursor-pointer thumb-img"; img.alt = "preview"; img.src = client.thumbnail; wrapper.appendChild(img);