mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-21 11:50:59 +03:00
refactor(cursor): separate native capture from rendering fixes
This commit is contained in:
@@ -25,7 +25,7 @@ inline = []
|
|||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
flutter = ["flutter_rust_bridge", "scrap/cursor", "dep:x11rb"]
|
flutter = ["flutter_rust_bridge", "dep:x11rb"]
|
||||||
default = ["use_dasp"]
|
default = ["use_dasp"]
|
||||||
hwcodec = ["scrap/hwcodec"]
|
hwcodec = ["scrap/hwcodec"]
|
||||||
vram = ["scrap/vram"]
|
vram = ["scrap/vram"]
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ class _FFI extends Fake implements FFI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<CursorData> _data(int density, String id) async {
|
Future<CursorData> _data(int density, String id) async {
|
||||||
final image = await createTestImage(width: 9 * density, height: 18 * density);
|
final bitmapDensity = density == 0 ? 1 : density;
|
||||||
|
final image = await createTestImage(
|
||||||
|
width: 9 * bitmapDensity, height: 18 * bitmapDensity);
|
||||||
return CursorData(
|
return CursorData(
|
||||||
peerId: 'dpi-policy',
|
peerId: 'dpi-policy',
|
||||||
id: id,
|
id: id,
|
||||||
@@ -110,8 +112,8 @@ Future<CursorData> _data(int density, String id) async {
|
|||||||
nativeImage: image,
|
nativeImage: image,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
data: null,
|
data: null,
|
||||||
hotxOrigin: 4.0 * density,
|
hotxOrigin: 4.0 * bitmapDensity,
|
||||||
hotyOrigin: 9.0 * density,
|
hotyOrigin: 9.0 * bitmapDensity,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
pixelRatio: density.toDouble(),
|
pixelRatio: density.toDouble(),
|
||||||
@@ -120,6 +122,8 @@ Future<CursorData> _data(int density, String id) async {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
for (final (style, zoom, density, viewScale, expectedScale) in [
|
for (final (style, zoom, density, viewScale, expectedScale) in [
|
||||||
|
(kRemoteViewStyleAdaptive, false, 0, 0.25, Platform.isWindows ? 1.0 : 4 / 3),
|
||||||
|
(kRemoteViewStyleCustom, false, 0, 0.25, Platform.isWindows ? 1.0 : 4 / 3),
|
||||||
(kRemoteViewStyleAdaptive, false, 1, 0.25, Platform.isWindows ? 2.0 : 1.0),
|
(kRemoteViewStyleAdaptive, false, 1, 0.25, Platform.isWindows ? 2.0 : 1.0),
|
||||||
(kRemoteViewStyleAdaptive, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5),
|
(kRemoteViewStyleAdaptive, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5),
|
||||||
(kRemoteViewStyleCustom, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5),
|
(kRemoteViewStyleCustom, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5),
|
||||||
@@ -173,8 +177,8 @@ void main() {
|
|||||||
tester.binding.defaultBinaryMessenger
|
tester.binding.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(channel, null);
|
.setMockMethodCallHandler(channel, null);
|
||||||
expect(data.scale, closeTo(expectedScale, 1e-9));
|
expect(data.scale, closeTo(expectedScale, 1e-9));
|
||||||
expect(data.hotx, closeTo(4 * density * expectedScale, 1e-9));
|
expect(data.hotx, closeTo(data.hotxOrigin * expectedScale, 1e-9));
|
||||||
expect(data.hoty, closeTo(9 * density * expectedScale, 1e-9));
|
expect(data.hoty, closeTo(data.hotyOrigin * expectedScale, 1e-9));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ authors = ["Ram <quadrupleslap@gmail.com>"]
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
cursor = ["winapi/shellscalingapi"]
|
|
||||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||||
# `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`)
|
# `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`)
|
||||||
# and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is
|
# and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
use super::wrap_hresult;
|
|
||||||
use std::{
|
|
||||||
collections::{hash_map::DefaultHasher, HashMap},
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
io,
|
|
||||||
sync::{Arc, Mutex, Weak},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
use winapi::shared::dxgi1_2::{
|
|
||||||
IDXGIOutputDuplication, DXGI_OUTDUPL_FRAME_INFO, DXGI_OUTDUPL_POINTER_SHAPE_INFO,
|
|
||||||
DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR, DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR,
|
|
||||||
DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME,
|
|
||||||
};
|
|
||||||
// USER handles use 32 bits; reserve a separate namespace for physical DXGI shapes.
|
|
||||||
pub const CURSOR_ID_FLAG: u64 = 1 << 63;
|
|
||||||
const CHANNELS: u32 = 4;
|
|
||||||
const BITS_PER_BYTE: u32 = 8;
|
|
||||||
const DEFAULT_DPI: f64 = 96.0;
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Shape {
|
|
||||||
pub id: u64,
|
|
||||||
pub scale: f64,
|
|
||||||
pub kind: u32,
|
|
||||||
pub width: u32,
|
|
||||||
pub height: u32,
|
|
||||||
pub pitch: u32,
|
|
||||||
pub hotspot: (i32, i32),
|
|
||||||
pub pixels: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shape {
|
|
||||||
fn new(info: DXGI_OUTDUPL_POINTER_SHAPE_INFO, pixels: Vec<u8>) -> io::Result<Self> {
|
|
||||||
let (height, minimum_pitch) = match info.Type {
|
|
||||||
DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME if info.Height % 2 == 0 => {
|
|
||||||
(info.Height / 2, info.Width.div_ceil(BITS_PER_BYTE))
|
|
||||||
}
|
|
||||||
DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR
|
|
||||||
| DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR => (
|
|
||||||
info.Height,
|
|
||||||
info.Width.checked_mul(CHANNELS).ok_or_else(invalid_shape)?,
|
|
||||||
),
|
|
||||||
_ => return Err(invalid_shape()),
|
|
||||||
};
|
|
||||||
let length = (info.Pitch as usize).checked_mul(info.Height as usize);
|
|
||||||
if info.Width == 0
|
|
||||||
|| height == 0
|
|
||||||
|| info.Width > i32::MAX as u32
|
|
||||||
|| info.Height > i32::MAX as u32
|
|
||||||
|| info.Pitch > i32::MAX as u32
|
|
||||||
|| info.Pitch < minimum_pitch
|
|
||||||
|| length != Some(pixels.len())
|
|
||||||
|| info.HotSpot.x < 0
|
|
||||||
|| info.HotSpot.x as u32 >= info.Width
|
|
||||||
|| info.HotSpot.y < 0
|
|
||||||
|| info.HotSpot.y as u32 >= height
|
|
||||||
{
|
|
||||||
return Err(invalid_shape());
|
|
||||||
}
|
|
||||||
let hotspot = (info.HotSpot.x, info.HotSpot.y);
|
|
||||||
Ok(Self {
|
|
||||||
id: 0,
|
|
||||||
scale: 0.0,
|
|
||||||
kind: info.Type,
|
|
||||||
width: info.Width,
|
|
||||||
height,
|
|
||||||
pitch: info.Pitch,
|
|
||||||
hotspot,
|
|
||||||
pixels,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_scale(self, scale: f64) -> Self {
|
|
||||||
let mut hash = DefaultHasher::new();
|
|
||||||
(
|
|
||||||
self.kind,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
self.pitch,
|
|
||||||
self.hotspot,
|
|
||||||
&self.pixels,
|
|
||||||
scale.to_bits(),
|
|
||||||
)
|
|
||||||
.hash(&mut hash);
|
|
||||||
Self {
|
|
||||||
id: hash.finish() | CURSOR_ID_FLAG,
|
|
||||||
scale,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invalid_shape() -> io::Error {
|
|
||||||
io::Error::new(io::ErrorKind::InvalidData, "Invalid DXGI cursor shape")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum Snapshot {
|
|
||||||
Unavailable,
|
|
||||||
Pending,
|
|
||||||
Ready(Arc<Shape>),
|
|
||||||
Failed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct State {
|
|
||||||
updated: Instant,
|
|
||||||
snapshot: Snapshot,
|
|
||||||
}
|
|
||||||
|
|
||||||
type SharedState = Arc<Mutex<State>>;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref CAPTURES: Mutex<HashMap<usize, Vec<Weak<Mutex<State>>>>> =
|
|
||||||
Mutex::new(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshot(monitor: usize) -> Snapshot {
|
|
||||||
let captures = CAPTURES.lock().unwrap();
|
|
||||||
captures
|
|
||||||
.get(&monitor)
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.filter_map(Weak::upgrade)
|
|
||||||
.map(|state| {
|
|
||||||
let state = state.lock().unwrap();
|
|
||||||
(state.updated, state.snapshot.clone())
|
|
||||||
})
|
|
||||||
.max_by_key(|(updated, _)| *updated)
|
|
||||||
.map(|(_, snapshot)| snapshot)
|
|
||||||
.unwrap_or(Snapshot::Unavailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shape(id: u64) -> Option<Arc<Shape>> {
|
|
||||||
CAPTURES
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.values()
|
|
||||||
.flatten()
|
|
||||||
.filter_map(Weak::upgrade)
|
|
||||||
.find_map(|state| match &state.lock().unwrap().snapshot {
|
|
||||||
Snapshot::Ready(shape) if shape.id == id => Some(shape.clone()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct Capture {
|
|
||||||
monitor: usize,
|
|
||||||
state: SharedState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Capture {
|
|
||||||
pub fn new(monitor: usize) -> Self {
|
|
||||||
let state = Arc::new(Mutex::new(State {
|
|
||||||
updated: Instant::now(),
|
|
||||||
snapshot: Snapshot::Pending,
|
|
||||||
}));
|
|
||||||
let capture = Self { monitor, state };
|
|
||||||
capture.activate();
|
|
||||||
capture
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activate(&self) {
|
|
||||||
let mut captures = CAPTURES.lock().unwrap();
|
|
||||||
let states = captures.entry(self.monitor).or_default();
|
|
||||||
let own = Arc::downgrade(&self.state);
|
|
||||||
if !states.iter().any(|state| state.ptr_eq(&own)) {
|
|
||||||
states.push(own);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deactivate(&self) {
|
|
||||||
let mut captures = CAPTURES.lock().unwrap();
|
|
||||||
if let Some(states) = captures.get_mut(&self.monitor) {
|
|
||||||
let own = Arc::downgrade(&self.state);
|
|
||||||
states.retain(|state| !state.ptr_eq(&own));
|
|
||||||
if states.is_empty() {
|
|
||||||
captures.remove(&self.monitor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe fn update(
|
|
||||||
&self,
|
|
||||||
duplication: *mut IDXGIOutputDuplication,
|
|
||||||
frame: &DXGI_OUTDUPL_FRAME_INFO,
|
|
||||||
) {
|
|
||||||
let snapshot = match self.read(duplication, frame) {
|
|
||||||
Ok(Some(shape)) => Snapshot::Ready(Arc::new(shape)),
|
|
||||||
Ok(None) => return,
|
|
||||||
Err(error) => {
|
|
||||||
hbb_common::log::error!("DXGI cursor capture failed: {error}");
|
|
||||||
Snapshot::Failed(error.to_string())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
*self.state.lock().unwrap() = State {
|
|
||||||
updated: Instant::now(),
|
|
||||||
snapshot,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn read(
|
|
||||||
&self,
|
|
||||||
duplication: *mut IDXGIOutputDuplication,
|
|
||||||
frame: &DXGI_OUTDUPL_FRAME_INFO,
|
|
||||||
) -> io::Result<Option<Shape>> {
|
|
||||||
use winapi::um::shellscalingapi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI};
|
|
||||||
// The Flutter runner is per-monitor DPI aware, so this is the output DPI.
|
|
||||||
let (mut x, mut y) = (0, 0);
|
|
||||||
wrap_hresult(GetDpiForMonitor(
|
|
||||||
self.monitor as _,
|
|
||||||
MDT_EFFECTIVE_DPI,
|
|
||||||
&mut x,
|
|
||||||
&mut y,
|
|
||||||
))?;
|
|
||||||
if x == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"Invalid monitor scale",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let scale = x as f64 / DEFAULT_DPI;
|
|
||||||
if frame.PointerShapeBufferSize > 0 {
|
|
||||||
return read(duplication, frame.PointerShapeBufferSize)
|
|
||||||
.map(|shape| Some(shape.with_scale(scale)));
|
|
||||||
}
|
|
||||||
// A DPI change can leave a custom cursor's physical bitmap unchanged.
|
|
||||||
Ok(match &self.state.lock().unwrap().snapshot {
|
|
||||||
Snapshot::Ready(shape) if shape.scale != scale => {
|
|
||||||
Some(shape.as_ref().clone().with_scale(scale))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn read(duplication: *mut IDXGIOutputDuplication, size: u32) -> io::Result<Shape> {
|
|
||||||
let mut pixels = vec![0; size as usize];
|
|
||||||
let mut required = 0;
|
|
||||||
let mut info = std::mem::zeroed();
|
|
||||||
wrap_hresult((*duplication).GetFramePointerShape(
|
|
||||||
size,
|
|
||||||
pixels.as_mut_ptr().cast(),
|
|
||||||
&mut required,
|
|
||||||
&mut info,
|
|
||||||
))?;
|
|
||||||
if required > size {
|
|
||||||
return Err(invalid_shape());
|
|
||||||
}
|
|
||||||
pixels.truncate(required as usize);
|
|
||||||
Shape::new(info, pixels)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Capture {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.deactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use winapi::shared::windef::POINT;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cursor_state_follows_capture_lifetime_and_gdi_switches() {
|
|
||||||
const MONITOR: usize = usize::MAX;
|
|
||||||
let first = Capture::new(MONITOR);
|
|
||||||
let second = Capture::new(MONITOR);
|
|
||||||
first.deactivate();
|
|
||||||
assert!(matches!(snapshot(MONITOR), Snapshot::Pending));
|
|
||||||
drop(second);
|
|
||||||
assert!(matches!(snapshot(MONITOR), Snapshot::Unavailable));
|
|
||||||
first.activate();
|
|
||||||
first.activate();
|
|
||||||
assert!(matches!(snapshot(MONITOR), Snapshot::Pending));
|
|
||||||
drop(first);
|
|
||||||
assert!(matches!(snapshot(MONITOR), Snapshot::Unavailable));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cursor_keeps_physical_hotspot_and_both_monochrome_planes() {
|
|
||||||
let info = DXGI_OUTDUPL_POINTER_SHAPE_INFO {
|
|
||||||
Type: DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME,
|
|
||||||
Width: 64,
|
|
||||||
Height: 128,
|
|
||||||
Pitch: 8,
|
|
||||||
HotSpot: POINT { x: 31, y: 29 },
|
|
||||||
};
|
|
||||||
let pixels = vec![0xa5; 1024];
|
|
||||||
let shape = Shape::new(info, pixels.clone()).unwrap().with_scale(1.0);
|
|
||||||
assert_eq!(
|
|
||||||
(shape.width, shape.height, shape.hotspot),
|
|
||||||
(64, 64, (31, 29))
|
|
||||||
);
|
|
||||||
assert_eq!(shape.pixels, pixels);
|
|
||||||
let scaled = shape.clone().with_scale(2.0);
|
|
||||||
assert_eq!(scaled.scale, 2.0);
|
|
||||||
assert_ne!(shape.id, scaled.id);
|
|
||||||
assert_eq!(shape.id, scaled.with_scale(1.0).id);
|
|
||||||
assert!(Shape::new(info, vec![0; 512]).is_err());
|
|
||||||
let mut changed = info;
|
|
||||||
changed.HotSpot.y += 1;
|
|
||||||
assert_ne!(
|
|
||||||
shape.id,
|
|
||||||
Shape::new(changed, pixels).unwrap().with_scale(1.0).id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@ use std::{io, mem, ptr, slice};
|
|||||||
pub mod gdi;
|
pub mod gdi;
|
||||||
pub use gdi::CapturerGDI;
|
pub use gdi::CapturerGDI;
|
||||||
pub mod mag;
|
pub mod mag;
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
pub mod cursor;
|
|
||||||
|
|
||||||
use winapi::{
|
use winapi::{
|
||||||
shared::{
|
shared::{
|
||||||
@@ -44,8 +42,6 @@ impl<T> Drop for ComPtr<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Capturer {
|
pub struct Capturer {
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
cursor: Option<cursor::Capture>,
|
|
||||||
device: ComPtr<ID3D11Device>,
|
device: ComPtr<ID3D11Device>,
|
||||||
display: Display,
|
display: Display,
|
||||||
context: ComPtr<ID3D11DeviceContext>,
|
context: ComPtr<ID3D11DeviceContext>,
|
||||||
@@ -162,9 +158,6 @@ impl Capturer {
|
|||||||
let rotate = Self::create_rotations(device.0, context.0, &display);
|
let rotate = Self::create_rotations(device.0, context.0, &display);
|
||||||
|
|
||||||
Ok(Capturer {
|
Ok(Capturer {
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
cursor: (!duplication.is_null())
|
|
||||||
.then(|| cursor::Capture::new(display.hmonitor() as usize)),
|
|
||||||
device,
|
device,
|
||||||
context,
|
context,
|
||||||
duplication: ComPtr(duplication),
|
duplication: ComPtr(duplication),
|
||||||
@@ -323,25 +316,12 @@ impl Capturer {
|
|||||||
|
|
||||||
pub fn set_gdi(&mut self) -> bool {
|
pub fn set_gdi(&mut self) -> bool {
|
||||||
self.gdi_capturer = self.display.create_gdi();
|
self.gdi_capturer = self.display.create_gdi();
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
if self.is_gdi() {
|
|
||||||
if let Some(cursor) = &self.cursor {
|
|
||||||
cursor.deactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.is_gdi()
|
self.is_gdi()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancel_gdi(&mut self) {
|
pub fn cancel_gdi(&mut self) {
|
||||||
self.gdi_buffer = Vec::new();
|
self.gdi_buffer = Vec::new();
|
||||||
self.gdi_capturer.take();
|
self.gdi_capturer.take();
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
if !self.duplication.is_null() {
|
|
||||||
let monitor = self.display.hmonitor() as usize;
|
|
||||||
self.cursor
|
|
||||||
.get_or_insert_with(|| cursor::Capture::new(monitor))
|
|
||||||
.activate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
@@ -356,10 +336,6 @@ impl Capturer {
|
|||||||
|
|
||||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||||
let frame = ComPtr(frame);
|
let frame = ComPtr(frame);
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
if let Some(cursor) = &self.cursor {
|
|
||||||
cursor.update(self.duplication.0, &info);
|
|
||||||
}
|
|
||||||
|
|
||||||
if *info.LastPresentTime.QuadPart() == 0 {
|
if *info.LastPresentTime.QuadPart() == 0 {
|
||||||
return Err(std::io::ErrorKind::WouldBlock.into());
|
return Err(std::io::ErrorKind::WouldBlock.into());
|
||||||
@@ -503,10 +479,6 @@ impl Capturer {
|
|||||||
|
|
||||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||||
let frame = ComPtr(frame);
|
let frame = ComPtr(frame);
|
||||||
#[cfg(feature = "cursor")]
|
|
||||||
if let Some(cursor) = &self.cursor {
|
|
||||||
cursor.update(self.duplication.0, &info);
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 {
|
if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 {
|
||||||
return Err(std::io::ErrorKind::WouldBlock.into());
|
return Err(std::io::ErrorKind::WouldBlock.into());
|
||||||
|
|||||||
@@ -97,8 +97,6 @@ use windows_service::{
|
|||||||
use winreg::{enums::*, RegKey};
|
use winreg::{enums::*, RegKey};
|
||||||
|
|
||||||
mod acl;
|
mod acl;
|
||||||
#[cfg(feature = "flutter")]
|
|
||||||
mod cursor;
|
|
||||||
mod installer_handoff;
|
mod installer_handoff;
|
||||||
mod installer_shell;
|
mod installer_shell;
|
||||||
mod msi_registry;
|
mod msi_registry;
|
||||||
@@ -217,14 +215,7 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
|
|||||||
if ci.flags & CURSOR_SHOWING == 0 {
|
if ci.flags & CURSOR_SHOWING == 0 {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
#[cfg(feature = "flutter")]
|
Ok(Some(ci.hCursor as _))
|
||||||
{
|
|
||||||
cursor::current(&ci)
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "flutter"))]
|
|
||||||
{
|
|
||||||
Ok(Some(ci.hCursor as _))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,10 +260,6 @@ impl Drop for IconInfo {
|
|||||||
// https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp
|
// https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp
|
||||||
// https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx
|
// https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx
|
||||||
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||||
#[cfg(feature = "flutter")]
|
|
||||||
if let Some(data) = cursor::data(hcursor)? {
|
|
||||||
return Ok(data);
|
|
||||||
}
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut ii = IconInfo::new(hcursor as _)?;
|
let mut ii = IconInfo::new(hcursor as _)?;
|
||||||
let bm_mask = get_bitmap(ii.0.hbmMask)?;
|
let bm_mask = get_bitmap(ii.0.hbmMask)?;
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
use super::{drawOutline, handleMask, CursorData};
|
|
||||||
use hbb_common::{anyhow::Context, bail, ResultType};
|
|
||||||
use scrap::dxgi::cursor::{self, Shape, Snapshot, CURSOR_ID_FLAG};
|
|
||||||
use winapi::{
|
|
||||||
shared::dxgi1_2::{
|
|
||||||
DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR, DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME,
|
|
||||||
},
|
|
||||||
um::winuser::{MonitorFromPoint, CURSORINFO, MONITOR_DEFAULTTONULL},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CHANNELS: usize = 4;
|
|
||||||
const BORDER: i32 = 1;
|
|
||||||
|
|
||||||
pub(super) fn current(info: &CURSORINFO) -> ResultType<Option<u64>> {
|
|
||||||
let monitor = unsafe { MonitorFromPoint(info.ptScreenPos, MONITOR_DEFAULTTONULL) };
|
|
||||||
match cursor::snapshot(monitor as usize) {
|
|
||||||
Snapshot::Unavailable => Ok(Some(info.hCursor as usize as u32 as u64)),
|
|
||||||
Snapshot::Pending => Ok(None),
|
|
||||||
Snapshot::Ready(shape) => Ok(Some(shape.id)),
|
|
||||||
Snapshot::Failed(error) => bail!("DXGI cursor capture: {error}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn data(id: u64) -> ResultType<Option<CursorData>> {
|
|
||||||
if id & CURSOR_ID_FLAG == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let shape = cursor::shape(id).context("DXGI cursor changed before export")?;
|
|
||||||
let (colors, outline) = colors(&shape)?;
|
|
||||||
let data = CursorData {
|
|
||||||
id,
|
|
||||||
colors: colors.into(),
|
|
||||||
width: shape.width as _,
|
|
||||||
height: shape.height as _,
|
|
||||||
hotx: shape.hotspot.0,
|
|
||||||
hoty: shape.hotspot.1,
|
|
||||||
scale: shape.scale,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
Ok(Some(if outline { outlined(data)? } else { data }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn colors(shape: &Shape) -> ResultType<(Vec<u8>, bool)> {
|
|
||||||
let length = (shape.width as usize)
|
|
||||||
.checked_mul(shape.height as usize)
|
|
||||||
.and_then(|pixels| pixels.checked_mul(CHANNELS))
|
|
||||||
.context("Cursor size overflow")?;
|
|
||||||
if shape.kind == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME {
|
|
||||||
let mut colors = vec![0; length];
|
|
||||||
let outline = unsafe {
|
|
||||||
handleMask(
|
|
||||||
colors.as_mut_ptr(),
|
|
||||||
shape.pixels.as_ptr(),
|
|
||||||
shape.width as _,
|
|
||||||
shape.height as _,
|
|
||||||
shape.pitch as _,
|
|
||||||
(shape.height * 2) as _,
|
|
||||||
)
|
|
||||||
} > 0;
|
|
||||||
return Ok((colors, outline));
|
|
||||||
}
|
|
||||||
let mut colors = Vec::with_capacity(length);
|
|
||||||
let mut outline = false;
|
|
||||||
for row in shape.pixels.chunks_exact(shape.pitch as usize) {
|
|
||||||
for pixel in row[..shape.width as usize * CHANNELS].chunks_exact(CHANNELS) {
|
|
||||||
let (rgba, xor) = rgba(pixel, shape.kind);
|
|
||||||
outline |= xor;
|
|
||||||
colors.extend_from_slice(&rgba);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((colors, outline))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rgba(pixel: &[u8], kind: u32) -> ([u8; CHANNELS], bool) {
|
|
||||||
if kind != DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR {
|
|
||||||
return ([pixel[2], pixel[1], pixel[0], pixel[3]], false);
|
|
||||||
}
|
|
||||||
if pixel[3] == 0 {
|
|
||||||
return ([pixel[2], pixel[1], pixel[0], 255], false);
|
|
||||||
}
|
|
||||||
// Match the Win32 exporter's outlined replacement for background-dependent XOR.
|
|
||||||
if pixel[..3].iter().any(|value| *value != 0) {
|
|
||||||
([0, 0, 0, 255], true)
|
|
||||||
} else {
|
|
||||||
([0; CHANNELS], false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn outlined(data: CursorData) -> ResultType<CursorData> {
|
|
||||||
let width = data
|
|
||||||
.width
|
|
||||||
.checked_add(BORDER * 2)
|
|
||||||
.context("Cursor width overflow")?;
|
|
||||||
let height = data
|
|
||||||
.height
|
|
||||||
.checked_add(BORDER * 2)
|
|
||||||
.context("Cursor height overflow")?;
|
|
||||||
let length = (width as usize)
|
|
||||||
.checked_mul(height as usize)
|
|
||||||
.and_then(|pixels| pixels.checked_mul(CHANNELS))
|
|
||||||
.context("Cursor size overflow")?;
|
|
||||||
let length_i32 =
|
|
||||||
i32::try_from(length).context("Cursor outline exceeds the native buffer size")?;
|
|
||||||
let mut colors = vec![0; length];
|
|
||||||
unsafe {
|
|
||||||
drawOutline(
|
|
||||||
colors.as_mut_ptr(),
|
|
||||||
data.colors.as_ptr(),
|
|
||||||
data.width,
|
|
||||||
data.height,
|
|
||||||
length_i32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(CursorData {
|
|
||||||
colors: colors.into(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
hotx: data.hotx + BORDER,
|
|
||||||
hoty: data.hoty + BORDER,
|
|
||||||
..data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use winapi::shared::dxgi1_2::DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn physical_cursor_preserves_alpha_and_ignores_row_padding() {
|
|
||||||
let shape = Shape {
|
|
||||||
id: CURSOR_ID_FLAG,
|
|
||||||
scale: 0.0,
|
|
||||||
kind: DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR,
|
|
||||||
width: 1,
|
|
||||||
height: 2,
|
|
||||||
pitch: 8,
|
|
||||||
hotspot: (0, 1),
|
|
||||||
pixels: vec![
|
|
||||||
32, 64, 128, 128, 255, 255, 255, 255, 1, 2, 3, 255, 255, 255, 255, 255,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
colors(&shape).unwrap(),
|
|
||||||
(vec![128, 64, 32, 128, 3, 2, 1, 255], false)
|
|
||||||
);
|
|
||||||
let masked = Shape {
|
|
||||||
kind: DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR,
|
|
||||||
pixels: vec![0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0],
|
|
||||||
..shape
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
colors(&masked).unwrap(),
|
|
||||||
(vec![0, 0, 0, 255, 0, 0, 0, 255], true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user