Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
497 changes: 497 additions & 0 deletions .agents/MIRI_PLAN.md

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions .github/workflows/miri.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Miri

# Undefined-behaviour verification for the `unsafe` slice reinterpretations
# backing the SIMD fast paths. Full rationale, inclusion/exclusion table and
# known coverage gaps: .agents/MIRI_PLAN.md

on:
push:
branches: [ "main", "dev" ]
# Tags matter here: release.yml waits on both Miri legs before publishing to
# crates.io, and that wait would hang forever if this workflow never ran on
# the tag. Keep in sync with ci.yml's tag trigger.
tags: [ "v*" ]
pull_request:
workflow_dispatch:

env:
CARGO_TERM_COLOR: always
# By default Miri injects a small random error into transcendental float ops
# (ln/sin/cos/exp) to catch code that relies on exact results. The Box-Muller
# transform in src/core/rng.rs uses all three, which makes
# test_randn_determinism fail on a last-ULP difference — a false positive, not
# UB. This flag restores deterministic float semantics.
# NOT the issue's `-Zmiri-strict-provenance`: that is now Miri's default and
# the flag is deprecated. See .agents/MIRI_PLAN.md §3.
MIRIFLAGS: -Zmiri-deterministic-floats

jobs:
miri:
name: Miri UB Check (${{ matrix.name }})
runs-on: ubuntu-latest
# Measured ~4 min of test execution locally; this is headroom for a cold
# cache and a slower runner. See .agents/MIRI_PLAN.md §9.
timeout-minutes: 30
continue-on-error: ${{ matrix.experimental }}

strategy:
fail-fast: false
matrix:
include:
# Safe code paths plus the data_ptr/data_ptr_mut tests in
# src/core/tests.rs — the only `unsafe` reachable without `simd`.
# Required gate.
- { name: baseline, features: "std", experimental: false }
# Reaches the real production `unsafe`: src/core/arithm.rs (119, 267)
# and src/imgproc/derivatives.rs (346, 349). Required gate.
# pulp is confirmed Miri-compatible: this leg passes clean on both
# ubuntu-latest and x86_64-pc-windows-msvc (355 passed / 0 failed,
# identical counts). See .agents/MIRI_PLAN.md §3.
- { name: simd, features: "std,simd", experimental: false }

steps:
- uses: actions/checkout@v6

- name: Install Rust nightly + Miri
uses: dtolnay/rust-toolchain@nightly
with:
components: miri

- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
key: miri-${{ matrix.name }}

# Kept as its own step: when a nightly ships without a usable miri
# component, the failure points at the toolchain rather than at our code.
# Fix is to temporarily pin `dtolnay/rust-toolchain@nightly-YYYY-MM-DD`.
- name: Build Miri sysroot
run: cargo miri setup

# `-p purecv` excludes crates/wasm — Miri has no wasm32 support.
# `--no-default-features` suppresses `parallel`, which default =
# ["std", "parallel"] would otherwise enable silently. Rayon under Miri is
# slow and its thread support is limited; the resulting coverage gap on
# src/core/arithm.rs (99, 248) is documented in .agents/MIRI_PLAN.md §5.
# `--lib` skips doc-tests: two ORB examples in src/features2d/mod.rs alone
# cost 767s under interpretation while duplicating unit-test coverage. There
# is no tests/ directory, so --lib is the whole suite. Doc-tests remain fully
# exercised by `cargo test` in ci.yml.
- name: Run Miri
run: cargo miri test -p purecv --lib --no-default-features --features ${{ matrix.features }}
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30

# Both Miri legs are required checks, so a UB regression must not be
# publishable to crates.io. These names must match the job names produced
# by miri.yml's matrix, and miri.yml must keep its `tags: [ "v*" ]`
# trigger — without it these steps would wait forever.
- name: Wait for CI (Miri UB Check - baseline)
uses: lewagon/wait-on-check-action@v1.5.0
with:
ref: ${{ github.ref }}
check-name: "Miri UB Check (baseline)"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30

