real cursor shit

This commit is contained in:
KingKDot
2026-07-19 01:22:42 -05:00
parent 06e8a2921e
commit 1a68b5ddf1
17 changed files with 889 additions and 146 deletions
@@ -3,18 +3,30 @@
package capture
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"sync"
"sync/atomic"
"unsafe"
)
var (
cursorCaptureEnabled atomic.Bool
cursorCaptureEnabled atomic.Bool
cursorMetadataEnabled atomic.Bool
cursorGeneration atomic.Uint64
)
func SetCursorCapture(enabled bool) {
cursorCaptureEnabled.Store(enabled)
// Cursor pixels are transported separately. Keeping frame compositing off
// preserves direct DXGI/video encoding and avoids a full-frame copy.
cursorMetadataEnabled.Store(enabled)
cursorCaptureEnabled.Store(false)
if enabled {
cursorGeneration.Add(1)
}
}
var (
@@ -22,6 +34,7 @@ var (
procGetCursorInfo = user32.NewProc("GetCursorInfo")
procGetIconInfo = user32.NewProc("GetIconInfo")
procDrawIconEx = user32.NewProc("DrawIconEx")
procGetObjectW = gdi32.NewProc("GetObjectW")
)
const (
@@ -50,149 +63,330 @@ type iconInfo struct {
hbmColor uintptr
}
func getCursorPosition() (x, y int32, visible bool) {
type gdiBitmap struct {
bmType int32
bmWidth int32
bmHeight int32
bmWidthBytes int32
bmPlanes uint16
bmBitsPixel uint16
bmBits unsafe.Pointer
}
type cursorShape struct {
width int
height int
hotspotX int
hotspotY int
image []byte
}
var cursorShapeCache struct {
sync.Mutex
hCursor uintptr
shape cursorShape
}
func queryCursor() (cursorInfo, bool) {
var ci cursorInfo
ci.cbSize = uint32(unsafe.Sizeof(ci))
ret, _, _ := procGetCursorInfo.Call(uintptr(unsafe.Pointer(&ci)))
return ci, ret != 0
}
func getCursorPosition() (x, y int32, visible bool) {
ci, ok := queryCursor()
if !ok {
return 0, 0, false
}
return ci.ptScreenPos.x, ci.ptScreenPos.y, (ci.flags & CURSOR_SHOWING) != 0
}
func DesktopCursorState(display, frameWidth, frameHeight int) DesktopCursorMetadata {
if !cursorMetadataEnabled.Load() {
return DesktopCursorMetadata{}
}
state := DesktopCursorMetadata{
Enabled: true,
Generation: cursorGeneration.Load(),
}
ci, ok := queryCursor()
if !ok {
return state
}
state.Shape = uint64(ci.hCursor)
if ci.hCursor != 0 {
if shape, err := getCursorShape(ci.hCursor); err == nil {
state.CursorWidth = shape.width
state.CursorHeight = shape.height
state.HotspotX = shape.hotspotX
state.HotspotY = shape.hotspotY
state.Image = shape.image
}
}
bounds := DisplayBounds(display)
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
return state
}
if frameWidth <= 0 {
frameWidth = bounds.Dx()
}
if frameHeight <= 0 {
frameHeight = bounds.Dy()
}
if state.CursorWidth > 0 && state.CursorHeight > 0 {
state.CursorWidth = scaleCursorMetric(state.CursorWidth, frameWidth, bounds.Dx())
state.CursorHeight = scaleCursorMetric(state.CursorHeight, frameHeight, bounds.Dy())
state.HotspotX = scaleCursorMetric(state.HotspotX, frameWidth, bounds.Dx())
state.HotspotY = scaleCursorMetric(state.HotspotY, frameHeight, bounds.Dy())
if state.HotspotX >= state.CursorWidth {
state.HotspotX = state.CursorWidth - 1
}
if state.HotspotY >= state.CursorHeight {
state.HotspotY = state.CursorHeight - 1
}
}
showing := (ci.flags & CURSOR_SHOWING) != 0
if !showing ||
ci.ptScreenPos.x < int32(bounds.Min.X) || ci.ptScreenPos.x >= int32(bounds.Max.X) ||
ci.ptScreenPos.y < int32(bounds.Min.Y) || ci.ptScreenPos.y >= int32(bounds.Max.Y) {
return state
}
state.X = int(ci.ptScreenPos.x-int32(bounds.Min.X)) * frameWidth / bounds.Dx()
state.Y = int(ci.ptScreenPos.y-int32(bounds.Min.Y)) * frameHeight / bounds.Dy()
state.Visible = true
return state
}
func scaleCursorMetric(value, frameSize, captureSize int) int {
if value <= 0 || frameSize <= 0 || captureSize <= 0 {
return 0
}
scaled := (value*frameSize + captureSize/2) / captureSize
if scaled < 1 {
return 1
}
return scaled
}
func getCursorShape(hCursor uintptr) (cursorShape, error) {
cursorShapeCache.Lock()
defer cursorShapeCache.Unlock()
if cursorShapeCache.hCursor == hCursor && len(cursorShapeCache.shape.image) != 0 {
return cursorShapeCache.shape, nil
}
shape, err := extractCursorShape(hCursor)
if err != nil {
return cursorShape{}, err
}
cursorShapeCache.hCursor = hCursor
cursorShapeCache.shape = shape
return shape, nil
}
func extractCursorShape(hCursor uintptr) (cursorShape, error) {
if hCursor == 0 {
return cursorShape{}, fmt.Errorf("cursor handle is null")
}
var icon iconInfo
ret, _, callErr := procGetIconInfo.Call(hCursor, uintptr(unsafe.Pointer(&icon)))
if ret == 0 {
return cursorShape{}, fmt.Errorf("GetIconInfo: %w", callErr)
}
if icon.hbmMask != 0 {
defer deleteObject(icon.hbmMask)
}
if icon.hbmColor != 0 {
defer deleteObject(icon.hbmColor)
}
width, height, err := cursorBitmapDimensions(icon)
if err != nil {
return cursorShape{}, err
}
black, err := renderCursorOnBackground(hCursor, width, height, 0)
if err != nil {
return cursorShape{}, err
}
white, err := renderCursorOnBackground(hCursor, width, height, 255)
if err != nil {
return cursorShape{}, err
}
img := reconstructCursorRGBA(black, white, width, height)
var encoded bytes.Buffer
if err := png.Encode(&encoded, img); err != nil {
return cursorShape{}, fmt.Errorf("encode cursor PNG: %w", err)
}
return cursorShape{
width: width,
height: height,
hotspotX: int(icon.xHotspot),
hotspotY: int(icon.yHotspot),
image: encoded.Bytes(),
}, nil
}
func cursorBitmapDimensions(icon iconInfo) (int, int, error) {
if icon.hbmColor != 0 {
if width, height, ok := bitmapDimensions(icon.hbmColor); ok {
return width, height, validateCursorDimensions(width, height)
}
}
if icon.hbmMask != 0 {
if width, maskHeight, ok := bitmapDimensions(icon.hbmMask); ok {
height := maskHeight
if icon.hbmColor == 0 {
// A monochrome cursor stores its AND and XOR masks vertically.
height /= 2
}
return width, height, validateCursorDimensions(width, height)
}
}
return 0, 0, fmt.Errorf("cursor has no readable bitmap")
}
func bitmapDimensions(hBitmap uintptr) (int, int, bool) {
var bitmap gdiBitmap
ret, _, _ := procGetObjectW.Call(
hBitmap,
uintptr(unsafe.Sizeof(bitmap)),
uintptr(unsafe.Pointer(&bitmap)),
)
if ret == 0 {
return 0, 0, false
}
width, height := int(bitmap.bmWidth), int(bitmap.bmHeight)
if width < 0 {
width = -width
}
if height < 0 {
height = -height
}
return width, height, true
}
visible = (ci.flags & CURSOR_SHOWING) != 0
return ci.ptScreenPos.x, ci.ptScreenPos.y, visible
func validateCursorDimensions(width, height int) error {
if width <= 0 || height <= 0 || width > 1024 || height > 1024 {
return fmt.Errorf("invalid cursor dimensions %dx%d", width, height)
}
return nil
}
func renderCursorOnBackground(hCursor uintptr, width, height int, background byte) ([]byte, error) {
hdcScreen := getDC(0)
if hdcScreen == 0 {
return nil, fmt.Errorf("GetDC failed")
}
defer releaseDC(0, hdcScreen)
hdc := createCompatibleDC(hdcScreen)
if hdc == 0 {
return nil, fmt.Errorf("CreateCompatibleDC failed")
}
defer deleteDC(hdc)
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
dib := createDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &bits)
if dib == 0 || bits == nil {
return nil, fmt.Errorf("CreateDIBSection failed")
}
defer deleteObject(dib)
previous := selectObject(hdc, dib)
if previous == 0 {
return nil, fmt.Errorf("SelectObject failed")
}
defer selectObject(hdc, previous)
pixels := unsafe.Slice((*byte)(bits), width*height*4)
for offset := 0; offset < len(pixels); offset += 4 {
pixels[offset] = background
pixels[offset+1] = background
pixels[offset+2] = background
pixels[offset+3] = 255
}
ret, _, callErr := procDrawIconEx.Call(
hdc,
0,
0,
hCursor,
uintptr(width),
uintptr(height),
0,
0,
uintptr(DI_NORMAL),
)
if ret == 0 {
return nil, fmt.Errorf("DrawIconEx: %w", callErr)
}
result := make([]byte, len(pixels))
copy(result, pixels)
return result, nil
}
func reconstructCursorRGBA(black, white []byte, width, height int) *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for src, dst := 0, 0; src+3 < len(black) && src+3 < len(white); src, dst = src+4, dst+4 {
blue := int(black[src])
green := int(black[src+1])
red := int(black[src+2])
transmission := max(
int(white[src])-blue,
int(white[src+1])-green,
int(white[src+2])-red,
)
if transmission < 0 {
transmission = 0
} else if transmission > 255 {
transmission = 255
}
alpha := 255 - transmission
img.Pix[dst+3] = byte(alpha)
if alpha == 0 {
continue
}
img.Pix[dst] = unpremultiplyCursorChannel(red, alpha)
img.Pix[dst+1] = unpremultiplyCursorChannel(green, alpha)
img.Pix[dst+2] = unpremultiplyCursorChannel(blue, alpha)
}
return img
}
func unpremultiplyCursorChannel(value, alpha int) byte {
value = (value*255 + alpha/2) / alpha
if value > 255 {
value = 255
}
return byte(value)
}
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() {
if img == nil {
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}},
ci, ok := queryCursor()
if !ok || ci.hCursor == 0 {
return
}
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)
}
shape, err := getCursorShape(ci.hCursor)
if err != nil {
return
}
cursorImage, err := png.Decode(bytes.NewReader(shape.image))
if err != nil {
return
}
x := int(cursorX) - bounds.Min.X - shape.hotspotX
y := int(cursorY) - bounds.Min.Y - shape.hotspotY
target := image.Rect(x, y, x+shape.width, y+shape.height)
draw.Draw(img, target, cursorImage, cursorImage.Bounds().Min, draw.Over)
}
func DrawCursorOnDC(hdc uintptr, captureBounds image.Rectangle) bool {
@@ -203,14 +397,12 @@ func DrawCursorOnDCScaled(hdc uintptr, captureBounds image.Rectangle, scaleX, sc
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 {
ci, ok := queryCursor()
if !ok || (ci.flags&CURSOR_SHOWING) == 0 || ci.hCursor == 0 {
return false
}
var icon iconInfo
ret, _, _ = procGetIconInfo.Call(ci.hCursor, uintptr(unsafe.Pointer(&icon)))
ret, _, _ := procGetIconInfo.Call(ci.hCursor, uintptr(unsafe.Pointer(&icon)))
if ret == 0 {
return false
}
@@ -221,7 +413,6 @@ func DrawCursorOnDCScaled(hdc uintptr, captureBounds image.Rectangle, scaleX, sc
defer deleteObject(icon.hbmColor)
}
// 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(
@@ -242,7 +433,6 @@ func DrawCursorOnImage(img *image.RGBA, captureBounds image.Rectangle) {
if !cursorCaptureEnabled.Load() {
return
}
x, y, visible := getCursorPosition()
if visible {
drawCursor(img, x, y, captureBounds)