Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ tests/fixtures/images/tiger_huge.png
tests/fixtures/images/viper_4k.png
tests/fixtures/images/viper_ultra_tall.png
tests/fixtures/images/viper_ultra_wide.png

# Local scratch experiments (not part of the crate)
/examples/mikey.rs
/examples/raphe.rs
/examples/truck.rs
/examples/gif_to_pack.rs
/src/image/*.jpg
/src/image/*.png
61 changes: 53 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ exclude = [
"docs/sprint-*.md",
# Test assets and fixtures (large images/files)
"tests/visual/",
# Depends on the excluded tests/visual/ module; would not compile from the tarball.
"tests/visual_regression.rs",
"tests/assets/",
"tests/fixtures/",
"tests/test_assets/",
Expand Down Expand Up @@ -292,20 +294,36 @@ name = "zone_stream"
required-features = ["raytracer", "image", "chess"]

[[example]]
name = "zone_stream_canvas_1"
required-features = ["raytracer", "image", "chess"]
name = "purple_rain"
required-features = ["raytracer", "image"]

[[test]]
name = "temporal_coherence_tests"
required-features = ["image"]

[[example]]
name = "zone_stream_canvas_2"
required-features = ["raytracer", "image", "chess"]
name = "generate_watermark"
required-features = ["image"]

[[example]]
name = "zone_stream_canvas_3"
required-features = ["raytracer", "image", "chess"]
name = "render_braille"
required-features = ["image"]

[[example]]
name = "purple_rain"
required-features = ["raytracer", "image"]
name = "render_cuda_dojo"
required-features = ["image"]

[[example]]
name = "webcam_viewer"
required-features = ["video"]

[[example]]
name = "webcam_selector"
required-features = ["video"]

[[example]]
name = "webcam_tuner"
required-features = ["image", "video"]

[lints.clippy]
all = { level = "deny", priority = -1 }
Expand All @@ -323,3 +341,30 @@ cast_lossless = "allow" # Explicit casts are clearer
unnecessary_cast = "allow" # Explicit casts aid readability
needless_pass_by_value = "allow" # Clearer in examples
imprecise_flops = "allow" # Performance not critical in examples

# Advisory (pedantic/nursery) lints that CI's `-D warnings` would otherwise make blocking.
# Each is allowed deliberately; `clippy::all` stays at deny and is the real quality bar.
#
# Fixing this would be a BREAKING API change: `ProgressStyle::{name,theme,describe}` are
# public trait methods returning `&str`, and the lint wants `&'static str`. Narrowing the
# trait's return type breaks every downstream implementor (see the doc example in progress/mod.rs).
unnecessary_literal_bound = "allow"
# Sibling of `imprecise_flops` (already allowed above). Rewriting `a * b + c` as `mul_add`
# fuses the multiply-add, which CHANGES floating-point rounding and can therefore shift
# rendered output (and the visual-regression baselines). It is also slower on CPUs without
# hardware FMA. Not a win for a software renderer.
suboptimal_flops = "allow"
# Graphics/math code legitimately uses short, similar names: x/y/z, cx/cy, nx/ny, r/g/b.
similar_names = "allow"
many_single_char_names = "allow"
# Packed color/bit constants (e.g. 0xRRGGBB) are more readable without digit separators.
unreadable_literal = "allow"
# Audited 2026-07-13: all 7 hits are false positives on standard formulas — the ray/sphere
# discriminant (h^2 - ac), circle tests (dx^2 + dy^2 <= r^2), and a complex quadratic map
# (z^2 = (x^2 - y^2) + i*2xy). The operand groupings are correct as written.
suspicious_operation_groupings = "allow"
# Noisy across a large rendering surface; not a correctness signal.
must_use_candidate = "allow"
missing_const_for_fn = "allow"
items_after_statements = "allow"
use_self = "allow"
9 changes: 5 additions & 4 deletions benches/braille_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

#![cfg(feature = "image")]

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use dotmax::image::{
auto_threshold, load_from_path, pixels_to_braille, resize_to_dimensions, to_grayscale,
};
use std::hint::black_box;
use std::path::Path;

/// Benchmark braille mapping for standard terminal size (160×96 pixels = 80×24 cells)
Expand All @@ -22,7 +23,7 @@ fn bench_pixels_to_braille_standard(c: &mut Criterion) {
.expect("Failed to load sample image");
let resized = resize_to_dimensions(&img, 160, 96, true).expect("Failed to resize");
let gray = to_grayscale(&resized);
let gray_dynamic = image::DynamicImage::ImageLuma8(gray.clone());
let gray_dynamic = image::DynamicImage::ImageLuma8(gray);
let binary = auto_threshold(&gray_dynamic);

c.bench_function("pixels_to_braille_160x96", |b| {
Expand All @@ -39,7 +40,7 @@ fn bench_pixels_to_braille_large(c: &mut Criterion) {
.expect("Failed to load sample image");
let resized = resize_to_dimensions(&img, 400, 200, true).expect("Failed to resize");
let gray = to_grayscale(&resized);
let gray_dynamic = image::DynamicImage::ImageLuma8(gray.clone());
let gray_dynamic = image::DynamicImage::ImageLuma8(gray);
let binary = auto_threshold(&gray_dynamic);

c.bench_function("pixels_to_braille_400x200", |b| {
Expand All @@ -56,7 +57,7 @@ fn bench_pixels_to_braille_small(c: &mut Criterion) {
.expect("Failed to load sample image");
let resized = resize_to_dimensions(&img, 40, 24, true).expect("Failed to resize");
let gray = to_grayscale(&resized);
let gray_dynamic = image::DynamicImage::ImageLuma8(gray.clone());
let gray_dynamic = image::DynamicImage::ImageLuma8(gray);
let binary = auto_threshold(&gray_dynamic);

c.bench_function("pixels_to_braille_40x24", |b| {
Expand Down
41 changes: 22 additions & 19 deletions benches/color_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
//! - `rgb_to_truecolor_escape`: <50ns per conversion
//! - `rgb_to_terminal_color`: <150ns per conversion (includes capability check)

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dotmax::color::convert::{
rgb_to_ansi16, rgb_to_ansi256, rgb_to_terminal_color, rgb_to_truecolor_escape,
};
use dotmax::ColorCapability;
use std::hint::black_box;

/// Benchmark rgb_to_ansi256 conversion.
///
Expand All @@ -20,23 +21,23 @@ fn bench_rgb_to_ansi256(c: &mut Criterion) {

// Test with different color types
group.bench_function("pure_red", |b| {
b.iter(|| rgb_to_ansi256(black_box(255), black_box(0), black_box(0)))
b.iter(|| rgb_to_ansi256(black_box(255), black_box(0), black_box(0)));
});

group.bench_function("pure_green", |b| {
b.iter(|| rgb_to_ansi256(black_box(0), black_box(255), black_box(0)))
b.iter(|| rgb_to_ansi256(black_box(0), black_box(255), black_box(0)));
});

group.bench_function("pure_blue", |b| {
b.iter(|| rgb_to_ansi256(black_box(0), black_box(0), black_box(255)))
b.iter(|| rgb_to_ansi256(black_box(0), black_box(0), black_box(255)));
});

group.bench_function("gray", |b| {
b.iter(|| rgb_to_ansi256(black_box(128), black_box(128), black_box(128)))
b.iter(|| rgb_to_ansi256(black_box(128), black_box(128), black_box(128)));
});

group.bench_function("random_color", |b| {
b.iter(|| rgb_to_ansi256(black_box(173), black_box(94), black_box(212)))
b.iter(|| rgb_to_ansi256(black_box(173), black_box(94), black_box(212)));
});

// Batch conversion benchmark (1000 colors)
Expand All @@ -49,7 +50,7 @@ fn bench_rgb_to_ansi256(c: &mut Criterion) {
}
}
}
})
});
});

group.finish();
Expand All @@ -62,19 +63,19 @@ fn bench_rgb_to_ansi16(c: &mut Criterion) {
let mut group = c.benchmark_group("rgb_to_ansi16");

group.bench_function("pure_red", |b| {
b.iter(|| rgb_to_ansi16(black_box(255), black_box(0), black_box(0)))
b.iter(|| rgb_to_ansi16(black_box(255), black_box(0), black_box(0)));
});

group.bench_function("pure_green", |b| {
b.iter(|| rgb_to_ansi16(black_box(0), black_box(255), black_box(0)))
b.iter(|| rgb_to_ansi16(black_box(0), black_box(255), black_box(0)));
});

group.bench_function("gray", |b| {
b.iter(|| rgb_to_ansi16(black_box(128), black_box(128), black_box(128)))
b.iter(|| rgb_to_ansi16(black_box(128), black_box(128), black_box(128)));
});

group.bench_function("random_color", |b| {
b.iter(|| rgb_to_ansi16(black_box(173), black_box(94), black_box(212)))
b.iter(|| rgb_to_ansi16(black_box(173), black_box(94), black_box(212)));
});

// Batch conversion benchmark
Expand All @@ -87,7 +88,7 @@ fn bench_rgb_to_ansi16(c: &mut Criterion) {
}
}
}
})
});
});

group.finish();
Expand All @@ -100,15 +101,15 @@ fn bench_rgb_to_truecolor_escape(c: &mut Criterion) {
let mut group = c.benchmark_group("rgb_to_truecolor_escape");

group.bench_function("typical", |b| {
b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(128), black_box(0)))
b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(128), black_box(0)));
});

group.bench_function("zeros", |b| {
b.iter(|| rgb_to_truecolor_escape(black_box(0), black_box(0), black_box(0)))
b.iter(|| rgb_to_truecolor_escape(black_box(0), black_box(0), black_box(0)));
});

group.bench_function("max_values", |b| {
b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(255), black_box(255)))
b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(255), black_box(255)));
});

group.finish();
Expand All @@ -131,7 +132,9 @@ fn bench_rgb_to_terminal_color(c: &mut Criterion) {
BenchmarkId::new("capability", format!("{:?}", capability)),
&capability,
|b, cap| {
b.iter(|| rgb_to_terminal_color(black_box(255), black_box(128), black_box(0), *cap))
b.iter(|| {
rgb_to_terminal_color(black_box(255), black_box(128), black_box(0), *cap)
});
},
);
}
Expand All @@ -151,7 +154,7 @@ fn bench_rgb_to_terminal_color(c: &mut Criterion) {
black_box(rgb_to_terminal_color(r, g, 128, cap));
}
}
})
});
});

group.finish();
Expand All @@ -174,7 +177,7 @@ fn bench_throughput(c: &mut Criterion) {
));
}
black_box(sum)
})
});
});

group.bench_function("ansi16_1m_conversions", |b| {
Expand All @@ -184,7 +187,7 @@ fn bench_throughput(c: &mut Criterion) {
sum += u32::from(rgb_to_ansi16(black_box(173), black_box(94), black_box(212)));
}
black_box(sum)
})
});
});

group.finish();
Expand Down
3 changes: 2 additions & 1 deletion benches/color_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@

#![cfg(feature = "image")]

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use dotmax::image::color_mode::extract_cell_colors;
use dotmax::image::{
load_from_path, render_image_with_color, resize_to_dimensions, ColorMode,
ColorSamplingStrategy, DitheringMethod,
};
use std::hint::black_box;
use std::path::Path;

/// Benchmark monochrome mode (baseline, no color overhead)
Expand Down
23 changes: 12 additions & 11 deletions benches/color_schemes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
//! Run with:
//! cargo bench --bench color_schemes

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dotmax::color::schemes::{
blue_purple, cyan_magenta, get_scheme, grayscale, green_yellow, heat_map, list_schemes,
monochrome, rainbow, ColorScheme,
};
use std::hint::black_box;

/// Benchmark sample() for a single scheme
fn bench_sample_single(c: &mut Criterion) {
let scheme = rainbow();

c.bench_function("sample_single_intensity", |b| {
b.iter(|| black_box(scheme.sample(black_box(0.5))))
b.iter(|| black_box(scheme.sample(black_box(0.5))));
});
}

Expand All @@ -30,7 +31,7 @@ fn bench_sample_gradient(c: &mut Criterion) {
let intensity = i as f32 / 99.0;
black_box(scheme.sample(black_box(intensity)));
}
})
});
});
}

Expand All @@ -50,7 +51,7 @@ fn bench_sample_all_schemes(c: &mut Criterion) {

for (name, scheme) in schemes {
group.bench_with_input(BenchmarkId::new("sample", name), &scheme, |b, scheme| {
b.iter(|| black_box(scheme.sample(black_box(0.5))))
b.iter(|| black_box(scheme.sample(black_box(0.5))));
});
}

Expand All @@ -77,11 +78,11 @@ fn bench_discovery(c: &mut Criterion) {
group.bench_function("list_schemes", |b| b.iter(|| black_box(list_schemes())));

group.bench_function("get_scheme_hit", |b| {
b.iter(|| black_box(get_scheme(black_box("rainbow"))))
b.iter(|| black_box(get_scheme(black_box("rainbow"))));
});

group.bench_function("get_scheme_miss", |b| {
b.iter(|| black_box(get_scheme(black_box("nonexistent"))))
b.iter(|| black_box(get_scheme(black_box("nonexistent"))));
});

group.finish();
Expand All @@ -99,7 +100,7 @@ fn bench_custom_scheme(c: &mut Criterion) {
Color::rgb(0, 0, 255),
];
black_box(ColorScheme::new("custom", colors))
})
});
});
}

Expand All @@ -109,19 +110,19 @@ fn bench_boundary_conditions(c: &mut Criterion) {
let mut group = c.benchmark_group("boundary_conditions");

group.bench_function("sample_0.0", |b| {
b.iter(|| black_box(scheme.sample(black_box(0.0))))
b.iter(|| black_box(scheme.sample(black_box(0.0))));
});

group.bench_function("sample_1.0", |b| {
b.iter(|| black_box(scheme.sample(black_box(1.0))))
b.iter(|| black_box(scheme.sample(black_box(1.0))));
});

group.bench_function("sample_negative_clamped", |b| {
b.iter(|| black_box(scheme.sample(black_box(-0.5))))
b.iter(|| black_box(scheme.sample(black_box(-0.5))));
});

group.bench_function("sample_above_1_clamped", |b| {
b.iter(|| black_box(scheme.sample(black_box(1.5))))
b.iter(|| black_box(scheme.sample(black_box(1.5))));
});

group.finish();
Expand Down
2 changes: 1 addition & 1 deletion benches/core_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fn bench_grid_clear(c: &mut Criterion) {
}
}

group.bench_with_input(BenchmarkId::new("clear", label), &(), |b, _| {
group.bench_with_input(BenchmarkId::new("clear", label), &(), |b, ()| {
b.iter(|| {
grid.clear();
black_box(&grid);
Expand Down
Loading
Loading