mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 18:08:13 +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 {
|
pub fn pushed(&self) -> bool {
|
||||||
self.level + 1 == self.depth
|
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,
|
_ => 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
|
/// 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)
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
use core_types::attribute::{Attr, Opacity, RemoveAttr};
|
use core_types::attribute::{Attr, Opacity, RemoveAttr};
|
||||||
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
|
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
|
||||||
use core_types::extent::{ExtentIn, LevelIn, ValueIn};
|
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;
|
use core_types::Ctx;
|
||||||
|
|
||||||
core_types::attribute! {
|
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"))]
|
#[node_macro::node(category("Test"))]
|
||||||
fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<Opacity>) {
|
fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<Opacity>) {
|
||||||
(element, 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 {
|
struct IndexSourceNode {
|
||||||
layout: Layout,
|
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]
|
#[test]
|
||||||
fn reducer_folds_a_repeated_level() {
|
fn reducer_folds_a_repeated_level() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user