mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Hoist a batch input only where the compiler proves it cannot vary per lane
This commit is contained in:
@@ -167,6 +167,7 @@ impl DocumentNode {
|
||||
original_location: self.original_location,
|
||||
skip_deduplication: self.skip_deduplication,
|
||||
context_features: self.context_features,
|
||||
lane_invariant_inputs: 0,
|
||||
resolved: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,10 @@ pub struct ProtoNode {
|
||||
pub original_location: OriginalLocation,
|
||||
pub skip_deduplication: bool,
|
||||
pub(crate) context_features: ContextDependencies,
|
||||
/// Bit `i` marks input `i` as invariant under the innermost index. Inputs at
|
||||
/// position 32 and above never set a bit, so they read as varying.
|
||||
#[serde(skip)]
|
||||
pub(crate) lane_invariant_inputs: u32,
|
||||
#[serde(skip)]
|
||||
pub(crate) resolved: Resolved,
|
||||
}
|
||||
@@ -164,6 +168,7 @@ impl Default for ProtoNode {
|
||||
original_location: OriginalLocation::default(),
|
||||
skip_deduplication: false,
|
||||
context_features: Default::default(),
|
||||
lane_invariant_inputs: 0,
|
||||
resolved: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -205,6 +210,7 @@ impl ProtoNode {
|
||||
},
|
||||
skip_deduplication: false,
|
||||
context_features: Default::default(),
|
||||
lane_invariant_inputs: 0,
|
||||
resolved: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -373,12 +379,14 @@ impl ProtoNetwork {
|
||||
|
||||
pub fn compute_layouts(&mut self) {
|
||||
for index in 0..self.nodes.len() {
|
||||
let lane_invariant = self.nodes[index].1.lane_invariant_inputs;
|
||||
let layout = {
|
||||
let node = &self.nodes[index].1;
|
||||
match &node.construction_args {
|
||||
ConstructionArgs::Value(value) => value.value_layout().map(|layout| core_types::record::RecordLayout {
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
lane_invariant,
|
||||
layout,
|
||||
}),
|
||||
ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| {
|
||||
@@ -386,7 +394,10 @@ impl ProtoNetwork {
|
||||
.iter()
|
||||
.map(|input| self.nodes[input.0 as usize].1.resolved.layout.as_ref().map(|resolved| &resolved.layout))
|
||||
.collect();
|
||||
meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| meta.resolve(&input_layouts))
|
||||
meta.sources.iter().all(|&source| input_layouts[source as usize].is_some()).then(|| core_types::record::RecordLayout {
|
||||
lane_invariant,
|
||||
..meta.resolve(&input_layouts)
|
||||
})
|
||||
}),
|
||||
ConstructionArgs::Inline(_) => None,
|
||||
}
|
||||
@@ -557,14 +568,21 @@ impl ProtoNetwork {
|
||||
};
|
||||
|
||||
// Compute the dependencies for each branch and combine all of them
|
||||
let mut lane_invariant_inputs = 0u32;
|
||||
for (input, &node) in inputs.iter().enumerate() {
|
||||
let branch = self.find_context_dependencies(node);
|
||||
|
||||
let reads_innermost = branch.0.features.contains(core_types::context::ContextFeatures::INDEX) && branch.0.index_levels.contains_level(0);
|
||||
if !reads_innermost && input < 32 {
|
||||
lane_invariant_inputs |= 1 << input;
|
||||
}
|
||||
|
||||
let mut lifted = branch.0.clone();
|
||||
lifted.index_levels = lifted.index_levels.lifted(0, pushed_levels.get(input).copied().unwrap_or(0));
|
||||
combined_deps |= &lifted;
|
||||
branch_dependencies.push(branch);
|
||||
}
|
||||
self.nodes[node_index].1.lane_invariant_inputs = lane_invariant_inputs;
|
||||
let mut new_deps = combined_deps.clone();
|
||||
|
||||
// Remove requirements which this node provides
|
||||
|
||||
@@ -300,6 +300,10 @@ pub struct RecordLayout {
|
||||
pub layout: Layout,
|
||||
pub frame_bytes: usize,
|
||||
pub plan: Vec<(usize, usize, usize)>,
|
||||
/// Inputs whose value cannot change with the innermost index, as a bitmask
|
||||
/// over input positions. Empty is the safe default: an uninstalled layout
|
||||
/// rebinds every input per lane.
|
||||
pub lane_invariant: u32,
|
||||
}
|
||||
|
||||
/// its registry entry so the compiler can fold each wire's layout without
|
||||
@@ -400,7 +404,12 @@ impl LayoutMeta {
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
RecordLayout { layout, frame_bytes, plan }
|
||||
RecordLayout {
|
||||
layout,
|
||||
frame_bytes,
|
||||
plan,
|
||||
lane_invariant: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -273,6 +273,7 @@ mod tests {
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
layout: layout.clone(),
|
||||
lane_invariant: u32::MAX,
|
||||
});
|
||||
RecordExtract::new(graph, &layout)
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>));
|
||||
}
|
||||
state.push(quote!(pub(super) __frame_bytes: usize));
|
||||
state.push(quote!(pub(super) __lane_invariant: u32));
|
||||
state.extend(reading_secondary_indices(&struct_regular_fields, record_skips_carrier).into_iter().map(|index| {
|
||||
let slot = format_ident!("__in_{index}");
|
||||
quote!(pub(super) #slot: gcore::record::Layout)
|
||||
@@ -231,7 +232,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
}));
|
||||
state
|
||||
} else if routing_generic.is_some() {
|
||||
let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)];
|
||||
let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout), quote!(pub(super) __lane_invariant: u32)];
|
||||
state.extend(
|
||||
routing_value_indices(&struct_regular_fields, routing_generic.as_ref().expect("guarded by the arm"))
|
||||
.into_iter()
|
||||
@@ -420,6 +421,9 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
// offsets from the carrier layout; `new` cannot fill that state.
|
||||
let routing_layout_param = (routing_generic.is_some() || opaque).then(|| quote!(__layout: &gcore::record::Layout,)).into_iter();
|
||||
let routing_layout_init = (routing_generic.is_some() || opaque).then(|| quote!(__layout: __layout.clone(),)).into_iter();
|
||||
// The lane-invariance mask arrives with the resolved layout, so `new` starts
|
||||
// from the safe empty mask.
|
||||
let routing_invariant_init = routing_generic.is_some().then(|| quote!(__lane_invariant: 0,)).into_iter();
|
||||
let routing_value_layouts: Vec<usize> = routing_generic.as_ref().map(|generic| routing_value_indices(&struct_regular_fields, generic)).unwrap_or_default();
|
||||
let routing_in_params = routing_value_layouts.iter().map(|index| {
|
||||
let slot = format_ident!("__in_{index}");
|
||||
@@ -503,6 +507,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
Self {
|
||||
#(#all_field_inits,)*
|
||||
#(#routing_layout_init)*
|
||||
#(#routing_invariant_init)*
|
||||
#(#routing_in_inits)*
|
||||
#(#flip_layout_inits)*
|
||||
#(#flip_read_inits)*
|
||||
@@ -2052,7 +2057,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
// in the loop (or in the tail, for a carrier); everything else is
|
||||
// batch-invariant and hoists.
|
||||
let hoists = |index: usize| matches!(ir::value_binding(&node, index), ValueBinding::Materialized) || !node.inputs[index].subject;
|
||||
let hoisted_binds = regular_fields
|
||||
let hoisted_binds: Vec<TokenStream2> = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| matches!(field.ty, ParsedFieldType::Regular(_)) && hoists(*index))
|
||||
@@ -2069,13 +2074,15 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let hoisted_clamps = regular_fields
|
||||
})
|
||||
.collect();
|
||||
let hoisted_clamps: Vec<TokenStream2> = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| matches!(field.ty, ParsedFieldType::Regular(_)) && hoists(*index))
|
||||
.filter_map(|(_, field)| clamp_tokens(field));
|
||||
let lane_binds = regular_fields
|
||||
.filter_map(|(_, field)| clamp_tokens(field))
|
||||
.collect();
|
||||
let lane_binds: Vec<TokenStream2> = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| match field.ty {
|
||||
@@ -2086,10 +2093,26 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
let body = bind_body(index, field, true);
|
||||
let clamp = clamp_tokens(field);
|
||||
quote!(#body #clamp)
|
||||
});
|
||||
})
|
||||
.collect();
|
||||
// The rebind path with nothing hoisted: every non-carrier input binds
|
||||
// fresh per lane, so an index-dependent edge reaches its own lane.
|
||||
let rebound_lane_binds: Vec<TokenStream2> = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| match field.ty {
|
||||
ParsedFieldType::Node(_) => true,
|
||||
ParsedFieldType::Regular(_) => !matches!(ir::value_binding(&node, *index), ValueBinding::Carrier),
|
||||
})
|
||||
.map(|(index, field)| {
|
||||
let body = bind_body(index, field, true);
|
||||
let clamp = clamp_tokens(field);
|
||||
quote!(#body #clamp)
|
||||
})
|
||||
.collect();
|
||||
// A hoisted value is moved into every lane's kernel call, so each
|
||||
// lane consumes a clone; view and borrow binds copy freely.
|
||||
let lane_rebinds = regular_fields
|
||||
let lane_rebinds: Vec<TokenStream2> = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| {
|
||||
@@ -2099,32 +2122,23 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
.map(|(_, field)| {
|
||||
let name = &field.pat_ident.ident;
|
||||
quote!(let #name = ::core::clone::Clone::clone(&#name);)
|
||||
});
|
||||
quote! {
|
||||
#batch_signature
|
||||
{
|
||||
let ::core::option::Option::Some(__scratch) = __scratch else {
|
||||
return #core_types::node::BatchStatus::NeedBuffer;
|
||||
};
|
||||
let ::core::option::Option::Some(__len) = __range.end.checked_sub(__range.start).and_then(|__len| usize::try_from(__len).ok()) else {
|
||||
return #core_types::node::BatchStatus::InvalidRange;
|
||||
};
|
||||
let __node_layout = <Self as #core_types::node::Node<#ctx_ident>>::layout(self);
|
||||
let __stride = __node_layout.lane_stride();
|
||||
if __scratch.len() * 8 < __len * __stride {
|
||||
return #core_types::node::BatchStatus::InvalidRange;
|
||||
}
|
||||
let __entry_mark = #core_types::record::stack::sp();
|
||||
let _entry_sp = __entry_mark;
|
||||
let __cell = #cell_constructor;
|
||||
let __base_ctx = {
|
||||
let mut __ctx = *__input;
|
||||
#core_types::context::InjectIndex::set_index(&mut __ctx, __range.start);
|
||||
__ctx
|
||||
};
|
||||
let __input = &__base_ctx;
|
||||
#(#hoisted_binds)*
|
||||
#(#hoisted_clamps)*
|
||||
})
|
||||
.collect();
|
||||
// A hoisted input past bit 31 has no bit to check, so it never reads
|
||||
// back as invariant and the node keeps rebinding it.
|
||||
let hoistable_mask: u32 = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| matches!(field.ty, ParsedFieldType::Regular(_)) && hoists(*index) && *index < 32)
|
||||
.fold(0, |mask, (index, _)| mask | (1u32 << index));
|
||||
let fill_loop = |hoisted: Vec<TokenStream2>, clamps: Vec<TokenStream2>, rebinds: Vec<TokenStream2>, binds: Vec<TokenStream2>| {
|
||||
let hoisted = hoisted.into_iter();
|
||||
let clamps = clamps.into_iter();
|
||||
let rebinds = rebinds.into_iter();
|
||||
let binds = binds.into_iter();
|
||||
quote! {
|
||||
#(#hoisted)*
|
||||
#(#clamps)*
|
||||
let __frames = __scratch.as_mut_ptr().cast::<u8>();
|
||||
let mut __finality = #core_types::gpoll::Finality::AllFinal;
|
||||
let mut __filled = __len;
|
||||
@@ -2136,8 +2150,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
let __lane_mark = #core_types::record::stack::sp();
|
||||
let _entry_sp = __lane_mark;
|
||||
let __cell = __cell.snapshot();
|
||||
#(#lane_rebinds)*
|
||||
#(#lane_binds)*
|
||||
#(#rebinds)*
|
||||
#(#binds)*
|
||||
#lane_poll
|
||||
let __value = match __poll {
|
||||
#core_types::gpoll::GPoll::Final(__value) => __value,
|
||||
@@ -2171,6 +2185,50 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
// records of the node's layout.
|
||||
#core_types::node::BatchStatus::Filled(unsafe { #core_types::node::RecordBatchMut::new(__scratch, __filled, __node_layout) }, __finality, __hint)
|
||||
}
|
||||
};
|
||||
let hoisted_fill = fill_loop(hoisted_binds, hoisted_clamps, lane_rebinds, lane_binds);
|
||||
let rebound_fill = fill_loop(Vec::new(), Vec::new(), Vec::new(), rebound_lane_binds);
|
||||
// With nothing hoisted the two fills are the same code, and the mask
|
||||
// test would read as an empty bit mask.
|
||||
let selected_fill = match hoistable_mask {
|
||||
0 => hoisted_fill,
|
||||
mask => quote! {
|
||||
// Binding once at the base lane is sound only where the
|
||||
// installed layout marks every hoisted input invariant under
|
||||
// the innermost index.
|
||||
const __HOISTABLE: u32 = #mask;
|
||||
if (self.__lane_invariant & __HOISTABLE) == __HOISTABLE {
|
||||
#hoisted_fill
|
||||
} else {
|
||||
#rebound_fill
|
||||
}
|
||||
},
|
||||
};
|
||||
quote! {
|
||||
#batch_signature
|
||||
{
|
||||
let ::core::option::Option::Some(__scratch) = __scratch else {
|
||||
return #core_types::node::BatchStatus::NeedBuffer;
|
||||
};
|
||||
let ::core::option::Option::Some(__len) = __range.end.checked_sub(__range.start).and_then(|__len| usize::try_from(__len).ok()) else {
|
||||
return #core_types::node::BatchStatus::InvalidRange;
|
||||
};
|
||||
let __node_layout = <Self as #core_types::node::Node<#ctx_ident>>::layout(self);
|
||||
let __stride = __node_layout.lane_stride();
|
||||
if __scratch.len() * 8 < __len * __stride {
|
||||
return #core_types::node::BatchStatus::InvalidRange;
|
||||
}
|
||||
let __entry_mark = #core_types::record::stack::sp();
|
||||
let _entry_sp = __entry_mark;
|
||||
let __cell = #cell_constructor;
|
||||
let __base_ctx = {
|
||||
let mut __ctx = *__input;
|
||||
#core_types::context::InjectIndex::set_index(&mut __ctx, __range.start);
|
||||
__ctx
|
||||
};
|
||||
let __input = &__base_ctx;
|
||||
#selected_fill
|
||||
}
|
||||
}
|
||||
}
|
||||
// The eager forward runs the shared copy-out loop with statically
|
||||
@@ -2273,11 +2331,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
Some(quote! {
|
||||
#(#write_installs)*
|
||||
self.__frame_bytes = __resolved.frame_bytes;
|
||||
self.__lane_invariant = __resolved.lane_invariant;
|
||||
#plan
|
||||
self.__layout = __resolved.layout;
|
||||
})
|
||||
} else if routing_generic.is_some() {
|
||||
Some(quote! {
|
||||
self.__lane_invariant = __resolved.lane_invariant;
|
||||
self.__layout = __resolved.layout;
|
||||
})
|
||||
} else {
|
||||
@@ -2442,6 +2502,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
#(#plan_default)*
|
||||
#(#marker_init)*
|
||||
__frame_bytes: 0,
|
||||
__lane_invariant: 0,
|
||||
#(#read_names)*
|
||||
#(#write_defaults)*
|
||||
#(#mat_cache_defaults)*
|
||||
|
||||
@@ -601,7 +601,13 @@ mod tests {
|
||||
}
|
||||
|
||||
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, meta.resolve(inputs));
|
||||
// The fixtures wire constants into every eager input, which the compiler
|
||||
// pass records as lane-invariant.
|
||||
let resolved = core_types::record::RecordLayout {
|
||||
lane_invariant: u32::MAX,
|
||||
..meta.resolve(inputs)
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
|
||||
node
|
||||
}
|
||||
|
||||
@@ -610,6 +616,7 @@ mod tests {
|
||||
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
|
||||
|
||||
@@ -288,7 +288,13 @@ mod tests {
|
||||
}
|
||||
|
||||
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, meta.resolve(inputs));
|
||||
// The fixtures wire constants into every eager input, which the compiler
|
||||
// pass records as lane-invariant.
|
||||
let resolved = record::RecordLayout {
|
||||
lane_invariant: u32::MAX,
|
||||
..meta.resolve(inputs)
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
|
||||
node
|
||||
}
|
||||
|
||||
@@ -297,6 +303,7 @@ mod tests {
|
||||
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
|
||||
|
||||
@@ -278,6 +278,7 @@ mod tests {
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
layout: layout.clone(),
|
||||
lane_invariant: u32::MAX,
|
||||
},
|
||||
);
|
||||
let GPoll::Final(result) = Node::<ContextImpl>::eval(&graph, &ctx) else {
|
||||
|
||||
Reference in New Issue
Block a user