@desklink/host 0.1.1 → 0.3.0

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 (78) hide show
  1. package/NOTICE +23 -0
  2. package/README.md +107 -55
  3. package/bin/desklink-host.mjs +2 -2
  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.map +1 -1
  9. package/dist/engineProcess.js +1 -0
  10. package/dist/engineProcess.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/keeper.d.ts +52 -0
  16. package/dist/keeper.d.ts.map +1 -0
  17. package/dist/keeper.js +101 -0
  18. package/dist/keeper.js.map +1 -0
  19. package/dist/protocol.d.ts +26 -0
  20. package/dist/protocol.d.ts.map +1 -1
  21. package/dist/protocol.js.map +1 -1
  22. package/dist/resolveEngine.d.ts +3 -3
  23. package/dist/resolveEngine.d.ts.map +1 -1
  24. package/dist/resolveEngine.js +24 -11
  25. package/dist/resolveEngine.js.map +1 -1
  26. package/docs/BOUNDARY.md +20 -8
  27. package/docs/PROTOCOL.md +347 -24
  28. package/engine/Cargo.lock +47 -1
  29. package/engine/Cargo.toml +13 -2
  30. package/engine/build.rs +28 -2
  31. package/engine/native/nvenc_shim.c +453 -0
  32. package/engine/native/vaapi_shim.c +727 -0
  33. package/engine/native/vpx_shim.c +54 -4
  34. package/engine/native/vt_shim.c +448 -0
  35. package/engine/src/capture.rs +75 -55
  36. package/engine/src/capture_mac.rs +24 -14
  37. package/engine/src/convert.rs +286 -61
  38. package/engine/src/encoder.rs +122 -13
  39. package/engine/src/h264.rs +188 -0
  40. package/engine/src/h264_nvenc.rs +308 -0
  41. package/engine/src/h264_openh264.rs +735 -0
  42. package/engine/src/h264_vaapi.rs +263 -0
  43. package/engine/src/h264_vt.rs +368 -0
  44. package/engine/src/keeper.rs +622 -0
  45. package/engine/src/mac.rs +9 -6
  46. package/engine/src/main.rs +64 -4
  47. package/engine/src/peer.rs +887 -42
  48. package/engine/src/protocol.rs +153 -4
  49. package/engine/src/session.rs +1547 -137
  50. package/engine/src/x11.rs +210 -9
  51. package/engine/src/x11_mac.rs +12 -0
  52. package/engine/vendor/libva/COPYING +19 -0
  53. package/engine/vendor/libva/va/va.h +5450 -0
  54. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  55. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  56. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  57. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  58. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  59. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  60. package/engine/vendor/libva/va/va_drm.h +61 -0
  61. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  62. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  63. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  64. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  65. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  66. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  67. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  68. package/engine/vendor/libva/va/va_fei.h +170 -0
  69. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  70. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  71. package/engine/vendor/libva/va/va_prot.h +525 -0
  72. package/engine/vendor/libva/va/va_version.h +87 -0
  73. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  74. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  75. package/engine/vendor/nv-codec-headers/README +7 -0
  76. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  77. package/examples/reference-client.html +1 -1
  78. package/package.json +3 -2
@@ -44,7 +44,11 @@ pub struct StreamGeometry {
44
44
  pub buffer_type: String,
45
45
  }
46
46
 
47
- pub type FrameSink = Box<dyn Fn(I420, u64, Vec<u8>) + Send + 'static>;
47
+ /// Receives each converted frame, its sequence number and a way to produce the
48
+ /// frame's packed BGRX pixels, which only a local frame consumer needs; the
49
+ /// sink calls it at most once, and only then are the pixels copied.
50
+ pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
51
+ pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
48
52
 
49
53
  /// A running capture. Dropping it quits the PipeWire loop and joins its thread.
