@desklink/host 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/NOTICE +26 -0
  2. package/README.md +134 -56
  3. package/bin/desklink-host.mjs +13 -5
  4. package/dist/bridge.d.ts +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +12 -4
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts +6 -1
  9. package/dist/engineProcess.d.ts.map +1 -1
  10. package/dist/engineProcess.js +35 -4
  11. package/dist/engineProcess.js.map +1 -1
  12. package/dist/protocol.d.ts +58 -0
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resolveEngine.d.ts +7 -1
  16. package/dist/resolveEngine.d.ts.map +1 -1
  17. package/dist/resolveEngine.js +66 -14
  18. package/dist/resolveEngine.js.map +1 -1
  19. package/docs/BOUNDARY.md +15 -7
  20. package/docs/PROTOCOL.md +267 -27
  21. package/engine/Cargo.lock +47 -2
  22. package/engine/Cargo.toml +18 -13
  23. package/engine/build.rs +86 -4
  24. package/engine/native/agent_overlay_mac.m +51 -4
  25. package/engine/native/agent_overlay_wayland.c +145 -12
  26. package/engine/native/agent_overlay_x11.c +72 -0
  27. package/engine/native/nvenc_shim.c +453 -0
  28. package/engine/native/vaapi_shim.c +727 -0
  29. package/engine/native/vpx_shim.c +54 -4
  30. package/engine/native/vt_shim.c +448 -0
  31. package/engine/src/capture.rs +167 -58
  32. package/engine/src/capture_mac.rs +24 -14
  33. package/engine/src/capture_win.rs +23 -0
  34. package/engine/src/clipboard_win.rs +8 -0
  35. package/engine/src/convert.rs +286 -61
  36. package/engine/src/cursor.rs +118 -0
  37. package/engine/src/encoder.rs +122 -13
  38. package/engine/src/h264.rs +188 -0
  39. package/engine/src/h264_nvenc.rs +308 -0
  40. package/engine/src/h264_openh264.rs +735 -0
  41. package/engine/src/h264_vaapi.rs +263 -0
  42. package/engine/src/h264_vt.rs +368 -0
  43. package/engine/src/indicator.rs +104 -3
  44. package/engine/src/input_win.rs +91 -0
  45. package/engine/src/keymap_win.rs +39 -0
  46. package/engine/src/mac.rs +34 -9
  47. package/engine/src/main.rs +186 -26
  48. package/engine/src/peer.rs +466 -28
  49. package/engine/src/portal.rs +42 -4
  50. package/engine/src/portal_win.rs +37 -0
  51. package/engine/src/protocol.rs +64 -2
  52. package/engine/src/session.rs +1252 -105
  53. package/engine/src/win.rs +110 -0
  54. package/engine/src/x11.rs +225 -9
  55. package/engine/src/x11_mac.rs +12 -0
  56. package/engine/vendor/libva/COPYING +19 -0
  57. package/engine/vendor/libva/va/va.h +5450 -0
  58. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  59. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  60. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  61. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  62. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  63. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  64. package/engine/vendor/libva/va/va_drm.h +61 -0
  65. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  66. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  67. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  68. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  69. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  70. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  71. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  72. package/engine/vendor/libva/va/va_fei.h +170 -0
  73. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  74. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  75. package/engine/vendor/libva/va/va_prot.h +525 -0
  76. package/engine/vendor/libva/va/va_version.h +87 -0
  77. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  78. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  79. package/engine/vendor/nv-codec-headers/README +7 -0
  80. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  81. package/examples/reference-client.html +1 -1
  82. package/package.json +4 -3