- name: Wait for CI (Miri UB Check - simd)
uses: lewagon/wait-on-check-action@v1.5.0
with:
ref: ${{ github.ref }}
check-name: "Miri UB Check (simd)"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30

- name: Generate Release Notes
id: git-cliff
uses: orhun/git-cliff-action@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ cpp_ref/opencv
!.agents/instructions.md
!.agents/workflows/
!.agents/workflows/add-license-headers.md
!.agents/MIRI_PLAN.md

# Examples
examples/data/out/
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

All notable changes to this project will be documented in this file.

## [0.7.1] - 2026-08-10

### ⚙️ Miscellaneous Tasks

- *(ci)* Add Miri UB check workflow (#81)
- *(ci)* Promote the Miri simd leg to a required check (#81)
- *(ci)* Gate releases on the Miri UB checks (#81)

### 📚 Documentation

- *(ci)* Add Miri UB verification plan (#81)
- *(readme)* Add Miri badge, correct SIMD unsafe claim (#81)
- *(ci)* Fix the local reproduction commands in the Miri plan (#81)

### 🧪 Testing

- *(core)* Annotate Miri-slow tests with cfg_attr(miri, ignore) (#81)

## [0.7.0] - 2026-08-07

### ⚙️ Miscellaneous Tasks
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "purecv"
version = "0.7.0"
version = "0.7.1"
authors = ["Walter Perdan <https://github.com/kalwalt>"]
edition = "2021"
rust-version = "1.88"
Expand Down Expand Up @@ -86,7 +86,7 @@ members = ["crates/wasm"]
exclude = ["crates/no-std-smoke"]

[workspace.package]
version = "0.7.0"
version = "0.7.1"
authors = ["Walter Perdan <https://github.com/kalwalt>"]
edition = "2021"
description = "A pure Rust, high-performance computer vision library focused on safety and portability."
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
![PureCv Banner](./assets/purecv_banner.png)

[![Rust CI](https://github.com/webarkit/purecv/actions/workflows/ci.yml/badge.svg)](https://github.com/webarkit/purecv/actions/workflows/ci.yml)
[![Miri](https://github.com/webarkit/purecv/actions/workflows/miri.yml/badge.svg)](https://github.com/webarkit/purecv/actions/workflows/miri.yml)
[![Crates.io](https://img.shields.io/crates/v/purecv.svg)](https://crates.io/crates/purecv)
[![Crates.io Downloads](https://img.shields.io/crates/d/purecv.svg)](https://crates.io/crates/purecv)
[![NPM version](https://img.shields.io/npm/v/@webarkit/purecv-wasm.svg)](https://www.npmjs.com/package/@webarkit/purecv-wasm)
Expand All @@ -20,7 +21,7 @@ Unlike existing wrappers, **PureCV** is a native rewrite. It aims to provide:
* **Zero-FFI:** No complex linking or C++ toolchain requirements.
* **Memory Safety:** Elimination of segmentation faults and buffer overflows via Rust's ownership model.
* **Modern Parallelism:** Native integration with **Rayon** for effortless multi-core processing.
* **Portable SIMD:** Optional SIMD acceleration via [`pulp`](https://crates.io/crates/pulp) — auto-detects x86 SSE/AVX, ARM NEON, and WASM `simd128` at runtime. Zero `unsafe`, zero `#[cfg(target_arch)]`.
* **Portable SIMD:** Optional SIMD acceleration via [`pulp`](https://crates.io/crates/pulp) — auto-detects x86 SSE/AVX, ARM NEON, and WASM `simd128` at runtime. Zero `#[cfg(target_arch)]`, and the few `unsafe` slice reinterpretations that feed the SIMD kernels are checked for undefined behaviour by [Miri](https://github.com/rust-lang/miri) in CI.
* **Embedded-ready:** Builds under `no_std` + `alloc` for bare-metal targets such as the ESP32 — the `core`, `imgproc`, `calib3d`, and `video` modules run without the standard library ([see below](#no_std--embedded-support)).

## ✨ Features
Expand Down
2 changes: 1 addition & 1 deletion crates/wasm/pkg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"Walter Perdan \u003chttps://github.com/kalwalt\u003e"
],
"description": "A pure Rust, high-performance computer vision library focused on safety and portability.",
"version": "0.6.1",
"version": "0.7.1",
"license": "LGPL-2.1-or-later",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purecv",
"version": "0.7.0",
"version": "0.7.1",
"description": "A pure Rust, high-performance computer vision library focused on safety and portability.",
"private": true,
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/calib3d/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,9 @@ mod calib3d_tests {
}
}

// miri: RANSAC iteration loop takes ~928s under interpretation — by far the
// slowest test in the suite. No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_find_fundamental_mat_ransac() {
use crate::calib3d::{find_fundamental_mat, FundamentalMatMethod};
Expand Down
3 changes: 3 additions & 0 deletions src/core/rng.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ mod tests {
assert!(max > min, "randu produced no variation");
}

// miri: draws a large sample to check distribution moments — ~88s under
// interpretation. No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_randn_statistics() {
set_rng_seed(7);
Expand Down
6 changes: 6 additions & 0 deletions src/features2d/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ fn test_orb_pyramid_grayscale_validation() {
}
}

// miri: ORB scale-pyramid construction takes ~30s under interpretation.
// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_orb_pyramid_dimensions() {
use crate::core::Matrix;
Expand Down Expand Up @@ -415,6 +418,9 @@ fn test_orb_descriptors() {
assert_ne!(desc0, desc90);
}

// miri: full ORB detect+describe pipeline takes ~806s under interpretation.
// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_orb_full_pipeline() {
use crate::core::Matrix;
Expand Down
16 changes: 16 additions & 0 deletions src/video/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ mod video_tests {
assert_eq!(pyr.levels[3].rows, 8);
}

// miri: ~45s under interpretation. The Sobel `unsafe` fast path it exercises
// is still covered by imgproc::tests::test_sobel (f32/ksize 3, ~0.8s under
// Miri), so no UB coverage is lost here. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_build_pyramid_with_derivatives() {
let img = Matrix::<u8>::new(64, 64, 1);
Expand Down Expand Up @@ -212,6 +216,9 @@ mod video_tests {

/// Tracking a stationary point in two identical frames should return a
/// flow vector close to zero and status = 1.
// miri: Lucas-Kanade pyramidal iteration — ~62s under interpretation.
// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_lk_stationary_point_identical_frames() {
// Create a 64×64 frame with a small bright blob so there are gradients.
Expand Down Expand Up @@ -251,6 +258,9 @@ mod video_tests {
}

/// Simulate a pure translation of +3 pixels in x by shifting the image.
// miri: Lucas-Kanade pyramidal iteration — ~105s under interpretation.
// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_lk_pure_translation_x() {
let rows = 64usize;
Expand Down Expand Up @@ -311,6 +321,9 @@ mod video_tests {
}

/// Test using the `OPTFLOW_LK_GET_MIN_EIGENVALS` flag.
// miri: Lucas-Kanade pyramidal iteration — ~61s under interpretation.
// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_lk_min_eigenvals_flag() {
let mut data = vec![0u8; 64 * 64];
Expand Down Expand Up @@ -343,6 +356,9 @@ mod video_tests {
}

/// Test the `OPTFLOW_USE_INITIAL_FLOW` flag with a good initial guess.
// miri: Lucas-Kanade pyramidal iteration — ~62s under interpretation.
// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4.
#[cfg_attr(miri, ignore)]
#[test]
fn test_lk_use_initial_flow() {
let mut data = vec![0u8; 64 * 64];
Expand Down