mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 10:38:11 +08:00
Structure record stack rewinds as scope guards and make reserve unsafe
This commit is contained in:
@@ -5,7 +5,7 @@ extern crate proc_macro;
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{DeriveInput, GenericParam, Lifetime, LifetimeParam, TypeParamBound, parse_macro_input};
|
use syn::{DeriveInput, GenericParam, Lifetime, TypeParamBound, parse_macro_input};
|
||||||
|
|
||||||
/// Derives an implementation for the [`DynAny`] trait.
|
/// Derives an implementation for the [`DynAny`] trait.
|
||||||
///
|
///
|
||||||
@@ -40,37 +40,45 @@ pub fn system_desc_derive(input: TokenStream) -> TokenStream {
|
|||||||
let struct_name = &ast.ident;
|
let struct_name = &ast.ident;
|
||||||
let generics = &ast.generics;
|
let generics = &ast.generics;
|
||||||
|
|
||||||
let static_params = replace_lifetimes(generics, "'static");
|
let static_params = generic_arguments(generics, "'static");
|
||||||
let dyn_params = replace_lifetimes(generics, "'dyn_any");
|
let dyn_params = generic_arguments(generics, "'dyn_any");
|
||||||
|
|
||||||
let old_params = &generics.params.iter().collect::<Vec<_>>();
|
let impl_params = generics.params.iter().map(|param| match param {
|
||||||
|
GenericParam::Type(t) => {
|
||||||
|
let mut t = t.clone();
|
||||||
|
t.bounds.push(TypeParamBound::Lifetime(Lifetime::new("'static", Span::call_site())));
|
||||||
|
quote! {#t}
|
||||||
|
}
|
||||||
|
param => quote! {#param},
|
||||||
|
});
|
||||||
quote! {
|
quote! {
|
||||||
unsafe impl<'dyn_any, #(#old_params,)*> dyn_any::StaticType for #struct_name <#(#dyn_params,)*> {
|
unsafe impl<'dyn_any, #(#impl_params,)*> dyn_any::StaticType for #struct_name <#(#dyn_params,)*> {
|
||||||
type Static = #struct_name <#(#static_params,)*>;
|
type Static = #struct_name <#(#static_params,)*>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_lifetimes(generics: &syn::Generics, replacement: &str) -> Vec<proc_macro2::TokenStream> {
|
/// The struct's generic parameters as argument tokens: bare idents for type
|
||||||
|
/// and const parameters (bounds are illegal in argument position), the
|
||||||
|
/// replacement for lifetimes.
|
||||||
|
fn generic_arguments(generics: &syn::Generics, replacement: &str) -> Vec<proc_macro2::TokenStream> {
|
||||||
generics
|
generics
|
||||||
.params
|
.params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|param| {
|
.map(|param| match param {
|
||||||
let param = match param {
|
GenericParam::Lifetime(_) => {
|
||||||
GenericParam::Lifetime(_) => GenericParam::Lifetime(LifetimeParam::new(Lifetime::new(replacement, Span::call_site()))),
|
let lifetime = Lifetime::new(replacement, Span::call_site());
|
||||||
GenericParam::Type(t) => {
|
quote! {#lifetime}
|
||||||
let mut t = t.clone();
|
}
|
||||||
t.bounds.iter_mut().for_each(|bond| {
|
GenericParam::Type(t) => {
|
||||||
if let TypeParamBound::Lifetime(t) = bond {
|
let ident = &t.ident;
|
||||||
*t = Lifetime::new(replacement, Span::call_site())
|
quote! {#ident}
|
||||||
}
|
}
|
||||||
});
|
GenericParam::Const(c) => {
|
||||||
GenericParam::Type(t.clone())
|
let ident = &c.ident;
|
||||||
}
|
quote! {#ident}
|
||||||
c => c.clone(),
|
}
|
||||||
};
|
|
||||||
quote! {#param}
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,14 +178,16 @@ impl DynamicExecutor {
|
|||||||
.and_then(EdgeHandle::record_edge)
|
.and_then(EdgeHandle::record_edge)
|
||||||
.ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?;
|
.ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?;
|
||||||
let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
|
let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
core_types::record::stack::reserve(self.tree.stack_need());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let generations = self.runtime.snapshot();
|
unsafe { core_types::record::stack::reserve(self.tree.stack_need()); } let generations = self.runtime.snapshot();
|
||||||
let scope = EvalScope::new(snapshot.try_real_time(), snapshot.try_animation_time(), snapshot.try_pointer_position(), &generations, &arena);
|
let scope = EvalScope::new(snapshot.try_real_time(), snapshot.try_animation_time(), snapshot.try_pointer_position(), &generations, &arena);
|
||||||
let Some(ctx) = snapshot.rehydrate(&scope) else {
|
let Some(ctx) = snapshot.rehydrate(&scope) else {
|
||||||
return Err(IntrospectError::NoData);
|
return Err(IntrospectError::NoData);
|
||||||
};
|
};
|
||||||
let layout = core_types::node::Node::<ContextImpl>::layout(&edge);
|
let layout = core_types::node::Node::<ContextImpl>::layout(&edge);
|
||||||
let mark = core_types::record::stack::sp();
|
// SAFETY: the read closure finishes inside the scope, so no record
|
||||||
|
// above the entry survives it.
|
||||||
|
let _scope = unsafe { core_types::record::stack::ScopeGuard::enter() };
|
||||||
let result = if layout.depth > 0 {
|
let result = if layout.depth > 0 {
|
||||||
match core_types::record::materialize_level(&edge, &ctx, &arena) {
|
match core_types::record::materialize_level(&edge, &ctx, &arena) {
|
||||||
core_types::record::LevelStatus::Batch(batch, _) => read(layout, batch, &arena),
|
core_types::record::LevelStatus::Batch(batch, _) => read(layout, batch, &arena),
|
||||||
@@ -209,8 +211,6 @@ impl DynamicExecutor {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// SAFETY: the read finished, so no record above the mark is live.
|
|
||||||
unsafe { core_types::record::stack::rewind(mark) };
|
|
||||||
result.ok_or(IntrospectError::NoData)
|
result.ok_or(IntrospectError::NoData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,8 +246,8 @@ where
|
|||||||
return Err("Output node not found in executor".into());
|
return Err("Output node not found in executor".into());
|
||||||
};
|
};
|
||||||
let mut arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
|
let mut arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
core_types::record::stack::reserve(self.tree.stack_need());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let result = eval_root(&mut arena, &self.runtime, &input, |ctx| match TaggedValue::from_edge(handle.duplicate(), ctx) {
|
unsafe { core_types::record::stack::reserve(self.tree.stack_need()); } let result = eval_root(&mut arena, &self.runtime, &input, |ctx| match TaggedValue::from_edge(handle.duplicate(), ctx) {
|
||||||
Ok(poll) => poll.map(Ok),
|
Ok(poll) => poll.map(Ok),
|
||||||
Err(error) => GPoll::Final(Err(error)),
|
Err(error) => GPoll::Final(Err(error)),
|
||||||
});
|
});
|
||||||
@@ -631,8 +631,8 @@ mod test {
|
|||||||
let generations = [];
|
let generations = [];
|
||||||
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
core_types::record::stack::reserve(layout.frame_bytes());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let GPoll::Final(value) = edge.eval(&ctx) else {
|
unsafe { core_types::record::stack::reserve(layout.frame_bytes()); } let GPoll::Final(value) = edge.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(unsafe { core_types::record::read_element::<u32>(layout.rec(&value)) }, 2);
|
assert_eq!(unsafe { core_types::record::read_element::<u32>(layout.rec(&value)) }, 2);
|
||||||
@@ -676,8 +676,8 @@ mod test {
|
|||||||
let handle = executor.tree().get(NodeId(1)).unwrap();
|
let handle = executor.tree().get(NodeId(1)).unwrap();
|
||||||
let layout = handle.layout().clone();
|
let layout = handle.layout().clone();
|
||||||
let edge = handle.duplicate().downcast_record::<f64>().unwrap();
|
let edge = handle.duplicate().downcast_record::<f64>().unwrap();
|
||||||
core_types::record::stack::reserve(executor.tree().stack_need());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let GPoll::Final(value) = edge.eval(&ctx) else {
|
unsafe { core_types::record::stack::reserve(executor.tree().stack_need()); } let GPoll::Final(value) = edge.eval(&ctx) else {
|
||||||
panic!("the flipped clone must evaluate over record wires, got a non-final poll");
|
panic!("the flipped clone must evaluate over record wires, got a non-final poll");
|
||||||
};
|
};
|
||||||
assert_eq!(unsafe { core_types::record::read_element::<f64>(layout.rec(&value)) }, 7.);
|
assert_eq!(unsafe { core_types::record::read_element::<f64>(layout.rec(&value)) }, 7.);
|
||||||
@@ -703,8 +703,8 @@ mod test {
|
|||||||
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
let edge = executor.tree().get(NodeId(2)).unwrap().downcast_record::<graphene_std::raster::color::Color>().unwrap();
|
let edge = executor.tree().get(NodeId(2)).unwrap().downcast_record::<graphene_std::raster::color::Color>().unwrap();
|
||||||
core_types::record::stack::reserve(executor.tree().stack_need());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let result = edge.eval(&ctx);
|
unsafe { core_types::record::stack::reserve(executor.tree().stack_need()); } let result = edge.eval(&ctx);
|
||||||
// The empty raster level folds to an empty palette: past-end at lane 0.
|
// The empty raster level folds to an empty palette: past-end at lane 0.
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&result, GPoll::Error(error) if error.kind == core_types::gpoll::ErrorKind::PastEnd),
|
matches!(&result, GPoll::Error(error) if error.kind == core_types::gpoll::ErrorKind::PastEnd),
|
||||||
|
|||||||
@@ -535,7 +535,9 @@ where
|
|||||||
let mut hint = crate::gpoll::Extent::AtLeast(range.end as usize);
|
let mut hint = crate::gpoll::Extent::AtLeast(range.end as usize);
|
||||||
for lane in 0..len {
|
for lane in 0..len {
|
||||||
local.set_index(range.start + lane as u64);
|
local.set_index(range.start + lane as u64);
|
||||||
let mark = stack::sp();
|
// SAFETY: the lane's record is copied out before the scope releases it,
|
||||||
|
// and valueless exits serve nothing above the entry.
|
||||||
|
let _lane_scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
let value = match node.eval(&local) {
|
let value = match node.eval(&local) {
|
||||||
GPoll::Final(value) => value,
|
GPoll::Final(value) => value,
|
||||||
GPoll::Partial(value) => {
|
GPoll::Partial(value) => {
|
||||||
@@ -549,19 +551,13 @@ where
|
|||||||
GPoll::Error(error) if error.kind == crate::gpoll::ErrorKind::PastEnd => {
|
GPoll::Error(error) if error.kind == crate::gpoll::ErrorKind::PastEnd => {
|
||||||
filled = lane;
|
filled = lane;
|
||||||
hint = crate::gpoll::Extent::Exactly(range.start as usize + lane);
|
hint = crate::gpoll::Extent::Exactly(range.start as usize + lane);
|
||||||
// SAFETY: the failed lane produced no record, so nothing above
|
|
||||||
// its mark is live.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
GPoll::Error(error) => return BatchStatus::Error(*error),
|
GPoll::Error(error) => return BatchStatus::Error(*error),
|
||||||
};
|
};
|
||||||
// SAFETY: the lane region is in-bounds by the scratch check, and the
|
// SAFETY: the lane region is in-bounds by the scratch check, and the
|
||||||
// frame is fully copied out before the rewind releases it.
|
// frame is fully copied out before the lane scope releases it.
|
||||||
unsafe {
|
unsafe { std::ptr::copy_nonoverlapping(layout.rec(&value).ptr(), base.add(lane * stride), stride) };
|
||||||
std::ptr::copy_nonoverlapping(layout.rec(&value).ptr(), base.add(lane * stride), stride);
|
|
||||||
stack::rewind(mark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// SAFETY: the first `filled` lanes were filled above with records of `layout`.
|
// SAFETY: the first `filled` lanes were filled above with records of `layout`.
|
||||||
BatchStatus::Filled(unsafe { crate::node::RecordBatchMut::new(scratch, filled, layout) }, finality, hint)
|
BatchStatus::Filled(unsafe { crate::node::RecordBatchMut::new(scratch, filled, layout) }, finality, hint)
|
||||||
@@ -771,14 +767,10 @@ impl<'a, Out, N> ElementEdge<'a, Out, N> {
|
|||||||
where
|
where
|
||||||
N: Node<C, Output = RecordValue<'d>>,
|
N: Node<C, Output = RecordValue<'d>>,
|
||||||
{
|
{
|
||||||
let mark = stack::sp();
|
// SAFETY: the read copies out by value, so no record above the entry
|
||||||
self.node.eval(ctx).map(|value| {
|
// (the edge's own frame) is live past the scope.
|
||||||
let out = unsafe { (self.read)(self.layout.rec(&value), self.reads) };
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
// SAFETY: the read copied out by value, so no record above `mark` (the
|
self.node.eval(ctx).map(|value| unsafe { (self.read)(self.layout.rec(&value), self.reads) })
|
||||||
// edge's own frame) is live.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
out
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,13 +818,12 @@ impl<'a, Out, N> ElementLazyInput<'a, Out, N> {
|
|||||||
where
|
where
|
||||||
N: Node<C, Output = RecordValue<'d>>,
|
N: Node<C, Output = RecordValue<'d>>,
|
||||||
{
|
{
|
||||||
let mark = stack::sp();
|
// SAFETY: the read copies the element and declared attributes out by
|
||||||
|
// value, so no record above the entry (the edge's own frame) is live
|
||||||
|
// past the scope.
|
||||||
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
let value = self.cell.eval_input(self.input_index, self.node, ctx)?;
|
let value = self.cell.eval_input(self.input_index, self.node, ctx)?;
|
||||||
let out = unsafe { (self.read)(self.layout.rec(&value), self.reads) };
|
Ok(unsafe { (self.read)(self.layout.rec(&value), self.reads) })
|
||||||
// SAFETY: the read copied the element and declared attributes out by value,
|
|
||||||
// so no record above `mark` (the edge's own frame) is live.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
Ok(out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,13 +910,12 @@ where
|
|||||||
let cell = crate::node::StatusCell::new();
|
let cell = crate::node::StatusCell::new();
|
||||||
let mut count: u64 = 0;
|
let mut count: u64 = 0;
|
||||||
loop {
|
loop {
|
||||||
let mark = stack::sp();
|
// SAFETY: the probed record is discarded, so nothing above the entry
|
||||||
|
// is live past the scope.
|
||||||
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
let mut frame = crate::context::IndexLink { index: 0, outer: None };
|
let mut frame = crate::context::IndexLink { index: 0, outer: None };
|
||||||
let probe = ctx.push_level(&mut frame, copy, count);
|
let probe = ctx.push_level(&mut frame, copy, count);
|
||||||
let result = node.eval_derived(&cell, input_index, &probe);
|
let result = node.eval_derived(&cell, input_index, &probe);
|
||||||
// SAFETY: the probed record is discarded, so nothing above the mark
|
|
||||||
// is live.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => count += 1,
|
Ok(_) => count += 1,
|
||||||
Err(crate::gpoll::Interrupt::Error(error)) if error.kind == crate::gpoll::ErrorKind::PastEnd => return Ok(count),
|
Err(crate::gpoll::Interrupt::Error(error)) if error.kind == crate::gpoll::ErrorKind::PastEnd => return Ok(count),
|
||||||
@@ -1036,7 +1026,11 @@ pub mod stack {
|
|||||||
/// derived stack need, and resets the stack pointer. Called only between
|
/// derived stack need, and resets the stack pointer. Called only between
|
||||||
/// evaluations, like the arena reset: nothing survives it, so frames
|
/// evaluations, like the arena reset: nothing survives it, so frames
|
||||||
/// leaked by an interrupted evaluation are reclaimed here.
|
/// leaked by an interrupted evaluation are reclaimed here.
|
||||||
pub fn reserve(bytes: usize) {
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// No record served on this thread's stack may be live: growth frees the
|
||||||
|
/// buffer and the reset releases every claimed frame.
|
||||||
|
pub unsafe fn reserve(bytes: usize) {
|
||||||
STACK.with(|stack| {
|
STACK.with(|stack| {
|
||||||
stack.sp.set(0);
|
stack.sp.set(0);
|
||||||
if stack.capacity.get() >= bytes {
|
if stack.capacity.get() >= bytes {
|
||||||
@@ -1106,6 +1100,45 @@ pub mod stack {
|
|||||||
stack.sp.set(mark);
|
stack.sp.set(mark);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rewinds to the entry stack pointer on drop, unwind included: the
|
||||||
|
/// structured form of a mark/rewind pair. A forgotten guard leaks its
|
||||||
|
/// region until the next reserve rather than releasing it.
|
||||||
|
pub struct ScopeGuard {
|
||||||
|
mark: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScopeGuard {
|
||||||
|
/// # Safety
|
||||||
|
/// No `Rec` or `RecordValue` served above the entry point may be used
|
||||||
|
/// after the guard drops; copy out everything that survives the scope.
|
||||||
|
pub unsafe fn enter() -> Self {
|
||||||
|
Self { mark: sp() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ScopeGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
STACK.with(|stack| {
|
||||||
|
// An interrupt close may already have rewound below the entry;
|
||||||
|
// the scope only ever releases, never re-claims.
|
||||||
|
if self.mark < stack.sp.get() {
|
||||||
|
stack.sp.set(self.mark);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs `body` under a [`ScopeGuard`]: every frame it claims releases on
|
||||||
|
/// return.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// As [`ScopeGuard::enter`]: nothing served inside the scope may escape
|
||||||
|
/// it, through the return value or a captured location.
|
||||||
|
pub unsafe fn scoped<R>(body: impl FnOnce() -> R) -> R {
|
||||||
|
let _scope = unsafe { ScopeGuard::enter() };
|
||||||
|
body()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reclaims the frames an inline node's inputs push. An inline node returns
|
/// Reclaims the frames an inline node's inputs push. An inline node returns
|
||||||
@@ -1114,7 +1147,7 @@ pub mod stack {
|
|||||||
/// pointer and rewinds to it on drop instead. Inactive (a no-op) for spilled
|
/// pointer and rewinds to it on drop instead. Inactive (a no-op) for spilled
|
||||||
/// nodes, which release their inputs through their own frame.
|
/// nodes, which release their inputs through their own frame.
|
||||||
pub struct ReclaimGuard {
|
pub struct ReclaimGuard {
|
||||||
target: usize,
|
scope: Option<stack::ScopeGuard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReclaimGuard {
|
impl ReclaimGuard {
|
||||||
@@ -1124,17 +1157,7 @@ impl ReclaimGuard {
|
|||||||
/// returns and the guard rewinds.
|
/// returns and the guard rewinds.
|
||||||
pub unsafe fn new(active: bool) -> Self {
|
pub unsafe fn new(active: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
target: if active { stack::sp() } else { usize::MAX },
|
scope: active.then(|| unsafe { stack::ScopeGuard::enter() }),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ReclaimGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.target != usize::MAX {
|
|
||||||
// SAFETY: an inline node returns its output by value, so no record into
|
|
||||||
// the reclaimed region is live once its eval returns.
|
|
||||||
unsafe { stack::rewind(self.target) };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1814,18 +1837,16 @@ impl ServedRecord {
|
|||||||
/// stays claimed. Assertion scaffolding for law tests; production consumers
|
/// stays claimed. Assertion scaffolding for law tests; production consumers
|
||||||
/// read served records in place.
|
/// read served records in place.
|
||||||
pub fn capture<'e, C, N: Node<C, Output = RecordValue<'e>>>(node: &N, ctx: &C) -> GPoll<ServedRecord> {
|
pub fn capture<'e, C, N: Node<C, Output = RecordValue<'e>>>(node: &N, ctx: &C) -> GPoll<ServedRecord> {
|
||||||
let mark = stack::sp();
|
// SAFETY: any served record is deep-copied out inside the scope, so
|
||||||
|
// nothing served above the entry escapes it.
|
||||||
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
let layout = node.layout().clone();
|
let layout = node.layout().clone();
|
||||||
let result = node.eval(ctx).map(|value| ServedRecord {
|
node.eval(ctx).map(|value| ServedRecord {
|
||||||
// SAFETY: the poll served `value` at the node's declared layout and
|
// SAFETY: the poll served `value` at the node's declared layout and
|
||||||
// nothing has claimed frames since.
|
// nothing has claimed frames since.
|
||||||
record: unsafe { OwnedRecord::copy_out(&layout, layout.rec(&value)) },
|
record: unsafe { OwnedRecord::copy_out(&layout, layout.rec(&value)) },
|
||||||
layout: layout.clone(),
|
layout: layout.clone(),
|
||||||
});
|
})
|
||||||
// SAFETY: any served record was deep-copied out above, so nothing above
|
|
||||||
// the mark is live.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Law-test scaffolding: wraps an arbitrary plain node onto a record wire
|
/// Law-test scaffolding: wraps an arbitrary plain node onto a record wire
|
||||||
@@ -1896,12 +1917,11 @@ where
|
|||||||
type Output = El;
|
type Output = El;
|
||||||
|
|
||||||
fn eval(&self, input: &C) -> GPoll<El> {
|
fn eval(&self, input: &C) -> GPoll<El> {
|
||||||
let mark = stack::sp();
|
// SAFETY: the element copies out by value, so no record above the
|
||||||
let result = self.edge.eval(input).map(|value| unsafe { read_element::<El>(self.layout.rec(&value)) });
|
// entry (the edge's frame) is live past the scope; a plain output
|
||||||
// SAFETY: the element copied out by value, so no record above the mark
|
// claims no frame itself.
|
||||||
// (the edge's frame) is live; a plain output claims no frame itself.
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
unsafe { stack::rewind(mark) };
|
self.edge.eval(input).map(|value| unsafe { read_element::<El>(self.layout.rec(&value)) })
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1915,16 +1935,14 @@ where
|
|||||||
match &self.plan {
|
match &self.plan {
|
||||||
None => self.edge.eval(input),
|
None => self.edge.eval(input),
|
||||||
Some(plan) if plan.union.frame_bytes() == 0 => {
|
Some(plan) if plan.union.frame_bytes() == 0 => {
|
||||||
let mark = stack::sp();
|
// SAFETY: the translation copies the record into the inline
|
||||||
let result = self.edge.eval(input).map(|value| {
|
// value, so no record above the entry is live past the scope.
|
||||||
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
|
self.edge.eval(input).map(|value| {
|
||||||
let mut out = RecordValue::zeroed();
|
let mut out = RecordValue::zeroed();
|
||||||
unsafe { plan.translate(plan.source.rec(&value), out.as_mut_ptr()) };
|
unsafe { plan.translate(plan.source.rec(&value), out.as_mut_ptr()) };
|
||||||
out
|
out
|
||||||
});
|
})
|
||||||
// SAFETY: the translation copied the record into the inline
|
|
||||||
// value, so no record above the mark is live.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
Some(plan) => {
|
Some(plan) => {
|
||||||
let dst = stack::push(plan.union.frame_bytes());
|
let dst = stack::push(plan.union.frame_bytes());
|
||||||
@@ -2720,8 +2738,8 @@ mod tests {
|
|||||||
buffer.fill(u64::MAX);
|
buffer.fill(u64::MAX);
|
||||||
|
|
||||||
let replay_arena = crate::arena::Arena::new(1024).unwrap();
|
let replay_arena = crate::arena::Arena::new(1024).unwrap();
|
||||||
stack::reserve(layout.frame_bytes());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let value = copy.replay(&layout, &replay_arena).unwrap();
|
unsafe { stack::reserve(layout.frame_bytes()); } let value = copy.replay(&layout, &replay_arena).unwrap();
|
||||||
let rec = layout.rec(&value);
|
let rec = layout.rec(&value);
|
||||||
assert_eq!(unsafe { read_element::<String>(rec) }, "element");
|
assert_eq!(unsafe { read_element::<String>(rec) }, "element");
|
||||||
assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field");
|
assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field");
|
||||||
@@ -2858,8 +2876,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_frames_nest_and_release() {
|
fn stack_frames_nest_and_release() {
|
||||||
stack::reserve(64);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let outer = stack::push(24);
|
unsafe { stack::reserve(64); } let outer = stack::push(24);
|
||||||
let inner = stack::push(8);
|
let inner = stack::push(8);
|
||||||
assert_eq!(inner as usize - outer as usize, 24);
|
assert_eq!(inner as usize - outer as usize, 24);
|
||||||
stack::pop(outer);
|
stack::pop(outer);
|
||||||
@@ -2869,8 +2887,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_rounds_frames_to_word_alignment() {
|
fn stack_rounds_frames_to_word_alignment() {
|
||||||
stack::reserve(64);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let first = stack::push(21);
|
unsafe { stack::reserve(64); } let first = stack::push(21);
|
||||||
let second = stack::push(8);
|
let second = stack::push(8);
|
||||||
assert_eq!(second as usize - first as usize, 24);
|
assert_eq!(second as usize - first as usize, 24);
|
||||||
stack::pop(first);
|
stack::pop(first);
|
||||||
@@ -2878,14 +2896,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn each_thread_gets_its_own_stack() {
|
fn each_thread_gets_its_own_stack() {
|
||||||
stack::reserve(64);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let here = stack::push(8);
|
unsafe { stack::reserve(64); } let here = stack::push(8);
|
||||||
let here_address = here as usize;
|
let here_address = here as usize;
|
||||||
std::thread::scope(|scope| {
|
std::thread::scope(|scope| {
|
||||||
scope
|
scope
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
stack::reserve(64);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let there = stack::push(8);
|
unsafe { stack::reserve(64); } let there = stack::push(8);
|
||||||
assert_ne!(here_address, there as usize, "stacks are per thread");
|
assert_ne!(here_address, there as usize, "stacks are per thread");
|
||||||
stack::pop(there);
|
stack::pop(there);
|
||||||
})
|
})
|
||||||
@@ -2923,8 +2941,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let layout = Layout::default().with_writes(0, element_write::<String>(), &[FieldWrite::of::<Transform>(0), FieldWrite::of::<Opacity>(0)]);
|
let layout = Layout::default().with_writes(0, element_write::<String>(), &[FieldWrite::of::<Transform>(0), FieldWrite::of::<Opacity>(0)]);
|
||||||
stack::reserve(1 << 10);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let arena = crate::arena::Arena::new(1024).unwrap();
|
unsafe { stack::reserve(1 << 10); } let arena = crate::arena::Arena::new(1024).unwrap();
|
||||||
let mark = stack::sp();
|
let mark = stack::sp();
|
||||||
let GPoll::Final(served) = capture(&Fixture { layout }, &&arena) else {
|
let GPoll::Final(served) = capture(&Fixture { layout }, &&arena) else {
|
||||||
panic!("the fixture serves finally");
|
panic!("the fixture serves finally");
|
||||||
@@ -2938,8 +2956,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "the served element must match the layout's element type")]
|
#[should_panic(expected = "the served element must match the layout's element type")]
|
||||||
fn a_mistyped_element_is_rejected_at_the_write() {
|
fn a_mistyped_element_is_rejected_at_the_write() {
|
||||||
stack::reserve(1 << 10);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let arena = crate::arena::Arena::new(256).unwrap();
|
unsafe { stack::reserve(1 << 10); } let arena = crate::arena::Arena::new(256).unwrap();
|
||||||
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[]);
|
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[]);
|
||||||
FrameBuilder::new(&layout, &arena).element(1u32);
|
FrameBuilder::new(&layout, &arena).element(1u32);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,8 +276,8 @@ mod tests {
|
|||||||
where
|
where
|
||||||
El::Static: Clone + Send + Sync,
|
El::Static: Clone + Send + Sync,
|
||||||
{
|
{
|
||||||
stack::reserve(1 << 12);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let layout = element_layout::<El>();
|
unsafe { stack::reserve(1 << 12); } let layout = element_layout::<El>();
|
||||||
graph.set_layout(crate::record::RecordLayout {
|
graph.set_layout(crate::record::RecordLayout {
|
||||||
frame_bytes: layout.frame_bytes(),
|
frame_bytes: layout.frame_bytes(),
|
||||||
plan: Vec::new(),
|
plan: Vec::new(),
|
||||||
|
|||||||
@@ -1647,8 +1647,8 @@ mod run_tests {
|
|||||||
drop(source);
|
drop(source);
|
||||||
|
|
||||||
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
|
||||||
core_types::record::stack::reserve(layout.frame_bytes());
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let value = owned.replay(&layout, &arena).expect("the arena holds the replay");
|
unsafe { core_types::record::stack::reserve(layout.frame_bytes()); } let value = owned.replay(&layout, &arena).expect("the arena holds the replay");
|
||||||
// SAFETY: the replay wrote a record of `layout`.
|
// SAFETY: the replay wrote a record of `layout`.
|
||||||
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
|
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
|
||||||
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
|
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
|
||||||
|
|||||||
@@ -1546,13 +1546,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let slot = format_ident!("__in_{index}");
|
let slot = format_ident!("__in_{index}");
|
||||||
quote! {
|
quote! {
|
||||||
let #query = || {
|
let #query = || {
|
||||||
let __mark = #core_types::record::stack::sp();
|
// SAFETY: the element copies out by value; extent
|
||||||
let __result = #core_types::node::Node::eval(&self.#name, __input)
|
|
||||||
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) });
|
|
||||||
// SAFETY: the element copied out by value; extent
|
|
||||||
// queries leave the record stack untouched.
|
// queries leave the record stack untouched.
|
||||||
unsafe { #core_types::record::stack::rewind(__mark) };
|
let __scope = unsafe { #core_types::record::stack::ScopeGuard::enter() };
|
||||||
__result
|
#core_types::node::Node::eval(&self.#name, __input)
|
||||||
|
.map(|__value| unsafe { #core_types::record::read_element::<#ty>(self.#slot.rec(&__value)) })
|
||||||
};
|
};
|
||||||
let #arg = #core_types::extent::ValueIn::new(&#query);
|
let #arg = #core_types::extent::ValueIn::new(&#query);
|
||||||
}
|
}
|
||||||
@@ -2117,11 +2115,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
match ir::value_binding(&node, index).reads_out() {
|
match ir::value_binding(&node, index).reads_out() {
|
||||||
false => body,
|
false => body,
|
||||||
true => {
|
true => {
|
||||||
let mark = format_ident!("__mark_{index}");
|
let mark = format_ident!("__scope_{index}");
|
||||||
quote! {
|
quote! {
|
||||||
let #mark = #core_types::record::stack::sp();
|
// SAFETY: the bind copies its reads out by value; the
|
||||||
|
// drop point matches the old rewind.
|
||||||
|
let #mark = unsafe { #core_types::record::stack::ScopeGuard::enter() };
|
||||||
#body
|
#body
|
||||||
unsafe { #core_types::record::stack::rewind(#mark) };
|
drop(#mark);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2198,8 +2198,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
for __lane in 0..__len {
|
for __lane in 0..__len {
|
||||||
#core_types::context::InjectIndex::set_index(&mut __lane_ctx, __range.start + __lane as u64);
|
#core_types::context::InjectIndex::set_index(&mut __lane_ctx, __range.start + __lane as u64);
|
||||||
let __input = &__lane_ctx;
|
let __input = &__lane_ctx;
|
||||||
let __lane_mark = #core_types::record::stack::sp();
|
// SAFETY: the lane's record is copied out before the scope
|
||||||
let _entry_sp = __lane_mark;
|
// releases it, and valueless exits serve nothing above it.
|
||||||
|
let __lane_scope = unsafe { #core_types::record::stack::ScopeGuard::enter() };
|
||||||
|
let _entry_sp = #core_types::record::stack::sp();
|
||||||
let __cell = __cell.snapshot();
|
let __cell = __cell.snapshot();
|
||||||
#(#rebinds)*
|
#(#rebinds)*
|
||||||
#(#binds)*
|
#(#binds)*
|
||||||
@@ -2217,21 +2219,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
#core_types::gpoll::GPoll::Error(__error) if __error.kind == #core_types::gpoll::ErrorKind::PastEnd => {
|
#core_types::gpoll::GPoll::Error(__error) if __error.kind == #core_types::gpoll::ErrorKind::PastEnd => {
|
||||||
__filled = __lane;
|
__filled = __lane;
|
||||||
__hint = #core_types::gpoll::Extent::Exactly(__range.start as usize + __lane);
|
__hint = #core_types::gpoll::Extent::Exactly(__range.start as usize + __lane);
|
||||||
unsafe { #core_types::record::stack::rewind(__lane_mark) };
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#core_types::gpoll::GPoll::Error(__error) => return #core_types::node::BatchStatus::Error(*__error),
|
#core_types::gpoll::GPoll::Error(__error) => return #core_types::node::BatchStatus::Error(*__error),
|
||||||
};
|
};
|
||||||
// SAFETY: the lane region is in-bounds by the scratch check,
|
// SAFETY: the lane region is in-bounds by the scratch check,
|
||||||
// and the frame is fully copied out before the rewind.
|
// and the frame is fully copied out before the lane scope
|
||||||
unsafe {
|
// releases it.
|
||||||
::core::ptr::copy_nonoverlapping(__node_layout.rec(&__value).ptr(), __frames.add(__lane * __stride), __stride);
|
unsafe { ::core::ptr::copy_nonoverlapping(__node_layout.rec(&__value).ptr(), __frames.add(__lane * __stride), __stride) };
|
||||||
#core_types::record::stack::rewind(__lane_mark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// SAFETY: every lane was copied into the caller's scratch, so
|
drop(__entry_scope);
|
||||||
// nothing above the entry mark is live.
|
|
||||||
unsafe { #core_types::record::stack::rewind(__entry_mark) };
|
|
||||||
// SAFETY: the first `__filled` lanes were filled above with
|
// SAFETY: the first `__filled` lanes were filled above with
|
||||||
// records of the node's layout.
|
// records of the node's layout.
|
||||||
#core_types::node::BatchStatus::Filled(unsafe { #core_types::node::RecordBatchMut::new(__scratch, __filled, __node_layout) }, __finality, __hint)
|
#core_types::node::BatchStatus::Filled(unsafe { #core_types::node::RecordBatchMut::new(__scratch, __filled, __node_layout) }, __finality, __hint)
|
||||||
@@ -2269,8 +2266,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
if __scratch.len() * 8 < __len * __stride {
|
if __scratch.len() * 8 < __len * __stride {
|
||||||
return #core_types::node::BatchStatus::InvalidRange;
|
return #core_types::node::BatchStatus::InvalidRange;
|
||||||
}
|
}
|
||||||
let __entry_mark = #core_types::record::stack::sp();
|
// SAFETY: every lane copies into the caller's scratch, so
|
||||||
let _entry_sp = __entry_mark;
|
// nothing served above the entry escapes the batch scope.
|
||||||
|
let __entry_scope = unsafe { #core_types::record::stack::ScopeGuard::enter() };
|
||||||
|
let _entry_sp = #core_types::record::stack::sp();
|
||||||
let __cell = #cell_constructor;
|
let __cell = #cell_constructor;
|
||||||
let __base_ctx = {
|
let __base_ctx = {
|
||||||
let mut __ctx = *__input;
|
let mut __ctx = *__input;
|
||||||
@@ -2616,11 +2615,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
match reads_out {
|
match reads_out {
|
||||||
false => body,
|
false => body,
|
||||||
true => {
|
true => {
|
||||||
let mark = format_ident!("__mark_{index}");
|
let mark = format_ident!("__scope_{index}");
|
||||||
quote! {
|
quote! {
|
||||||
let #mark = #core_types::record::stack::sp();
|
// SAFETY: the bind copies its reads out by value; the drop
|
||||||
|
// point matches the old rewind.
|
||||||
|
let #mark = unsafe { #core_types::record::stack::ScopeGuard::enter() };
|
||||||
#body
|
#body
|
||||||
unsafe { #core_types::record::stack::rewind(#mark) };
|
drop(#mark);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,13 @@ use raster_types::BitmapMut;
|
|||||||
use raster_types::Image;
|
use raster_types::Image;
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq, dyn_any::DynAny)]
|
||||||
pub struct BrushStampGenerator<P: Pixel + Alpha> {
|
pub struct BrushStampGenerator<P: Pixel + Alpha> {
|
||||||
color: P,
|
color: P,
|
||||||
feather_exponent: f32,
|
feather_exponent: f32,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: `Static` is `Self` with `P` at its own static projection.
|
|
||||||
unsafe impl<P: Pixel + Alpha + dyn_any::StaticTypeSized> dyn_any::StaticType for BrushStampGenerator<P>
|
|
||||||
where
|
|
||||||
P::Static: Pixel + Alpha,
|
|
||||||
{
|
|
||||||
type Static = BrushStampGenerator<P::Static>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P: Pixel + Alpha> Transform for BrushStampGenerator<P> {
|
impl<P: Pixel + Alpha> Transform for BrushStampGenerator<P> {
|
||||||
fn transform(&self) -> DAffine2 {
|
fn transform(&self) -> DAffine2 {
|
||||||
self.transform
|
self.transform
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||||
core_types::record::stack::reserve(1 << 16);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
unsafe { core_types::record::stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn element_layout<T: Clone + Send + Sync + core_types::StaticTypeSized>() -> core_types::record::Layout
|
fn element_layout<T: Clone + Send + Sync + core_types::StaticTypeSized>() -> core_types::record::Layout
|
||||||
|
|||||||
@@ -538,8 +538,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||||
stack::reserve(1 << 16);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
unsafe { stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn f64_layout(names: &[&'static str]) -> Layout {
|
fn f64_layout(names: &[&'static str]) -> Layout {
|
||||||
@@ -579,8 +579,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reserve_for(layouts: &[&Layout]) {
|
fn reserve_for(layouts: &[&Layout]) {
|
||||||
stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12));
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
}
|
unsafe { stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12)); } }
|
||||||
|
|
||||||
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||||
// The fixtures wire constants into every eager input, which the compiler
|
// The fixtures wire constants into every eager input, which the compiler
|
||||||
@@ -1660,14 +1660,13 @@ mod tests {
|
|||||||
|
|
||||||
let head = ctx.index_head();
|
let head = ctx.index_head();
|
||||||
for (lane, element) in [(0u64, 10.), (1, 11.)] {
|
for (lane, element) in [(0u64, 10.), (1, 11.)] {
|
||||||
let mark = stack::sp();
|
let _lane_scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
let GPoll::Final(value) = node.eval(&ctx.promoted(&head, lane)) else {
|
let GPoll::Final(value) = node.eval(&ctx.promoted(&head, lane)) else {
|
||||||
panic!("expected a final record at lane {lane}");
|
panic!("expected a final record at lane {lane}");
|
||||||
};
|
};
|
||||||
let rec = out.rec(&value);
|
let rec = out.rec(&value);
|
||||||
assert_eq!(unsafe { rec.element::<f64>() }, element, "lane {lane}");
|
assert_eq!(unsafe { rec.element::<f64>() }, element, "lane {lane}");
|
||||||
assert_eq!(unsafe { rec.read::<&[NodeId]>(offset) }, path.as_slice(), "lane {lane}");
|
assert_eq!(unsafe { rec.read::<&[NodeId]>(offset) }, path.as_slice(), "lane {lane}");
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,8 +264,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||||
stack::reserve(1 << 16);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
unsafe { stack::reserve(1 << 16); } EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||||
@@ -841,13 +841,13 @@ mod tests {
|
|||||||
let wrap_out = Node::<ContextImpl>::layout(&wrapped).clone();
|
let wrap_out = Node::<ContextImpl>::layout(&wrapped).clone();
|
||||||
let head = ctx.index_head();
|
let head = ctx.index_head();
|
||||||
let group = {
|
let group = {
|
||||||
let mark = stack::sp();
|
// SAFETY: the element is cloned out inside the scope, so no borrow
|
||||||
|
// into the frame escapes it.
|
||||||
|
let _scope = unsafe { stack::ScopeGuard::enter() };
|
||||||
let GPoll::Final(value) = wrapped.eval(&ctx.promoted(&head, 0)) else {
|
let GPoll::Final(value) = wrapped.eval(&ctx.promoted(&head, 0)) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
let group = unsafe { record::borrow_element::<Graphic>(wrap_out.rec(&value)) }.clone();
|
let group = unsafe { record::borrow_element::<Graphic>(wrap_out.rec(&value)) }.clone();
|
||||||
// SAFETY: the element was cloned out above, so no borrow into the frame remains.
|
|
||||||
unsafe { stack::rewind(mark) };
|
|
||||||
group
|
group
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -267,8 +267,8 @@ mod tests {
|
|||||||
|
|
||||||
let probe = core_types::record::RecordLift::<RenderOutput, _>::new(ProbeNode);
|
let probe = core_types::record::RecordLift::<RenderOutput, _>::new(ProbeNode);
|
||||||
let layout = Node::<ContextImpl>::layout(&probe).clone();
|
let layout = Node::<ContextImpl>::layout(&probe).clone();
|
||||||
core_types::record::stack::reserve(layout.frame_bytes().max(1 << 12));
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let mut graph = CreateContextNode::new(probe, &layout);
|
unsafe { core_types::record::stack::reserve(layout.frame_bytes().max(1 << 12)); } let mut graph = CreateContextNode::new(probe, &layout);
|
||||||
// The executor resolves and installs the node's own layout at wiring;
|
// The executor resolves and installs the node's own layout at wiring;
|
||||||
// without it the flip tail writes through the default empty layout.
|
// without it the flip tail writes through the default empty layout.
|
||||||
Node::<ContextImpl>::set_layout(
|
Node::<ContextImpl>::set_layout(
|
||||||
|
|||||||
@@ -1044,8 +1044,8 @@ mod graphene_test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reserve_for(layouts: &[&Layout]) {
|
fn reserve_for(layouts: &[&Layout]) {
|
||||||
stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12));
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
}
|
unsafe { stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12)); } }
|
||||||
|
|
||||||
/// Lifts a plain-element test source onto a record wire, returned beside its
|
/// Lifts a plain-element test source onto a record wire, returned beside its
|
||||||
/// element-only layout for the generated node's constructor.
|
/// element-only layout for the generated node's constructor.
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_image_color_palette() {
|
fn test_image_color_palette() {
|
||||||
core_types::record::stack::reserve(1 << 16);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let arena = core_types::arena::Arena::new(1 << 22).unwrap();
|
unsafe { core_types::record::stack::reserve(1 << 16); } let arena = core_types::arena::Arena::new(1 << 22).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena);
|
let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena);
|
||||||
let ctx = core_types::context::ContextImpl::root(&scope);
|
let ctx = core_types::context::ContextImpl::root(&scope);
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||||
stack::reserve(1 << 12);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
unsafe { stack::reserve(1 << 12); } EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VectorRows {
|
struct VectorRows {
|
||||||
|
|||||||
@@ -3870,8 +3870,8 @@ mod test {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn path_length() {
|
fn path_length() {
|
||||||
core_types::record::stack::reserve(1 << 16);
|
// SAFETY: between evaluations, nothing served on the stack is live.
|
||||||
let arena = core_types::arena::Arena::new(1 << 20).unwrap();
|
unsafe { core_types::record::stack::reserve(1 << 16); } let arena = core_types::arena::Arena::new(1 << 20).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena);
|
let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena);
|
||||||
let ctx = core_types::context::ContextImpl::root(&scope);
|
let ctx = core_types::context::ContextImpl::root(&scope);
|
||||||
|
|||||||
Reference in New Issue
Block a user