Files
photocraft/crates/engine/examples/bench_display.rs
Brandon ThomasandClaude Opus 5.5 d5eb44e9fd Colour-managed canvas: document profile -> monitor (P3/Adobe RGB/ProPhoto/Gray/CMYK), linear EXR, embedded CMYK (#66)
* CMYK documents display and export through their embedded CMYK profile

A thread-local CmykSpace (photocraft_color::convert::with_cmyk_space) replaces the built-in
coated CMYK while the CPU compositor, the GPU compositor's uploads and the PSD merged
composite convert a document's CMYK pixels; GPU residents track the profile they were
converted with.

Refs #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* EXR/HDR are linear: tag imports linear sRGB, linearise exports

Refs #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Colour-managed canvas: document profile -> monitor profile on GPU and CPU paths

* engine display_color: CanvasDisplay per (document profile, mode, monitor), identity fast
  path when they match (sRGB on sRGB costs nothing); linear composites are stored
  sRGB-encoded in the 8-bit texture. canvas_lut now always carries the CM transform, with
  Proof Colors / Gamut Warning / 32-bit preview on top.
* GPU canvas applies the display LUT whenever the transform isn't the identity (also for
  filter/proxy previews); the CPU canvas and Navigator transform the 8-bit composite.
* Edit > Color Settings > Monitor Profile (auto|built-in|.icc). macOS 'auto' reads the main
  display's ICC profile at launch via osascript/AppKit (no unsafe); elsewhere sRGB.
* Tests: P3 (hand-computed), Adobe RGB, ProPhoto, Gray Gamma 2.2, synthetic CMYK, linear,
  monitor setting; GPU+CPU canvas test via kittest (skips without Rgba32Float).

Fixes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* io: tag EXR/HDR linear in import_flat (adapt to the shared image_to_document)

#65 moved flat decoding into image_to_document(name, img) so camera raws can reuse it; the
linear-EXR tagging needs the file bytes for format detection, so it now runs in import_flat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:44:13 -04:00

77 lines
3.2 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Colour-managed canvas display cost on a 36 MP document (#46).
//!
//! ```sh
//! cargo run --release -p photocraft-engine --example bench_display -- [--size 7360x4912] [--reps 5]
//! ```
//!
//! Times, per profile: the composite (shared by every path), the CPU canvas conversion before
//! (`Buffer::to_rgba8`, no colour management) and after (`CanvasDisplay::to_rgba8`), the
//! per-frame display lookup (`display_signature`, what the GPU canvas checks each frame) and
//! building the 33³ GPU display LUT (once per profile/monitor change).
use std::time::Instant;
use photocraft_cms::Builtin;
use photocraft_color::{Color, ColorMode, SampleType};
use photocraft_doc::{Document, Size};
use photocraft_engine::Session;
fn arg(args: &[String], name: &str) -> Option<String> {
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
}
fn median(mut v: Vec<f64>) -> f64 {
v.sort_by(|a, b| a.total_cmp(b));
v[v.len() / 2]
}
fn time(reps: usize, mut f: impl FnMut()) -> f64 {
median(
(0..reps)
.map(|_| {
let t = Instant::now();
f();
t.elapsed().as_secs_f64() * 1000.0
})
.collect(),
)
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let (w, h) =
arg(&args, "--size").and_then(|s| s.split_once('x').map(|(a, b)| (a.parse().unwrap_or(7360), b.parse().unwrap_or(4912)))).unwrap_or((7360, 4912));
let reps: usize = arg(&args, "--reps").and_then(|v| v.parse().ok()).unwrap_or(5).max(1);
println!("document {w}×{h} ({:.1} MP), median of {reps}", (w * h) as f64 / 1e6);
println!("{:<22} {:>10} {:>14} {:>14} {:>12} {:>10}", "profile", "composite", "CPU before", "CPU after", "per frame", "LUT build");
for (name, profile, depth) in [
("sRGB (untagged)", None, SampleType::U8),
("Display P3", Some(Builtin::DisplayP3), SampleType::U8),
("ProPhoto", Some(Builtin::ProPhotoCompat), SampleType::U16),
("linear sRGB (EXR)", Some(Builtin::LinearSrgb), SampleType::F32),
] {
let mut d = Document::with_background("b", Size::new(w, h), ColorMode::Rgb, depth, Color::rgb(0.7, 0.4, 0.2));
d.icc_profile = profile.map(|b| b.profile().to_bytes());
let s = Session::new();
let mut buf = None;
let composite = time(reps, || buf = Some(photocraft_compose::flatten(&d)));
let buf = buf.expect("composited");
let before = time(reps, || {
std::hint::black_box(buf.to_rgba8());
});
let display = s.color.canvas_display(&d).expect("display");
let after = time(reps, || {
let d = s.color.canvas_display(&d).expect("display");
std::hint::black_box(if d.is_identity() { buf.to_rgba8() } else { d.to_rgba8(&buf) });
});
let frame = time(reps * 200, || {
std::hint::black_box(s.color.display_signature(&d));
});
let lut = time(reps, || {
std::hint::black_box(s.color.canvas_lut(&d, 33).expect("lut"));
});
let tag = if display.is_identity() { " (identity)" } else { "" };
println!("{:<22} {composite:>8.1}ms {before:>12.1}ms {after:>12.1}ms {:>10.1}µs {lut:>8.1}ms{tag}", name, frame * 1000.0);
}
}