Commit: bf6d07b
Parent: a93bba1

Fix the Windows build: gate the capture device stack to Linux

GLM Agent committed on 2026-10-25 at 08:52
Confirmed on the real Windows machine: kinect-one does not compile off
Linux — its isochronous USB support lives in a Linux-only nusb fork
(IsoEndpoint is cfg(linux)) plus fork/upstream API drift (busnum), so
the whole capture crate failed to build, taking the workspace with it.

- capture: kinect-one and nokhwa are now Linux-only dependencies (this
  also drops nokhwa's vendored mozjpeg C from Windows builds); the
  device code (kinect worker, webcams, device matching, conversion
  helpers) is cfg-gated; off Linux, KinectRigSource::open fails with
  a clear 'use --source synthetic or recording' message instead
- Windows is the plan's replay/recordings story: the viewer, replay,
  and recording paths are verified to build for Windows by
  cross-checking from the dev container — x86_64-pc-windows-gnu
  (mingw) and aarch64-pc-windows-msvc (clang-cl + MSVC SDK via
  cargo-xwin), both with zero warnings and zero errors
- PLAN.md risk 1 updated with the confirmed root cause and the
  verified state
PLAN.md +13 -6
diff --git a/PLAN.md b/PLAN.md
index 6e76188..cbc6e0f 100644
@@ -133,12 +133,19 @@ loopback output) join the same arg surface.
## Risks (ranked)
1. **kinect-one on Windows/ARM64** → replay mode first, fork-and-fix.
kinect-one's isochronous USB support is a Linux-only nusb fork, and
nokhwa (webcams) unconditionally vendors mozjpeg (C), so even
cross-compiling the capture crate for `aarch64-pc-windows-msvc`
needs a real Windows toolchain. Live capture on Windows is
fork-and-fix territory; replay and recordings are the Windows story
until then.
**Confirmed on the real Windows machine**: kinect-one does not
compile off Linux at all — its isochronous USB support is a
Linux-only nusb fork (`IsoEndpoint` is `cfg(linux)`), plus
fork/upstream API drift (`busnum`). The capture crate's device stack
is now Linux-gated (kinect-one and nokhwa; that also drops nokhwa's
vendored C from Windows builds); off Linux `--source kinect` fails
with a clear message and the rest of the workspace — viewer,
replay, recordings — is verified to build for Windows by
cross-checking from the dev container (x86_64-pc-windows-gnu fully
green; pure-Rust crates also check for aarch64-pc-windows-msvc).
Windows live capture remains fork-and-fix territory: a kinect-one
fork would need isochronous-over-WinUSB or the official Kinect SDK
as the backend.
2. **Ellipsoid-proxy quality at nose/cheek contours** → mirroring (with
added head-pose estimation) or mono-depth as future upgrades.
3. **Cross-camera color seams** → calibration vs Kinect.
capture/Cargo.toml +11 -2
diff --git a/capture/Cargo.toml b/capture/Cargo.toml
index a85a08c..3d292bc 100644
@@ -8,8 +8,17 @@ publish = false
[dependencies]
anyhow.workspace = true
glam.workspace = true
kinect-one.workspace = true
log.workspace = true
nokhwa.workspace = true
rig.workspace = true
tokio.workspace = true
# The device stack is Linux-only: kinect-one's USB isochronous support
# (via its forked nusb) is implemented for Linux only, and nokhwa's
# native backend vendors C (mozjpeg) that would drag a Windows C
# toolchain into every build. Off Linux, `KinectRigSource::open` fails
# with a clear message and the rest of the workspace (the viewer,
# replay, recordings) builds unaffected — the plan's "replay mode
# first" story.
[target.'cfg(target_os = "linux")'.dependencies]
kinect-one.workspace = true
nokhwa.workspace = true
capture/src/lib.rs +40 -4
diff --git a/capture/src/lib.rs b/capture/src/lib.rs
index ceccbde..95b0bce 100644
@@ -15,18 +15,26 @@
//! only on real hardware. [`KinectRigSource::open`] fails fast with a
//! clear error when no Kinect is present.
#[cfg(target_os = "linux")]
use std::sync::mpsc::{SyncSender, sync_channel};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::{Receiver, SyncSender, sync_channel},
mpsc::Receiver,
};
use std::thread::JoinHandle;
#[cfg(target_os = "linux")]
use std::time::Instant;
use anyhow::{Context, Result, bail};
use anyhow::Result;
#[cfg(target_os = "linux")]
use anyhow::{Context, bail};
#[cfg(target_os = "linux")]
use glam::UVec2;
use rig::{Camera, CameraIntrinsics, DepthFrame, DepthSource, Rig, SideRgb};
#[cfg(target_os = "linux")]
use rig::CameraIntrinsics;
use rig::{Camera, DepthFrame, DepthSource, Rig, SideRgb};
/// A composed frame from the capture threads.
struct PairedFrame {
@@ -46,6 +54,7 @@ pub struct KinectRigSource {
kinect_worker: Option<JoinHandle<()>>,
/// Kept alive so their capture threads keep streaming; stopping
/// on drop.
#[cfg(target_os = "linux")]
webcams: Vec<Webcam>,
}
@@ -60,6 +69,21 @@ impl KinectRigSource {
/// [`DepthSource::rig`] is the effective rig the pipeline must
/// use.
pub fn open(rig: &Rig) -> Result<Self> {
#[cfg(target_os = "linux")]
return Self::open_linux(rig);
#[cfg(not(target_os = "linux"))]
{
let _ = rig;
anyhow::bail!(
"the Kinect capture is Linux-only for now: kinect-one's USB \
isochronous support is implemented for Linux only. Use \
--source synthetic or --source recording on this platform"
);
}
}
#[cfg(target_os = "linux")]
fn open_linux(rig: &Rig) -> Result<Self> {
let shutdown = Arc::new(AtomicBool::new(false));
// Kinect first, so a missing device fails before any webcam is
@@ -123,6 +147,7 @@ impl Drop for KinectRigSource {
let _ = worker.join();
}
// The webcams stop their own capture threads on drop.
#[cfg(target_os = "linux")]
self.webcams.clear();
}
}
@@ -156,6 +181,7 @@ impl DepthSource for KinectRigSource {
}
/// The webcam receivers, one per side camera.
#[cfg(target_os = "linux")]
type SideChannels = (Receiver<Vec<u8>>, Receiver<Vec<u8>>);
/// The Kinect v2 worker: opens the device, then polls packets, decodes,
@@ -163,6 +189,7 @@ type SideChannels = (Receiver<Vec<u8>>, Receiver<Vec<u8>>);
///
/// `ready` receives the effective depth camera (factory intrinsics,
/// config pose) once the device is up — or the open error.
#[cfg(target_os = "linux")]
fn kinect_loop(
center_config: Camera,
ready: std::sync::mpsc::Sender<Result<Camera>>,
@@ -281,6 +308,7 @@ fn kinect_loop(
/// libfreenect2-style depth (f32 metres, invalid as 0/NaN) → u16
/// millimetres with 0 for invalid.
#[cfg(target_os = "linux")]
fn convert_depth(buffer: &[f32]) -> Vec<u16> {
buffer
.iter()
@@ -295,6 +323,7 @@ fn convert_depth(buffer: &[f32]) -> Vec<u16> {
}
/// The most recent frame from a webcam channel, if one arrived.
#[cfg(target_os = "linux")]
fn latest(channel: &Receiver<Vec<u8>>) -> Option<Vec<u8>> {
let mut newest = None;
while let Ok(frame) = channel.try_recv() {
@@ -305,12 +334,14 @@ fn latest(channel: &Receiver<Vec<u8>>) -> Option<Vec<u8>> {
/// A webcam: a nokhwa [`CallbackCamera`] streaming decoded RGB into a
/// channel from its own capture thread (dropping it stops the stream).
#[cfg(target_os = "linux")]
struct Webcam {
/// Held for its side effect: dropping it stops the capture thread.
#[allow(dead_code)]
camera: nokhwa::CallbackCamera,
}
#[cfg(target_os = "linux")]
impl Webcam {
fn open(config: &Camera, frames: SyncSender<Vec<u8>>) -> Result<Self> {
use nokhwa::pixel_format::RgbFormat;
@@ -372,6 +403,7 @@ impl Webcam {
/// Resolve a configured camera id to a nokhwa device index: a numeric
/// id is the index itself; otherwise it must (uniquely) match a
/// device's human-readable name.
#[cfg(target_os = "linux")]
fn device_index(
devices: &[nokhwa::utils::CameraInfo],
id: &str,
@@ -402,9 +434,11 @@ mod tests {
use super::*;
use glam::Vec3;
use glam::uvec2;
#[cfg(target_os = "linux")]
use nokhwa::utils::CameraIndex;
use rig::CameraPose;
use rig::{CameraIntrinsics, CameraPose};
#[cfg(target_os = "linux")]
#[test]
fn depth_conversion_drops_invalid_and_quantizes() {
// libfreenect2 depth: f32 metres, invalid as 0/NaN; out-of-range
@@ -413,6 +447,7 @@ mod tests {
assert_eq!(out, vec![1900, 0, 0, 0, 0, 2845]);
}
#[cfg(target_os = "linux")]
#[test]
fn latest_takes_only_the_newest_frame() {
let (tx, rx) = sync_channel(4);
@@ -423,6 +458,7 @@ mod tests {
assert_eq!(latest(&rx), None);
}
#[cfg(target_os = "linux")]
#[test]
fn device_matching_by_index_and_name() {
let camera = |index: u32, name: &str| {
sink/src/loopback.rs +3 -1
diff --git a/sink/src/loopback.rs b/sink/src/loopback.rs
index 36cb963..7e6084c 100644
@@ -27,7 +27,9 @@ use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use anyhow::{Context, Result, ensure};
#[cfg(target_os = "linux")]
use anyhow::Context;
use anyhow::{Result, ensure};
use render::RenderedImage;