Convert create_context to a sync derivation kernel over the bare-root contract

This commit is contained in:
Dennis Kobert
2026-07-27 11:40:16 +00:00
parent 3d17fa0f0c
commit c3fdc97f81
8 changed files with 95 additions and 22 deletions

1
Cargo.lock generated
View File

@@ -2038,6 +2038,7 @@ dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-hash",
"graphene-resource",
"log",
"raster-types",

View File

@@ -84,12 +84,12 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
..Default::default()
},
DocumentNode {
call_argument: concrete!(graphene_std::application_io::RenderConfig),
call_argument: concrete!(Context),
inputs: vec![NodeInput::node(NodeId(4), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
context_features: graphene_std::ContextDependencies {
// We add the extract index annotation here to force the compiler to add a context nullification node before this node so the render context is properly nullified so the render cache node can do its's work
extract: ContextFeatures::INDEX,
extract: ContextFeatures::INDEX | ContextFeatures::VARARGS,
inject: ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::POINTER_POSITION | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
},
..Default::default()

View File

@@ -16,6 +16,7 @@ wgpu = ["dep:raster-types", "raster-types/wgpu"]
# Local dependencies
dyn-any = { workspace = true }
core-types = { workspace = true }
graphene-hash = { workspace = true }
vector-types = { workspace = true }
text-nodes = { workspace = true }
graphene-resource = { workspace = true }

View File

@@ -61,7 +61,7 @@ pub trait GetEditorPreferences {
fn max_render_region_area(&self) -> u32;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExportFormat {
#[default]
@@ -69,14 +69,14 @@ pub enum ExportFormat {
Raster,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimingInformation {
pub time: f64,
pub animation_time: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderConfig {
pub viewport: Footprint,

View File

@@ -770,6 +770,12 @@ impl<'a> EvalScope<'a> {
scope
}
pub fn with_pointer_position(&self, pointer_position: Option<DVec2>) -> EvalScope<'a> {
let mut scope = EvalScope { pointer_position, ..*self };
scope.hash = scope.compute_hash(None);
scope
}
pub fn nullified(&self, keep: ContextFeatures) -> EvalScope<'a> {
let mut scope = EvalScope {
real_time: self.real_time.filter(|_| keep.contains(ContextFeatures::REAL_TIME)),

View File

@@ -225,7 +225,7 @@ mod tests {
);
assert_eq!(GNode::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(runtime.drain(), vec![], "an interrupted prologue must not spawn or claim the slot");
assert_eq!(runtime.drain(), Vec::<SourceId>::new(), "an interrupted prologue must not spawn or claim the slot");
gate.store(true, Ordering::Relaxed);
assert_eq!(GNode::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(runtime.drain(), vec![9]);

View File

@@ -68,6 +68,7 @@ impl_via_hash! {
#[cfg(feature = "std")]
impl_via_hash! {
String,
core::time::Duration,
}
impl<'a> CacheHash for std::borrow::Cow<'a, str> {

View File

@@ -1,7 +1,7 @@
use core_types::gpoll::Interrupt;
use core_types::list::List;
use core_types::transform::{Footprint, Transform};
use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractVarArgs, OwnedContextImpl, WasmNotSend};
use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots, WasmNotSend};
use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphene_application_io::{ExportFormat, RenderConfig};
use graphic_types::raster_types::{CPU, Raster};
@@ -141,11 +141,13 @@ fn render<'a>(
}
#[node_macro::node(category(""))]
fn create_context<'a>(
// Context injections are defined in the wrap_network_in_scope function
render_config: RenderConfig,
data: impl Node<Context<'_>, Output = RenderOutput>,
) -> RenderOutput {
fn create_context(ctx: impl Ctx + ExtractVarArgs + DeriveCtx, data: impl Node<Context<'_>, Output = RenderOutput>) -> Result<RenderOutput, Interrupt> {
let render_config = *ctx
.vararg(0)
.expect("Did not find var args")
.downcast_ref::<RenderConfig>()
.expect("Downcasting render config yielded invalid type");
let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
ExportFormat::Raster => RenderOutputTypeRequest::Vello,
@@ -166,16 +168,78 @@ fn create_context<'a>(
..Default::default()
};
let ctx = OwnedContextImpl::default()
.with_footprint(footprint)
.with_real_time(render_config.time.time)
.with_animation_time(render_config.time.animation_time.as_secs_f64())
.with_pointer_position(render_config.pointer)
.with_vararg(Box::new(render_params))
.into_context();
let mut result = data.eval(ctx).await;
let scope = ctx
.scope()
.with_real_time(Some(render_config.time.time))
.with_animation_time(Some(render_config.time.animation_time.as_secs_f64()))
.with_pointer_position(Some(render_config.pointer));
let varargs = VarArgLink {
args: VarArgSlots::Single(&render_params),
outer: None,
};
let scoped = ctx.with_scope(&scope);
let with_params = scoped.with_varargs(&varargs);
let mut result = data.eval(&with_params.with_footprint(&footprint))?;
result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale)));
result
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, EvalScope, VarArgsResult};
use core_types::gnode::GNode;
use core_types::gpoll::GPoll;
use core_types::{ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
use graphene_application_io::TimingInformation;
struct ProbeNode;
impl<'a> GNode<ContextImpl<'a>> for ProbeNode {
type Output = RenderOutput;
fn eval(&self, ctx: &ContextImpl<'a>) -> GPoll<RenderOutput> {
let render_params = ctx.vararg(0).unwrap().downcast_ref::<RenderParams>().expect("the vararg chain must start with RenderParams");
assert_eq!(render_params.scale, 2.0);
assert!(matches!(ctx.vararg(1), Err(VarArgsResult::IndexOutOfBounds)), "the RenderConfig must not leak downstream");
assert_eq!(ctx.footprint().transform, glam::DAffine2::from_scale(glam::DVec2::splat(2.0)) * Footprint::DEFAULT.transform);
assert_eq!(ctx.try_real_time(), Some(1.5));
assert_eq!(ctx.try_animation_time(), Some(2.0));
assert_eq!(ctx.try_pointer_position(), Some(glam::DVec2::new(3.0, 4.0)));
GPoll::Final(RenderOutput {
data: RenderOutputType::Buffer { data: Vec::new(), width: 0, height: 0 },
metadata: RenderMetadata::default(),
})
}
}
#[test]
fn create_context_builds_the_render_context_from_the_root_vararg() {
let arena = Arena::new(256);
let generations = [];
let scope = EvalScope::new(None, None, None, &generations, &arena);
let root = ContextImpl::root(&scope);
let render_config = RenderConfig {
scale: 2.0,
time: TimingInformation {
time: 1.5,
animation_time: std::time::Duration::from_secs(2),
},
pointer: glam::DVec2::new(3.0, 4.0),
..Default::default()
};
let varargs = VarArgLink {
args: VarArgSlots::Single(&render_config),
outer: None,
};
let ctx = root.with_varargs(&varargs);
let graph = CreateContextNode::new(ProbeNode);
let GPoll::Final(result) = <CreateContextNode<ProbeNode> as GNode<ContextImpl>>::eval(&graph, &ctx) else {
panic!("create_context must complete synchronously");
};
assert_eq!(result.data, RenderOutputType::Buffer { data: Vec::new(), width: 0, height: 0 });
}
}