mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 18:47:07 -06:00
Render: 8 and 16 bpc quantisation rounds with floor in the browser
Cause: quantize_px rounded with `(v + 0.5) as u32 as f32`. Converting back from u32 is several instructions in V8's wasm code on x86 (no unsigned conversion), so quantising a 1080p layer took about 15 ms in the browser: every 8/16 bpc layer is quantised after its source and every effect, and the CPU compositor quantises the comp after every layer. Fix: round_half_up uses floor in wasm (one instruction there) and keeps the integer conversion elsewhere (x86-64's baseline has no floor instruction, where floor is slower). The values are the same: both round non-negative values half up. About 1.5x faster quantising in the browser (15 -> 10 ms per 1080p layer in a wasm micro-benchmark), unchanged on the desktop. Tests: color::tests::rounding_forms_agree: both forms agree on 0..=levels in sixteenths (exact halves included) and on 100 001 products, at 8 and 16 bpc. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
74b151cedc
commit
ddfa566d7d
@@ -195,14 +195,22 @@ pub fn quantize(img: &mut Image, levels: f32) {
|
||||
#[allow(clippy::manual_clamp)] // max/min map NaN to 0; clamp would keep NaN
|
||||
#[inline(always)]
|
||||
fn quantize_px(p: &mut [f32; 4], levels: f32, inv: f32) {
|
||||
let a = (p[3].max(0.0).min(1.0) * levels + 0.5) as u32 as f32 * inv;
|
||||
let a = round_half_up(p[3].max(0.0).min(1.0) * levels) * inv;
|
||||
let hi = [a, a, a, a];
|
||||
for c in 0..3 {
|
||||
p[c] = (p[c].max(0.0).min(hi[c]) * levels + 0.5) as u32 as f32 * inv;
|
||||
p[c] = round_half_up(p[c].max(0.0).min(hi[c]) * levels) * inv;
|
||||
}
|
||||
p[3] = a;
|
||||
}
|
||||
|
||||
/// `v` (0..=65 535) rounded half up. The same either way; the fastest on each target: x86-64's
|
||||
/// baseline converts to integers in one instruction but has no `floor` (SSE4.1), while in wasm
|
||||
/// `floor` is one instruction and converting back from `u32` several (about 1.5× faster there).
|
||||
#[inline(always)]
|
||||
fn round_half_up(v: f32) -> f32 {
|
||||
if cfg!(target_arch = "wasm32") { (v + 0.5).floor() } else { (v + 0.5) as u32 as f32 }
|
||||
}
|
||||
|
||||
/// [`quantize`] restricted to a rectangle (already clamped to the image), row-parallel.
|
||||
fn quantize_rect(img: &mut Image, r: PxRect, levels: f32) {
|
||||
if r.x0 >= r.x1 || r.y0 >= r.y1 {
|
||||
@@ -268,3 +276,22 @@ pub fn output_hdr(img: &mut Image, to_linear: Option<Conversion>, mode: HdrMode,
|
||||
*p = [o[0] * a, o[1] * a, o[2] * a, a];
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Both forms `round_half_up` picks between agree on everything quantising feeds it:
|
||||
/// 0..=levels, exact halves included.
|
||||
#[test]
|
||||
fn rounding_forms_agree() {
|
||||
for levels in [255.0f32, 32_768.0] {
|
||||
for i in 0..=(levels as u32 * 16) {
|
||||
let v = i as f32 / 16.0;
|
||||
assert_eq!((v + 0.5).floor(), (v + 0.5) as u32 as f32, "{v}");
|
||||
}
|
||||
for i in 0..=100_000 {
|
||||
let v = (i as f32 / 100_000.0) * levels;
|
||||
assert_eq!((v + 0.5).floor(), (v + 0.5) as u32 as f32, "{v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user