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
18 changes: 18 additions & 0 deletions crates/n0/src/drawlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ pub(crate) enum ResolvedFilterDisplacementChannel {
Alpha,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResolvedFilterConvolveEdgeMode {
Duplicate,
Wrap,
None,
}

/// The private filter-operation vocabulary admitted by the painter.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ResolvedFilterPrimitive {
Expand Down Expand Up @@ -171,6 +178,17 @@ pub(crate) enum ResolvedFilterPrimitive {
x_channel: ResolvedFilterDisplacementChannel,
y_channel: ResolvedFilterDisplacementChannel,
},
ConvolveMatrix {
order_x: u16,
order_y: u16,
kernel: Arc<[f32]>,
gain: f32,
bias: f32,
target_x: u16,
target_y: u16,
edge_mode: ResolvedFilterConvolveEdgeMode,
preserve_alpha: bool,
},
Merge,
}

Expand Down
39 changes: 33 additions & 6 deletions crates/n0/src/glyphless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ use n0_model::model::{
};
use n0_model::path::ResolvedPathArtifact;
use rframe::{
ClipPath, FilterBlend, FilterColorSpace, FilterComposite, FilterDisplacementChannel,
FilterInput, FilterMorphology, FilterPrimitive, FilterTurbulenceKind, Frame, FrameItem,
Geometry, MaskMode, PaintStack, ScopeEffect, VisualRef,
ClipPath, FilterBlend, FilterColorSpace, FilterComposite, FilterConvolveEdgeMode,
FilterDisplacementChannel, FilterInput, FilterMorphology, FilterPrimitive,
FilterTurbulenceKind, Frame, FrameItem, Geometry, MaskMode, PaintStack, ScopeEffect, VisualRef,
};

use crate::damage::{diff_inputs, DamageOwner, FrameDamageInput};
use crate::drawlist::{
DrawList, GlyphlessOwnerSlot, Item, ItemKind, PostPaintOpacity, ResolvedClipGeometry,
ResolvedClipGeometryKind, ResolvedClipLayer, ResolvedClipPath, ResolvedFilter,
ResolvedFilterBlend, ResolvedFilterColorSpace, ResolvedFilterComposite,
ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterMorphology,
ResolvedFilterNode, ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode,
StrokeDashPhase,
ResolvedFilterConvolveEdgeMode, ResolvedFilterDisplacementChannel, ResolvedFilterInput,
ResolvedFilterMorphology, ResolvedFilterNode, ResolvedFilterPrimitive,
ResolvedFilterTurbulenceKind, ResolvedMaskMode, StrokeDashPhase,
};
use crate::frame::FrameExecutionError;
use crate::paint::PaintCtx;
Expand Down Expand Up @@ -1027,6 +1027,33 @@ fn compile_filter(filter: &rframe::Filter) -> ResolvedFilter {
}
},
},
FilterPrimitive::ConvolveMatrix {
order_x,
order_y,
kernel,
gain,
bias,
target_x,
target_y,
edge_mode,
preserve_alpha,
} => ResolvedFilterPrimitive::ConvolveMatrix {
order_x,
order_y,
kernel,
gain,
bias,
target_x,
target_y,
edge_mode: match edge_mode {
FilterConvolveEdgeMode::Duplicate => {
ResolvedFilterConvolveEdgeMode::Duplicate
}
FilterConvolveEdgeMode::Wrap => ResolvedFilterConvolveEdgeMode::Wrap,
FilterConvolveEdgeMode::None => ResolvedFilterConvolveEdgeMode::None,
},
preserve_alpha,
},
FilterPrimitive::Merge => ResolvedFilterPrimitive::Merge,
},
})
Expand Down
112 changes: 108 additions & 4 deletions crates/n0/src/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ use skia_safe::{
use crate::drawlist::{
DrawList, ItemKind, PostPaintOpacity, ResolvedClipGeometry, ResolvedClipGeometryKind,
ResolvedClipLayer, ResolvedClipPath, ResolvedFilter, ResolvedFilterBlend,
ResolvedFilterColorSpace, ResolvedFilterComposite, ResolvedFilterDisplacementChannel,
ResolvedFilterInput, ResolvedFilterMorphology, ResolvedFilterPrimitive,
ResolvedFilterTurbulenceKind, ResolvedMaskMode, StrokeDashPhase,
ResolvedFilterColorSpace, ResolvedFilterComposite, ResolvedFilterConvolveEdgeMode,
ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterMorphology,
ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode, StrokeDashPhase,
};

/// The gradient family whose local matrix could not be represented by the
Expand Down Expand Up @@ -2331,6 +2331,14 @@ fn sk_displacement_channel(channel: ResolvedFilterDisplacementChannel) -> ColorC
}
}

