From c372a0f78d7cd71ed77af31e512bae5b0f8e40a5 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Thu, 16 Jul 2026 22:36:12 +0200 Subject: [PATCH 1/3] doc(kpm): correct stale backend docs + document KPM public API (#226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the #226 documentation pass, covering the consumer-facing KPM/NFT surface. Fix docs that actively misled: - kpm/mod.rs described `ffi-backend` as "(default)" and pure-Rust as "(future)" — backwards since M9-3 (#142). Pure Rust is the default and needs no C++ toolchain; ffi-backend is opt-in for cross-validation. Rewrote the quick start around RustFreakMatcher with a real load -> kpm_matching -> get_pose flow, and added the missing `freak` and `rust_backend` rows to the module layout table. - lib.rs recommended `--features ffi-backend` for NFT marker generation; since #179 nft_marker_gen produces .iset + .fset + .fset3 entirely in pure Rust, so no C++ toolchain is needed. Fill the 22 missing_docs sites across the kpm tree (Point3d, Point2f/2i, FeaturePoint, HoughMatch, HoughSimilarityVoting fields, and the pyramid error-variant fields), and fix two broken intra-doc links in lib.rs / kpm/mod.rs (KpmRefDataSet path; feature-gated cpp_backend items). Add `#![warn(missing_docs)]` to kpm/mod.rs: the KPM tree is now at 100% rustdoc coverage, and CI's `clippy -D warnings` escalates the lint, so a new undocumented `pub` item there fails the build. Remaining modules (types.rs, ar2/tracking.rs, icp.rs) are tracked for later slices in #226. Refs #226 Co-Authored-By: Claude Opus 4.8 --- crates/core/src/kpm/backend.rs | 3 ++ crates/core/src/kpm/freak/gaussian_pyramid.rs | 10 +++- crates/core/src/kpm/freak/hough.rs | 13 ++++- crates/core/src/kpm/freak/pyramid.rs | 5 +- crates/core/src/kpm/mod.rs | 53 ++++++++++++++----- crates/core/src/kpm/types.rs | 4 ++ crates/core/src/lib.rs | 10 +++- 7 files changed, 80 insertions(+), 18 deletions(-) diff --git a/crates/core/src/kpm/backend.rs b/crates/core/src/kpm/backend.rs index 3237550..ed88ef2 100644 --- a/crates/core/src/kpm/backend.rs +++ b/crates/core/src/kpm/backend.rs @@ -74,8 +74,11 @@ pub struct FeaturePoint { /// Z coordinate is always `0.0` for planar targets. #[derive(Debug, Default, Clone, PartialEq)] pub struct Point3d { + /// X coordinate in millimetres. pub x: f32, + /// Y coordinate in millimetres. pub y: f32, + /// Z coordinate in millimetres. Always `0.0` for planar targets. pub z: f32, } diff --git a/crates/core/src/kpm/freak/gaussian_pyramid.rs b/crates/core/src/kpm/freak/gaussian_pyramid.rs index cb2d1e9..000a620 100644 --- a/crates/core/src/kpm/freak/gaussian_pyramid.rs +++ b/crates/core/src/kpm/freak/gaussian_pyramid.rs @@ -58,13 +58,21 @@ pub enum GaussianPyramidError { EmptyImage, /// Input image is smaller than 5x5; the binomial filter needs 2 border /// pixels on each side. - ImageTooSmall { rows: usize, cols: usize }, + ImageTooSmall { + /// Row count of the offending input image. + rows: usize, + /// Column count of the offending input image. + cols: usize, + }, /// `num_octaves` was 0 — a pyramid must have at least one octave. ZeroOctaves, /// Halving for `octave` would produce a level smaller than 5x5. OctaveTooSmall { + /// Index of the octave that could not be built. octave: usize, + /// Row count the octave would have had. rows: usize, + /// Column count the octave would have had. cols: usize, }, } diff --git a/crates/core/src/kpm/freak/hough.rs b/crates/core/src/kpm/freak/hough.rs index 0ee4b32..1a3d234 100644 --- a/crates/core/src/kpm/freak/hough.rs +++ b/crates/core/src/kpm/freak/hough.rs @@ -306,11 +306,13 @@ impl BinParams { /// Accumulates votes for similarity transformations in discretized 4D space. pub struct HoughSimilarityVoting { params: BinParams, - /// Object center in reference image. + /// Object center in the reference image (x, in pixels). pub center_x: f32, + /// Object center in the reference image (y, in pixels). pub center_y: f32, - /// Reference image dimensions. + /// Reference image width in pixels. pub ref_image_width: i32, + /// Reference image height in pixels. pub ref_image_height: i32, /// Vote map: bin_index → vote count. /// @@ -537,9 +539,13 @@ impl HoughSimilarityVoting { /// C equivalent: vision::FeaturePoint #[derive(Clone, Debug, Copy)] pub struct FeaturePoint { + /// Horizontal position in pixels. pub x: f32, + /// Vertical position in pixels. pub y: f32, + /// Dominant orientation in radians. pub angle: f32, + /// Scale (pyramid octave level) at which the keypoint was detected. pub scale: f32, /// True if this is a maxima, false if a minima (used to filter matches). pub maxima: bool, @@ -570,8 +576,11 @@ pub struct Match { /// A scored match used internally by Hough voting (carries distance for ranking). #[derive(Clone, Copy, Debug)] pub struct HoughMatch { + /// Index into the query image's feature-point list. pub query_idx: u32, + /// Index into the reference database's feature-point list. pub ref_idx: u32, + /// Descriptor distance for this match; lower is a closer match. pub distance: f32, } diff --git a/crates/core/src/kpm/freak/pyramid.rs b/crates/core/src/kpm/freak/pyramid.rs index 5b03df0..d800eea 100644 --- a/crates/core/src/kpm/freak/pyramid.rs +++ b/crates/core/src/kpm/freak/pyramid.rs @@ -67,7 +67,10 @@ pub enum PyramidError { /// Downsampling at this level would produce a 0-sized image. /// Returned when the input image is too small for the requested /// number of levels. - LevelTooSmall { level: usize }, + LevelTooSmall { + /// Index of the level that could not be built. + level: usize, + }, } impl std::fmt::Display for PyramidError { diff --git a/crates/core/src/kpm/mod.rs b/crates/core/src/kpm/mod.rs index 96a0003..adcfa65 100644 --- a/crates/core/src/kpm/mod.rs +++ b/crates/core/src/kpm/mod.rs @@ -38,38 +38,67 @@ //! ported from the C++ FreakMatcher library. //! //! This module provides a Rust interface to the FREAK-descriptor-based -//! feature matching pipeline used for planar image tracking. It supports -//! two operation modes: +//! feature matching pipeline used for planar image tracking. The matcher is +//! pluggable via the [`FreakMatcherBackend`] trait, with two implementations: //! -//! - **`ffi-backend`** (default) — delegates to the compiled C++ FreakMatcher -//! static library through a thin `extern "C"` wrapper (`kpm_c_api.h`). -//! - *(future)* **pure-Rust** — a native Rust re-implementation of the same -//! pipeline, swappable via the [`FreakMatcherBackend`] trait. +//! - **pure Rust** ([`RustFreakMatcher`]) — the **default** since M9-3 +//! (#142). `cargo build` needs no C++ toolchain (no clang / libclang / cc). +//! - **`ffi-backend`** (`CppFreakMatcher`) — **opt-in**, delegates to the +//! compiled C++ FreakMatcher static library through a thin `extern "C"` +//! wrapper (`kpm_c_api.h`). Enable with `--features ffi-backend`. Used for +//! cross-validation (`dual-mode`, `cross_stack_parity`), not required at +//! runtime. //! //! ## Quick start //! +//! Detect a marker in a grayscale frame with the pure-Rust backend: +//! //! ```rust,ignore -//! use webarkitlib_rs::kpm::{CppFreakMatcher, KpmHandle}; +//! use webarkitlib_rs::kpm::{KpmHandle, RustFreakMatcher}; +//! use webarkitlib_rs::kpm::types::KpmRefDataSet; //! -//! // Create a C++ backend for a 640x480 camera frame. -//! let backend = CppFreakMatcher::new(640, 480).unwrap(); +//! // Pure-Rust backend for a 640x480 camera frame. +//! let backend = RustFreakMatcher::new(640, 480)?; //! //! // Wrap it in a KpmHandle (the central coordinator). //! let mut handle = KpmHandle::new(640, 480, None, Box::new(backend)); +//! +//! // Load reference data generated by `nft_marker_gen` (.fset3). +//! let ref_data = KpmRefDataSet::load("marker.fset3")?; +//! handle.set_ref_data_set(ref_data)?; +//! +//! // Run detection on one frame; `luma` is width * height grayscale bytes. +//! handle.kpm_matching(&luma)?; +//! if let Some((pose, page_no, error)) = handle.get_pose() { +//! // `pose` is a 3x4 camera pose matrix for `page_no`. +//! } //! ``` //! +//! For a runnable end-to-end version (including AR2 tracking), see the +//! `simple_nft` example; for the browser, see `crates/wasm`'s +//! `WasmKpmHandle`. +//! //! ## Module layout //! //! | Module | Purpose | //! |------------------|---------| -//! | [`backend`] | `FreakMatcherBackend` trait and associated error / data types | +//! | [`backend`] | [`FreakMatcherBackend`] trait and associated error / data types | //! | [`handle`] | [`KpmHandle`] struct — central coordinator for KPM operations | -//! | [`types`] | Ported C structs (`KpmRefDataSet`, `KpmResult`, etc.) | -//! | [`cpp_backend`] | [`CppFreakMatcher`] — FFI backend (feature-gated) | +//! | [`types`] | Ported C structs ([`KpmRefDataSet`](types::KpmRefDataSet), etc.) | +//! | [`freak`] | The pure-Rust FREAK pipeline — detector, descriptor, pyramids, Hough voting, homography, `VisualDatabase` | +//! | [`rust_backend`] | [`RustFreakMatcher`] (default) and `DualFreakMatcher` (parity-asserting) | +//! | `cpp_backend` | `CppFreakMatcher` — FFI backend (only compiled with `--features ffi-backend`) | //! | [`kpm_ffi`] | Raw bindgen bindings (feature-gated) | //! | [`matching`] | [`MatchResult`](matching::MatchResult) wrapper | //! | [`ref_data_set`] | [`RefDataSet`](ref_data_set::RefDataSet) collection | +// The KPM/NFT surface is the primary public API for library consumers, so it +// is held to full rustdoc coverage. This module tree is documented to 100% +// (#226); the warning is escalated to an error by CI's `clippy -D warnings`, +// so a new undocumented `pub` item here fails the build. The rest of the +// crate is still being back-filled — see #226 for the remaining modules. +#![warn(missing_docs)] + pub mod backend; pub mod freak; pub mod handle; diff --git a/crates/core/src/kpm/types.rs b/crates/core/src/kpm/types.rs index b5ff0d4..c54640c 100644 --- a/crates/core/src/kpm/types.rs +++ b/crates/core/src/kpm/types.rs @@ -209,14 +209,18 @@ pub struct KpmResult { /// 2D point with `f32` coordinates. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Point2f { + /// Horizontal position in pixels. pub x: f32, + /// Vertical position in pixels. pub y: f32, } /// 2D point with integer coordinates, used for corner detection. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Point2i { + /// Horizontal position in pixels. pub x: i32, + /// Vertical position in pixels. pub y: i32, } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b415559..868083f 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -53,7 +53,7 @@ //! - [`ar2::ar2_gen_feature_map`] — extracts gradient-based features at each pyramid level (`.fset`) //! - [`ar2::AR2ImageSetT::save`] — writes the image pyramid as a JPEG-compressed `.iset` file //! matching `ar2WriteImageSet()` format (~300 KB vs ~10 MB raw) -//! - [`kpm::KpmRefDataSet::generate`] — extracts FREAK descriptors for KPM recognition (`.fset3`) +//! - [`kpm::types::KpmRefDataSet::generate`] — extracts FREAK descriptors for KPM recognition (`.fset3`) //! //! See the `nft_marker_gen` example for a complete end-to-end usage. //! @@ -84,12 +84,18 @@ //! ### Recommended build for NFT marker generation (x86_64) //! //! ```bash -//! cargo run --release --features "ffi-backend,simd-x86-sse41,simd-x86-avx2" \ +//! cargo run --release --features "log-helpers,simd-x86-sse41,simd-x86-avx2" \ //! --example nft_marker_gen -- --input marker.jpg --output marker --dpi 220 //! ``` //! //! > **Always use `--release`** — debug builds are 5–10× slower. //! +//! No C++ toolchain is required: since #179 `nft_marker_gen` produces the +//! complete marker set (`.iset` + `.fset` + `.fset3`) through the pure-Rust +//! [`RustFreakMatcher`](kpm::RustFreakMatcher). The `ffi-backend` feature is +//! **not** needed for marker generation — it exists only for development +//! cross-validation against the C++ backend. +//! //! Detailed benchmark results can be found in `crates/core/benches/BENCHMARKS.md`. //! //! From 29117ab8f1281c147fe2812ee5a6af7a7d21925c Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Fri, 17 Jul 2026 00:13:28 +0200 Subject: [PATCH 2/3] fix(kpm): exempt bindgen-generated bindings from the missing_docs gate (#226) The `#![warn(missing_docs)]` gate added to kpm/mod.rs fires on the bindgen output included into kpm_ffi's `bindings` module (34 errors under `--features ffi-backend`, escalated by CI's `clippy -D warnings`). Those items are generated verbatim from kpm_c_api.h by build.rs, so there is no source to attach rustdoc to. Add `#![allow(missing_docs)]` alongside the existing bindgen naming-lint allows in the same module, with a rationale. The module-level docs already describe the surface, and consumers are pointed at the safe CppFreakMatcher wrapper. Only surfaced in CI: this path needs --features ffi-backend, which cannot be built locally here (bindgen 0.72.1 panics on a 32-bit libclang vs a 64-bit target). Refs #226 Co-Authored-By: Claude Opus 4.8 --- crates/core/src/kpm/kpm_ffi.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/core/src/kpm/kpm_ffi.rs b/crates/core/src/kpm/kpm_ffi.rs index 4868291..e5b8ec4 100644 --- a/crates/core/src/kpm/kpm_ffi.rs +++ b/crates/core/src/kpm/kpm_ffi.rs @@ -49,6 +49,12 @@ #[cfg(feature = "ffi-backend")] mod bindings { #![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)] + // rationale: the contents are generated verbatim by bindgen in build.rs + // from kpm_c_api.h, so there is no source to attach rustdoc to. The + // module-level docs above describe the surface instead; the safe wrapper + // (CppFreakMatcher) is what consumers should use. Needed because the kpm + // tree opts into `#![warn(missing_docs)]` (#226). + #![allow(missing_docs)] include!(concat!(env!("OUT_DIR"), "/kpm_bindings.rs")); } From bc9ff1b3e8678e19745d845b5018df1fe7b99976 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Fri, 17 Jul 2026 13:42:27 +0200 Subject: [PATCH 3/3] doc(contributing): document the libclang/bindgen ffi-backend build failure (#226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opting into `--features ffi-backend` (or `--all-features` / `dual-mode`) fails in build.rs with a bindgen panic — `assertion left == right failed`, `left: 4, right: 8` — when LIBCLANG_PATH points at a libclang built for a 32-bit target. The assertion prints the same triple on both sides, so the cause is not obvious from the message. A common trigger on Windows is a cross-compilation toolchain's libclang on LIBCLANG_PATH, e.g. the Xtensa clang from the esp-rs toolchain (32-bit ESP32). Add a Troubleshooting section covering the symptom, the cause, and the per-session fix (point LIBCLANG_PATH at an x86_64 libclang), plus why we don't ship a .cargo/config.toml [env] override (it would need force = true and has no per-OS conditionals, so it would break Linux CI). Also notes that the default build is pure Rust and needs no C++ toolchain at all — this only affects contributors who opt into the FFI backend. Refs #226 Co-Authored-By: Claude Opus 4.8 --- CONTRIBUTING.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6140662..84fe922 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,58 @@ Run the following commands in the project root: If you add a new feature, make sure to include appropriate tests in `src/**/tests.rs` or as integration tests. +## Troubleshooting the build + +### `ffi-backend` / `--all-features` fails in `build.rs` with a bindgen panic + +The default build is **pure Rust and needs no C++ toolchain** — you only hit +this when you opt into the C++ FreakMatcher via `--features ffi-backend` +(or `--all-features`, or `dual-mode`, which implies it). + +**Symptom** + +```text +error: failed to run custom build command for `webarkitlib-rs` + thread 'main' panicked at .../bindgen-0.72.1/lib.rs:917:13: + assertion `left == right` failed: "x86_64-pc-windows-msvc" "x86_64-pc-windows-msvc" + left: 4 + right: 8 +``` + +**Cause** — `LIBCLANG_PATH` points at a `libclang` built for a **32-bit** +target, so bindgen sees a pointer width of `4` while the host target +(`x86_64`) expects `8`. The confusing part is that both sides of the +assertion print the same triple; only the widths differ. + +A common way to end up here on Windows is having a cross-compilation +toolchain's `libclang` on `LIBCLANG_PATH` — for example the Xtensa clang +that ships with the [esp-rs](https://github.com/esp-rs) toolchain +(`~/.rustup/toolchains/esp/xtensa-esp32-elf-clang/...`), which targets the +32-bit ESP32. + +**Fix** — point `LIBCLANG_PATH` at an `x86_64` `libclang` for the shell you +build this repo in: + +```powershell +$env:LIBCLANG_PATH = "C:\Program Files\LLVM\bin" +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +The `x64` libclang bundled with Visual Studio works too: +`C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin`. + +**Set it per-session, not globally**, if you also do embedded work — the +cross-toolchain needs its own `LIBCLANG_PATH`. For the same reason we do +**not** ship a `.cargo/config.toml` `[env]` override: it would need +`force = true` to beat an already-set variable, and cargo's `[env]` has no +per-OS conditionals, so it would push a Windows path onto Linux CI and +override its `libclang-dev`. + +> Worth fixing rather than ignoring: without it you cannot run the +> `--all-features` gate locally, so breakage that only exists behind +> `ffi-backend` (e.g. lints firing on bindgen-generated code) reaches CI +> instead of your machine. + ## Commit Message Conventions To maintain a clean and automated release history, this project strictly adheres to the [Conventional Commits](https://www.conventionalcommits.org/) specification.