mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Composite cursor in Rust (#66697)
* Composite the cursor in Rust during ResizeCrop * Apply code review feedback * Update test * Move CursorBitmap to an enum, remove Cow, avoid cloning the bitmap * Remove dereference * Invalidate E2E Rust cache on decoder changes
This commit is contained in:
@@ -111,7 +111,8 @@ jobs:
|
||||
target/x86_64-unknown-linux-gnu/release/librdp_client.a
|
||||
lib/srv/desktop/rdp/rdpclient/librdpclient.h
|
||||
key: rust-output-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '**/Cargo.toml', 'Makefile', 'build.assets/Makefile',
|
||||
'build.assets/versions.mk', 'rust-toolchain.toml', 'lib/srv/desktop/rdp/rdpclient/**/*.rs', 'crates/**/*.rs', 'web/packages/shared/libs/ironrdp/**/*.rs') }}
|
||||
'build.assets/versions.mk', 'rust-toolchain.toml', 'lib/srv/desktop/rdp/rdpclient/**/*.rs', 'lib/srv/desktop/rdp/decoder/**/*.rs',
|
||||
'crates/**/*.rs', 'web/packages/shared/libs/ironrdp/**/*.rs') }}
|
||||
|
||||
- name: Build Teleport, tctl, and tsh
|
||||
run: ${{ inputs.build-command }}
|
||||
@@ -164,7 +165,8 @@ jobs:
|
||||
target/x86_64-unknown-linux-gnu/release/librdp_client.a
|
||||
lib/srv/desktop/rdp/rdpclient/librdpclient.h
|
||||
key: rust-output-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '**/Cargo.toml', 'Makefile', 'build.assets/Makefile',
|
||||
'build.assets/versions.mk', 'rust-toolchain.toml', 'lib/srv/desktop/rdp/rdpclient/**/*.rs', 'crates/**/*.rs', 'web/packages/shared/libs/ironrdp/**/*.rs') }}
|
||||
'build.assets/versions.mk', 'rust-toolchain.toml', 'lib/srv/desktop/rdp/rdpclient/**/*.rs', 'lib/srv/desktop/rdp/decoder/**/*.rs',
|
||||
'crates/**/*.rs', 'web/packages/shared/libs/ironrdp/**/*.rs') }}
|
||||
|
||||
test:
|
||||
name: E2E Tests
|
||||
|
||||
@@ -135,6 +135,7 @@ func (d *desktopThumbnailGenerator) produceThumbnail(maxDim int) (*pb.SessionRec
|
||||
uint16(bounds.Min.X), uint16(bounds.Min.Y),
|
||||
uint16(cropW), uint16(cropH),
|
||||
uint16(thumbW), uint16(thumbH),
|
||||
true,
|
||||
)
|
||||
if err != nil { //nolint:staticcheck // err is always non-nil in nop build but nil in RDP build
|
||||
return nil, trace.Wrap(err, "resizing thumbnail crop")
|
||||
|
||||
@@ -42,8 +42,6 @@ package decoder
|
||||
#cgo noescape rdp_decoder_image_data
|
||||
#cgo nocallback rdp_decoder_cursor_state
|
||||
#cgo noescape rdp_decoder_cursor_state
|
||||
#cgo nocallback rdp_decoder_cursor_bitmap
|
||||
#cgo noescape rdp_decoder_cursor_bitmap
|
||||
#cgo nocallback rdp_decoder_updated_regions_count
|
||||
#cgo noescape rdp_decoder_updated_regions_count
|
||||
#cgo nocallback rdp_decoder_updated_regions
|
||||
@@ -52,6 +50,8 @@ package decoder
|
||||
#cgo noescape rdp_decoder_reset_updated_regions
|
||||
#cgo nocallback rdp_decoder_resize_crop
|
||||
#cgo noescape rdp_decoder_resize_crop
|
||||
#cgo nocallback rdp_decoder_set_cursor_position
|
||||
#cgo noescape rdp_decoder_set_cursor_position
|
||||
#cgo nocallback rdp_decoder_dimensions
|
||||
#cgo noescape rdp_decoder_dimensions
|
||||
#cgo nocallback rdp_decoder_bytes_per_pixel
|
||||
@@ -71,11 +71,11 @@ void rdp_decoder_resize(RdpDecoder* ptr, uint16_t width, uint16_t height);
|
||||
void rdp_decoder_process(RdpDecoder* ptr, const uint8_t* data, size_t len);
|
||||
const uint8_t* rdp_decoder_image_data(RdpDecoder* ptr, uint16_t* width, uint16_t* height);
|
||||
void rdp_decoder_cursor_state(RdpDecoder* ptr, uint8_t* out_visible, uint16_t* out_x, uint16_t* out_y);
|
||||
const uint8_t* rdp_decoder_cursor_bitmap(RdpDecoder* ptr, uint16_t* out_width, uint16_t* out_height, uint16_t* out_hotspot_x, uint16_t* out_hotspot_y);
|
||||
uint32_t rdp_decoder_updated_regions_count(RdpDecoder* ptr);
|
||||
uint32_t rdp_decoder_updated_regions(RdpDecoder* ptr, uint16_t* out_buf, uint32_t max_count);
|
||||
void rdp_decoder_reset_updated_regions(RdpDecoder* ptr);
|
||||
bool rdp_decoder_resize_crop(RdpDecoder* ptr, uint16_t crop_x, uint16_t crop_y, uint16_t crop_w, uint16_t crop_h, uint16_t out_width, uint16_t out_height, uint8_t* out_buf, size_t out_buf_len);
|
||||
bool rdp_decoder_resize_crop(RdpDecoder* ptr, uint16_t crop_x, uint16_t crop_y, uint16_t crop_w, uint16_t crop_h, uint16_t out_width, uint16_t out_height, uint8_t* out_buf, size_t out_buf_len, uint8_t with_cursor);
|
||||
void rdp_decoder_set_cursor_position(RdpDecoder* ptr, uint16_t x, uint16_t y);
|
||||
void rdp_decoder_dimensions(RdpDecoder* ptr, uint16_t* out_width, uint16_t* out_height);
|
||||
uint8_t rdp_decoder_bytes_per_pixel(RdpDecoder* ptr);
|
||||
uint64_t rdp_decoder_sample_hash(RdpDecoder* ptr, uint16_t sample_count);
|
||||
@@ -161,8 +161,10 @@ func (d *Decoder) Image() *image.RGBA {
|
||||
}
|
||||
|
||||
// ResizeCrop returns the source crop region (cropX, cropY, cropW, cropH) scaled to exactly outWidth x outHeight using
|
||||
// high-quality CatmullRom convolution. The crop must lie within the current frame bounds.
|
||||
func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uint16) (*image.RGBA, error) {
|
||||
// high-quality CatmullRom convolution. The crop must lie within the current frame bounds. When withCursor is true and
|
||||
// the decoder's tracked cursor is visible, it is composited onto the source frame before the crop is taken, so the
|
||||
// cursor scales with the screen.
|
||||
func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uint16, withCursor bool) (*image.RGBA, error) {
|
||||
if d == nil || d.ptr == nil {
|
||||
return nil, errors.New("decoder not initialized")
|
||||
}
|
||||
@@ -178,6 +180,11 @@ func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uin
|
||||
w, h := int(outWidth), int(outHeight)
|
||||
buf := make([]byte, w*h*bpp)
|
||||
|
||||
var withCursorC C.uint8_t
|
||||
if withCursor {
|
||||
withCursorC = 1
|
||||
}
|
||||
|
||||
ok := C.rdp_decoder_resize_crop(
|
||||
d.ptr,
|
||||
C.uint16_t(cropX),
|
||||
@@ -188,6 +195,7 @@ func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uin
|
||||
C.uint16_t(outHeight),
|
||||
(*C.uint8_t)(unsafe.SliceData(buf)),
|
||||
C.size_t(len(buf)),
|
||||
withCursorC,
|
||||
)
|
||||
if !bool(ok) {
|
||||
return nil, errors.New("failed to resize crop region")
|
||||
@@ -200,6 +208,28 @@ func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uin
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetCursorPosition overrides the decoder's tracked cursor position. Useful for cursor updates that arrive outside of
|
||||
// the RDP fast-path stream (e.g., TDPB MouseMove events). Does not change visibility — visibility is still driven by
|
||||
// RDP fast-path pointer updates.
|
||||
func (d *Decoder) SetCursorPosition(x, y uint16) {
|
||||
if d == nil || d.ptr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
C.rdp_decoder_set_cursor_position(d.ptr, C.uint16_t(x), C.uint16_t(y))
|
||||
}
|
||||
|
||||
// SampleHash returns a 64-bit FNV-1a digest of pixels sampled on a fixed grid from the current frame buffer.
|
||||
// sampleCount controls the per-axis sample density; the effective step is max(1, dim / sampleCount).
|
||||
// Matches Go hash/fnv.New64a() byte-for-byte for the same inputs.
|
||||
func (d *Decoder) SampleHash(sampleCount uint16) uint64 {
|
||||
if d == nil || d.ptr == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint64(C.rdp_decoder_sample_hash(d.ptr, C.uint16_t(sampleCount)))
|
||||
}
|
||||
|
||||
// Dimensions returns the current frame width and height in pixels. Returns (0, 0) if the decoder has not been initialized.
|
||||
func (d *Decoder) Dimensions() (width, height uint16) {
|
||||
if d == nil || d.ptr == nil {
|
||||
@@ -212,16 +242,6 @@ func (d *Decoder) Dimensions() (width, height uint16) {
|
||||
return uint16(w), uint16(h)
|
||||
}
|
||||
|
||||
// SampleHash returns a 64-bit FNV-1a digest of pixels sampled on a fixed grid from the current frame buffer.
|
||||
// sampleCount controls the per-axis sample density. Returns 0 if the decoder has not been initialized.
|
||||
func (d *Decoder) SampleHash(sampleCount uint16) uint64 {
|
||||
if d == nil || d.ptr == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint64(C.rdp_decoder_sample_hash(d.ptr, C.uint16_t(sampleCount)))
|
||||
}
|
||||
|
||||
// CursorState returns the cursor position and visibility as tracked by the
|
||||
// Rust decoder.
|
||||
func (d *Decoder) CursorState() CursorState {
|
||||
@@ -241,35 +261,6 @@ func (d *Decoder) CursorState() CursorState {
|
||||
}
|
||||
}
|
||||
|
||||
// CursorBitmap returns the current cursor bitmap, or nil if none is available.
|
||||
func (d *Decoder) CursorBitmap() *CursorBitmapData {
|
||||
if d == nil || d.ptr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bmpW, bmpH, hotX, hotY C.uint16_t
|
||||
bmpData := C.rdp_decoder_cursor_bitmap(d.ptr, &bmpW, &bmpH, &hotX, &hotY)
|
||||
if bmpData == nil || bmpW == 0 || bmpH == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := int(bmpW)
|
||||
h := int(bmpH)
|
||||
|
||||
cursorPix := make([]byte, w*h*4)
|
||||
copy(cursorPix, unsafe.Slice((*uint8)(bmpData), w*h*4))
|
||||
|
||||
return &CursorBitmapData{
|
||||
Image: &image.RGBA{
|
||||
Pix: cursorPix,
|
||||
Stride: w * 4,
|
||||
Rect: image.Rect(0, 0, w, h),
|
||||
},
|
||||
HotspotX: int(hotX),
|
||||
HotspotY: int(hotY),
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatedRegions returns the individual screen regions updated since the last
|
||||
// call to ResetUpdatedRegions. Each rectangle uses Go's exclusive Max convention
|
||||
// (converted from the Rust decoder's inclusive coordinates).
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"image"
|
||||
)
|
||||
|
||||
// This file contains types and functions that are shared between the nop decoder (built without RDP decoder flag) and
|
||||
// the decoder built with the RDP decoder flag.
|
||||
|
||||
@@ -29,13 +25,6 @@ type CursorState struct {
|
||||
X, Y uint16
|
||||
}
|
||||
|
||||
// CursorBitmapData holds the cursor bitmap image and hotspot offset.
|
||||
type CursorBitmapData struct {
|
||||
Image *image.RGBA
|
||||
HotspotX int
|
||||
HotspotY int
|
||||
}
|
||||
|
||||
type decoderConfig struct {
|
||||
ioChannelID, userChannelID uint16
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ func (d *Decoder) Resize(w, h uint16) {}
|
||||
func (d *Decoder) Process([]byte) {}
|
||||
func (d *Decoder) Image() *image.RGBA { return nil }
|
||||
func (d *Decoder) CursorState() CursorState { return CursorState{} }
|
||||
func (d *Decoder) CursorBitmap() *CursorBitmapData { return nil }
|
||||
func (d *Decoder) SetCursorPosition(x, y uint16) {}
|
||||
func (d *Decoder) UpdatedRegions() []image.Rectangle { return nil }
|
||||
func (d *Decoder) ResetUpdatedRegions() {}
|
||||
|
||||
func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outW, outH uint16) (*image.RGBA, error) {
|
||||
func (d *Decoder) ResizeCrop(cropX, cropY, cropW, cropH, outW, outH uint16, withCursor bool) (*image.RGBA, error) {
|
||||
return nil, trace.NotImplemented("the RDP decoder is not included in this build")
|
||||
}
|
||||
func (d *Decoder) Dimensions() (uint16, uint16) { return 0, 0 }
|
||||
|
||||
@@ -14,32 +14,36 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
pub(crate) struct CursorBitmap {
|
||||
width: u16,
|
||||
height: u16,
|
||||
hotspot_x: u16,
|
||||
hotspot_y: u16,
|
||||
data: Vec<u8>, // RGBA, 4 bytes per pixel
|
||||
use ironrdp_graphics::pointer::DecodedPointer;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) enum CursorBitmap {
|
||||
#[default]
|
||||
Default,
|
||||
Server(Arc<DecodedPointer>),
|
||||
}
|
||||
|
||||
impl CursorBitmap {
|
||||
pub(crate) unsafe fn write_metadata(
|
||||
&self,
|
||||
out_width: *mut u16,
|
||||
out_height: *mut u16,
|
||||
out_hotspot_x: *mut u16,
|
||||
out_hotspot_y: *mut u16,
|
||||
) {
|
||||
unsafe {
|
||||
*out_width = self.width;
|
||||
*out_height = self.height;
|
||||
*out_hotspot_x = self.hotspot_x;
|
||||
*out_hotspot_y = self.hotspot_y;
|
||||
pub(crate) fn data(&self) -> &[u8] {
|
||||
match self {
|
||||
CursorBitmap::Default => &DEFAULT_CURSOR_RGBA,
|
||||
CursorBitmap::Server(ptr) => &ptr.bitmap_data,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn data_ptr(&self) -> *const u8 {
|
||||
self.data.as_ptr()
|
||||
pub(crate) fn dimensions(&self) -> (u16, u16) {
|
||||
match self {
|
||||
CursorBitmap::Default => (DEFAULT_CURSOR_WIDTH as u16, DEFAULT_CURSOR_HEIGHT as u16),
|
||||
CursorBitmap::Server(ptr) => (ptr.width, ptr.height),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hotspot(&self) -> (u16, u16) {
|
||||
match self {
|
||||
CursorBitmap::Default => (0, 0),
|
||||
CursorBitmap::Server(ptr) => (ptr.hotspot_x, ptr.hotspot_y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,26 +52,20 @@ pub(crate) struct CursorState {
|
||||
visible: bool,
|
||||
x: u16,
|
||||
y: u16,
|
||||
bitmap: Option<CursorBitmap>,
|
||||
bitmap: CursorBitmap,
|
||||
}
|
||||
|
||||
impl CursorState {
|
||||
pub(crate) fn set_bitmap(&mut self, pointer: &ironrdp_graphics::pointer::DecodedPointer) {
|
||||
pub(crate) fn set_bitmap(&mut self, pointer: Arc<DecodedPointer>) {
|
||||
if pointer.bitmap_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.bitmap = Some(CursorBitmap {
|
||||
width: pointer.width,
|
||||
height: pointer.height,
|
||||
hotspot_x: pointer.hotspot_x,
|
||||
hotspot_y: pointer.hotspot_y,
|
||||
data: pointer.bitmap_data.clone(),
|
||||
});
|
||||
self.bitmap = CursorBitmap::Server(pointer);
|
||||
}
|
||||
|
||||
pub(crate) fn clear_bitmap(&mut self) {
|
||||
self.bitmap = None;
|
||||
self.bitmap = CursorBitmap::Default;
|
||||
}
|
||||
|
||||
pub(crate) fn move_cursor(&mut self, x: u16, y: u16) {
|
||||
@@ -87,11 +85,59 @@ impl CursorState {
|
||||
(self.x, self.y)
|
||||
}
|
||||
|
||||
pub(crate) fn bitmap(&self) -> Option<&CursorBitmap> {
|
||||
self.bitmap.as_ref()
|
||||
pub(crate) fn bitmap(&self) -> &CursorBitmap {
|
||||
&self.bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// Synthetic default cursor used when the server signals `PTR_DEFAULT` (no
|
||||
// bitmap supplied) or when a PointerPosition arrives before any bitmap.
|
||||
const DEFAULT_CURSOR_WIDTH: usize = 12;
|
||||
const DEFAULT_CURSOR_HEIGHT: usize = 17;
|
||||
|
||||
#[rustfmt::skip]
|
||||
const DEFAULT_CURSOR_SHAPE: [u8; DEFAULT_CURSOR_WIDTH*DEFAULT_CURSOR_HEIGHT] = [
|
||||
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0,
|
||||
1, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0,
|
||||
1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0,
|
||||
1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1,
|
||||
1, 2, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0,
|
||||
1, 2, 2, 1, 0, 1, 2, 2, 1, 0, 0, 0,
|
||||
1, 2, 1, 0, 0, 1, 2, 2, 1, 0, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
const DEFAULT_CURSOR_RGBA: [u8; DEFAULT_CURSOR_WIDTH * DEFAULT_CURSOR_HEIGHT * 4] = {
|
||||
let mut data = [0; DEFAULT_CURSOR_WIDTH * DEFAULT_CURSOR_HEIGHT * 4];
|
||||
let mut i = 0;
|
||||
while i < DEFAULT_CURSOR_WIDTH * DEFAULT_CURSOR_HEIGHT {
|
||||
let p = match DEFAULT_CURSOR_SHAPE[i] {
|
||||
// black outline
|
||||
1 => [0, 0, 0, 255],
|
||||
// white body
|
||||
2 => [255, 255, 255, 255],
|
||||
_ => [0, 0, 0, 0],
|
||||
};
|
||||
|
||||
data[4 * i] = p[0];
|
||||
data[4 * i + 1] = p[1];
|
||||
data[4 * i + 2] = p[2];
|
||||
data[4 * i + 3] = p[3];
|
||||
i += 1;
|
||||
}
|
||||
|
||||
data
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -100,23 +146,55 @@ mod tests {
|
||||
fn sample_pointer() -> DecodedPointer {
|
||||
DecodedPointer {
|
||||
width: 2,
|
||||
height: 2,
|
||||
hotspot_x: 0,
|
||||
hotspot_y: 0,
|
||||
bitmap_data: vec![0xFF; 2 * 2 * 4],
|
||||
height: 3,
|
||||
hotspot_x: 1,
|
||||
hotspot_y: 2,
|
||||
bitmap_data: vec![0xAB; 2 * 3 * 4],
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_is_default(bitmap: &CursorBitmap) {
|
||||
let (width, height) = bitmap.dimensions();
|
||||
|
||||
assert_eq!(width, DEFAULT_CURSOR_WIDTH as u16);
|
||||
assert_eq!(height, DEFAULT_CURSOR_HEIGHT as u16);
|
||||
|
||||
let (hotspot_x, hotspot_y) = bitmap.hotspot();
|
||||
|
||||
assert_eq!(hotspot_x, 0);
|
||||
assert_eq!(hotspot_y, 0);
|
||||
|
||||
let data = bitmap.data();
|
||||
|
||||
assert_eq!(data, &DEFAULT_CURSOR_RGBA[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_default_clears_cached_bitmap() {
|
||||
let mut state = CursorState::default();
|
||||
state.set_bitmap(&sample_pointer());
|
||||
assert!(state.bitmap().is_some());
|
||||
|
||||
assert_is_default(state.bitmap());
|
||||
|
||||
state.set_bitmap(Arc::new(sample_pointer()));
|
||||
|
||||
let cached = state.bitmap();
|
||||
let (width, height) = cached.dimensions();
|
||||
|
||||
assert_eq!(width, 2);
|
||||
assert_eq!(height, 3);
|
||||
|
||||
let (hotspot_x, hotspot_y) = cached.hotspot();
|
||||
|
||||
assert_eq!(hotspot_x, 1);
|
||||
assert_eq!(hotspot_y, 2);
|
||||
|
||||
let data = cached.data();
|
||||
assert_eq!(data, &vec![0xAB; 2 * 3 * 4][..]);
|
||||
|
||||
state.set_visible(true);
|
||||
state.clear_bitmap();
|
||||
|
||||
assert!(state.bitmap().is_none());
|
||||
assert_is_default(state.bitmap());
|
||||
assert!(state.is_visible());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,18 @@ use std::io::Error;
|
||||
|
||||
impl RdpDecoder {
|
||||
/// Writes a CatmullRom-resized copy of the source crop region into `dst`, scaled to
|
||||
/// `out_w` x `out_h`. The crop must lie within the current frame bounds.
|
||||
/// `out_w` x `out_h`.
|
||||
/// When `with_cursor` is true and the tracked cursor is visible, it is composited onto the
|
||||
/// cropped pixels before the resize, so the cursor scales with the screen.
|
||||
///
|
||||
/// The crop must lie within the current frame bounds.
|
||||
pub fn resize_crop_into(
|
||||
&mut self,
|
||||
(crop_x, crop_y): (u16, u16),
|
||||
(crop_w, crop_h): (u16, u16),
|
||||
(out_w, out_h): (u16, u16),
|
||||
dst: &mut [u8],
|
||||
with_cursor: bool,
|
||||
) -> Result<(), Error> {
|
||||
let bpp = self.image.pixel_format().bytes_per_pixel();
|
||||
let pixel_type = match bpp {
|
||||
@@ -50,18 +55,68 @@ impl RdpDecoder {
|
||||
|
||||
let opts = ResizeOptions::new()
|
||||
.resize_alg(ResizeAlg::Convolution(FilterType::CatmullRom))
|
||||
.use_alpha(false)
|
||||
.crop(
|
||||
f64::from(crop_x),
|
||||
f64::from(crop_y),
|
||||
f64::from(crop_w),
|
||||
f64::from(crop_h),
|
||||
.use_alpha(false);
|
||||
|
||||
let composite_cursor = with_cursor && self.cursor_state.is_visible();
|
||||
|
||||
let (src_buffer, src_pixel_w, src_pixel_h, opts) = if composite_cursor {
|
||||
let (cx, cy) = self.cursor_state.position();
|
||||
let cursor_bitmap = self.cursor_state.bitmap();
|
||||
|
||||
let bpp_usize = usize::from(bpp);
|
||||
let src_stride = (src_w as usize) * bpp_usize;
|
||||
let crop_row_bytes = (crop_w as usize) * bpp_usize;
|
||||
let scratch_bytes = crop_row_bytes * (crop_h as usize);
|
||||
if self.composite_scratch.len() < scratch_bytes {
|
||||
self.composite_scratch.resize(scratch_bytes, 0);
|
||||
}
|
||||
|
||||
let image_data = self.image.data();
|
||||
let crop_x_off = (crop_x as usize) * bpp_usize;
|
||||
for y in 0..(crop_h as usize) {
|
||||
let src_off = ((crop_y as usize) + y) * src_stride + crop_x_off;
|
||||
let dst_off = y * crop_row_bytes;
|
||||
self.composite_scratch[dst_off..dst_off + crop_row_bytes]
|
||||
.copy_from_slice(&image_data[src_off..src_off + crop_row_bytes]);
|
||||
}
|
||||
|
||||
let (cursor_hotspot_x, cursor_hotspot_y) = cursor_bitmap.hotspot();
|
||||
|
||||
composite_over(
|
||||
&mut self.composite_scratch[..scratch_bytes],
|
||||
(crop_w, crop_h),
|
||||
cursor_bitmap.data(),
|
||||
cursor_bitmap.dimensions(),
|
||||
(
|
||||
i32::from(cx) - i32::from(cursor_hotspot_x) - i32::from(crop_x),
|
||||
i32::from(cy) - i32::from(cursor_hotspot_y) - i32::from(crop_y),
|
||||
),
|
||||
);
|
||||
|
||||
(
|
||||
&self.composite_scratch[..scratch_bytes],
|
||||
crop_w,
|
||||
crop_h,
|
||||
opts,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.image.data(),
|
||||
src_w,
|
||||
src_h,
|
||||
opts.crop(
|
||||
f64::from(crop_x),
|
||||
f64::from(crop_y),
|
||||
f64::from(crop_w),
|
||||
f64::from(crop_h),
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let src = ImageRef::new(
|
||||
u32::from(src_w),
|
||||
u32::from(src_h),
|
||||
self.image.data(),
|
||||
u32::from(src_pixel_w),
|
||||
u32::from(src_pixel_h),
|
||||
src_buffer,
|
||||
pixel_type,
|
||||
)
|
||||
.map_err(Error::other)?;
|
||||
@@ -81,3 +136,200 @@ impl RdpDecoder {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Source-over blend of `bmp_pix` onto `dst` at (`draw_x`, `draw_y`).
|
||||
// RGBA8-only: the per-pixel math assumes 4 channels with alpha at byte index 3.
|
||||
fn composite_over(
|
||||
dst: &mut [u8],
|
||||
(dst_w, dst_h): (u16, u16),
|
||||
bmp_pix: &[u8],
|
||||
(bmp_w, bmp_h): (u16, u16),
|
||||
(draw_x, draw_y): (i32, i32),
|
||||
) {
|
||||
let dw = i32::from(dst_w);
|
||||
let dh = i32::from(dst_h);
|
||||
let bw = i32::from(bmp_w);
|
||||
let bh = i32::from(bmp_h);
|
||||
|
||||
let x0 = draw_x.max(0);
|
||||
let y0 = draw_y.max(0);
|
||||
let x1 = (draw_x + bw).min(dw);
|
||||
let y1 = (draw_y + bh).min(dh);
|
||||
if x0 >= x1 || y0 >= y1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let dst_stride = (dw as usize) * 4;
|
||||
let bmp_stride = (bw as usize) * 4;
|
||||
let row_bytes = ((x1 - x0) as usize) * 4;
|
||||
let dst_x_off = (x0 as usize) * 4;
|
||||
let bmp_x_off = ((x0 - draw_x) as usize) * 4;
|
||||
|
||||
for y in y0..y1 {
|
||||
let dst_start = (y as usize) * dst_stride + dst_x_off;
|
||||
let bmp_start = ((y - draw_y) as usize) * bmp_stride + bmp_x_off;
|
||||
|
||||
let dst_row = &mut dst[dst_start..dst_start + row_bytes];
|
||||
let bmp_row = &bmp_pix[bmp_start..bmp_start + row_bytes];
|
||||
|
||||
for (dst_px, bmp_px) in dst_row.chunks_exact_mut(4).zip(bmp_row.chunks_exact(4)) {
|
||||
let sa = bmp_px[3];
|
||||
if sa == 0 {
|
||||
continue;
|
||||
}
|
||||
if sa == 255 {
|
||||
dst_px.copy_from_slice(bmp_px);
|
||||
continue;
|
||||
}
|
||||
|
||||
let inv = 255 - u32::from(sa);
|
||||
for c in 0..4 {
|
||||
let s = u32::from(bmp_px[c]);
|
||||
let d = u32::from(dst_px[c]);
|
||||
// (x * 0x8081) >> 23 is a fast `x / 255` for x up to ~16 bits.
|
||||
let blended = s + ((d * inv * 0x8081) >> 23);
|
||||
|
||||
dst_px[c] = blended.min(255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_dst(w: u16, h: u16, fill: u8) -> Vec<u8> {
|
||||
vec![fill; (w as usize) * (h as usize) * 4]
|
||||
}
|
||||
|
||||
fn make_bmp(w: u16, h: u16, fill: [u8; 4]) -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity((w as usize) * (h as usize) * 4);
|
||||
for _ in 0..(w as usize) * (h as usize) {
|
||||
v.extend_from_slice(&fill);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn pixel(buf: &[u8], stride: usize, x: usize, y: usize) -> [u8; 4] {
|
||||
let off = y * stride + x * 4;
|
||||
[buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_transparent_source_leaves_dst_unchanged() {
|
||||
let mut dst = make_dst(2, 2, 200);
|
||||
let bmp = make_bmp(2, 2, [10, 20, 30, 0]);
|
||||
|
||||
composite_over(&mut dst, (2, 2), &bmp, (2, 2), (0, 0));
|
||||
|
||||
assert_eq!(dst, make_dst(2, 2, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_opaque_source_overwrites_dst() {
|
||||
let mut dst = make_dst(2, 2, 200);
|
||||
let bmp = make_bmp(2, 2, [10, 20, 30, 255]);
|
||||
|
||||
composite_over(&mut dst, (2, 2), &bmp, (2, 2), (0, 0));
|
||||
|
||||
for y in 0..2 {
|
||||
for x in 0..2 {
|
||||
assert_eq!(pixel(&dst, 8, x, y), [10, 20, 30, 255]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_half_alpha_blends_source_over_dst() {
|
||||
// inv = 255 - 128 = 127; (200 * 127 * 0x8081) >> 23 == 99.
|
||||
// RGB: s=50, d=200 → 50 + 99 = 149.
|
||||
// A: s=128, d=200 → 128 + 99 = 227.
|
||||
let mut dst = make_dst(1, 1, 200);
|
||||
let bmp = make_bmp(1, 1, [50, 50, 50, 128]);
|
||||
|
||||
composite_over(&mut dst, (1, 1), &bmp, (1, 1), (0, 0));
|
||||
|
||||
assert_eq!(pixel(&dst, 4, 0, 0), [149, 149, 149, 227]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_skips_when_fully_off_screen() {
|
||||
let original = make_dst(2, 2, 200);
|
||||
let bmp = make_bmp(2, 2, [10, 20, 30, 255]);
|
||||
|
||||
for (dx, dy) in [(-2, 0), (2, 0), (0, -2), (0, 2), (-5, -5), (10, 10)] {
|
||||
let mut dst = original.clone();
|
||||
composite_over(&mut dst, (2, 2), &bmp, (2, 2), (dx, dy));
|
||||
assert_eq!(dst, original, "expected no change at draw=({dx},{dy})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_clips_negative_draw_x() {
|
||||
// 4x1 bmp at draw_x=-2, dst is 4x1: only bmp cols 2,3 land in dst cols 0,1.
|
||||
let mut dst = make_dst(4, 1, 0);
|
||||
let mut bmp = Vec::new();
|
||||
for v in [1u8, 2, 3, 4] {
|
||||
bmp.extend_from_slice(&[v, v, v, 255]);
|
||||
}
|
||||
|
||||
composite_over(&mut dst, (4, 1), &bmp, (4, 1), (-2, 0));
|
||||
|
||||
assert_eq!(pixel(&dst, 16, 0, 0), [3, 3, 3, 255]);
|
||||
assert_eq!(pixel(&dst, 16, 1, 0), [4, 4, 4, 255]);
|
||||
assert_eq!(pixel(&dst, 16, 2, 0), [0, 0, 0, 0]);
|
||||
assert_eq!(pixel(&dst, 16, 3, 0), [0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_clips_negative_draw_y() {
|
||||
// 1x4 bmp at draw_y=-2, dst is 1x4: only bmp rows 2,3 land in dst rows 0,1.
|
||||
let mut dst = make_dst(1, 4, 0);
|
||||
let mut bmp = Vec::new();
|
||||
for v in [1u8, 2, 3, 4] {
|
||||
bmp.extend_from_slice(&[v, v, v, 255]);
|
||||
}
|
||||
|
||||
composite_over(&mut dst, (1, 4), &bmp, (1, 4), (0, -2));
|
||||
|
||||
assert_eq!(pixel(&dst, 4, 0, 0), [3, 3, 3, 255]);
|
||||
assert_eq!(pixel(&dst, 4, 0, 1), [4, 4, 4, 255]);
|
||||
assert_eq!(pixel(&dst, 4, 0, 2), [0, 0, 0, 0]);
|
||||
assert_eq!(pixel(&dst, 4, 0, 3), [0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_clips_right_overflow() {
|
||||
// 4x1 bmp at draw_x=2, dst is 4x1: only bmp cols 0,1 land in dst cols 2,3.
|
||||
let mut dst = make_dst(4, 1, 0);
|
||||
let mut bmp = Vec::new();
|
||||
for v in [1u8, 2, 3, 4] {
|
||||
bmp.extend_from_slice(&[v, v, v, 255]);
|
||||
}
|
||||
|
||||
composite_over(&mut dst, (4, 1), &bmp, (4, 1), (2, 0));
|
||||
|
||||
assert_eq!(pixel(&dst, 16, 0, 0), [0, 0, 0, 0]);
|
||||
assert_eq!(pixel(&dst, 16, 1, 0), [0, 0, 0, 0]);
|
||||
assert_eq!(pixel(&dst, 16, 2, 0), [1, 1, 1, 255]);
|
||||
assert_eq!(pixel(&dst, 16, 3, 0), [2, 2, 2, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_over_clips_bottom_overflow() {
|
||||
// 1x4 bmp at draw_y=2, dst is 1x4: only bmp rows 0,1 land in dst rows 2,3.
|
||||
let mut dst = make_dst(1, 4, 0);
|
||||
let mut bmp = Vec::new();
|
||||
for v in [1u8, 2, 3, 4] {
|
||||
bmp.extend_from_slice(&[v, v, v, 255]);
|
||||
}
|
||||
|
||||
composite_over(&mut dst, (1, 4), &bmp, (1, 4), (0, 2));
|
||||
|
||||
assert_eq!(pixel(&dst, 4, 0, 0), [0, 0, 0, 0]);
|
||||
assert_eq!(pixel(&dst, 4, 0, 1), [0, 0, 0, 0]);
|
||||
assert_eq!(pixel(&dst, 4, 0, 2), [1, 1, 1, 255]);
|
||||
assert_eq!(pixel(&dst, 4, 0, 3), [2, 2, 2, 255]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ pub struct RdpDecoder {
|
||||
cursor_state: CursorState,
|
||||
updated_regions: UpdatedRegions,
|
||||
resizer: Resizer,
|
||||
composite_scratch: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RdpDecoder {
|
||||
@@ -69,6 +70,7 @@ impl RdpDecoder {
|
||||
cursor_state: Default::default(),
|
||||
updated_regions: Default::default(),
|
||||
resizer: Resizer::new(),
|
||||
composite_scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +114,7 @@ impl RdpDecoder {
|
||||
}
|
||||
UpdateKind::PointerBitmap(pointer) => {
|
||||
self.cursor_state.set_visible(true);
|
||||
self.cursor_state.set_bitmap(&pointer);
|
||||
self.cursor_state.set_bitmap(pointer);
|
||||
}
|
||||
UpdateKind::PointerDefault => {
|
||||
self.cursor_state.set_visible(true);
|
||||
@@ -299,14 +301,18 @@ pub unsafe extern "C" fn rdp_decoder_image_data(
|
||||
}
|
||||
|
||||
/// Writes a CatmullRom-resized copy of the source crop region into `out_buf`,
|
||||
/// scaled to exactly `out_width` x `out_height`. The crop must be within the
|
||||
/// current frame bounds; out-of-bounds crops are rejected.
|
||||
/// scaled to exactly `out_width` x `out_height`. When `with_cursor` is non-zero
|
||||
/// and the decoder's tracked cursor is visible, it is composited onto the full
|
||||
/// source frame before the crop is taken, so the cursor scales with the screen.
|
||||
/// The crop must lie within the current frame bounds; out-of-bounds crops are
|
||||
/// rejected.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `ptr` must be a valid pointer previously returned by `rdp_decoder_new`.
|
||||
/// - `out_buf` must point to `out_buf_len` writable bytes.
|
||||
#[no_mangle]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub unsafe extern "C" fn rdp_decoder_resize_crop(
|
||||
ptr: *mut RdpDecoder,
|
||||
crop_x: u16,
|
||||
@@ -317,6 +323,7 @@ pub unsafe extern "C" fn rdp_decoder_resize_crop(
|
||||
out_height: u16,
|
||||
out_buf: *mut u8,
|
||||
out_buf_len: usize,
|
||||
with_cursor: u8,
|
||||
) -> bool {
|
||||
if ptr.is_null()
|
||||
|| out_buf.is_null()
|
||||
@@ -328,6 +335,8 @@ pub unsafe extern "C" fn rdp_decoder_resize_crop(
|
||||
return false;
|
||||
}
|
||||
|
||||
let with_cursor = with_cursor != 0;
|
||||
|
||||
catch_unwind(AssertUnwindSafe(move || unsafe {
|
||||
let decoder = &mut *ptr;
|
||||
let needed =
|
||||
@@ -343,12 +352,54 @@ pub unsafe extern "C" fn rdp_decoder_resize_crop(
|
||||
(crop_w, crop_h),
|
||||
(out_width, out_height),
|
||||
dst,
|
||||
with_cursor,
|
||||
)
|
||||
.is_ok()
|
||||
}))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Override the decoder's tracked cursor position. Useful for cursor updates
|
||||
/// that arrive outside of the RDP fast-path stream (e.g., Teleport TDPB
|
||||
/// MouseMove events). Does not change visibility — visibility is still driven
|
||||
/// by RDP fast-path pointer updates.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `ptr` must be a valid pointer previously returned by `rdp_decoder_new`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rdp_decoder_set_cursor_position(ptr: *mut RdpDecoder, x: u16, y: u16) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = catch_unwind(AssertUnwindSafe(move || unsafe {
|
||||
let decoder = &mut *ptr;
|
||||
decoder.cursor_state.move_cursor(x, y);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Returns an FNV-1a 64-bit digest of pixels sampled on a fixed grid from the
|
||||
/// current frame buffer. `sample_count` controls the per-axis sample density.
|
||||
/// Returns 0 on null pointer or empty frame; the cursor is not composited so
|
||||
/// callers can match a non-cursor Go-side sampler exactly.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `ptr` must be a valid pointer previously returned by `rdp_decoder_new`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rdp_decoder_sample_hash(ptr: *mut RdpDecoder, sample_count: u16) -> u64 {
|
||||
if ptr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
catch_unwind(AssertUnwindSafe(move || unsafe {
|
||||
let decoder = &*ptr;
|
||||
decoder.sample_hash(sample_count)
|
||||
}))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the current frame dimensions via out-params. Sets both to 0 on
|
||||
/// failure (null pointer, panic).
|
||||
///
|
||||
@@ -400,27 +451,6 @@ pub unsafe extern "C" fn rdp_decoder_bytes_per_pixel(ptr: *mut RdpDecoder) -> u8
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns an FNV-1a 64-bit digest of pixels sampled on a fixed grid from the
|
||||
/// current frame buffer. `sample_count` controls the per-axis sample density.
|
||||
/// Returns 0 on null pointer or empty frame; the cursor is not composited so
|
||||
/// callers can match a non-cursor Go-side sampler exactly.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `ptr` must be a valid pointer previously returned by `rdp_decoder_new`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rdp_decoder_sample_hash(ptr: *mut RdpDecoder, sample_count: u16) -> u64 {
|
||||
if ptr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
catch_unwind(AssertUnwindSafe(move || unsafe {
|
||||
let decoder = &*ptr;
|
||||
decoder.sample_hash(sample_count)
|
||||
}))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the current cursor position and visibility state.
|
||||
///
|
||||
/// # Safety
|
||||
@@ -448,42 +478,6 @@ pub unsafe extern "C" fn rdp_decoder_cursor_state(
|
||||
}));
|
||||
}
|
||||
|
||||
/// Returns a pointer to the cursor bitmap data and its dimensions via out-params.
|
||||
/// Returns null if no cursor bitmap is available. The returned pointer is valid
|
||||
/// as long as the decoder is alive and no new PointerBitmap update is processed.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `ptr` must be a valid pointer previously returned by `rdp_decoder_new`.
|
||||
/// - All out-params must be valid, non-null pointers.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rdp_decoder_cursor_bitmap(
|
||||
ptr: *mut RdpDecoder,
|
||||
out_width: *mut u16,
|
||||
out_height: *mut u16,
|
||||
out_hotspot_x: *mut u16,
|
||||
out_hotspot_y: *mut u16,
|
||||
) -> *const u8 {
|
||||
if ptr.is_null()
|
||||
|| out_width.is_null()
|
||||
|| out_height.is_null()
|
||||
|| out_hotspot_x.is_null()
|
||||
|| out_hotspot_y.is_null()
|
||||
{
|
||||
return ptr::null();
|
||||
}
|
||||
|
||||
catch_unwind(AssertUnwindSafe(move || unsafe {
|
||||
let decoder = &*ptr;
|
||||
let Some(bmp) = decoder.cursor_state.bitmap() else {
|
||||
return ptr::null();
|
||||
};
|
||||
bmp.write_metadata(out_width, out_height, out_hotspot_x, out_hotspot_y);
|
||||
bmp.data_ptr()
|
||||
}))
|
||||
.unwrap_or(ptr::null())
|
||||
}
|
||||
|
||||
/// Copies update regions into the caller-provided buffer as (left, top, right, bottom)
|
||||
/// u16 tuples. Returns the number of regions written. Coordinates use inclusive
|
||||
/// right/bottom matching the RDP InclusiveRectangle convention.
|
||||
@@ -567,7 +561,7 @@ mod tests {
|
||||
let mut buf = vec![0xAAu8; 10 * 10 * 4];
|
||||
|
||||
let ok = unsafe {
|
||||
rdp_decoder_resize_crop(ptr, 90, 0, 20, 10, 10, 10, buf.as_mut_ptr(), buf.len())
|
||||
rdp_decoder_resize_crop(ptr, 90, 0, 20, 10, 10, 10, buf.as_mut_ptr(), buf.len(), 0)
|
||||
};
|
||||
|
||||
// Crop extends past right edge → call returns without writing.
|
||||
@@ -583,7 +577,7 @@ mod tests {
|
||||
let mut buf = vec![0xAAu8; 10 * 10 * 4];
|
||||
|
||||
let ok = unsafe {
|
||||
rdp_decoder_resize_crop(ptr, 0, 95, 10, 10, 10, 10, buf.as_mut_ptr(), buf.len())
|
||||
rdp_decoder_resize_crop(ptr, 0, 95, 10, 10, 10, 10, buf.as_mut_ptr(), buf.len(), 0)
|
||||
};
|
||||
|
||||
assert!(!ok);
|
||||
@@ -599,12 +593,14 @@ mod tests {
|
||||
|
||||
unsafe {
|
||||
// crop_w = 0
|
||||
let ok = rdp_decoder_resize_crop(ptr, 0, 0, 0, 10, 10, 10, buf.as_mut_ptr(), buf.len());
|
||||
let ok =
|
||||
rdp_decoder_resize_crop(ptr, 0, 0, 0, 10, 10, 10, buf.as_mut_ptr(), buf.len(), 0);
|
||||
assert!(!ok);
|
||||
assert!(buf.iter().all(|&b| b == 0xAA));
|
||||
|
||||
// crop_h = 0
|
||||
let ok = rdp_decoder_resize_crop(ptr, 0, 0, 10, 0, 10, 10, buf.as_mut_ptr(), buf.len());
|
||||
let ok =
|
||||
rdp_decoder_resize_crop(ptr, 0, 0, 10, 0, 10, 10, buf.as_mut_ptr(), buf.len(), 0);
|
||||
assert!(!ok);
|
||||
assert!(buf.iter().all(|&b| b == 0xAA));
|
||||
|
||||
@@ -619,7 +615,7 @@ mod tests {
|
||||
|
||||
// Exactly fills the right edge: 90 + 10 == 100.
|
||||
let ok = unsafe {
|
||||
rdp_decoder_resize_crop(ptr, 90, 90, 10, 10, 10, 10, buf.as_mut_ptr(), buf.len())
|
||||
rdp_decoder_resize_crop(ptr, 90, 90, 10, 10, 10, 10, buf.as_mut_ptr(), buf.len(), 0)
|
||||
};
|
||||
|
||||
assert!(ok);
|
||||
@@ -643,6 +639,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sample_hash_matches_fnv1a_reference() {
|
||||
// 1x1 RGBA = single 4-byte tuple; sampler walks exactly that pixel.
|
||||
// Reference value computed by hand against FNV-1a 64-bit (offset
|
||||
// 0xcbf29ce484222325, prime 0x100000001b3), processing bytes 1,2,3,4.
|
||||
let mut h = FNV_OFFSET_BASIS;
|
||||
for b in [1u8, 2, 3, 4] {
|
||||
h ^= u64::from(b);
|
||||
@@ -677,4 +676,47 @@ mod tests {
|
||||
b[0] = 2;
|
||||
assert_ne!(sample_hash(&a, 1, 1, 4, 64), sample_hash(&b, 1, 1, 4, 64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_crop_with_cursor_accepts_in_bounds_crop() {
|
||||
let ptr = make_decoder(100, 100);
|
||||
let mut buf = vec![0xAAu8; 10 * 10 * 4];
|
||||
|
||||
unsafe {
|
||||
(*ptr).cursor_state.set_visible(true);
|
||||
rdp_decoder_set_cursor_position(ptr, 50, 50);
|
||||
}
|
||||
|
||||
let ok = unsafe {
|
||||
rdp_decoder_resize_crop(ptr, 0, 0, 100, 100, 10, 10, buf.as_mut_ptr(), buf.len(), 1)
|
||||
};
|
||||
|
||||
assert!(ok);
|
||||
assert!(
|
||||
buf.iter().any(|&b| b != 0xAA),
|
||||
"expected buffer to be written"
|
||||
);
|
||||
|
||||
unsafe { rdp_decoder_free(ptr) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_crop_with_cursor_rejects_out_of_bounds_crop() {
|
||||
let ptr = make_decoder(100, 100);
|
||||
let mut buf = vec![0xAAu8; 10 * 10 * 4];
|
||||
|
||||
unsafe {
|
||||
(*ptr).cursor_state.set_visible(true);
|
||||
rdp_decoder_set_cursor_position(ptr, 50, 50);
|
||||
}
|
||||
|
||||
let ok = unsafe {
|
||||
rdp_decoder_resize_crop(ptr, 95, 0, 10, 10, 10, 10, buf.as_mut_ptr(), buf.len(), 1)
|
||||
};
|
||||
|
||||
assert!(!ok);
|
||||
assert!(buf.iter().all(|&b| b == 0xAA));
|
||||
|
||||
unsafe { rdp_decoder_free(ptr) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"image/draw"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
@@ -58,9 +57,6 @@ const (
|
||||
// Subsequent FastPathPDU messages are fed to the decoder, which maintains the screen framebuffer and cursor state.
|
||||
type RDPState struct {
|
||||
decoder *decoder.Decoder
|
||||
|
||||
mouseX, mouseY uint16
|
||||
hasMouse bool
|
||||
}
|
||||
|
||||
// New creates a new RDPState.
|
||||
@@ -103,14 +99,15 @@ func (s *RDPState) Image() *image.RGBA {
|
||||
}
|
||||
|
||||
// ResizeCrop returns the source crop region of the current screen image scaled to exactly outWidth x outHeight using
|
||||
// high-quality CatmullRom convolution. The crop must lie within the current frame bounds. Returns an error if the
|
||||
// decoder has not been initialized or the resize fails.
|
||||
func (s *RDPState) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uint16) (*image.RGBA, error) {
|
||||
// high-quality CatmullRom convolution. The crop must lie within the current frame bounds. When withCursor is true and
|
||||
// the decoder's tracked cursor is visible, it is composited onto the source frame before the crop, so the cursor
|
||||
// scales with the screen. Returns an error if the decoder has not been initialized or the resize fails.
|
||||
func (s *RDPState) ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight uint16, withCursor bool) (*image.RGBA, error) {
|
||||
if s.decoder == nil {
|
||||
return nil, trace.BadParameter("rdp state has no image")
|
||||
}
|
||||
|
||||
return s.decoder.ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight)
|
||||
return s.decoder.ResizeCrop(cropX, cropY, cropW, cropH, outWidth, outHeight, withCursor)
|
||||
}
|
||||
|
||||
// Dimensions returns the current screen width and height in pixels. Returns (0, 0) if the decoder has not been
|
||||
@@ -141,40 +138,6 @@ func (s *RDPState) Release() {
|
||||
}
|
||||
}
|
||||
|
||||
// ImageWithCursor returns the current screen image with the cursor composited at its current position, along with the
|
||||
// cursor state.
|
||||
// When MouseMove TDPB messages have set a position, that overrides the Rust decoder's internal cursor position for compositing.
|
||||
func (s *RDPState) ImageWithCursor() (*image.RGBA, decoder.CursorState) {
|
||||
if s.decoder == nil {
|
||||
return nil, decoder.CursorState{}
|
||||
}
|
||||
|
||||
img := s.decoder.Image()
|
||||
cs := s.CursorState()
|
||||
if img == nil || !cs.Visible {
|
||||
return img, cs
|
||||
}
|
||||
|
||||
bmp := s.decoder.CursorBitmap()
|
||||
if bmp == nil {
|
||||
return img, cs
|
||||
}
|
||||
|
||||
cursorX, cursorY := cs.X, cs.Y
|
||||
if s.hasMouse {
|
||||
cursorX, cursorY = s.mouseX, s.mouseY
|
||||
}
|
||||
|
||||
drawX := int(cursorX) - bmp.HotspotX
|
||||
drawY := int(cursorY) - bmp.HotspotY
|
||||
cb := bmp.Image.Bounds()
|
||||
|
||||
dstRect := image.Rect(drawX, drawY, drawX+cb.Dx(), drawY+cb.Dy())
|
||||
draw.Draw(img, dstRect, bmp.Image, image.Point{}, draw.Over)
|
||||
|
||||
return img, cs
|
||||
}
|
||||
|
||||
// UpdatedRegions returns the individual screen regions updated since the last call to ResetUpdatedRegions.
|
||||
func (s *RDPState) UpdatedRegions() []image.Rectangle {
|
||||
if s.decoder == nil {
|
||||
@@ -347,9 +310,9 @@ func (s *RDPState) handleMouseMove(msg *tdpbv1.MouseMove) error {
|
||||
return trace.BadParameter("mouse coordinates out of range: (%d, %d)", msg.GetX(), msg.GetY())
|
||||
}
|
||||
|
||||
s.mouseX = uint16(msg.GetX())
|
||||
s.mouseY = uint16(msg.GetY())
|
||||
s.hasMouse = true
|
||||
// MouseMove may arrive before ServerHello initializes the decoder, in which case there's nowhere to record
|
||||
// the position. SetCursorPosition is a no-op when the decoder is nil, so drop the update silently.
|
||||
s.decoder.SetCursorPosition(uint16(msg.GetX()), uint16(msg.GetY()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ func TestProperty_RDPState_AfterReleaseNeverPanics(t *testing.T) {
|
||||
_ = s.UpdatedRegions()
|
||||
|
||||
s.ResetUpdatedRegions()
|
||||
img, cs := s.ImageWithCursor()
|
||||
img, err := s.ResizeCrop(0, 0, 1, 1, 1, 1, true)
|
||||
_ = img
|
||||
_ = cs
|
||||
_ = err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestResizeCrop_BeforeServerHello(t *testing.T) {
|
||||
|
||||
s := New()
|
||||
t.Cleanup(s.Release)
|
||||
img, err := s.ResizeCrop(0, 0, 100, 100, 50, 50)
|
||||
img, err := s.ResizeCrop(0, 0, 100, 100, 50, 50, false)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, img)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func TestResizeCrop_ReturnsCorrectDimensions(t *testing.T) {
|
||||
t.Cleanup(s.Release)
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 800, 600)))
|
||||
|
||||
img, err := s.ResizeCrop(tc.cropX, tc.cropY, tc.cropW, tc.cropH, tc.outW, tc.outH)
|
||||
img, err := s.ResizeCrop(tc.cropX, tc.cropY, tc.cropW, tc.cropH, tc.outW, tc.outH, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, img)
|
||||
require.Equal(t, image.Rect(0, 0, int(tc.outW), int(tc.outH)), img.Bounds())
|
||||
@@ -117,7 +117,7 @@ func TestResizeCrop_PreservesSolidColor(t *testing.T) {
|
||||
sendPDU(t, s, rdpstatetest.BuildBitmapPDU(0, 0, 100, 100, rdpstatetest.RGB565Red))
|
||||
|
||||
// Crop a 10x10 region from the middle and scale up; every output pixel must still be red.
|
||||
img, err := s.ResizeCrop(45, 45, 10, 10, 50, 50)
|
||||
img, err := s.ResizeCrop(45, 45, 10, 10, 50, 50, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, img)
|
||||
require.Equal(t, image.Rect(0, 0, 50, 50), img.Bounds())
|
||||
@@ -155,7 +155,7 @@ func TestResizeCrop_RejectsInvalidInputs(t *testing.T) {
|
||||
t.Cleanup(s.Release)
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
|
||||
img, err := s.ResizeCrop(tc.cropX, tc.cropY, tc.cropW, tc.cropH, tc.outW, tc.outH)
|
||||
img, err := s.ResizeCrop(tc.cropX, tc.cropY, tc.cropW, tc.cropH, tc.outW, tc.outH, false)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, img)
|
||||
})
|
||||
|
||||
@@ -109,10 +109,10 @@ func TestMouseMove_Legacy(t *testing.T) {
|
||||
evt, err := rdpstatetest.LegacyMouseMove(123, 456)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Position updates flow into the decoder via SetCursorPosition; this build uses the nop
|
||||
// decoder so we just verify the message parses and dispatches without error. End-to-end
|
||||
// position propagation is covered by the Rust FFI tests.
|
||||
require.NoError(t, s.HandleMessage(evt))
|
||||
require.True(t, s.hasMouse)
|
||||
require.Equal(t, uint16(123), s.mouseX)
|
||||
require.Equal(t, uint16(456), s.mouseY)
|
||||
}
|
||||
|
||||
func TestMouseMove_LegacyTruncated(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user