fn sk_convolve_tile_mode(mode: ResolvedFilterConvolveEdgeMode) -> skia_safe::TileMode {
match mode {
ResolvedFilterConvolveEdgeMode::Duplicate => skia_safe::TileMode::Clamp,
ResolvedFilterConvolveEdgeMode::Wrap => skia_safe::TileMode::Repeat,
ResolvedFilterConvolveEdgeMode::None => skia_safe::TileMode::Decal,
}
}

/// Build one checked private filter graph and its final-composition policy.
fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
let explicit_transparent_source = if filter.source_is_transparent {
Expand Down Expand Up @@ -2428,6 +2436,10 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
&node.primitive,
ResolvedFilterPrimitive::Morphology { radius_x, radius_y, .. }
if source_dependent && (*radius_x > 0.0 || *radius_y > 0.0)
)
|| matches!(
&node.primitive,
ResolvedFilterPrimitive::ConvolveMatrix { .. } if source_dependent
);
let has_procedural_input = inputs.iter().any(|input| input.procedural_provenance);
let mut procedural_provenance = has_procedural_input
Expand Down Expand Up @@ -2845,6 +2857,48 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
requires_exact_restore || node.color_space == ResolvedFilterColorSpace::Srgb,
)
}
ResolvedFilterPrimitive::ConvolveMatrix {
order_x,
order_y,
kernel,
gain,
bias,
target_x,
target_y,
edge_mode,
preserve_alpha,
} => {
let input = inputs
.pop()
.expect("convolution matrix has one checked input");
procedural_unorm8_blend = false;
if node.color_space == ResolvedFilterColorSpace::Srgb
&& !input.requires_exact_restore
{
procedural_provenance = false;
}
let filter = skia_safe::image_filters::matrix_convolution(
(i32::from(order_x), i32::from(order_y)),
&kernel,
gain,
bias * 255.0,
(i32::from(target_x), i32::from(target_y)),
sk_convolve_tile_mode(edge_mode),
!preserve_alpha,
input.image_filter,
crop,
)
.ok_or_else(|| {
"the backend could not construct a convolution-matrix operation".to_string()
})?;
(
Some(filter),
node.color_space,
input.source_dependent,
input.requires_exact_restore
|| node.color_space == ResolvedFilterColorSpace::Srgb,
)
}
ResolvedFilterPrimitive::Merge => {
let mut inputs = inputs.into_iter();
let image_filter = if let Some(first) = inputs.next() {
Expand Down Expand Up @@ -2948,7 +3002,9 @@ mod filter_policy_tests {
use n0_model::math::RectF;
use n0_model::model::Color32F;

use crate::drawlist::{ResolvedFilterMorphology, ResolvedFilterNode};
use crate::drawlist::{
ResolvedFilterConvolveEdgeMode, ResolvedFilterMorphology, ResolvedFilterNode,
};

use super::{
build_filter, ResolvedFilter, ResolvedFilterColorSpace, ResolvedFilterInput,
Expand Down Expand Up @@ -3173,6 +3229,54 @@ mod filter_policy_tests {
let generated = build_filter(&generated).expect("generated morphology builds");
assert!(!generated.source_preflatten);
}

#[test]
fn checked_convolution_builds_at_the_kernel_bound_and_keeps_source_policy() {
let convolve = |input, count, bias, preserve_alpha| ResolvedFilterNode {
inputs: Arc::from([input]),
region: REGION,
color_space: ResolvedFilterColorSpace::Srgb,
primitive: ResolvedFilterPrimitive::ConvolveMatrix {
order_x: count,
order_y: 1,
kernel: vec![0.0; usize::from(count)].into(),
gain: 1.0,
bias,
target_x: count / 2,
target_y: 0,
edge_mode: ResolvedFilterConvolveEdgeMode::Wrap,
preserve_alpha,
},
};
let one = |node, may_paint_transparent_input| ResolvedFilter {
region: REGION,
nodes: Arc::from([node]),
may_paint_transparent_input,
source_is_transparent: false,
};

let bounded = build_filter(&one(
convolve(ResolvedFilterInput::Source, 256, 0.0, false),
false,
))
.expect("the checked maximum kernel builds transactionally");
assert!(bounded.source_preflatten);
assert!(bounded.restore_blender.is_some());

let alpha_creating = build_filter(&one(
convolve(ResolvedFilterInput::Source, 1, 0.25, false),
true,
))
.expect("a biased convolution builds over an explicit source");
assert!(alpha_creating.source_preflatten);

let alpha_preserving = build_filter(&one(
convolve(ResolvedFilterInput::SourceAlpha, 1, 0.25, true),
false,
))
.expect("preserved alpha remains a checked native operation");
assert!(alpha_preserving.source_preflatten);
}
}

/// Product-build preflight for a resolved image-filter graph. Replay repeats
Expand Down
63 changes: 46 additions & 17 deletions crates/n0_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,8 @@ cargo run -p n0_cli --bin n0 -- \
is sampled from effective `.5` through `1.875`, while `.25` and `2` are
exact (measured, not celled). The patrol conservatively refuses the open
interval between those endpoints after target mapping. Current Chromium
ignores `edgeMode` on blur (measured, not celled); its global row remains open
because the attribute also applies to `<feConvolveMatrix>`.
ignores `edgeMode` on blur; a dedicated Chromium-baked drop cell and the
complete convolution behavior now close that shared attribute row.
Native drop shadow carries its own operation rather than lowering to the
blur-plus-offset graph. Missing `dx`, `dy`, and `stdDeviation` use `2`; one
or two sigma axes, negative-axis clamping, measured number spellings,
Expand Down Expand Up @@ -403,6 +403,33 @@ cargo run -p n0_cli --bin n0 -- \
retained fill-only ellipse coverage boundary. Rounded rectangles, curved
paths, and circle/path strokes stay admitted. That last patrol leaves
gridaco/nothing#88 separate and unchanged.
Convolve matrix carries one checked rectangular kernel of at most 256 finite
coefficients. One/two-member `order` values normalize by truncation toward
zero; the matrix must contain exactly the resulting product. The authored
coefficients reverse once to state SVG convolution rather than correlation.
Missing or malformed order selects 3×3, while non-positive order, a missing
or wrong-count matrix, and an over-bound kernel produce Chromium's
transparent result instead of an unfiltered fallback.
`divisor` carries one signed number. Missing, exactly empty, and signed-zero
values use the ordered binary32 kernel sum, with a zero sum becoming one; a
present nonempty malformed value uses one. `bias` carries one signed number
with initial zero. `targetX`/`targetY` use signed integer text, default to
half their respective order axes, reset malformed authored text to zero, and
produce transparent when a valid value lies outside the kernel. The complete
case-sensitive `duplicate | wrap | none` edge vocabulary and `false | true`
alpha-preservation vocabulary are admitted. Chromium ignores
`kernelUnitLength` on convolution; that drop is baked, while its shared
lighting applicability remains open.
Both filter color spaces, SourceGraphic/SourceAlpha/previous/named/generated
inputs, result reuse, hard regions, primitive units, paths, strokes, groups,
safe axis mappings, exact quarter turns, target opacity/clip/mask,
blur/morphology ordering, `<use>`, and `viewBox` are exact. General affine target
mappings, source-dependent paint servers, and divisors whose reciprocal is
not finite refuse by three stable convolution names before paint. Fractional
axis maps, reflections, exact quarter turns, generated inputs, and every
accepted kernel-size strategy through 256 stay admitted. Representative
fallback branches are celled; the wider invalid-spelling matrix is measured,
not all separately celled.
Turbulence carries both procedural formulas: the case-sensitive values
`turbulence` and `fractalNoise`, one/two-axis non-negative `baseFrequency`,
integer `numOctaves` capped at nine, signed `seed`, and the case-sensitive
Expand Down Expand Up @@ -469,27 +496,29 @@ cargo run -p n0_cli --bin n0 -- \
Skia source located them in an uninitialized runtime raster-pipeline dispatch:
x86 stayed on baseline non-fused Perlin arithmetic while ARM used fused NEON.
Initializing Skia before drawlist replay selects the fused AVX2 path on x86.
The complete 700-cell gate is byte-exact on ARM and hosted x86 without a
tolerance.
The same hosted workspace test covers the earlier two hundred
forty-eight-cell estate; all three hundred thirty-nine
Chromium-baked filter cells are exact on the current ARM host without a
tolerance. The filter estate contains 26 chassis/blur cells, 60 shadow-graph,
28 native drop-shadow, 27 color-matrix, 32 component-transfer, 38 blend, 37
morphology, and 91 turbulence/displacement cells. The complete corpus
contains 700 Chromium-baked cells plus 10 sampled frames, with 143 named
The 700-cell baseline is byte-exact on ARM and hosted x86 without a
tolerance. The forty-one-cell convolution rung keeps the complete 741-cell
gate byte-exact on ARM and hosted x86 without a new tolerance. All three
hundred eighty Chromium-baked filter cells are exact on both hosts.
The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native
drop-shadow, 27 color-matrix, 32 component-transfer, 38 blend, 37 morphology,
91 turbulence/displacement, and 41 convolution-rung cells. The complete corpus
contains 741 Chromium-baked cells plus 10 sampled frames, with 146 named
refusal rows. `feFlood`, `feComposite`,
`feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`,
`feComponentTransfer`, `feBlend`,
`feMorphology`, `feTurbulence`, `feDisplacementMap`, `feFuncR`, `feFuncG`,
`feFuncB`, `feFuncA`, `k1`–`k4`, `amplitude`, `exponent`, `intercept`,
`feMorphology`, `feConvolveMatrix`, `feTurbulence`, `feDisplacementMap`,
`feFuncR`, `feFuncG`, `feFuncB`, `feFuncA`, `k1`–`k4`, `amplitude`,
`exponent`, `intercept`,
`slope`, `tableValues`, blend-only `mode`, `baseFrequency`, `numOctaves`,
`seed`, `stitchTiles`, displacement `scale`, `xChannelSelector`, and
`yChannelSelector` close; `feOffset`, `feGaussianBlur`, `<filter>`,
`yChannelSelector`, `bias`, `divisor`, `edgeMode`, `kernelMatrix`,
convolution `order`, `preserveAlpha`, `targetX`, and `targetY` close;
`feOffset`, `feGaussianBlur`, `<filter>`,
`filter`, `color-interpolation-filters`, `in`, `in2`, `operator`, `result`,
`radius`, `dx`, `dy`, `stdDeviation`, `flood-color`, and `flood-opacity`
remain open for the named precision, applicability, resource, cascade, or
value remainder.
`radius`, `kernelUnitLength`, `dx`, `dy`, `stdDeviation`, `flood-color`, and
`flood-opacity` remain open for the named precision, applicability, resource,
cascade, or value remainder.
A stroke is centred, its width is a cascaded length in either spelling —
numbers, absolute units, `em`/`rem` against an authored or default
font-size, percentages against the normalized diagonal, and pure-length
Expand Down
Loading
Loading