mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
New nodes: Sum, Average, Minimum, Maximum, Any, All (#4344)
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
committed by
Dennis Kobert
parent
04d6c0d5cf
commit
20eb5ccb8a
@@ -33,4 +33,4 @@ pub use promote::{Promotion, assert_promoted, register_element_promote, register
|
||||
pub use route::{RecordSource, SourcePlan};
|
||||
pub use run::{Group, GroupItem, RunBuilder, RunColumn, RunView, run_to_owned_list};
|
||||
pub use serve::{FrameClaim, MaterializedSpan, Served, SlotRun, serve_input};
|
||||
pub use testkit::{LiftedSource, ServedRecord, capture, test_frames};
|
||||
pub use testkit::{LiftedSource, ServedRecord, capture, fixtures as test_fixtures, test_frames};
|
||||
|
||||
@@ -180,3 +180,330 @@ mod tests {
|
||||
assert_eq!(served.attr::<Transform>(), DAffine2::from_translation(DVec2::new(3., 4.)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The node-test fixtures: hand-wired record sources and layout installers,
|
||||
/// so a node crate's tests drive a generated node without the compiler pass.
|
||||
pub mod fixtures {
|
||||
use super::super::frames::Frames;
|
||||
use super::super::layout::{FieldWrite, Layout, LayoutMeta, RecordLayout, element_write};
|
||||
use super::super::serve::{FrameClaim, Served};
|
||||
use super::test_frames;
|
||||
use crate::SourceId;
|
||||
use crate::arena::Arena;
|
||||
use crate::attribute::{Attribute, Opacity, Transform};
|
||||
use crate::context::{ContextImpl, EvalScope, ExtractArena, ExtractIndex, ExtractIndices};
|
||||
use crate::gpoll::{Extent, GPoll};
|
||||
use crate::node::Node;
|
||||
use crate::value::ValueSource;
|
||||
use glam::DAffine2;
|
||||
|
||||
/// A one-record source with fixed `f64` fields, optionally served partial.
|
||||
pub struct RecordSourceNode<E> {
|
||||
pub layout: Layout,
|
||||
pub element: E,
|
||||
pub fields: Vec<(&'static str, f64)>,
|
||||
pub partial: bool,
|
||||
}
|
||||
|
||||
impl<C, E: Copy + Send + Sync + dyn_any::StaticTypeSized + 'static> Node<C> for RecordSourceNode<E> {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(self.element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
for (name, field) in &self.fields {
|
||||
write_field_at(&mut frame, &self.layout, name, 0, *field);
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
let served = unsafe { frame.finish_served() };
|
||||
match self.partial {
|
||||
true => GPoll::Partial(served),
|
||||
false => GPoll::Final(served),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A level of `f64` lanes, each optionally carrying one fixed field.
|
||||
pub struct LeveledSourceNode {
|
||||
pub layout: Layout,
|
||||
pub elements: Vec<f64>,
|
||||
pub field: Option<(&'static str, f64)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let element = self.elements[input.innermost_index() as usize % self.elements.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
if let Some((name, value)) = self.field {
|
||||
write_field_at(&mut frame, &self.layout, name, 0, value);
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.elements.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A level of `f64` lanes each carrying a transform.
|
||||
pub struct LeveledTransformSource {
|
||||
pub layout: Layout,
|
||||
pub rows: Vec<(f64, DAffine2)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledTransformSource {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let (element, transform) = self.rows[input.innermost_index() as usize % self.rows.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.rows.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves lanes carrying both a Transform no gather kernel declares and an
|
||||
/// Opacity one does, so a carried column can be told apart from a written one.
|
||||
pub struct LeveledCarriedSource {
|
||||
pub layout: Layout,
|
||||
pub rows: Vec<(f64, DAffine2, f64)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledCarriedSource {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let (element, transform, opacity) = self.rows[input.innermost_index() as usize % self.rows.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
|
||||
write_attr_at::<Opacity>(&mut frame, &self.layout, opacity);
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.rows.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A leveled source that keeps its count to itself: the extent is a lower
|
||||
/// bound and lanes past the data answer the past-end signal.
|
||||
pub struct DrainSourceNode {
|
||||
pub layout: Layout,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for DrainSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let lane = input.innermost_index();
|
||||
if lane >= self.count as u64 {
|
||||
return GPoll::past_end();
|
||||
}
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(lane as f64, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::AtLeast(0))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Depth-0 content varying per copy: serves the enclosing (pushed) level's
|
||||
/// index, which sits one link above the content's own innermost lane.
|
||||
pub struct IndexSourceNode {
|
||||
pub layout: Layout,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex + ExtractIndices> Node<C> for IndexSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let element = input.try_index().and_then(|mut indices| indices.nth(1)).unwrap_or(0) as f64;
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a field at the layout's resolved offset, the wiring-proven pairing
|
||||
/// a generated node performs.
|
||||
pub fn write_field_at<T: Copy + 'static>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, name: &str, level: u8, value: T) {
|
||||
let field = layout
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| field.name == name && field.level == level)
|
||||
.expect("the layout carries the written field");
|
||||
assert_eq!(field.type_id, std::any::TypeId::of::<T>(), "the field was declared at this value type");
|
||||
// SAFETY: the offset is this layout's own, at the field's declared type.
|
||||
unsafe { frame.attr_at(field.offset, value) };
|
||||
}
|
||||
|
||||
/// [`write_field_at`] for a census marker at level 0.
|
||||
pub fn write_attr_at<A: Attribute>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, value: A::Value<'static>)
|
||||
where
|
||||
A::Value<'static>: Copy + 'static,
|
||||
{
|
||||
write_field_at(frame, layout, A::NAME, 0, value);
|
||||
}
|
||||
|
||||
pub fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
fn f64_fields(names: &[&'static str]) -> Vec<FieldWrite> {
|
||||
names
|
||||
.iter()
|
||||
.map(|name| FieldWrite {
|
||||
name,
|
||||
level: 0,
|
||||
size: 8,
|
||||
align: 8,
|
||||
type_id: std::any::TypeId::of::<f64>(),
|
||||
read_erased: <Opacity as Attribute>::read_erased,
|
||||
repark: None,
|
||||
content_hash: None,
|
||||
content_eq: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A depth-0 `f64` record layout carrying the named `f64` fields.
|
||||
pub fn f64_layout(names: &[&'static str]) -> Layout {
|
||||
Layout::default().with_writes(0, element_write::<f64>(), &f64_fields(names))
|
||||
}
|
||||
|
||||
/// A one-level `f64` layout carrying the named `f64` fields.
|
||||
pub fn leveled_f64_layout(names: &[&'static str]) -> Layout {
|
||||
Layout::default().with_writes(1, element_write::<f64>(), &f64_fields(names))
|
||||
}
|
||||
|
||||
/// Frame space sized for the layouts, with a floor for small fixtures.
|
||||
pub fn frames_for(layouts: &[&Layout]) -> Frames<'static> {
|
||||
test_frames(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12))
|
||||
}
|
||||
|
||||
/// Installs the layout the compiler pass would resolve for `node` over the
|
||||
/// given input layouts. The fixtures wire constants into every eager input,
|
||||
/// which the pass records as lane-invariant.
|
||||
pub fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||
let resolved = RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
lane_invariant: u32::MAX,
|
||||
..meta.resolve(inputs)
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
|
||||
node
|
||||
}
|
||||
|
||||
/// Installs a flipped node's output layout directly.
|
||||
pub fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
|
||||
let bundle = RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
layout: layout.clone(),
|
||||
lane_invariant: u32::MAX,
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
|
||||
node
|
||||
}
|
||||
|
||||
/// A constant as a value source with its layout.
|
||||
pub fn lifted_value<T: Clone + Send + Sync + dyn_any::StaticTypeSized + 'static>(value: T) -> (ValueSource<T>, Layout)
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
let lift = ValueSource::new(value);
|
||||
let layout = Node::<ContextImpl>::layout(&lift).clone();
|
||||
(lift, layout)
|
||||
}
|
||||
|
||||
pub fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode<f64> {
|
||||
RecordSourceNode {
|
||||
layout: layout.clone(),
|
||||
element,
|
||||
fields: vec![],
|
||||
partial: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,14 @@ pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// The lane-normalized cache key and arena generation of one evaluation, so a
|
||||
/// node can keep per-evaluation scratch across its lanes.
|
||||
pub fn eval_key<'e, C: CacheHash + crate::context::InjectIndex + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena> + Copy>(ctx: &C) -> (u64, u64) {
|
||||
let mut keyed = *ctx;
|
||||
keyed.set_index(0);
|
||||
(cache_key(&keyed), ctx.arena().generation())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ConstructionError {
|
||||
Arity { expected: usize, got: usize },
|
||||
|
||||
Reference in New Issue
Block a user