mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Drop the list driver, make attribute values Copy over arena references, and land record-tier offset io on a per-thread record stack
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
//! collects every declaration so name resolution, defaults, and diagnostics
|
||||
//! run at graph compile time. One name belongs to one marker, so a name can
|
||||
//! never mean two different types.
|
||||
//!
|
||||
//! Values are `Copy` and pack directly into record fields. Data with drop
|
||||
//! glue rides the arena instead: the marker declares a reference value
|
||||
//! (`&str`), the writing kernel parks the payload in the arena, and the
|
||||
//! record field carries the eval-lifetime reference.
|
||||
|
||||
use crate::list::AnyAttributeValue;
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -18,11 +23,15 @@ use std::sync::{LazyLock, Mutex};
|
||||
pub trait Attribute: 'static {
|
||||
/// The name as it appears in documents and diagnostics.
|
||||
const NAME: &'static str;
|
||||
/// The value type every read and write of this name shares.
|
||||
type Value: AnyAttributeValue + Clone + Default + std::fmt::Debug;
|
||||
/// The value type every read and write of this name shares. The lifetime
|
||||
/// is the evaluation the value flows in; non-reference values ignore it.
|
||||
type Value<'e>: Copy + Default + std::fmt::Debug;
|
||||
/// The name-specific default, filled where an item lacks the attribute.
|
||||
fn default() -> Self::Value {
|
||||
Self::Value::default()
|
||||
/// Producing a value for any `'e` from no inputs, reference defaults can
|
||||
/// only point at `'static` data, which is what lets the census fill them
|
||||
/// as plain bytes.
|
||||
fn default<'e>() -> Self::Value<'e> {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,23 +39,25 @@ pub trait Attribute: 'static {
|
||||
/// (yielding the declared default where the attribute is absent upstream), an
|
||||
/// `Attr<A>` in the return tuple is a write, and the same marker on both
|
||||
/// sides is a modify.
|
||||
pub struct Attr<A: Attribute>(pub A::Value);
|
||||
pub struct Attr<'e, A: Attribute>(pub A::Value<'e>);
|
||||
|
||||
impl<A: Attribute> Deref for Attr<A> {
|
||||
type Target = A::Value;
|
||||
impl<'e, A: Attribute> Deref for Attr<'e, A> {
|
||||
type Target = A::Value<'e>;
|
||||
|
||||
fn deref(&self) -> &A::Value {
|
||||
fn deref(&self) -> &A::Value<'e> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Attribute> Clone for Attr<A> {
|
||||
impl<'e, A: Attribute> Clone for Attr<'e, A> {
|
||||
fn clone(&self) -> Self {
|
||||
Attr(self.0.clone())
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Attribute> std::fmt::Debug for Attr<A> {
|
||||
impl<'e, A: Attribute> Copy for Attr<'e, A> {}
|
||||
|
||||
impl<'e, A: Attribute> std::fmt::Debug for Attr<'e, A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple(A::NAME).field(&self.0).finish()
|
||||
}
|
||||
@@ -61,20 +72,14 @@ pub struct AttributeInfo {
|
||||
pub default: fn() -> Box<dyn AnyAttributeValue>,
|
||||
pub size: usize,
|
||||
pub align: usize,
|
||||
/// Whether the value is eligible for packed-record fields: no drop glue,
|
||||
/// so its bytes copy freely. Droppable values stay on the erased path
|
||||
/// until the per-type clone/drop glue lands.
|
||||
pub packable: bool,
|
||||
/// Writes the declared default's bytes into a `size`-long slice.
|
||||
/// Meaningful only when `packable`.
|
||||
pub write_default_bytes: fn(&mut [u8]),
|
||||
}
|
||||
|
||||
fn write_default_bytes<A: Attribute>(out: &mut [u8]) {
|
||||
assert!(!std::mem::needs_drop::<A::Value>(), "default bytes exist only for packable values");
|
||||
assert_eq!(out.len(), size_of::<A::Value>());
|
||||
let value = A::default();
|
||||
unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value>()) };
|
||||
assert_eq!(out.len(), size_of::<A::Value<'static>>());
|
||||
let value: A::Value<'static> = A::default();
|
||||
unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value<'static>>()) };
|
||||
}
|
||||
|
||||
/// All declared attribute names, keyed by name.
|
||||
@@ -84,15 +89,17 @@ pub static ATTRIBUTE_REGISTRY: LazyLock<Mutex<HashMap<&'static str, AttributeInf
|
||||
/// startup (ctor natively, a `__node_registry_attribute_*` export on wasm).
|
||||
/// Re-registration at the same value type is idempotent; a second marker
|
||||
/// claiming the name at a different value type panics.
|
||||
pub fn register<A: Attribute>() {
|
||||
pub fn register<A: Attribute>()
|
||||
where
|
||||
A::Value<'static>: AnyAttributeValue,
|
||||
{
|
||||
let info = AttributeInfo {
|
||||
name: A::NAME,
|
||||
value_type: TypeId::of::<A::Value>(),
|
||||
value_type_name: std::any::type_name::<A::Value>(),
|
||||
value_type: TypeId::of::<A::Value<'static>>(),
|
||||
value_type_name: std::any::type_name::<A::Value<'static>>(),
|
||||
default: || Box::new(A::default()),
|
||||
size: size_of::<A::Value>(),
|
||||
align: align_of::<A::Value>(),
|
||||
packable: !std::mem::needs_drop::<A::Value>(),
|
||||
size: size_of::<A::Value<'static>>(),
|
||||
align: align_of::<A::Value<'static>>(),
|
||||
write_default_bytes: write_default_bytes::<A>,
|
||||
};
|
||||
let conflict = match ATTRIBUTE_REGISTRY.lock().unwrap().entry(A::NAME) {
|
||||
@@ -124,44 +131,65 @@ pub fn default_value(name: &str) -> Option<Box<dyn AnyAttributeValue>> {
|
||||
/// core_types::attribute! {
|
||||
/// /// How visible the content is.
|
||||
/// pub Opacity("opacity"): f64 = 1.;
|
||||
/// /// The item's transformation.
|
||||
/// pub Transform("transform"): glam::DAffine2;
|
||||
/// /// The item's label, parked in the arena by the writer.
|
||||
/// pub Label("label"): &str;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The trailing `= expr` is the name-specific default; without it the value
|
||||
/// type's `Default` applies.
|
||||
/// type's `Default` applies. A `&T` value carries the eval lifetime, so its
|
||||
/// default must be `'static` data.
|
||||
#[macro_export]
|
||||
macro_rules! attribute {
|
||||
($($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?;)+) => {
|
||||
$(
|
||||
$(#[$meta])*
|
||||
$vis struct $marker;
|
||||
() => {};
|
||||
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): &$value:ty $(= $default:expr)?; $($rest:tt)*) => {
|
||||
$(#[$meta])*
|
||||
$vis struct $marker;
|
||||
|
||||
impl $crate::attribute::Attribute for $marker {
|
||||
const NAME: &'static str = $name;
|
||||
type Value = $value;
|
||||
$(
|
||||
fn default() -> $value {
|
||||
$default
|
||||
}
|
||||
)?
|
||||
impl $crate::attribute::Attribute for $marker {
|
||||
const NAME: &'static str = $name;
|
||||
type Value<'e> = &'e $value;
|
||||
$(
|
||||
fn default<'e>() -> Self::Value<'e> {
|
||||
$default
|
||||
}
|
||||
)?
|
||||
}
|
||||
|
||||
$crate::attribute!(@register $marker);
|
||||
$crate::attribute!($($rest)*);
|
||||
};
|
||||
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?; $($rest:tt)*) => {
|
||||
$(#[$meta])*
|
||||
$vis struct $marker;
|
||||
|
||||
impl $crate::attribute::Attribute for $marker {
|
||||
const NAME: &'static str = $name;
|
||||
type Value<'e> = $value;
|
||||
$(
|
||||
fn default<'e>() -> Self::Value<'e> {
|
||||
$default
|
||||
}
|
||||
)?
|
||||
}
|
||||
|
||||
$crate::attribute!(@register $marker);
|
||||
$crate::attribute!($($rest)*);
|
||||
};
|
||||
(@register $marker:ident) => {
|
||||
const _: () = {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[$crate::ctor::ctor]
|
||||
fn register() {
|
||||
$crate::attribute::register::<$marker>();
|
||||
}
|
||||
|
||||
const _: () = {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[$crate::ctor::ctor]
|
||||
fn register() {
|
||||
$crate::attribute::register::<$marker>();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))]
|
||||
extern "C" fn register() {
|
||||
$crate::attribute::register::<$marker>();
|
||||
}
|
||||
};
|
||||
)+
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))]
|
||||
extern "C" fn register() {
|
||||
$crate::attribute::register::<$marker>();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,7 +208,7 @@ attribute! {
|
||||
/// Artboard's top-left corner in document coordinates.
|
||||
pub Location("location"): DVec2;
|
||||
/// A regex named-capture-group's name, or empty for unnamed groups.
|
||||
pub Name("name"): String;
|
||||
pub Name("name"): &str;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -198,7 +226,7 @@ mod tests {
|
||||
#[test]
|
||||
fn name_specific_default_overrides_the_type_default() {
|
||||
assert_eq!(<Opacity as Attribute>::default(), 1.);
|
||||
assert_eq!(<Name as Attribute>::default(), String::new());
|
||||
assert_eq!(<Name as Attribute>::default(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -207,6 +235,13 @@ mod tests {
|
||||
assert_eq!(*value.as_any().downcast_ref::<f64>().unwrap(), 1.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_values_register_at_the_static_instantiation() {
|
||||
let row = info("name").unwrap();
|
||||
assert_eq!(row.value_type, TypeId::of::<&'static str>());
|
||||
assert_eq!(row.size, size_of::<&str>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reregistration_at_the_same_type_is_idempotent() {
|
||||
register::<Opacity>();
|
||||
@@ -220,7 +255,7 @@ mod tests {
|
||||
struct Conflict;
|
||||
impl Attribute for Conflict {
|
||||
const NAME: &'static str = "opacity";
|
||||
type Value = bool;
|
||||
type Value<'e> = bool;
|
||||
}
|
||||
register::<Conflict>();
|
||||
}
|
||||
|
||||
@@ -624,7 +624,6 @@ pub struct EvalScope<'a> {
|
||||
pointer_position: Option<DVec2>,
|
||||
generations: &'a [(SourceId, u64)],
|
||||
arena: &'a Arena,
|
||||
frame: Option<&'a crate::record::Frame>,
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
@@ -636,19 +635,12 @@ impl<'a> EvalScope<'a> {
|
||||
pointer_position,
|
||||
generations,
|
||||
arena,
|
||||
frame: None,
|
||||
hash: 0,
|
||||
};
|
||||
scope.hash = scope.compute_hash(|_| true);
|
||||
scope
|
||||
}
|
||||
|
||||
/// Attaches the record frame. Operational like the arena: not part of the
|
||||
/// scope hash.
|
||||
pub fn with_frame(&self, frame: &'a crate::record::Frame) -> EvalScope<'a> {
|
||||
EvalScope { frame: Some(frame), ..*self }
|
||||
}
|
||||
|
||||
pub fn with_real_time(&self, real_time: Option<f64>) -> EvalScope<'a> {
|
||||
let mut scope = EvalScope { real_time, ..*self };
|
||||
scope.hash = scope.compute_hash(|_| true);
|
||||
@@ -708,23 +700,6 @@ impl<'a> EvalScope<'a> {
|
||||
pub fn arena(&self) -> &'a Arena {
|
||||
self.arena
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> Option<&'a crate::record::Frame> {
|
||||
self.frame
|
||||
}
|
||||
}
|
||||
|
||||
/// Read access to the record frame, the operational sibling of
|
||||
/// [`ExtractArena`]. `None` on contexts whose scope carries no frame (a
|
||||
/// graph without record edges allocates none).
|
||||
pub trait ExtractFrame<'e> {
|
||||
fn frame(&self) -> Option<&'e crate::record::Frame>;
|
||||
}
|
||||
|
||||
impl<'a> ExtractFrame<'a> for ContextImpl<'a> {
|
||||
fn frame(&self) -> Option<&'a crate::record::Frame> {
|
||||
self.scope.frame()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtractArena {
|
||||
|
||||
@@ -954,15 +954,6 @@ impl<T> List<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a list from element values with no attributes.
|
||||
pub fn from_element_values(element: Vec<T>) -> Self {
|
||||
let len = element.len();
|
||||
Self {
|
||||
element,
|
||||
attributes: Attributes::with_len(len),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a list containing a single item from the given [`Item`], preserving its attributes.
|
||||
pub fn new_from_item(item: Item<T>) -> Self {
|
||||
let mut attributes = Attributes::new();
|
||||
@@ -1009,11 +1000,6 @@ impl<T> List<T> {
|
||||
self.element.iter()
|
||||
}
|
||||
|
||||
/// Consumes the list, returning its element values and dropping its attributes.
|
||||
pub fn into_element_values(self) -> Vec<T> {
|
||||
self.element
|
||||
}
|
||||
|
||||
/// Returns an iterator over mutable references to all element values.
|
||||
pub fn iter_element_values_mut(&mut self) -> std::slice::IterMut<'_, T> {
|
||||
self.element.iter_mut()
|
||||
@@ -1101,20 +1087,6 @@ impl<T> List<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes and returns the attribute column for the given key, if present.
|
||||
pub fn take_attribute_dyn(&mut self, key: &str) -> Option<AttributeDyn> {
|
||||
let position = self.attributes.attributes.iter().position(|(k, _)| k == key)?;
|
||||
Some(AttributeDyn(self.attributes.attributes.remove(position).1))
|
||||
}
|
||||
|
||||
/// Replaces (or adds) an attribute, taking ownership of the column, whose length must equal this list's item count.
|
||||
pub fn insert_attribute_dyn(&mut self, key: impl Into<String>, column: AttributeDyn) {
|
||||
assert_eq!(column.len(), self.element.len(), "attribute column length must match the list's item count");
|
||||
let key = key.into();
|
||||
self.attributes.attributes.retain(|(k, _)| k != &key);
|
||||
self.attributes.attributes.push((key, column.0));
|
||||
}
|
||||
|
||||
/// Removes the entire attribute for the given key, if present.
|
||||
pub fn remove_attribute(&mut self, key: &str) {
|
||||
self.attributes.remove_attribute(key);
|
||||
|
||||
@@ -71,6 +71,14 @@ pub trait Node<Input> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The record layout of this node's output; `None` for element-only
|
||||
/// producers. Consumers read their carrier's layout through this at
|
||||
/// wiring, and the wiring layer derives stack sizing from the same
|
||||
/// layouts, in the dynamic executor and exported source alike.
|
||||
fn layout(&self) -> Option<&crate::record::Layout> {
|
||||
None
|
||||
}
|
||||
|
||||
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
|
||||
where
|
||||
Input: InjectIndex + Copy,
|
||||
@@ -133,6 +141,10 @@ where
|
||||
(**self).serialize()
|
||||
}
|
||||
|
||||
fn layout(&self) -> Option<&crate::record::Layout> {
|
||||
(**self).layout()
|
||||
}
|
||||
|
||||
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
|
||||
where
|
||||
Input: InjectIndex + Copy,
|
||||
@@ -159,6 +171,10 @@ where
|
||||
(**self).serialize()
|
||||
}
|
||||
|
||||
fn layout(&self) -> Option<&crate::record::Layout> {
|
||||
(**self).layout()
|
||||
}
|
||||
|
||||
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
|
||||
where
|
||||
Input: InjectIndex + Copy,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
//! The packed-record tier at rank 0. A record is the element at offset 0
|
||||
//! plus one field per written attribute; its [`Layout`] is computed at
|
||||
//! wiring from the upstream write set and never serialized. Records live as
|
||||
//! per-lane views in a per-worker [`Frame`] whose slots are assigned at
|
||||
//! wiring, and kernels route them as opaque [`RecordValue`]s that carry
|
||||
//! per-lane views on the per-thread record [`stack`], claimed per
|
||||
//! evaluation, and kernels route them as opaque [`RecordValue`]s that carry
|
||||
//! their provenance. Only generated or wiring code touches offsets, so a
|
||||
//! safe kernel cannot misalign a field.
|
||||
|
||||
use crate::attribute;
|
||||
use crate::gpoll::GPoll;
|
||||
use crate::node::Node;
|
||||
use std::cell::UnsafeCell;
|
||||
|
||||
/// One field of a [`Layout`]: a (name, level) key resolved to an offset.
|
||||
/// Levels are numbered innermost-out; only level 0 exists at rank 0.
|
||||
@@ -97,6 +96,13 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/// The stand-in fed to a kernel's unbounded `element: T` parameter. The type
|
||||
/// system forces the kernel to route it to the element position of its return
|
||||
/// tuple, so the passthrough is explicit in the signature while the lowering
|
||||
/// carries the element bytes untyped through the copy plan.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ElToken;
|
||||
|
||||
/// A view of one record: a pointer whose layout is proven at wiring.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Rec(*const u8);
|
||||
@@ -148,58 +154,89 @@ impl<'e> RecordValue<'e> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assigns frame slots at wiring: a bump allocator over slot sizes, aligned
|
||||
/// to at most 8 (record layouts never exceed word alignment).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FrameLayout {
|
||||
size: usize,
|
||||
}
|
||||
/// The per-thread record stack: every record evaluation claims its activation
|
||||
/// frame at the stack pointer and evaluates its carrier beyond it, so slot
|
||||
/// addresses are a property of the evaluating thread and no global assignment
|
||||
/// exists. Thread-local by construction, so access is single-threaded without
|
||||
/// claims or gates. Records are overwritten per lane and never touch the
|
||||
/// arena.
|
||||
pub mod stack {
|
||||
use std::cell::Cell;
|
||||
|
||||
impl FrameLayout {
|
||||
pub fn slot(&mut self, layout: &Layout) -> usize {
|
||||
assert!(layout.align <= 8, "record layouts align to at most 8");
|
||||
self.size = self.size.next_multiple_of(layout.align.max(1));
|
||||
let offset = self.size;
|
||||
self.size += layout.size;
|
||||
offset
|
||||
struct Stack {
|
||||
base: Cell<*mut u8>,
|
||||
capacity: Cell<usize>,
|
||||
sp: Cell<usize>,
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-worker record frame: every record-producing slot lives at a
|
||||
/// wiring-assigned offset. Slots are overwritten per lane and never touch
|
||||
/// the arena.
|
||||
pub struct Frame {
|
||||
words: Box<[UnsafeCell<u64>]>,
|
||||
}
|
||||
|
||||
// SAFETY: a frame belongs to one worker; slot writes happen only inside that
|
||||
// worker's evaluation, and wiring assigns disjoint offsets per slot.
|
||||
unsafe impl Send for Frame {}
|
||||
unsafe impl Sync for Frame {}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(size: usize) -> Self {
|
||||
Self {
|
||||
words: (0..size.div_ceil(8).max(1)).map(|_| UnsafeCell::new(0)).collect(),
|
||||
impl Stack {
|
||||
fn free(&self) {
|
||||
let base = self.base.get();
|
||||
if !base.is_null() {
|
||||
drop(unsafe { Vec::from_raw_parts(base.cast::<u64>(), 0, self.capacity.get() / 8) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `offset` must be a slot offset assigned by [`FrameLayout`] for this
|
||||
/// frame, and the caller must be the slot's owning node evaluation.
|
||||
pub unsafe fn slot(&self, offset: usize) -> *mut u8 {
|
||||
debug_assert!(offset <= self.words.len() * 8);
|
||||
unsafe { self.words.as_ptr().cast::<u8>().cast_mut().add(offset) }
|
||||
impl Drop for Stack {
|
||||
fn drop(&mut self) {
|
||||
self.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Frame {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Frame").field("bytes", &(self.words.len() * 8)).finish()
|
||||
thread_local! {
|
||||
static STACK: Stack = const {
|
||||
Stack {
|
||||
base: Cell::new(std::ptr::null_mut()),
|
||||
capacity: Cell::new(0),
|
||||
sp: Cell::new(0),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Ensures the calling thread's stack holds `bytes`, the root's wiring-
|
||||
/// derived stack need, and resets the stack pointer. Called only between
|
||||
/// evaluations, like the arena reset: nothing survives it, so frames
|
||||
/// leaked by an interrupted evaluation are reclaimed here.
|
||||
pub fn reserve(bytes: usize) {
|
||||
STACK.with(|stack| {
|
||||
stack.sp.set(0);
|
||||
if stack.capacity.get() >= bytes {
|
||||
return;
|
||||
}
|
||||
let words = bytes.div_ceil(8).max(1);
|
||||
let mut memory = vec![0u64; words];
|
||||
let base = memory.as_mut_ptr().cast::<u8>();
|
||||
std::mem::forget(memory);
|
||||
stack.free();
|
||||
stack.base.set(base);
|
||||
stack.capacity.set(words * 8);
|
||||
});
|
||||
}
|
||||
|
||||
/// Claims `bytes` (rounded to word alignment) at the stack pointer and
|
||||
/// advances past them. The region stays claimed until [`pop`], and stays
|
||||
/// readable until the next `push`. `reserve` derived from the root's
|
||||
/// stack need makes the capacity bound exact, so overflow is a debug
|
||||
/// assertion, not a hot-path branch.
|
||||
pub fn push(bytes: usize) -> *mut u8 {
|
||||
STACK.with(|stack| {
|
||||
let sp = stack.sp.get();
|
||||
let next = sp + bytes.next_multiple_of(8);
|
||||
debug_assert!(next <= stack.capacity.get(), "record stack overflow: reserve() must cover the root's stack need");
|
||||
stack.sp.set(next);
|
||||
unsafe { stack.base.get().add(sp) }
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the stack pointer to `frame`, a pointer earlier returned by
|
||||
/// [`push`] on this thread, releasing everything above it.
|
||||
pub fn pop(frame: *mut u8) {
|
||||
STACK.with(|stack| {
|
||||
let offset = frame as usize - stack.base.get() as usize;
|
||||
debug_assert!(offset <= stack.sp.get(), "pop target must lie within the claimed stack");
|
||||
stack.sp.set(offset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +258,13 @@ pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize,
|
||||
plan
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `offset` must be a field offset of the layout of the record under
|
||||
/// construction at `dst` and `T` the field's type; both are proven at wiring.
|
||||
pub unsafe fn write_field<T>(dst: *mut u8, offset: usize, value: T) {
|
||||
unsafe { dst.add(offset).cast::<T>().write(value) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `src` must be a record of the plan's source layout and `dst` a buffer of
|
||||
/// the plan's target layout; both are proven at wiring.
|
||||
@@ -235,7 +279,6 @@ pub unsafe fn apply_plan(src: Rec, dst: *mut u8, plan: &[(usize, usize, usize)])
|
||||
fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> {
|
||||
let mut bytes = vec![0u8; size].into_boxed_slice();
|
||||
if let Some(info) = attribute::info(name)
|
||||
&& info.packable
|
||||
&& info.size == size
|
||||
{
|
||||
(info.write_default_bytes)(&mut bytes);
|
||||
@@ -244,18 +287,18 @@ fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> {
|
||||
}
|
||||
|
||||
/// A routing source's wiring-resolved translation: field moves into the
|
||||
/// consumer's frame buffer plus census default fill for union fields the
|
||||
/// source lacks. Absent when the source's layout already equals the union,
|
||||
/// in which case the record pointer forwards untouched.
|
||||
/// union layout plus census default fill for union fields the source lacks.
|
||||
/// Absent when the source's layout already equals the union, in which case
|
||||
/// the record pointer forwards untouched.
|
||||
#[derive(Debug)]
|
||||
pub struct SourcePlan {
|
||||
moves: Vec<(usize, usize, usize)>,
|
||||
fills: Vec<(usize, Box<[u8]>)>,
|
||||
slot: usize,
|
||||
union_bytes: usize,
|
||||
}
|
||||
|
||||
impl SourcePlan {
|
||||
pub fn new(source: &Layout, union: &Layout, slot: usize) -> Option<SourcePlan> {
|
||||
pub fn new(source: &Layout, union: &Layout) -> Option<SourcePlan> {
|
||||
if source == union {
|
||||
return None;
|
||||
}
|
||||
@@ -266,15 +309,18 @@ impl SourcePlan {
|
||||
.filter(|field| source.offset_of(field.name, field.level).is_none())
|
||||
.map(|field| (field.offset, default_fill_bytes(field.name, field.size)))
|
||||
.collect();
|
||||
Some(SourcePlan { moves, fills, slot })
|
||||
Some(SourcePlan {
|
||||
moves,
|
||||
fills,
|
||||
union_bytes: union.size,
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `src` must be a record of this plan's source layout and `frame` the
|
||||
/// frame whose slot was assigned to this plan at wiring.
|
||||
pub unsafe fn translate(&self, src: Rec, frame: &Frame) -> Rec {
|
||||
/// `src` must be a record of this plan's source layout and `dst` a
|
||||
/// buffer of the plan's union layout.
|
||||
pub unsafe fn translate(&self, src: Rec, dst: *mut u8) -> Rec {
|
||||
unsafe {
|
||||
let dst = frame.slot(self.slot);
|
||||
apply_plan(src, dst, &self.moves);
|
||||
for (offset, bytes) in &self.fills {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst.add(*offset), bytes.len());
|
||||
@@ -288,36 +334,37 @@ impl SourcePlan {
|
||||
/// Evaluating it yields the source's record translated to the union layout
|
||||
/// (or forwarded untouched when the layouts already agree), so the kernel
|
||||
/// holds and returns record values without ever seeing the representation.
|
||||
/// A translation claims its landing region from the record stack without
|
||||
/// popping, so the value survives sibling evaluations; the region is
|
||||
/// released with the enclosing frame, which bounds claims at one per source
|
||||
/// evaluation the kernel performs.
|
||||
pub struct RecordSource<N> {
|
||||
edge: N,
|
||||
plan: Option<SourcePlan>,
|
||||
}
|
||||
|
||||
impl<N> RecordSource<N> {
|
||||
pub fn wire(edge: N, source: &Layout, union: &Layout, slot: usize) -> Self {
|
||||
pub fn wire(edge: N, source: &Layout, union: &Layout) -> Self {
|
||||
Self {
|
||||
edge,
|
||||
plan: SourcePlan::new(source, union, slot),
|
||||
plan: SourcePlan::new(source, union),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e, C, N> Node<C> for RecordSource<N>
|
||||
where
|
||||
C: crate::context::ExtractFrame<'e>,
|
||||
N: Node<C, Output = RecordValue<'e>>,
|
||||
{
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
|
||||
let value = self.edge.eval(input);
|
||||
match &self.plan {
|
||||
None => value,
|
||||
None => self.edge.eval(input),
|
||||
Some(plan) => {
|
||||
let Some(frame) = crate::context::ExtractFrame::frame(input) else {
|
||||
return GPoll::error("record frame missing");
|
||||
};
|
||||
value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), frame) }))
|
||||
let dst = stack::push(plan.union_bytes);
|
||||
let value = self.edge.eval(input);
|
||||
value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), dst) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,27 +403,15 @@ mod tests {
|
||||
assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_slots_bump_aligned() {
|
||||
let flag = Layout::default().with_writes(0, (1, 1), &[]);
|
||||
let wide = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
|
||||
let mut frame = FrameLayout::default();
|
||||
assert_eq!(frame.slot(&flag), 0);
|
||||
assert_eq!(frame.slot(&wide), 8);
|
||||
assert_eq!(frame.size(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_moves_fields_and_fills_census_defaults() {
|
||||
let source = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]);
|
||||
let union = Layout::union(&[&source, &Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")])]);
|
||||
let mut frame_layout = FrameLayout::default();
|
||||
let slot = frame_layout.slot(&union);
|
||||
let frame = Frame::new(frame_layout.size());
|
||||
|
||||
let plan = SourcePlan::new(&source, &union, slot).unwrap();
|
||||
let plan = SourcePlan::new(&source, &union).unwrap();
|
||||
let record = [5f64, 7f64];
|
||||
let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), &frame) };
|
||||
let mut buffer = vec![0u64; union.size.div_ceil(8)];
|
||||
let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), buffer.as_mut_ptr().cast()) };
|
||||
assert_eq!(unsafe { translated.element::<f64>() }, 5.);
|
||||
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("length", 0).unwrap()) }, 7.);
|
||||
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("opacity", 0).unwrap()) }, 1.);
|
||||
@@ -385,6 +420,45 @@ mod tests {
|
||||
#[test]
|
||||
fn identity_layouts_forward() {
|
||||
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
|
||||
assert!(SourcePlan::new(&layout, &layout.clone(), 0).is_none());
|
||||
assert!(SourcePlan::new(&layout, &layout.clone()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_frames_nest_and_release() {
|
||||
stack::reserve(64);
|
||||
let outer = stack::push(24);
|
||||
let inner = stack::push(8);
|
||||
assert_eq!(inner as usize - outer as usize, 24);
|
||||
stack::pop(outer);
|
||||
assert_eq!(stack::push(8), outer);
|
||||
stack::pop(outer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_rounds_frames_to_word_alignment() {
|
||||
stack::reserve(64);
|
||||
let first = stack::push(21);
|
||||
let second = stack::push(8);
|
||||
assert_eq!(second as usize - first as usize, 24);
|
||||
stack::pop(first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_thread_gets_its_own_stack() {
|
||||
stack::reserve(64);
|
||||
let here = stack::push(8);
|
||||
let here_address = here as usize;
|
||||
std::thread::scope(|scope| {
|
||||
scope
|
||||
.spawn(move || {
|
||||
stack::reserve(64);
|
||||
let there = stack::push(8);
|
||||
assert_ne!(here_address, there as usize, "stacks are per thread");
|
||||
stack::pop(there);
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
stack::pop(here);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,11 @@ where
|
||||
unsafe { self.ptr.as_ref() }.serialize()
|
||||
}
|
||||
|
||||
fn layout(&self) -> Option<&crate::record::Layout> {
|
||||
// SAFETY: as in eval.
|
||||
unsafe { self.ptr.as_ref() }.layout()
|
||||
}
|
||||
|
||||
fn eval_batch<'a>(&self, input: &'a Input, range: std::ops::Range<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<Self::Output>]>) -> crate::node::BatchStatus<'a, Self::Output>
|
||||
where
|
||||
Input: crate::context::InjectIndex + Copy,
|
||||
@@ -167,6 +172,7 @@ pub struct EdgeHandle {
|
||||
node: Box<DynEdge>,
|
||||
share: fn(&DynEdge) -> Box<DynEdge>,
|
||||
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
|
||||
layout: fn(&DynEdge) -> Option<&crate::record::Layout>,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
@@ -201,6 +207,7 @@ impl EdgeHandle {
|
||||
node: Box::new(SharedEdge::new(node)),
|
||||
share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()),
|
||||
serialize: |edge| Node::<ContextImpl>::serialize(edge.downcast_ref::<SharedEdge<N>>().expect("serialize hook matches the stored edge type")),
|
||||
layout: |edge| Node::<ContextImpl>::layout(edge.downcast_ref::<SharedEdge<N>>().expect("layout hook matches the stored edge type")),
|
||||
ty,
|
||||
}
|
||||
}
|
||||
@@ -214,6 +221,7 @@ impl EdgeHandle {
|
||||
node: (self.share)(&*self.node),
|
||||
share: self.share,
|
||||
serialize: self.serialize,
|
||||
layout: self.layout,
|
||||
ty: self.ty.clone(),
|
||||
}
|
||||
}
|
||||
@@ -222,6 +230,10 @@ impl EdgeHandle {
|
||||
(self.serialize)(&*self.node)
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> Option<&crate::record::Layout> {
|
||||
(self.layout)(&*self.node)
|
||||
}
|
||||
|
||||
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedNode<T>>, ConstructionError> {
|
||||
self.downcast_erased(edge_type::<T>())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user