mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add the rank-model extend concatenating two sources at the top level
This commit is contained in:
@@ -75,4 +75,10 @@ impl LevelIn {
|
||||
pub fn pushed(&self) -> bool {
|
||||
self.level + 1 == self.depth
|
||||
}
|
||||
|
||||
/// Whether the query targets the topmost level; `pushed` under the name a
|
||||
/// non-creator (concat, remap) reads naturally.
|
||||
pub fn top(&self) -> bool {
|
||||
self.pushed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,16 @@ impl Extent {
|
||||
_ => Extent::Free,
|
||||
})
|
||||
}
|
||||
|
||||
/// The sum of two extents, used to concatenate a level; a free operand
|
||||
/// counts as one lane, so a scalar edge joins a concat as a single item.
|
||||
pub fn sum(a: GPoll<Extent>, b: GPoll<Extent>) -> GPoll<Extent> {
|
||||
let lanes = |extent| match extent {
|
||||
Extent::Exactly(count) => count,
|
||||
Extent::Free => 1,
|
||||
};
|
||||
a.zip(b).map(|(a, b)| Extent::Exactly(lanes(a) + lanes(b)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A query over a node's nesting levels: one level, the product below or above
|
||||
|
||||
@@ -490,6 +490,15 @@ impl<'a, N> LazyInput<'a, N> {
|
||||
{
|
||||
self.cell.eval_input(self.input_index, self.node, ctx)
|
||||
}
|
||||
|
||||
/// The edge's composite extent, for kernels that split or shift indices
|
||||
/// over their sources.
|
||||
pub fn extent<Input>(&self, ctx: &Input, at: Level) -> GPoll<Extent>
|
||||
where
|
||||
N: Node<Input>,
|
||||
{
|
||||
self.node.extent(ctx, at)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
use core_types::attribute::{Attr, Opacity, RemoveAttr};
|
||||
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
|
||||
use core_types::extent::{ExtentIn, LevelIn, ValueIn};
|
||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt};
|
||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
|
||||
use core_types::Ctx;
|
||||
|
||||
core_types::attribute! {
|
||||
@@ -132,6 +132,39 @@ fn repeat_faded_extent(content: ExtentIn<'_>, count: ValueIn<'_, u32>, level: Le
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank-model Extend: the output's top level is `base`'s lanes followed by
|
||||
/// `new`'s, each side evaluated within its own index range.
|
||||
#[node_macro::node(category("Test"), extent(extend_extent))]
|
||||
fn extend<T>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
base: impl Node<Context<'_>, Output = T>,
|
||||
new: impl Node<Context<'_>, Output = T>,
|
||||
) -> Result<T, Interrupt> {
|
||||
let split = match base.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("extend over a non-exact base extent").into()),
|
||||
};
|
||||
let lane = ctx.innermost_index();
|
||||
match lane < split {
|
||||
true => base.eval(ctx),
|
||||
false => {
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(lane - split);
|
||||
new.eval(&shifted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The top level sums both sides; inner levels forward the base's, which the
|
||||
/// new side must match (rectangular).
|
||||
fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => Extent::sum(base.at(level), new.at(level)),
|
||||
false => base.at(level),
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<Opacity>) {
|
||||
(element, Attr(opacity))
|
||||
@@ -264,6 +297,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct LeveledSourceNode {
|
||||
layout: Layout,
|
||||
elements: Vec<f64>,
|
||||
field: Option<(usize, f64)>,
|
||||
}
|
||||
|
||||
impl<'e> Node<ContextImpl<'e>> for LeveledSourceNode {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
let element = self.elements[input.innermost_index() as usize % self.elements.len()];
|
||||
let dst = stack::push(self.layout.frame_bytes());
|
||||
unsafe {
|
||||
dst.cast::<f64>().write(element);
|
||||
if let Some((offset, value)) = self.field {
|
||||
dst.add(offset).cast::<f64>().write(value);
|
||||
}
|
||||
}
|
||||
stack::pop(dst);
|
||||
GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }))
|
||||
}
|
||||
|
||||
fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll<Extent> {
|
||||
GPoll::Final(Extent::Exactly(self.elements.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
struct IndexSourceNode {
|
||||
layout: Layout,
|
||||
}
|
||||
@@ -597,6 +661,82 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_concatenates_the_top_level_and_fills_the_union() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let leveled_f64_layout = |names: &[&'static str]| {
|
||||
let writes: Vec<core_types::record::FieldWrite> = names
|
||||
.iter()
|
||||
.map(|name| core_types::record::FieldWrite {
|
||||
name,
|
||||
level: 0,
|
||||
size: 8,
|
||||
align: 8,
|
||||
read_erased: <Opacity as AttributeMarker>::read_erased,
|
||||
repark: None,
|
||||
})
|
||||
.collect();
|
||||
Layout::default().with_writes(1, core_types::record::element_write::<f64>(), &writes)
|
||||
};
|
||||
let base_layout = leveled_f64_layout(&[Opacity::NAME]);
|
||||
let new_layout = leveled_f64_layout(&[Length::NAME]);
|
||||
let union = Layout::union(&[&base_layout, &new_layout]);
|
||||
reserve_for(&[&base_layout, &new_layout, &union]);
|
||||
|
||||
let base = LeveledSourceNode {
|
||||
layout: base_layout.clone(),
|
||||
elements: vec![10., 11.],
|
||||
field: Some((base_layout.offset_of(Opacity::NAME, 0).unwrap(), 0.5)),
|
||||
};
|
||||
let new = LeveledSourceNode {
|
||||
layout: new_layout.clone(),
|
||||
elements: vec![100., 101., 102.],
|
||||
field: Some((new_layout.offset_of(Length::NAME, 0).unwrap(), 7.)),
|
||||
};
|
||||
let meta = core_types::record::LayoutMeta {
|
||||
sources: vec![0, 1],
|
||||
reads: vec![],
|
||||
element: core_types::record::ElementSpec::Carried,
|
||||
writes: vec![],
|
||||
removes: vec![],
|
||||
level_delta: 0,
|
||||
};
|
||||
let node = install(
|
||||
ExtendNode::new(RecordSource::new(base, &base_layout, &union), RecordSource::new(new, &new_layout, &union), &union),
|
||||
meta,
|
||||
&[Some(&base_layout), Some(&new_layout)],
|
||||
);
|
||||
let out = Node::<ContextImpl>::layout(&node).clone();
|
||||
assert_eq!(out.depth, 1);
|
||||
assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(5)), "the top level sums both sides");
|
||||
|
||||
let head = ctx.index_head();
|
||||
let expected = [10., 11., 100., 101., 102.];
|
||||
for (lane, &element) in expected.iter().enumerate() {
|
||||
let mark = stack::sp();
|
||||
let scoped = ctx.promoted(&head, lane as u64);
|
||||
let GPoll::Final(value) = node.eval(&scoped) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = out.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, element);
|
||||
let opacity = unsafe { rec.read::<f64>(out.offset_of(Opacity::NAME, 0).unwrap()) };
|
||||
let length = unsafe { rec.read::<f64>(out.offset_of(Length::NAME, 0).unwrap()) };
|
||||
match lane < 2 {
|
||||
// The base side wrote its opacity; length fills from the census.
|
||||
true => assert_eq!((opacity, length), (0.5, 0.)),
|
||||
// The new side wrote its length; opacity fills from the census.
|
||||
false => assert_eq!((opacity, length), (1., 7.)),
|
||||
}
|
||||
// SAFETY: the element and attrs were read out above, so no borrow into this lane's frames remains.
|
||||
unsafe { stack::rewind(mark) };
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reducer_folds_a_repeated_level() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user