50
54
  pub struct Capture {
@@ -316,14 +320,26 @@ fn run_loop(
316
320
  let mut ready = Some(ready);
317
321
  let _listener = stream
318
322
  .add_local_listener_with_user_data(&mut state)
319
- .state_changed({ let main_loop = main_loop.clone(); move |_, state, old, new| {
320
- let reason = match new {
321
- pw::stream::StreamState::Error(message) => Some(format!("PipeWire stream stopped: {message}")),
322
- pw::stream::StreamState::Unconnected if old == pw::stream::StreamState::Streaming => Some(String::from("PipeWire stream disconnected")),
323
- _ => None,
324
- };
325
- if let Some(reason) = reason { (state.on_stop)(reason); main_loop.quit(); }
326
- } })
323
+ .state_changed({
324
+ let main_loop = main_loop.clone();
325
+ move |_, state, old, new| {
326
+ let reason = match new {
327
+ pw::stream::StreamState::Error(message) => {
328
+ Some(format!("PipeWire stream stopped: {message}"))
329
+ }
330
+ pw::stream::StreamState::Unconnected
331
+ if old == pw::stream::StreamState::Streaming =>
332
+ {
333
+ Some(String::from("PipeWire stream disconnected"))
334
+ }
335
+ _ => None,
336
+ };
337
+ if let Some(reason) = reason {
338
+ (state.on_stop)(reason);
339
+ main_loop.quit();
340
+ }
341
+ }
342
+ })
327
343
  .param_changed(|_, state, id, param| {
328
344
  if id != pw::spa::param::ParamType::Format.as_raw() {
329
345
  return;
@@ -383,54 +399,58 @@ fn run_loop(
383
399
  // second time and hand the encoder a frame it refuses whenever the
384
400
  // stream's size differs from the one the portal reported.
385
401
  let seq = state.frames.load(Ordering::Relaxed);
386
- let raw = crate::convert::to_bgrx(
387
- chunk_bytes,
388
- w,
389
- h,
390
- stride,
391
- format,
392
- state.box_w,
393
- state.box_h,
394
- );
395
- if let Some(mut raw) = raw {
396
- let frame = if let Some(mask) = state.mask.as_mut() {
397
- mask.apply(&mut raw, state.box_w, state.box_h, w, h);
398
- to_i420(
399
- &raw,
400
- state.box_w,
401
- state.box_h,
402
- state.box_w * 4,
403
- PixelFormat::Bgrx,
404
- state.box_w,
405
- state.box_h,
406
- )
407
- } else {
408
- to_i420(chunk_bytes, w, h, stride, format, state.box_w, state.box_h)
402
+ let (box_w, box_h) = (state.box_w, state.box_h);
403
+ // The capture indicator is masked out of BGRX pixels, so a masked
404
+ // frame is converted from that copy; otherwise straight from the
405
+ // stream, and BGRX is only made if the sink asks for it.
406
+ let mut masked = None;
407
+ let frame = if let Some(mask) = state.mask.as_mut() {
408
+ let Some(mut raw) =
409
+ crate::convert::to_bgrx(chunk_bytes, w, h, stride, format, box_w, box_h)
410
+ else {
411
+ state.dropped.fetch_add(1, Ordering::Relaxed);
412
+ return;
409
413
  };
410
- if let Some(i420) = frame {
411
- if state.geometry.lock().map(|g| g.is_none()).unwrap_or(false) {
412
- if let Ok(mut g) = state.geometry.lock() {
413
- *g = Some(StreamGeometry {
414
- source_width: w,
415
- source_height: h,
416
- origin_x: 0,
417
- origin_y: 0,
418
- format: format!("{format:?}").to_lowercase(),
419
- buffer_type: state.buffer_type.clone(),
420
- });
421
- }
422
- }
423
- state.frames.fetch_add(1, Ordering::Relaxed);
424
- (state.sink)(i420, seq, raw);
425
- // The first frame the loop actually delivers is the only
426
- // proof capture started; sending this when `run_loop`
427
- // returns would be after the main loop quits, too late for
428
- // `start` to wait on.
429
- if let Some(sender) = ready.take() {
430
- let _ = sender.send(Ok(()));
414
+ mask.apply(&mut raw, box_w, box_h, w, h);
415
+ let frame = to_i420(
416
+ &raw,
417
+ box_w,
418
+ box_h,
419
+ box_w * 4,
420
+ PixelFormat::Bgrx,
421
+ box_w,
422
+ box_h,
423
+ );
424
+ masked = Some(raw);
425
+ frame
426
+ } else {
427
+ to_i420(chunk_bytes, w, h, stride, format, box_w, box_h)
428
+ };
429
+ if let Some(i420) = frame {
430
+ if state.geometry.lock().map(|g| g.is_none()).unwrap_or(false) {
431
+ if let Ok(mut g) = state.geometry.lock() {
432
+ *g = Some(StreamGeometry {
433
+ source_width: w,
434
+ source_height: h,
435
+ origin_x: 0,
436
+ origin_y: 0,
437
+ format: format!("{format:?}").to_lowercase(),
438
+ buffer_type: state.buffer_type.clone(),
439
+ });
431
440
  }
432
- } else {
433
- state.dropped.fetch_add(1, Ordering::Relaxed);
441
+ }
442
+ state.frames.fetch_add(1, Ordering::Relaxed);
443
+ (state.sink)(i420, seq, &mut || {
444
+ masked.take().or_else(|| {
445
+ crate::convert::to_bgrx(chunk_bytes, w, h, stride, format, box_w, box_h)
446
+ })
447
+ });
448
+ // The first frame the loop actually delivers is the only
449
+ // proof capture started; sending this when `run_loop`
450
+ // returns would be after the main loop quits, too late for
451
+ // `start` to wait on.
452
+ if let Some(sender) = ready.take() {
453
+ let _ = sender.send(Ok(()));
434
454
  }
435
455
  } else {
436
456
  state.dropped.fetch_add(1, Ordering::Relaxed);
@@ -4,7 +4,10 @@ use anyhow::{bail, Result};
4
4
  use std::ffi::{c_char, c_void, CStr};
5
5
  use std::sync::Mutex;
6
6
 
7
- pub type FrameSink = Box<dyn Fn(I420, u64, Vec<u8>) + Send + 'static>;
7
+ /// See the Linux `capture::FrameSink`: the closure makes the BGRX copy, only
8
+ /// when the sink asks for it.
9
+ pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
10
+ pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
8
11
 
9
12
  struct State {
10
13
  sink: FrameSink,
@@ -41,8 +44,13 @@ unsafe extern "C" {
41
44
  extern "C" fn stream_status(context: *mut c_void, reason: *const c_char, running: bool) {
42
45
  let state = unsafe { &*(context as *const Mutex<State>) };
43
46
  if let Ok(state) = state.lock() {
44
- let reason = if reason.is_null() { String::from("stream stopped") }
45
- else { unsafe { CStr::from_ptr(reason) }.to_string_lossy().into_owned() };
47
+ let reason = if reason.is_null() {
48
+ String::from("stream stopped")
49
+ } else {
50
+ unsafe { CStr::from_ptr(reason) }
51
+ .to_string_lossy()
52
+ .into_owned()
53
+ };
46
54
  (state.status)(running, reason);
47
55
  }
48
56
  }
@@ -67,15 +75,6 @@ extern "C" fn receive_frame(
67
75
  let state = unsafe { &*(context as *const Mutex<State>) };
68
76
  let Ok(mut state) = state.lock() else { return };
69
77
  let source = unsafe { std::slice::from_raw_parts(pixels, length) };
70
- let raw = to_bgrx(
71
- source,
72
- source_width,
73
- source_height,
74
- stride,
75
- PixelFormat::Bgra,
76
- state.width,
77
- state.height,
78
- );
79
78
  let frame = to_i420(
80
79
  source,
81
80
  source_width,
@@ -85,8 +84,19 @@ extern "C" fn receive_frame(
85
84
  state.width,
86
85
  state.height,
87
86
  );
88
- if let (Some(frame), Some(raw)) = (frame, raw) {
89
- (state.sink)(frame, state.sequence, raw);
87
+ if let Some(frame) = frame {
88
+ let (width, height) = (state.width, state.height);
89
+ (state.sink)(frame, state.sequence, &mut || {
90
+ to_bgrx(
91
+ source,
92
+ source_width,
93
+ source_height,
94
+ stride,
95
+ PixelFormat::Bgra,
96
+ width,
97
+ height,
98
+ )
99
+ });
90
100
  state.sequence += 1;
91
101
  }
92
102
  }
@@ -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());