Define the RDPBackend interface, RDPConfig, and FrameUpdate types that abstract FreeRDP behind a pluggable backend. Add PixelBuffer for shared RGBA frame management with partial-update support and dirty tracking. Implement full 104-key US keyboard scancode map (JS KeyboardEvent.code to RDP hardware scancodes) with extended-key detection helpers and mouse event flag constants matching MS-RDPBCGR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
2.3 KiB
Go
104 lines
2.3 KiB
Go
package rdp
|
|
|
|
import "sync"
|
|
|
|
// PixelBuffer holds a shared RGBA pixel buffer that is updated by the RDP
|
|
// backend (via partial region updates) and read by the frame-serving path.
|
|
type PixelBuffer struct {
|
|
Width int
|
|
Height int
|
|
Data []byte // RGBA pixel data, len = Width * Height * 4
|
|
mu sync.RWMutex
|
|
dirty bool
|
|
}
|
|
|
|
// NewPixelBuffer allocates a buffer for the given resolution.
|
|
// The buffer is initialized to all zeros (transparent black).
|
|
func NewPixelBuffer(width, height int) *PixelBuffer {
|
|
return &PixelBuffer{
|
|
Width: width,
|
|
Height: height,
|
|
Data: make([]byte, width*height*4),
|
|
}
|
|
}
|
|
|
|
// Update applies a partial region update to the pixel buffer.
|
|
// The data slice must contain w*h*4 bytes of RGBA pixel data.
|
|
// Pixels outside the buffer bounds are silently clipped.
|
|
func (p *PixelBuffer) Update(x, y, w, h int, data []byte) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
for row := 0; row < h; row++ {
|
|
destY := y + row
|
|
if destY < 0 || destY >= p.Height {
|
|
continue
|
|
}
|
|
|
|
srcOffset := row * w * 4
|
|
destOffset := (destY*p.Width + x) * 4
|
|
|
|
// Calculate how many pixels we can copy on this row
|
|
copyWidth := w
|
|
if x < 0 {
|
|
srcOffset += (-x) * 4
|
|
copyWidth += x // reduce by the clipped amount
|
|
destOffset = destY * p.Width * 4
|
|
}
|
|
if x+copyWidth > p.Width {
|
|
copyWidth = p.Width - x
|
|
if x < 0 {
|
|
copyWidth = p.Width
|
|
}
|
|
}
|
|
if copyWidth <= 0 {
|
|
continue
|
|
}
|
|
|
|
srcEnd := srcOffset + copyWidth*4
|
|
if srcEnd > len(data) {
|
|
srcEnd = len(data)
|
|
}
|
|
if srcOffset >= len(data) {
|
|
continue
|
|
}
|
|
|
|
destEnd := destOffset + copyWidth*4
|
|
if destEnd > len(p.Data) {
|
|
destEnd = len(p.Data)
|
|
}
|
|
|
|
copy(p.Data[destOffset:destEnd], data[srcOffset:srcEnd])
|
|
}
|
|
|
|
p.dirty = true
|
|
}
|
|
|
|
// GetFrame returns a copy of the full pixel buffer.
|
|
// The caller receives an independent copy that will not be affected
|
|
// by subsequent updates.
|
|
func (p *PixelBuffer) GetFrame() []byte {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
frame := make([]byte, len(p.Data))
|
|
copy(frame, p.Data)
|
|
return frame
|
|
}
|
|
|
|
// IsDirty reports whether the buffer has been updated since the last
|
|
// ClearDirty call.
|
|
func (p *PixelBuffer) IsDirty() bool {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
return p.dirty
|
|
}
|
|
|
|
// ClearDirty resets the dirty flag. Typically called after a frame has
|
|
// been sent to the frontend.
|
|
func (p *PixelBuffer) ClearDirty() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.dirty = false
|
|
}
|