@@ -181,6 +181,9 @@ pub fn to_i420(
181
181
  let pairs = dst_h / 2;
182
182
  let per_band = pairs.div_ceil(bands());
183
183
  let cw = dst_w / 2;
184
+ let (columns, rows) = (spans(src_w, dst_w), spans(src_h, dst_h));
185
+ let widest = |spans: &[(usize, usize)]| spans.iter().map(|(a, b)| b - a).max().unwrap_or(1);
186
+ let largest_box = (widest(&columns) * widest(&rows)).min(4095) as u64;
184
187
  let source = Source {
185
188
  data: src,
186
189
  width: src_w,
@@ -188,6 +191,11 @@ pub fn to_i420(
188
191
  stride: src_stride,
189
192
  bytes_per_pixel,
190
193
  order,
194
+ columns,
195
+ rows,
196
+ reciprocals: (0..=largest_box)
197
+ .map(|n| (1 << 32) / n.max(1) + 1)
198
+ .collect(),
191
199
  };
192
200
  std::thread::scope(|scope| {
193
201
  let y_bands = y_plane.chunks_mut(per_band * 2 * dst_w);
@@ -196,21 +204,84 @@ pub fn to_i420(
196
204
  for (band, ((y, u), v)) in y_bands.zip(u_bands).zip(v_bands).enumerate() {
197
205
  let source = &source;
198
206
  scope.spawn(move || {
199
- let first_pair = band * per_band;
200
- for pair in 0..u.len() / cw {
201
- let dy = (first_pair + pair) * 2;
202
- let (top, bottom) =
203
- y[pair * 2 * dst_w..(pair + 1) * 2 * dst_w].split_at_mut(dst_w);
204
- let u_row = &mut u[pair * cw..(pair + 1) * cw];
205
- let v_row = &mut v[pair * cw..(pair + 1) * cw];
206
- source.row_pair(dy, dst_w, dst_h, top, bottom, u_row, v_row);
207
+ #[cfg(target_arch = "x86_64")]
208
+ if std::arch::is_x86_feature_detected!("avx2") {
209
+ // SAFETY: the CPU was just checked for AVX2.
210
+ return unsafe { band_avx2(source, band * per_band, dst_w, dst_h, y, u, v) };
207
211
  }
212
+ convert_band(source, band * per_band, dst_w, dst_h, y, u, v)
208
213
  });
209
214
  }
210
215
  });
211
216
  Some(out)
212
217
  }
213
218
 
219
+ /// Rows `first_pair * 2..` of the destination into this band's planes.
220
+ #[inline(always)]
221
+ fn convert_band(
222
+ source: &Source,
223
+ first_pair: usize,
224
+ dst_w: usize,
225
+ dst_h: usize,
226
+ y: &mut [u8],
227
+ u: &mut [u8],
228
+ v: &mut [u8],
229
+ ) {
230
+ let cw = dst_w / 2;
231
+ // A destination row pair of box averages and a row of box-row sums, made
232
+ // once per band and only when this is not the native path.
233
+ let mut rgb = Vec::new();
234
+ let mut sums = Vec::new();
235
+ for pair in 0..u.len() / cw {
236
+ let dy = (first_pair + pair) * 2;
237
+ let (top, bottom) = y[pair * 2 * dst_w..(pair + 1) * 2 * dst_w].split_at_mut(dst_w);
238
+ let u_row = &mut u[pair * cw..(pair + 1) * cw];
239
+ let v_row = &mut v[pair * cw..(pair + 1) * cw];
240
+ if source.native(dst_w, dst_h) && source.bytes_per_pixel == 4 {
241
+ source.native_pair(dy, top, bottom, u_row, v_row);
242
+ } else {
243
+ rgb.resize(dst_w * 2, [0i32; 3]);
244
+ let (a, b) = rgb.split_at_mut(dst_w);
245
+ if source.bytes_per_pixel == 4 {
246
+ source.box_row::<4>(dy, a, &mut sums);
247
+ source.box_row::<4>(dy + 1, b, &mut sums);
248
+ } else {
249
+ source.box_row::<3>(dy, a, &mut sums);
250
+ source.box_row::<3>(dy + 1, b, &mut sums);
251
+ }
252
+ pack(a, b, top, bottom, u_row, v_row);
253
+ }
254
+ }
255
+ }
256
+
257
+ /// `convert_band` compiled for AVX2, for the x86-64 machines that have it; the
258
+ /// build itself targets the x86-64 baseline.
259
+ #[cfg(target_arch = "x86_64")]
260
+ #[target_feature(enable = "avx2")]
261
+ unsafe fn band_avx2(
262
+ source: &Source,
263
+ first_pair: usize,
264
+ dst_w: usize,
265
+ dst_h: usize,
266
+ y: &mut [u8],
267
+ u: &mut [u8],
268
+ v: &mut [u8],
269
+ ) {
270
+ convert_band(source, first_pair, dst_w, dst_h, y, u, v)
271
+ }
272
+
273
+ /// The source span `[start, end)` each of `dst` destination pixels covers: the
274
+ /// box a downscale averages, or at least the one nearest pixel when the
275
+ /// destination is a little larger than the source.
276
+ fn spans(src: usize, dst: usize) -> Vec<(usize, usize)> {
277
+ (0..dst)
278
+ .map(|d| {
279
+ let start = d * src / dst;
280
+ (start, ((d + 1) * src).div_ceil(dst).clamp(start + 1, src))
281
+ })
282
+ .collect()
283
+ }
284
+
214
285
  struct Source<'a> {
215
286
  data: &'a [u8],
216
287
  width: usize,
@@ -218,80 +289,126 @@ struct Source<'a> {
218
289
  stride: usize,
219
290
  bytes_per_pixel: usize,
220
291
  order: [usize; 3],
292
+ columns: Vec<(usize, usize)>,
293
+ rows: Vec<(usize, usize)>,
294
+ /// `2^32 / n + 1` for each box area `n` up to this conversion's largest,
295
+ /// capped at 4095; a larger box divides.
296
+ reciprocals: Vec<u64>,
221
297
  }
222
298
 
223
299
  impl Source<'_> {
224
- #[inline]
225
- fn rgb(&self, x: usize, y: usize) -> (i32, i32, i32) {
226
- let i = y * self.stride + x * self.bytes_per_pixel;
227
- let p = &self.data[i..i + 3];
228
- (
229
- p[self.order[0]] as i32,
230
- p[self.order[1]] as i32,
231
- p[self.order[2]] as i32,
232
- )
300
+ fn native(&self, dst_w: usize, dst_h: usize) -> bool {
301
+ dst_w == self.width && dst_h == self.height
233
302
  }
234
303
 
235
- /// The average colour of the source pixels destination pixel (dx, dy) covers.
236
- #[inline]
237
- fn sample(&self, dx: usize, dy: usize, dst_w: usize, dst_h: usize) -> (i32, i32, i32) {
238
- if dst_w == self.width && dst_h == self.height {
239
- return self.rgb(dx, dy);
240
- }
241
- let x0 = dx * self.width / dst_w;
242
- let x1 = ((dx + 1) * self.width)
243
- .div_ceil(dst_w)
244
- .clamp(x0 + 1, self.width);
245
- let y0 = dy * self.height / dst_h;
246
- let y1 = ((dy + 1) * self.height)
247
- .div_ceil(dst_h)
248
- .clamp(y0 + 1, self.height);
249
- let (mut r, mut g, mut b) = (0i32, 0i32, 0i32);
304
+ /// Destination row `dy` as the average colour of the source box each pixel
305
+ /// covers (at the source's own size, a box of one pixel: its exact colour).
306
+ /// The box's rows are summed byte for byte first, which vectorises, so the
307
+ /// per-pixel work is only the few columns of each box.
308
+ #[inline(always)]
309
+ fn box_row<const BPP: usize>(&self, dy: usize, out: &mut [[i32; 3]], sums: &mut Vec<u32>) {
310
+ let (y0, y1) = self.rows[dy];
311
+ let bytes = self.width * BPP;
312
+ sums.clear();
313
+ sums.resize(bytes, 0);
250
314
  for y in y0..y1 {
251
- for x in x0..x1 {
252
- let (pr, pg, pb) = self.rgb(x, y);
253
- r += pr;
254
- g += pg;
255
- b += pb;
315
+ let line = &self.data[y * self.stride..y * self.stride + bytes];
316
+ for (sum, &byte) in sums.iter_mut().zip(line) {
317
+ *sum += byte as u32;
318
+ }
319
+ }
320
+ let height = (y1 - y0) as u32;
321
+ let [r, g, b] = self.order;
322
+ for (sum, &(x0, x1)) in out.iter_mut().zip(&self.columns) {
323
+ let mut rgb = [0u32; 3];
324
+ for pixel in sums[x0 * BPP..x1 * BPP].chunks_exact(BPP) {
325
+ rgb = [rgb[0] + pixel[r], rgb[1] + pixel[g], rgb[2] + pixel[b]];
256
326
  }
327
+ let n = (x1 - x0) * height as usize;
328
+ *sum = match self.reciprocals.get(n) {
329
+ // floor(c / n) as a multiply by 2^32 / n + 1, exact while
330
+ // c * n < 2^32; c is at most 255 * n, and 255 * 4095^2 < 2^32.
331
+ Some(&m) => rgb.map(|c| ((c as u64 * m) >> 32) as i32),
332
+ None => rgb.map(|c| (c / n as u32) as i32),
333
+ };
257
334
  }
258
- let n = ((x1 - x0) * (y1 - y0)) as i32;
259
- (r / n, g / n, b / n)
260
335
  }
261
336
 
262
- /// Two destination rows and the chroma row they share.
263
- #[allow(clippy::too_many_arguments)]
264
- fn row_pair(
337
+ /// A 4-byte source at its own size: the path every full-size desktop takes,
338
+ /// so it reads the two rows directly and in a shape the compiler vectorises.
339
+ #[inline(always)]
340
+ fn native_pair(
265
341
  &self,
266
342
  dy: usize,
267
- dst_w: usize,
268
- dst_h: usize,
269
343
  top: &mut [u8],
270
344
  bottom: &mut [u8],
271
345
  u: &mut [u8],
272
346
  v: &mut [u8],
273
347
  ) {
274
- for cx in 0..dst_w / 2 {
275
- let dx = cx * 2;
276
- let a = self.sample(dx, dy, dst_w, dst_h);
277
- let b = self.sample(dx + 1, dy, dst_w, dst_h);
278
- let c = self.sample(dx, dy + 1, dst_w, dst_h);
279
- let d = self.sample(dx + 1, dy + 1, dst_w, dst_h);
280
- top[dx] = luma(a.0, a.1, a.2);
281
- top[dx + 1] = luma(b.0, b.1, b.2);
282
- bottom[dx] = luma(c.0, c.1, c.2);
283
- bottom[dx + 1] = luma(d.0, d.1, d.2);
284
- let (cu, cv) = chroma(
285
- (a.0 + b.0 + c.0 + d.0 + 2) >> 2,
286
- (a.1 + b.1 + c.1 + d.1 + 2) >> 2,
287
- (a.2 + b.2 + c.2 + d.2 + 2) >> 2,
288
- );
289
- u[cx] = cu;
290
- v[cx] = cv;
348
+ let width = top.len();
349
+ let row = |y: usize| &self.data[y * self.stride..y * self.stride + width * 4];
350
+ let (upper, lower) = (row(dy), row(dy + 1));
351
+ match self.order {
352
+ [2, 1, 0] => pair4::<2, 1, 0>(upper, lower, top, bottom, u, v),
353
+ _ => pair4::<0, 1, 2>(upper, lower, top, bottom, u, v),
291
354
  }
292
355
  }
293
356
  }
294
357
 
358
+ #[inline(always)]
359
+ fn pair4<const R: usize, const G: usize, const B: usize>(
360
+ upper: &[u8],
361
+ lower: &[u8],
362
+ top: &mut [u8],
363
+ bottom: &mut [u8],
364
+ u: &mut [u8],
365
+ v: &mut [u8],
366
+ ) {
367
+ // The same integer formulas as `luma` and `chroma`, in 16 bits: every
368
+ // intermediate fits (luma below 2^16 unsigned, chroma within ±2^15) and the
369
+ // results need no clamp, which lets the compiler use 16-bit vector lanes.
370
+ for (src, dst) in [(upper, &mut *top), (lower, &mut *bottom)] {
371
+ for (pixel, y) in src.chunks_exact(4).zip(dst.iter_mut()) {
372
+ let (r, g, b) = (pixel[R] as u16, pixel[G] as u16, pixel[B] as u16);
373
+ *y = (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16) as u8;
374
+ }
375
+ }
376
+ let blocks = upper.chunks_exact(8).zip(lower.chunks_exact(8));
377
+ for ((a, b), (u, v)) in blocks.zip(u.iter_mut().zip(v.iter_mut())) {
378
+ let sum = |c: usize| {
379
+ ((a[c] as u16 + a[c + 4] as u16 + b[c] as u16 + b[c + 4] as u16 + 2) >> 2) as i16
380
+ };
381
+ let (r, g, b) = (sum(R), sum(G), sum(B));
382
+ *u = (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128) as u8;
383
+ *v = (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128) as u8;
384
+ }
385
+ }
386
+
387
+ /// Two rows of RGB into two luma rows and the chroma row they share, chroma
388
+ /// being the average of each 2x2 block.
389
+ #[inline(always)]
390
+ fn pack(
391
+ a: &[[i32; 3]],
392
+ b: &[[i32; 3]],
393
+ top: &mut [u8],
394
+ bottom: &mut [u8],
395
+ u: &mut [u8],
396
+ v: &mut [u8],
397
+ ) {
398
+ for (p, y) in a
399
+ .iter()
400
+ .zip(top.iter_mut())
401
+ .chain(b.iter().zip(bottom.iter_mut()))
402
+ {
403
+ *y = luma(p[0], p[1], p[2]);
404
+ }
405
+ for (cx, (u, v)) in u.iter_mut().zip(v.iter_mut()).enumerate() {
406
+ let sum =
407
+ |c: usize| (a[cx * 2][c] + a[cx * 2 + 1][c] + b[cx * 2][c] + b[cx * 2 + 1][c] + 2) >> 2;
408
+ (*u, *v) = chroma(sum(0), sum(1), sum(2));
409
+ }
410
+ }
411
+
295
412
  #[cfg(test)]
296
413
  mod tests {
297
414
  use super::*;
@@ -348,6 +465,114 @@ mod tests {
348
465
  );
349
466
  }
350
467
 
468
+ /// The per-pixel conversion this module used to run, kept as the definition
469
+ /// the fast paths must reproduce bit for bit.
470
+ fn reference(
471
+ src: &[u8],
472
+ sw: usize,
473
+ sh: usize,
474
+ stride: usize,
475
+ format: PixelFormat,
476
+ dw: usize,
477
+ dh: usize,
478
+ ) -> Vec<u8> {
479
+ let (bpp, o) = layout(format).unwrap();
480
+ let rgb = |x: usize, y: usize| {
481
+ let p = &src[y * stride + x * bpp..];
482
+ (p[o[0]] as i32, p[o[1]] as i32, p[o[2]] as i32)
483
+ };
484
+ let sample = |dx: usize, dy: usize| {
485
+ let x0 = dx * sw / dw;
486
+ let x1 = ((dx + 1) * sw).div_ceil(dw).clamp(x0 + 1, sw);
487
+ let y0 = dy * sh / dh;
488
+ let y1 = ((dy + 1) * sh).div_ceil(dh).clamp(y0 + 1, sh);
489
+ let (mut r, mut g, mut b) = (0, 0, 0);
490
+ for y in y0..y1 {
491
+ for x in x0..x1 {
492
+ let p = rgb(x, y);
493
+ (r, g, b) = (r + p.0, g + p.1, b + p.2);
494
+ }
495
+ }
496
+ let n = ((x1 - x0) * (y1 - y0)) as i32;
497
+ (r / n, g / n, b / n)
498
+ };
499
+ let (mut y, mut u, mut v) = (vec![0; dw * dh], vec![], vec![]);
500
+ for cy in 0..dh / 2 {
501
+ for cx in 0..dw / 2 {
502
+ let px = [(0, 0), (1, 0), (0, 1), (1, 1)]
503
+ .map(|(i, j)| (cx * 2 + i, cy * 2 + j, sample(cx * 2 + i, cy * 2 + j)));
504
+ for (x, yy, p) in px {
505
+ y[yy * dw + x] = luma(p.0, p.1, p.2);
506
+ }
507
+ let s = |f: fn(&(i32, i32, i32)) -> i32| {
508
+ (px.iter().map(|p| f(&p.2)).sum::<i32>() + 2) >> 2
509
+ };
510
+ let (cu, cv) = chroma(s(|p| p.0), s(|p| p.1), s(|p| p.2));
511
+ u.push(cu);
512
+ v.push(cv);
513
+ }
514
+ }
515
+ [y, u, v].concat()
516
+ }
517
+
518
+ #[test]
519
+ fn every_path_matches_the_per_pixel_definition_exactly() {
520
+ let mut seed = 0x9e3779b9u32;
521
+ let mut noise = |n: usize| {
522
+ (0..n)
523
+ .map(|_| {
524
+ seed ^= seed << 13;
525
+ seed ^= seed >> 17;
526
+ seed ^= seed << 5;
527
+ seed as u8
528
+ })
529
+ .collect::<Vec<u8>>()
530
+ };
531
+ let formats = [
532
+ PixelFormat::Bgrx,
533
+ PixelFormat::Rgba,
534
+ PixelFormat::Bgr,
535
+ PixelFormat::Rgb,
536
+ ];
537
+ // Native, a few pixels off, box downscales, and boxes near and past the
538
+ // largest area the reciprocal table covers.
539
+ for (sw, sh, dw, dh) in [
540
+ (256, 126, 4, 2),
541
+ (400, 200, 4, 2),
542
+ (64, 36, 64, 36),
543
+ (67, 41, 66, 40),
544
+ (100, 60, 38, 22),
545
+ (90, 50, 92, 50),
546
+ (33, 17, 8, 6),
547
+ ] {
548
+ for format in formats {
549
+ let bpp = layout(format).unwrap().0;
550
+ let stride = sw * bpp + 12;
551
+ let src = noise(stride * sh);
552
+ let fast = to_i420(&src, sw, sh, stride, format, dw, dh).unwrap();
553
+ assert!(
554
+ fast.data == reference(&src, sw, sh, stride, format, dw & !1, dh & !1),
555
+ "{format:?} {sw}x{sh} -> {dw}x{dh}"
556
+ );
557
+ }
558
+ }
559
+ }
560
+
561
+ #[test]
562
+ fn padded_bgra_rows_remain_aligned_in_stills_and_video() {
563
+ // ScreenCaptureKit pads 3420 BGRA pixels from 13680 to 13696 bytes/row.
564
+ let src = [
565
+ 0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 0, 0, // red row + green padding
566
+ 255, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 0, // blue row + green padding
567
+ ];
568
+ assert_eq!(
569
+ to_bgrx(&src, 2, 2, 12, PixelFormat::Bgra, 2, 2).unwrap(),
570
+ [src[0..8].to_vec(), src[12..20].to_vec()].concat()
571
+ );
572
+ let video = to_i420(&src, 2, 2, 12, PixelFormat::Bgra, 2, 2).unwrap();
573
+ assert_eq!(video.y_plane(), &[82, 82, 41, 41]);
574
+ }
575
+
351
576
  #[test]
352
577
  fn unsupported_format_is_reported_not_guessed() {
353
578
  assert!(to_i420(&vec![0u8; 64], 4, 4, 16, PixelFormat::Unsupported, 2, 2).is_none());
@@ -0,0 +1,118 @@
1
+ //! Cursor metadata is independent of pixel damage and never painted into video.
2
+ use crate::protocol::{timestamp_us, CursorPosition};
3
+ use std::time::Instant;
4
+
5
+ pub type CursorSink = Box<dyn Fn(CursorPosition) + Send + 'static>;
6
+
7
+ pub struct Reporter {
8
+ sink: CursorSink,
9
+ last: Option<(i32, i32, bool)>,
10
+ bitmap_visible: bool,
11
+ }
12
+ impl Reporter {
13
+ pub fn new(sink: CursorSink) -> Self {
14
+ Self {
15
+ sink,
16
+ last: None,
17
+ bitmap_visible: true,
18
+ }
19
+ }
20
+ pub fn update_spa(
21
+ &mut self,
22
+ meta: &pipewire::spa::sys::spa_meta_cursor,
23
+ bitmap: Option<&pipewire::spa::sys::spa_meta_bitmap>,
24
+ width: usize,
25
+ height: usize,
26
+ ) {
27
+ // SPA id zero is no new data, not a visibility change.
28
+ if meta.id == 0 {
29
+ return;
30
+ }
31
+ if let Some(bitmap) = bitmap.filter(|b| b.format != 0) {
32
+ self.bitmap_visible = bitmap.offset != 0;
33
+ }
34
+ let (x, y) = (meta.position.x, meta.position.y);
35
+ self.update(
36
+ x,
37
+ y,
38
+ self.bitmap_visible
39
+ && x >= 0
40
+ && y >= 0
41
+ && (x as usize) < width
42
+ && (y as usize) < height,
43
+ );
44
+ }
45
+ pub fn update(&mut self, x: i32, y: i32, visible: bool) {
46
+ if self.last == Some((x, y, visible)) {
47
+ return;
48
+ }
49
+ self.last = Some((x, y, visible));
50
+ (self.sink)(CursorPosition {
51
+ x,
52
+ y,
53
+ visible,
54
+ timestamp_us: timestamp_us(Instant::now()),
55
+ });
56
+ }
57
+ }
58
+
59
+ #[cfg(test)]
60
+ mod tests {
61
+ use super::*;
62
+ #[test]
63
+ fn spa_no_update_off_source_and_invisible_bitmap_are_distinct() {
64
+ use pipewire::spa::sys::{spa_meta_bitmap, spa_meta_cursor, spa_point, spa_rectangle};
65
+ let (tx, rx) = std::sync::mpsc::channel();
66
+ let mut reporter = Reporter::new(Box::new(move |p| {
67
+ tx.send(p).unwrap();
68
+ }));
69
+ let mut meta = spa_meta_cursor {
70
+ id: 1,
71
+ flags: 0,
72
+ position: spa_point { x: 40, y: 60 },
73
+ hotspot: spa_point { x: 2, y: 3 },
74
+ bitmap_offset: 0,
75
+ };
76
+ reporter.update_spa(&meta, None, 800, 600);
77
+ meta.id = 0;
78
+ reporter.update_spa(&meta, None, 800, 600);
79
+ assert_eq!(rx.try_iter().count(), 1);
80
+ meta.id = 1;
81
+ meta.position.x = -1;
82
+ reporter.update_spa(&meta, None, 800, 600);
83
+ assert!(!rx.try_recv().unwrap().visible);
84
+ meta.position.x = 40;
85
+ let bitmap = spa_meta_bitmap {
86
+ format: 1,
87
+ size: spa_rectangle {
88
+ width: 16,
89
+ height: 16,
90
+ },
91
+ stride: 64,
92
+ offset: 0,
93
+ };
94
+ reporter.update_spa(&meta, Some(&bitmap), 800, 600);
95
+ assert!(!rx.try_recv().unwrap().visible);
96
+ reporter.update_spa(&meta, None, 800, 600);
97
+ assert!(rx.try_recv().is_err(), "no new bitmap retains invisibility");
98
+ }
99
+
100
+ #[test]
101
+ fn reports_initial_motion_and_visibility_but_not_unchanged_polls() {
102
+ let (tx, rx) = std::sync::mpsc::channel();
103
+ let mut reporter = Reporter::new(Box::new(move |p| {
104
+ tx.send(p).unwrap();
105
+ }));
106
+ reporter.update(10, 20, true);
107
+ reporter.update(10, 20, true);
108
+ reporter.update(30, 40, true);
109
+ reporter.update(30, 40, false);
110
+ let notices: Vec<_> = rx.try_iter().collect();
111
+ assert_eq!(notices.len(), 3);
112
+ assert_eq!((notices[1].x, notices[1].y), (30, 40));
113
+ assert!(!notices[2].visible);
114
+ assert!(notices
115
+ .windows(2)
116
+ .all(|p| p[0].timestamp_us <= p[1].timestamp_us));
117
+ }
118
+ }