mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-14 01:19:09 +02:00
Composite remote cursor into the framebuffer when the dashboard toggles it on
This commit is contained in:
@@ -38,9 +38,18 @@ var (
|
|||||||
cfRelease func(uintptr)
|
cfRelease func(uintptr)
|
||||||
cgPreflightScreenCaptureAccess func() bool
|
cgPreflightScreenCaptureAccess func() bool
|
||||||
cgRequestScreenCaptureAccess func() bool
|
cgRequestScreenCaptureAccess func() bool
|
||||||
|
cgEventCreate func(uintptr) uintptr
|
||||||
|
cgEventGetLocation func(uintptr) cgPoint
|
||||||
darwinCaptureReady bool
|
darwinCaptureReady bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// cgPoint mirrors CoreGraphics CGPoint: two doubles, 16 bytes, returned
|
||||||
|
// in registers on Darwin amd64/arm64. Used to receive cursor coordinates
|
||||||
|
// from CGEventGetLocation via purego.
|
||||||
|
type cgPoint struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
func initDarwinCapture() {
|
func initDarwinCapture() {
|
||||||
darwinCaptureOnce.Do(func() {
|
darwinCaptureOnce.Do(func() {
|
||||||
cg, err := purego.Dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
cg, err := purego.Dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||||
@@ -76,6 +85,15 @@ func initDarwinCapture() {
|
|||||||
if sym, err := purego.Dlsym(cg, "CGRequestScreenCaptureAccess"); err == nil {
|
if sym, err := purego.Dlsym(cg, "CGRequestScreenCaptureAccess"); err == nil {
|
||||||
purego.RegisterFunc(&cgRequestScreenCaptureAccess, sym)
|
purego.RegisterFunc(&cgRequestScreenCaptureAccess, sym)
|
||||||
}
|
}
|
||||||
|
// CGEventCreate / CGEventGetLocation feed the cursor position used
|
||||||
|
// by remote-cursor compositing. Optional; absence reports as a
|
||||||
|
// position-source error and disables that feature on this host.
|
||||||
|
if sym, err := purego.Dlsym(cg, "CGEventCreate"); err == nil {
|
||||||
|
purego.RegisterFunc(&cgEventCreate, sym)
|
||||||
|
}
|
||||||
|
if sym, err := purego.Dlsym(cg, "CGEventGetLocation"); err == nil {
|
||||||
|
purego.RegisterFunc(&cgEventGetLocation, sym)
|
||||||
|
}
|
||||||
|
|
||||||
darwinCaptureReady = true
|
darwinCaptureReady = true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -425,6 +425,16 @@ func (p *X11Poller) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|||||||
return p.capturer.Cursor()
|
return p.capturer.Cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorPos satisfies cursorPositionSource by forwarding to the X11Capturer.
|
||||||
|
func (p *X11Poller) CursorPos() (int, int, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if err := p.ensureCapturerLocked(); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return p.capturer.CursorPos()
|
||||||
|
}
|
||||||
|
|
||||||
// Capture returns a fresh frame, serving from the short-lived cache if a
|
// Capture returns a fresh frame, serving from the short-lived cache if a
|
||||||
// previous caller captured within freshWindow.
|
// previous caller captured within freshWindow.
|
||||||
func (p *X11Poller) Capture() (*image.RGBA, error) {
|
func (p *X11Poller) Capture() (*image.RGBA, error) {
|
||||||
|
|||||||
@@ -156,6 +156,21 @@ func (c *CGCapturer) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|||||||
return c.cursor.Cursor()
|
return c.cursor.Cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorPos returns the current global mouse location via CGEventCreate /
|
||||||
|
// CGEventGetLocation. Coordinates are screen pixels in the main display.
|
||||||
|
func (c *CGCapturer) CursorPos() (int, int, error) {
|
||||||
|
if cgEventCreate == nil || cgEventGetLocation == nil {
|
||||||
|
return 0, 0, fmt.Errorf("CGEvent location APIs unavailable")
|
||||||
|
}
|
||||||
|
ev := cgEventCreate(0)
|
||||||
|
if ev == 0 {
|
||||||
|
return 0, 0, fmt.Errorf("CGEventCreate returned nil")
|
||||||
|
}
|
||||||
|
defer cfRelease(ev)
|
||||||
|
pt := cgEventGetLocation(ev)
|
||||||
|
return int(pt.X), int(pt.Y), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Cursor on MacPoller forwards to the lazy CGCapturer. ensureCapturerLocked
|
// Cursor on MacPoller forwards to the lazy CGCapturer. ensureCapturerLocked
|
||||||
// returns an error when Screen Recording permission has not been granted;
|
// returns an error when Screen Recording permission has not been granted;
|
||||||
// in that case there is no usable cursor source either.
|
// in that case there is no usable cursor source either.
|
||||||
@@ -167,3 +182,13 @@ func (p *MacPoller) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|||||||
}
|
}
|
||||||
return p.capturer.Cursor()
|
return p.capturer.Cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorPos forwards to the lazy CGCapturer.
|
||||||
|
func (p *MacPoller) CursorPos() (int, int, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if err := p.ensureCapturerLocked(); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return p.capturer.CursorPos()
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ type cursorSnapshot struct {
|
|||||||
img *image.RGBA
|
img *image.RGBA
|
||||||
hotX int
|
hotX int
|
||||||
hotY int
|
hotY int
|
||||||
|
posX int
|
||||||
|
posY int
|
||||||
|
hasPos bool
|
||||||
serial uint64
|
serial uint64
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -118,14 +121,26 @@ func (s *cursorSampler) sample() (*cursorSnapshot, error) {
|
|||||||
// treating this as a hard failure that would latch us off for
|
// treating this as a hard failure that would latch us off for
|
||||||
// the session.
|
// the session.
|
||||||
if s.lastHandle == hiddenHandle {
|
if s.lastHandle == hiddenHandle {
|
||||||
|
s.snapshot.posX = int(ci.PtPos.X)
|
||||||
|
s.snapshot.posY = int(ci.PtPos.Y)
|
||||||
|
s.snapshot.hasPos = true
|
||||||
return s.snapshot, nil
|
return s.snapshot, nil
|
||||||
}
|
}
|
||||||
s.lastHandle = hiddenHandle
|
s.lastHandle = hiddenHandle
|
||||||
s.serial++
|
s.serial++
|
||||||
s.snapshot = &cursorSnapshot{img: transparentCursorImage(), serial: s.serial}
|
s.snapshot = &cursorSnapshot{
|
||||||
|
img: transparentCursorImage(),
|
||||||
|
posX: int(ci.PtPos.X),
|
||||||
|
posY: int(ci.PtPos.Y),
|
||||||
|
hasPos: true,
|
||||||
|
serial: s.serial,
|
||||||
|
}
|
||||||
return s.snapshot, nil
|
return s.snapshot, nil
|
||||||
}
|
}
|
||||||
if ci.Cursor == s.lastHandle && s.snapshot != nil {
|
if ci.Cursor == s.lastHandle && s.snapshot != nil {
|
||||||
|
s.snapshot.posX = int(ci.PtPos.X)
|
||||||
|
s.snapshot.posY = int(ci.PtPos.Y)
|
||||||
|
s.snapshot.hasPos = true
|
||||||
return s.snapshot, nil
|
return s.snapshot, nil
|
||||||
}
|
}
|
||||||
img, hotX, hotY, err := decodeCursor(ci.Cursor)
|
img, hotX, hotY, err := decodeCursor(ci.Cursor)
|
||||||
@@ -134,7 +149,15 @@ func (s *cursorSampler) sample() (*cursorSnapshot, error) {
|
|||||||
}
|
}
|
||||||
s.lastHandle = ci.Cursor
|
s.lastHandle = ci.Cursor
|
||||||
s.serial++
|
s.serial++
|
||||||
s.snapshot = &cursorSnapshot{img: img, hotX: hotX, hotY: hotY, serial: s.serial}
|
s.snapshot = &cursorSnapshot{
|
||||||
|
img: img,
|
||||||
|
hotX: hotX,
|
||||||
|
hotY: hotY,
|
||||||
|
posX: int(ci.PtPos.X),
|
||||||
|
posY: int(ci.PtPos.Y),
|
||||||
|
hasPos: true,
|
||||||
|
serial: s.serial,
|
||||||
|
}
|
||||||
return s.snapshot, nil
|
return s.snapshot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,3 +371,20 @@ func (c *DesktopCapturer) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|||||||
}
|
}
|
||||||
return snap.img, snap.hotX, snap.hotY, snap.serial, nil
|
return snap.img, snap.hotX, snap.hotY, snap.serial, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorPos returns the cursor screen position observed by the worker on
|
||||||
|
// its last sample. Errors out if the worker hasn't yet captured a frame
|
||||||
|
// or the most recent sample failed.
|
||||||
|
func (c *DesktopCapturer) CursorPos() (int, int, error) {
|
||||||
|
snap := c.cursorState.load()
|
||||||
|
if snap == nil {
|
||||||
|
return 0, 0, fmt.Errorf("cursor position not sampled yet")
|
||||||
|
}
|
||||||
|
if snap.err != nil {
|
||||||
|
return 0, 0, snap.err
|
||||||
|
}
|
||||||
|
if !snap.hasPos {
|
||||||
|
return 0, 0, fmt.Errorf("cursor position unavailable")
|
||||||
|
}
|
||||||
|
return snap.posX, snap.posY, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ type xfixesCursor struct {
|
|||||||
// calls return quickly without another X round-trip. Some virtual
|
// calls return quickly without another X round-trip. Some virtual
|
||||||
// displays advertise XFixes but reject GetCursorImage (Xvfb).
|
// displays advertise XFixes but reject GetCursorImage (Xvfb).
|
||||||
runtimeErr error
|
runtimeErr error
|
||||||
|
// lastPosX/lastPosY hold the cursor screen position observed on the
|
||||||
|
// most recent successful GetCursorImage. cursorPositionSource readers
|
||||||
|
// share this value so we do not pay a second X round-trip per frame.
|
||||||
|
lastPosX, lastPosY int
|
||||||
|
hasPos bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newXFixesCursor initialises the XFixes extension on conn. Returns an
|
// newXFixesCursor initialises the XFixes extension on conn. Returns an
|
||||||
@@ -49,6 +54,7 @@ func (c *xfixesCursor) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|||||||
c.runtimeErr = fmt.Errorf("xfixes GetCursorImage: %w", err)
|
c.runtimeErr = fmt.Errorf("xfixes GetCursorImage: %w", err)
|
||||||
return nil, 0, 0, 0, c.runtimeErr
|
return nil, 0, 0, 0, c.runtimeErr
|
||||||
}
|
}
|
||||||
|
c.lastPosX, c.lastPosY, c.hasPos = int(reply.X), int(reply.Y), true
|
||||||
w, h := int(reply.Width), int(reply.Height)
|
w, h := int(reply.Width), int(reply.Height)
|
||||||
if w <= 0 || h <= 0 {
|
if w <= 0 || h <= 0 {
|
||||||
return nil, 0, 0, 0, fmt.Errorf("cursor has zero extent")
|
return nil, 0, 0, 0, fmt.Errorf("cursor has zero extent")
|
||||||
@@ -85,3 +91,21 @@ func (x *X11Capturer) Cursor() (*image.RGBA, int, int, uint64, error) {
|
|||||||
}
|
}
|
||||||
return cur.Cursor()
|
return cur.Cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CursorPos on X11Capturer returns the screen position from the most
|
||||||
|
// recent successful Cursor() call. Sessions call Cursor() once per encode
|
||||||
|
// cycle, so this stays current without a second X round-trip.
|
||||||
|
func (x *X11Capturer) CursorPos() (int, int, error) {
|
||||||
|
x.mu.Lock()
|
||||||
|
cur := x.cursor
|
||||||
|
x.mu.Unlock()
|
||||||
|
if cur == nil {
|
||||||
|
return 0, 0, fmt.Errorf("cursor source not initialised")
|
||||||
|
}
|
||||||
|
cur.mu.Lock()
|
||||||
|
defer cur.mu.Unlock()
|
||||||
|
if !cur.hasPos {
|
||||||
|
return 0, 0, fmt.Errorf("cursor position not sampled yet")
|
||||||
|
}
|
||||||
|
return cur.lastPosX, cur.lastPosY, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ const (
|
|||||||
// The opcode is in the vendor-specific range (>=128).
|
// The opcode is in the vendor-specific range (>=128).
|
||||||
clientNetbirdTypeText = 250
|
clientNetbirdTypeText = 250
|
||||||
|
|
||||||
|
// clientNetbirdShowRemoteCursor toggles "show remote cursor" mode.
|
||||||
|
// When enabled the encoder composites the server cursor sprite into
|
||||||
|
// the captured framebuffer and suppresses the Cursor pseudo-encoding
|
||||||
|
// so the dashboard sees a single pointer at the remote position.
|
||||||
|
// Wire format: 1-byte msgType + 1-byte enable flag + 6 padding bytes
|
||||||
|
// reserved for future arguments (so the message is fixed-size).
|
||||||
|
clientNetbirdShowRemoteCursor = 251
|
||||||
|
|
||||||
// Server message types.
|
// Server message types.
|
||||||
serverFramebufferUpdate = 0
|
serverFramebufferUpdate = 0
|
||||||
serverCutText = 3
|
serverCutText = 3
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ type cursorSource interface {
|
|||||||
Cursor() (img *image.RGBA, hotX, hotY int, serial uint64, err error)
|
Cursor() (img *image.RGBA, hotX, hotY int, serial uint64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cursorPositionSource adds the cursor's current screen-space position to
|
||||||
|
// cursorSource so the encoder can alpha-blend the sprite into the captured
|
||||||
|
// framebuffer for "show remote cursor" mode. Implementations should be
|
||||||
|
// cheap; most platforms already get the position alongside the sprite.
|
||||||
|
type cursorPositionSource interface {
|
||||||
|
CursorPos() (x, y int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
// errFrameUnchanged is returned by capturers that hash the raw source
|
// errFrameUnchanged is returned by capturers that hash the raw source
|
||||||
// bytes (currently macOS) when the new frame is byte-identical to the
|
// bytes (currently macOS) when the new frame is byte-identical to the
|
||||||
// last one, so the encoder can short-circuit to an empty update.
|
// last one, so the encoder can short-circuit to an empty update.
|
||||||
|
|||||||
@@ -92,6 +92,15 @@ type session struct {
|
|||||||
// what the client advertises. Set for virtual sessions where no
|
// what the client advertises. Set for virtual sessions where no
|
||||||
// usable cursor source exists. Constant for the session lifetime.
|
// usable cursor source exists. Constant for the session lifetime.
|
||||||
disableCursor bool
|
disableCursor bool
|
||||||
|
// showRemoteCursor switches the encoder to compositing the server
|
||||||
|
// cursor sprite into the captured framebuffer at the remote position
|
||||||
|
// instead of emitting the Cursor pseudo-encoding. Toggled by the
|
||||||
|
// dashboard via clientNetbirdShowRemoteCursor.
|
||||||
|
showRemoteCursor bool
|
||||||
|
// cursorWarnOnce throttles the diagnostic emitted when remote-cursor
|
||||||
|
// compositing falls back to a no-op (capturer cannot supply a sprite
|
||||||
|
// or position). One line per session is enough to point at the cause.
|
||||||
|
cursorWarnOnce sync.Once
|
||||||
// clientJPEGQuality and clientZlibLevel hold the 0..9 levels the client
|
// clientJPEGQuality and clientZlibLevel hold the 0..9 levels the client
|
||||||
// advertised via the QualityLevel / CompressLevel pseudo-encodings, or
|
// advertised via the QualityLevel / CompressLevel pseudo-encodings, or
|
||||||
// -1 when the client has not expressed a preference. Applied to the
|
// -1 when the client has not expressed a preference. Applied to the
|
||||||
@@ -274,6 +283,8 @@ func (s *session) messageLoop() error {
|
|||||||
err = s.handleQEMUMessage()
|
err = s.handleQEMUMessage()
|
||||||
case clientNetbirdTypeText:
|
case clientNetbirdTypeText:
|
||||||
err = s.handleTypeText()
|
err = s.handleTypeText()
|
||||||
|
case clientNetbirdShowRemoteCursor:
|
||||||
|
err = s.handleShowRemoteCursor()
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown client message type: %d", msgType[0])
|
return fmt.Errorf("unknown client message type: %d", msgType[0])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ func (s *session) pendingCursorRect() []byte {
|
|||||||
s.encMu.RLock()
|
s.encMu.RLock()
|
||||||
supported := s.clientSupportsCursor
|
supported := s.clientSupportsCursor
|
||||||
failed := s.cursorSourceFailed
|
failed := s.cursorSourceFailed
|
||||||
|
composite := s.showRemoteCursor
|
||||||
lastSerial := s.lastCursorSerial
|
lastSerial := s.lastCursorSerial
|
||||||
s.encMu.RUnlock()
|
s.encMu.RUnlock()
|
||||||
if !supported || failed {
|
if !supported || failed || composite {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
src, ok := s.capturer.(cursorSource)
|
src, ok := s.capturer.(cursorSource)
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ func (s *session) processFBRequest(req fbRequest) error {
|
|||||||
}
|
}
|
||||||
s.captureRecovered()
|
s.captureRecovered()
|
||||||
|
|
||||||
|
s.maybeCompositeCursor(img)
|
||||||
|
|
||||||
if req.incremental && s.prevFrame != nil {
|
if req.incremental && s.prevFrame != nil {
|
||||||
return s.processIncremental(img)
|
return s.processIncremental(img)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//go:build !js && !ios && !android
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleShowRemoteCursor handles the NetBird-specific RFB message used by
|
||||||
|
// the dashboard to toggle "show remote cursor" mode. Wire format: 1-byte
|
||||||
|
// enable flag (0/1) plus 6 padding bytes reserved for future arguments.
|
||||||
|
func (s *session) handleShowRemoteCursor() error {
|
||||||
|
var data [7]byte
|
||||||
|
if _, err := io.ReadFull(s.conn, data[:]); err != nil {
|
||||||
|
return fmt.Errorf("read showRemoteCursor: %w", err)
|
||||||
|
}
|
||||||
|
enable := data[0] != 0
|
||||||
|
s.encMu.Lock()
|
||||||
|
s.showRemoteCursor = enable
|
||||||
|
s.encMu.Unlock()
|
||||||
|
s.log.Debugf("show remote cursor: %v", enable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeCompositeCursor blends the current server cursor into img when the
|
||||||
|
// dashboard has enabled "show remote cursor" mode. Returns silently in
|
||||||
|
// every error path: a failed compositing must not stop the regular encode
|
||||||
|
// flow.
|
||||||
|
func (s *session) maybeCompositeCursor(img *image.RGBA) {
|
||||||
|
s.encMu.RLock()
|
||||||
|
enabled := s.showRemoteCursor
|
||||||
|
s.encMu.RUnlock()
|
||||||
|
if !enabled || img == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
src, ok := s.capturer.(cursorSource)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pos, ok := s.capturer.(cursorPositionSource)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cursorImg, hotX, hotY, _, err := src.Cursor()
|
||||||
|
if err != nil || cursorImg == nil {
|
||||||
|
s.cursorWarnOnce.Do(func() {
|
||||||
|
s.log.Warnf("remote cursor unavailable: %v", err)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
posX, posY, err := pos.CursorPos()
|
||||||
|
if err != nil {
|
||||||
|
s.cursorWarnOnce.Do(func() {
|
||||||
|
s.log.Warnf("remote cursor position unavailable: %v", err)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
compositeCursor(img, cursorImg, posX-hotX, posY-hotY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeCursor alpha-blends sprite onto frame at (dstX, dstY) using
|
||||||
|
// straight (non-premultiplied) alpha. Out-of-bounds destinations are
|
||||||
|
// clipped. Frames captured by our X11/Windows/macOS paths all advertise
|
||||||
|
// RGBA with a 255-only alpha channel, so the result keeps the framebuffer
|
||||||
|
// invariant ("opaque pixels everywhere") that the encoder depends on.
|
||||||
|
func compositeCursor(frame, sprite *image.RGBA, dstX, dstY int) {
|
||||||
|
fw, fh := frame.Rect.Dx(), frame.Rect.Dy()
|
||||||
|
sw, sh := sprite.Rect.Dx(), sprite.Rect.Dy()
|
||||||
|
if sw == 0 || sh == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
x0, y0 := dstX, dstY
|
||||||
|
x1, y1 := dstX+sw, dstY+sh
|
||||||
|
if x0 < 0 {
|
||||||
|
x0 = 0
|
||||||
|
}
|
||||||
|
if y0 < 0 {
|
||||||
|
y0 = 0
|
||||||
|
}
|
||||||
|
if x1 > fw {
|
||||||
|
x1 = fw
|
||||||
|
}
|
||||||
|
if y1 > fh {
|
||||||
|
y1 = fh
|
||||||
|
}
|
||||||
|
if x0 >= x1 || y0 >= y1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fStride := frame.Stride
|
||||||
|
sStride := sprite.Stride
|
||||||
|
for y := y0; y < y1; y++ {
|
||||||
|
sy := y - dstY
|
||||||
|
fbRow := y * fStride
|
||||||
|
sRow := sy * sStride
|
||||||
|
for x := x0; x < x1; x++ {
|
||||||
|
sx := x - dstX
|
||||||
|
fbOff := fbRow + x*4
|
||||||
|
sOff := sRow + sx*4
|
||||||
|
a := uint32(sprite.Pix[sOff+3])
|
||||||
|
if a == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if a == 255 {
|
||||||
|
frame.Pix[fbOff+0] = sprite.Pix[sOff+0]
|
||||||
|
frame.Pix[fbOff+1] = sprite.Pix[sOff+1]
|
||||||
|
frame.Pix[fbOff+2] = sprite.Pix[sOff+2]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inv := 255 - a
|
||||||
|
frame.Pix[fbOff+0] = byte((uint32(sprite.Pix[sOff+0])*a + uint32(frame.Pix[fbOff+0])*inv) / 255)
|
||||||
|
frame.Pix[fbOff+1] = byte((uint32(sprite.Pix[sOff+1])*a + uint32(frame.Pix[fbOff+1])*inv) / 255)
|
||||||
|
frame.Pix[fbOff+2] = byte((uint32(sprite.Pix[sOff+2])*a + uint32(frame.Pix[fbOff+2])*inv) / 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user