Files
hcie-rust-v3.05/hcie-composite/src/tiled.rs
T

839 lines
27 KiB
Rust

use hcie_blend::blend_pixels;
use hcie_fx::apply_layer_effects;
use hcie_protocol::Layer;
use hcie_tile::{TiledLayer, TILE_SIZE};
use rayon::prelude::*;
/// Pixel-area threshold below which the compositor uses a sequential row
/// loop instead of Rayon parallelism. For small dirty regions (e.g. a single
/// brush stamp) the thread-pool overhead of `par_chunks_exact_mut` dominates
/// the actual blending work. 256x256 matches the tile size and is a safe
/// crossover point where sequential iteration is consistently faster.
const SEQUENTIAL_PIXEL_THRESHOLD: u32 = 256 * 256;
/// Composite layers using tile storage.
/// Each layer's dense buffer is read via `layer.get_pixel()`.
/// This is the same as the parallel version but optimized to skip
/// transparent tiles. Uses dirty region to further cull work.
pub fn composite_tiled(
layers: &[Layer],
tile_layers: &[Option<TiledLayer>],
canvas_width: u32,
canvas_height: u32,
) -> Vec<u8> {
let px_count = (canvas_width * canvas_height) as usize;
let mut output = vec![0u8; px_count * 4];
composite_tiled_into(
layers,
tile_layers,
canvas_width,
canvas_height,
0,
0,
canvas_width,
canvas_height,
&mut output,
);
output
}
/// Composite tile layers into an existing buffer within a region.
pub fn composite_tiled_into(
layers: &[Layer],
tile_layers: &[Option<TiledLayer>],
canvas_width: u32,
canvas_height: u32,
x0: u32,
y0: u32,
x1: u32,
y1: u32,
output: &mut [u8],
) {
let region_w = x1.saturating_sub(x0);
let region_h = y1.saturating_sub(y0);
if region_w == 0 || region_h == 0 {
return;
}
let use_sequential = region_w.saturating_mul(region_h) <= SEQUENTIAL_PIXEL_THRESHOLD;
let row_stride = canvas_width as usize * 4;
// Pre-compute the base layer index for every clipping mask layer once.
// This avoids an O(i) reverse scan per pixel during blending.
let mut base_indices = Vec::with_capacity(layers.len());
for i in 0..layers.len() {
if layers[i].clipping_mask {
let mut base = None;
for j in (0..i).rev() {
if !layers[j].clipping_mask {
base = Some(j);
break;
}
}
base_indices.push(base);
} else {
base_indices.push(None);
}
}
for (i, layer) in layers.iter().enumerate() {
if !layer.visible {
continue;
}
if layer.layer_type == hcie_protocol::LayerType::Group {
continue;
}
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
let opacity = layer.opacity.clamp(0.0, 1.0);
let is_clipping = layer.clipping_mask;
let base_idx = base_indices[i];
let (offset_x, offset_y) = match &layer.data {
hcie_protocol::LayerData::Text {
offset_x, offset_y, ..
} => (*offset_x as i32, *offset_y as i32),
_ => (0, 0),
};
let cx0 = (x0 as i32).max(offset_x).max(0) as u32;
let cx1 = (x1 as i32)
.min(offset_x + layer.width as i32)
.min(canvas_width as i32)
.max(0) as u32;
let cy0 = (y0 as i32).max(offset_y).max(0) as u32;
let cy1 = (y1 as i32)
.min(offset_y + layer.height as i32)
.min(canvas_height as i32)
.max(0) as u32;
let layer_w = layer.width.min(canvas_width);
let layer_h = layer.height.min(canvas_height);
let is_adj = layer.adjustment.is_some();
if is_adj {
let region_y0 = y0;
let region_y1 = y1.min(canvas_height);
let region_x0 = x0;
let region_x1 = x1.min(canvas_width);
if use_sequential {
for y in region_y0..region_y1 {
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
for x in region_x0..region_x1 {
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
}
}
} else {
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < region_y0 || gy >= region_y1 {
return;
}
for x in region_x0..region_x1 {
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
}
});
}
continue;
}
// If layer has effects or non-default fill_opacity, compute effects buffer.
// Prefer the cached `effects_cache` when available to avoid re-running the
// expensive effects pipeline on every partial composite.
let has_effects =
!layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer
.effects
.iter()
.map(hcie_fx::protocol_to_hcie_fx_effect)
.collect();
fx_effects.extend(
layer
.styles
.iter()
.filter_map(|s| hcie_fx::layer_style_to_effect(s)),
);
let effect_buf = if has_effects {
if let Ok(Some(cached)) = layer.effects_cache.lock().as_deref() {
if cached.width == layer.width && cached.height == layer.height {
Some(cached.rendered.clone())
} else {
None
}
} else {
None
}
.unwrap_or_else(|| {
apply_layer_effects(
&layer.pixels,
layer.width,
layer.height,
&fx_effects,
layer.fill_opacity,
)
})
} else {
Vec::new()
};
// Try tile path first (only if no effects — effects need full dense buffer)
if !has_effects {
if let Some(Some(tl)) = tile_layers
.get(i)
.filter(|_| layer.layer_type != hcie_protocol::LayerType::Text)
{
if tl.tile_count() == 0 && i > 0 {
continue; // fully transparent layer with no tiles
}
let t_start_x = x0 / TILE_SIZE;
let _t_start_y = y0 / TILE_SIZE;
let t_end_x = div_ceil(x1, TILE_SIZE);
let _t_end_y = div_ceil(y1, TILE_SIZE);
if use_sequential {
for y in y0..y1.min(layer_h) {
let row =
&mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ty = gy / TILE_SIZE;
let ly = (gy % TILE_SIZE) as usize;
for tx in t_start_x..t_end_x {
let gx_start = tx * TILE_SIZE;
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
if gx_end <= gx_start {
continue;
}
if let Some(tile) = tl.get_tile(tx, ty) {
let start_x = gx_start.max(x0);
for gx in start_x..gx_end {
blend_tile_pixel(
row,
gx,
gy,
ly,
tile,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
}
}
} else {
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < y0 || gy >= y1 || gy >= layer_h {
return;
}
let ty = gy / TILE_SIZE;
let ly = (gy % TILE_SIZE) as usize;
for tx in t_start_x..t_end_x {
let gx_start = tx * TILE_SIZE;
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
if gx_end <= gx_start {
continue;
}
if let Some(tile) = tl.get_tile(tx, ty) {
let start_x = gx_start.max(x0);
for gx in start_x..gx_end {
blend_tile_pixel(
row,
gx,
gy,
ly,
tile,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
}
});
}
} else {
// Fallback to dense pixel path (no effects)
if cx0 >= cx1 || cy0 >= cy1 {
continue;
}
if use_sequential {
for y in cy0..cy1 {
let row =
&mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_dense_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
} else {
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 {
return;
}
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_dense_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
});
}
}
} else {
// Effects path — use pre-computed effects buffer
if cx0 >= cx1 || cy0 >= cy1 {
continue;
}
let eb = effect_buf.as_slice();
if use_sequential {
for y in cy0..cy1 {
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_effects_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
eb,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
} else {
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 {
return;
}
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_effects_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
eb,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
});
}
}
}
}
/// Applies one adjustment-layer pixel to the current composite row.
///
/// **Purpose:** Evaluates curves, gradient-map, or hue/saturation data while
/// respecting masks, opacity, and blend mode. **Logic & Workflow:** Transparent
/// or masked-out destinations exit early; Normal blend bypasses the generic
/// blend dispatcher, and fully opaque adjustment pixels are assigned directly.
/// **Arguments:** `row` is the destination row, `x`/`gy` are canvas coordinates,
/// `layer` owns adjustment and mask data, and `blend`/`opacity` control mixing.
/// **Returns:** Nothing. **Side Effects / Dependencies:** Mutates only the RGB
/// channels of one destination pixel and uses `hcie-blend` for non-Normal modes.
#[inline]
fn blend_adjustment_pixel(
row: &mut [u8],
x: u32,
gy: u32,
layer: &Layer,
blend: hcie_blend::BlendMode,
opacity: f32,
) {
let out_idx = (x as usize) * 4;
let dst = [
row[out_idx],
row[out_idx + 1],
row[out_idx + 2],
row[out_idx + 3],
];
if dst[3] == 0 {
return;
}
let mask_val = layer.get_mask_value(x, gy);
if mask_val == 0 {
return;
}
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
hcie_blend::Adjustment::Curves {
lut_r,
lut_g,
lut_b,
} => crate::parallel::apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::GradientMap {
lut_r,
lut_g,
lut_b,
} => crate::parallel::apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::HueSaturation {
hue,
saturation,
lightness,
} => crate::parallel::apply_hue_saturation(
dst[0],
dst[1],
dst[2],
*hue,
*saturation,
*lightness,
),
};
let eff_opacity = opacity * (mask_val as f32 / 255.0);
if eff_opacity <= 0.0 {
return;
}
let blended_rgb = if blend == hcie_blend::BlendMode::Normal {
adj_rgb
} else {
let blended = blend_pixels(
[dst[0], dst[1], dst[2], 255],
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
blend,
1.0,
);
[blended[0], blended[1], blended[2]]
};
if eff_opacity >= 1.0 {
row[out_idx] = blended_rgb[0];
row[out_idx + 1] = blended_rgb[1];
row[out_idx + 2] = blended_rgb[2];
return;
}
let inverse_opacity = 1.0 - eff_opacity;
row[out_idx] =
(inverse_opacity * dst[0] as f32 + eff_opacity * blended_rgb[0] as f32).round() as u8;
row[out_idx + 1] =
(inverse_opacity * dst[1] as f32 + eff_opacity * blended_rgb[1] as f32).round() as u8;
row[out_idx + 2] =
(inverse_opacity * dst[2] as f32 + eff_opacity * blended_rgb[2] as f32).round() as u8;
}
#[inline]
fn blend_tile_pixel(
row: &mut [u8],
gx: u32,
gy: u32,
ly: usize,
tile: &hcie_tile::Tile,
layer: &Layer,
is_clipping: bool,
base_idx: Option<usize>,
layers: &[Layer],
blend: hcie_blend::BlendMode,
opacity: f32,
) {
let lx = (gx % TILE_SIZE) as usize;
let src_idx = (ly * TILE_SIZE as usize + lx) * 4;
let mut src = [
tile.pixels[src_idx],
tile.pixels[src_idx + 1],
tile.pixels[src_idx + 2],
tile.pixels[src_idx + 3],
];
// Inline get_mask_value — hot path
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
// Inline get_pixel for clipping base layer
let base_i = (gy * layers[bi].width + gx) as usize * 4;
let base = [
layers[bi].pixels[base_i],
layers[bi].pixels[base_i + 1],
layers[bi].pixels[base_i + 2],
layers[bi].pixels[base_i + 3],
];
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
}
#[inline]
fn blend_dense_pixel(
row: &mut [u8],
gx: u32,
gy: u32,
layer: &Layer,
is_clipping: bool,
base_idx: Option<usize>,
layers: &[Layer],
blend: hcie_blend::BlendMode,
opacity: f32,
) {
// Inline get_pixel — hot path
let src_i = (gy * layer.width + gx) as usize * 4;
if src_i + 3 >= layer.pixels.len() {
return;
}
let mut src = [
layer.pixels[src_i],
layer.pixels[src_i + 1],
layer.pixels[src_i + 2],
layer.pixels[src_i + 3],
];
// Inline get_mask_value — hot path
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
// Inline get_pixel for clipping base layer
let base_i = (gy * layers[bi].width + gx) as usize * 4;
let base = [
layers[bi].pixels[base_i],
layers[bi].pixels[base_i + 1],
layers[bi].pixels[base_i + 2],
layers[bi].pixels[base_i + 3],
];
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
}
#[inline]
fn blend_effects_pixel(
row: &mut [u8],
gx: u32,
gy: u32,
layer: &Layer,
eb: &[u8],
is_clipping: bool,
base_idx: Option<usize>,
layers: &[Layer],
blend: hcie_blend::BlendMode,
opacity: f32,
) {
let src_idx = (gy as usize * layer.width as usize + gx as usize) * 4;
let mut src = [
eb[src_idx],
eb[src_idx + 1],
eb[src_idx + 2],
eb[src_idx + 3],
];
// Inline get_mask_value — hot path
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
// Inline get_pixel for clipping base layer
let base_i = (gy * layers[bi].width + gx) as usize * 4;
let base = [
layers[bi].pixels[base_i],
layers[bi].pixels[base_i + 1],
layers[bi].pixels[base_i + 2],
layers[bi].pixels[base_i + 3],
];
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
}
#[inline]
fn blend_dense_pixel_offset(
row: &mut [u8],
gx: u32,
gy: u32,
lx: u32,
ly: u32,
layer: &Layer,
is_clipping: bool,
base_idx: Option<usize>,
layers: &[Layer],
blend: hcie_blend::BlendMode,
opacity: f32,
) {
let src_i = (ly * layer.width + lx) as usize * 4;
if src_i + 3 >= layer.pixels.len() {
return;
}
let mut src = [
layer.pixels[src_i],
layer.pixels[src_i + 1],
layer.pixels[src_i + 2],
layer.pixels[src_i + 3],
];
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
let base = layers[bi].get_pixel(gx, gy);
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
}
#[inline]
fn blend_effects_pixel_offset(
row: &mut [u8],
gx: u32,
gy: u32,
lx: u32,
ly: u32,
layer: &Layer,
eb: &[u8],
is_clipping: bool,
base_idx: Option<usize>,
layers: &[Layer],
blend: hcie_blend::BlendMode,
opacity: f32,
) {
let src_idx = (ly as usize * layer.width as usize + lx as usize) * 4;
let mut src = [
eb[src_idx],
eb[src_idx + 1],
eb[src_idx + 2],
eb[src_idx + 3],
];
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
let base = layers[bi].get_pixel(gx, gy);
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
}
#[inline]
fn div_ceil(a: u32, b: u32) -> u32 {
(a + b - 1) / b
}