use std::collections::HashMap;
use std::sync::Arc;
use crate::gpu::params::ParamValue;
use crate::nodegraph::{
ExecStep, ExecutionPlan, Graph, InputSlot, NodeId, NodeRegistration, PortDef, PortDir, PortRef,
};
use super::curve_math::CurveLut;
use super::nodes::{paint_color, pen_input};
use super::gpu_context::BrushGpuContext;
use super::wgsl::CompiledBrush;
use super::wire::{BrushWireType, ScalarValue};
pub struct EvalContext<'a> {
pub input_slots: &'a [InputSlot],
pub input_values: &'a [Option<ScalarValue>],
pub params: &'a [ParamValue],
pub port_defs: &'a [PortDef<BrushWireType>],
pub lut: Option<&'a CurveLut>,
pub stroke_seed: u32,
pub dab_index: u32,
pub node_id: NodeId,
}
impl EvalContext<'_> {
pub fn input(&self, name: &str) -> ScalarValue {
for (i, slot) in self.input_slots.iter().enumerate() {
if slot.port_name == name {
if let Some(val) = self.input_values[i] {
return val;
}
break;
}
}
for port in self.port_defs {
if port.name == name && port.dir == PortDir::Input {
return ScalarValue::Scalar(port.default);
}
}
ScalarValue::default()
}
pub fn input_f32(&self, name: &str) -> f32 {
self.input(name).as_f32()
}
pub fn param_f32(&self, index: usize) -> f32 {
match self.params.get(index) {
Some(ParamValue::Float(v)) => *v,
Some(ParamValue::Int(v)) => *v as f32,
_ => 0.0,
}
}
pub fn param_str(&self, index: usize) -> &str {
match self.params.get(index) {
Some(ParamValue::String(s)) => s.as_str(),
_ => "",
}
}
pub fn param_curve(&self, index: usize) -> &[[f32; 2]] {
match self.params.get(index) {
Some(ParamValue::Curve(pts)) => pts.as_slice(),
_ => &[[0.0, 0.0], [1.0, 1.0]],
}
}
#[inline]
pub fn curve_lookup(&self, t: f32) -> f32 {
match self.lut {
Some(lut) => lut.evaluate(t),
None => t,
}
}
#[inline]
pub fn prng_at(&self, index: u32) -> f32 {
let salt = self.node_id.0 as u32;
let seed = self.stroke_seed.wrapping_add(salt.wrapping_mul(0x9E3779B9));
prng_f32(seed, index)
}
}
fn gather_inputs_into(
slots: &[Option<ScalarValue>],
input_slots: &[InputSlot],
dest_node: NodeId,
node_data: &HashMap<NodeId, NodeData>,
scratch: &mut Vec<Option<ScalarValue>>,
) {
scratch.clear();
scratch.reserve(input_slots.len());
for slot_info in input_slots {
let entry = slots[slot_info.slot].map(|val| {
remap_for_wire(
val,
&slot_info.source,
dest_node,
&slot_info.port_name,
node_data,
)
});
scratch.push(entry);
}
}
fn remap_for_wire(
value: ScalarValue,
source: &PortRef,
dest_node: NodeId,
dest_port: &str,
node_data: &HashMap<NodeId, NodeData>,
) -> ScalarValue {
let src_range = node_data
.get(&source.node)
.and_then(|n| {
n.port_defs
.iter()
.find(|p| p.name == source.port && p.dir == PortDir::Output)
})
.and_then(|p| p.natural_range);
let dst_range = node_data
.get(&dest_node)
.and_then(|n| {
n.port_defs
.iter()
.find(|p| p.name == dest_port && p.dir == PortDir::Input)
})
.and_then(|p| p.natural_range);
let (Some(src), Some(dst)) = (src_range, dst_range) else {
return value;
};
match value {
ScalarValue::Scalar(_) | ScalarValue::Int(_) | ScalarValue::Bool(_) => {
ScalarValue::Scalar(remap_scalar(value.as_f32(), src, dst))
}
_ => value,
}
}
#[inline]
fn remap_scalar(value: f32, src: (f32, f32), dst: (f32, f32)) -> f32 {
let (src_min, src_max) = src;
let (dst_min, dst_max) = dst;
let denom = src_max - src_min;
if denom == 0.0 {
return dst_min;
}
let fraction = (value - src_min) / denom;
dst_min + fraction * (dst_max - dst_min)
}
fn apply_lifecycle(lifecycle: super::node::Lifecycle, gpu: &mut BrushGpuContext) {
use super::node::Lifecycle;
let Some(stroke) = &gpu.stroke else { return };
match lifecycle {
Lifecycle::None => {}
Lifecycle::ClearScratchToTransparent => {
stroke.scratch.clear_to_transparent(&mut gpu.encoder);
}
Lifecycle::SeedScratchFromPreStroke => {
stroke
.scratch
.seed_from_pre_stroke(&mut gpu.encoder, stroke.pre_stroke_texture);
}
}
}
#[inline]
fn prng_f32(seed: u32, index: u32) -> f32 {
let mut h = seed.wrapping_add(index.wrapping_mul(2654435761));
h ^= h >> 16;
h = h.wrapping_mul(0x45d9f3b);
h ^= h >> 16;
h = h.wrapping_mul(0x45d9f3b);
h ^= h >> 16;
(h & 0x00FF_FFFF) as f32 / 0x0100_0000 as f32
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BrushCursorPreviewInfo {
pub half_extent_canvas_px: [f32; 2],
}
pub trait BrushNodeEvaluator: Send + Sync {
fn evaluate_cpu(&self, ctx: &EvalContext) -> Vec<(String, ScalarValue)>;
fn evaluate_gpu(
&self,
_ctx: &EvalContext,
_gpu: &mut BrushGpuContext,
) -> Vec<(String, ScalarValue)> {
vec![]
}
fn render_cursor_preview(
&self,
ctx: &EvalContext,
gpu: &mut BrushGpuContext,
) -> Vec<(String, ScalarValue)> {
self.evaluate_gpu(ctx, gpu)
}
fn begin_stroke(&self, _ctx: &EvalContext, _gpu: &mut BrushGpuContext) {}
fn commit(&self, _ctx: &EvalContext, _gpu: &mut BrushGpuContext) {}
fn flush_dabs(&self, _ctx: &EvalContext, _gpu: &mut BrushGpuContext) {}
fn compile_wgsl(
&self,
_cctx: &crate::brush::wgsl::CompileWgslCtx,
) -> Result<crate::brush::wgsl::NodeWgsl, String> {
Err("node has no WGSL implementation".into())
}
fn compile_cursor_preview_body(
&self,
cctx: &crate::brush::wgsl::CompileWgslCtx,
) -> Result<crate::brush::wgsl::NodeWgsl, String> {
self.compile_wgsl(cctx)
}
fn extent(
&self,
_ctx: &crate::brush::wgsl::ExtentCtx,
) -> crate::brush::wgsl::ExtentContribution {
crate::brush::wgsl::ExtentContribution::Identity
}
}
pub struct BrushGraphRunner {
plan: ExecutionPlan,
step_evaluators: Vec<Option<Arc<dyn BrushNodeEvaluator>>>,
slots: Vec<Option<ScalarValue>>,
inputs_scratch: Vec<Option<ScalarValue>>,
node_data: HashMap<NodeId, NodeData>,
pen_input_slots: Vec<(String, usize)>,
paint_color_slot: Option<usize>,
dab_size_slot: Option<usize>,
stroke_seed: u32,
dab_index: u32,
compiled: Option<Arc<CompiledBrush>>,
}
struct NodeData {
params: Vec<ParamValue>,
port_defs: Vec<PortDef<BrushWireType>>,
lut: Option<CurveLut>,
}
fn build_eval_ctx<'a>(
step: &'a ExecStep,
input_slots: &'a [InputSlot],
input_values: &'a [Option<ScalarValue>],
node_data: &'a HashMap<NodeId, NodeData>,
stroke_seed: u32,
dab_index: u32,
) -> EvalContext<'a> {
let node = node_data.get(&step.node_id);
EvalContext {
input_slots,
input_values,
params: node.map(|n| n.params.as_slice()).unwrap_or(&[]),
port_defs: node.map(|n| n.port_defs.as_slice()).unwrap_or(&[]),
lut: node.and_then(|n| n.lut.as_ref()),
stroke_seed,
dab_index,
node_id: step.node_id,
}
}
impl BrushGraphRunner {
pub fn new(
graph: &Graph<BrushWireType>,
registry: &HashMap<String, NodeRegistration<BrushWireType>>,
evaluators: HashMap<String, Box<dyn BrushNodeEvaluator>>,
) -> Result<Self, crate::nodegraph::GraphError> {
let plan = crate::nodegraph::compile(graph, registry)?;
let slots = vec![None; plan.slot_count];
let evaluators: HashMap<String, Arc<dyn BrushNodeEvaluator>> = evaluators
.into_iter()
.map(|(k, v)| (k, Arc::from(v)))
.collect();
let step_evaluators: Vec<Option<Arc<dyn BrushNodeEvaluator>>> = plan
.steps
.iter()
.map(|step| evaluators.get(&step.type_id).cloned())
.collect();
let mut node_data = HashMap::new();
for step in &plan.steps {
if let Some(node) = graph.nodes().get(&step.node_id) {
let lut = node.params.iter().find_map(|p| match p {
ParamValue::Curve(pts) if pts.len() >= 2 => Some(CurveLut::from_points(pts)),
_ => None,
});
node_data.insert(
step.node_id,
NodeData {
params: node.params.clone(),
port_defs: node.ports.clone(),
lut,
},
);
}
}
let pen_input_slots = plan
.steps
.iter()
.find(|s| s.type_id == pen_input::TYPE_ID)
.map(|s| s.output_slots.clone())
.unwrap_or_default();
let paint_color_slot = plan
.steps
.iter()
.find(|s| s.type_id == paint_color::TYPE_ID)
.and_then(|s| s.output_slots.iter().find(|(name, _)| name == "color"))
.map(|(_, slot)| *slot);
let dab_size_slot = plan.steps.iter().filter(|s| s.is_terminal).find_map(|s| {
s.output_slots
.iter()
.find(|(name, _)| name == "dab_size")
.map(|(_, slot)| *slot)
});
let max_inputs = plan
.steps
.iter()
.map(|s| s.input_slots.len())
.max()
.unwrap_or(0);
Ok(Self {
plan,
step_evaluators,
slots,
inputs_scratch: Vec::with_capacity(max_inputs),
node_data,
pen_input_slots,
paint_color_slot,
dab_size_slot,
stroke_seed: 0,
dab_index: 0,
compiled: None,
})
}
pub fn set_compiled_brush(&mut self, compiled: Arc<CompiledBrush>) {
self.compiled = Some(compiled);
}
pub fn compiled_brush(&self) -> Option<Arc<CompiledBrush>> {
self.compiled.clone()
}
pub fn has_terminal(&self) -> bool {
self.plan.steps.iter().any(|step| step.is_terminal)
}
fn build_slot_outputs(&self) -> HashMap<String, ScalarValue> {
let mut out = HashMap::with_capacity(self.slots.len());
for step in &self.plan.steps {
for (port_name, slot_idx) in &step.output_slots {
if let Some(val) = self.slots[*slot_idx] {
out.insert(format!("n{}_{}", step.node_id.0, port_name), val);
}
}
}
out
}
pub fn seed_sensors(
&mut self,
info: &super::paint_info::PaintInformation,
color: [f32; 4],
stroke_seed: u32,
dab_index: u32,
) {
self.stroke_seed = stroke_seed;
self.dab_index = dab_index;
for (name, slot) in &self.pen_input_slots {
let value = match name.as_str() {
"pressure" => ScalarValue::Scalar(info.pressure),
"x_tilt" => ScalarValue::Scalar(info.x_tilt),
"y_tilt" => ScalarValue::Scalar(info.y_tilt),
"tilt_magnitude" => ScalarValue::Scalar(info.tilt_magnitude),
"tilt_direction" => ScalarValue::Scalar(info.tilt_direction),
"rotation" => ScalarValue::Scalar(info.rotation),
"tangential_pressure" => ScalarValue::Scalar(info.tangential_pressure),
"speed" => ScalarValue::Scalar(info.speed),
"distance" => ScalarValue::Scalar(info.distance),
"drawing_angle" => ScalarValue::Scalar(info.drawing_angle),
"time" => ScalarValue::Scalar(info.time),
"position" => ScalarValue::Vec2(info.pos),
"motion" => ScalarValue::Vec2(info.motion),
"index" => ScalarValue::Int(info.index as i32),
"fade" => ScalarValue::Scalar(info.fade),
_ => continue,
};
self.slots[*slot] = Some(value);
}
if let Some(slot) = self.paint_color_slot {
self.slots[slot] = Some(ScalarValue::Vec4(color));
}
}
pub fn execute_cpu(&mut self) {
let n = self.plan.steps.len();
for idx in 0..n {
let step = &self.plan.steps[idx];
if step.type_id == pen_input::TYPE_ID
|| step.type_id == paint_color::TYPE_ID
|| step.is_gpu
{
continue;
}
let Some(evaluator) = self.step_evaluators[idx].clone() else {
continue;
};
gather_inputs_into(
&self.slots,
&step.input_slots,
step.node_id,
&self.node_data,
&mut self.inputs_scratch,
);
let ctx = build_eval_ctx(
step,
&step.input_slots,
&self.inputs_scratch,
&self.node_data,
self.stroke_seed,
self.dab_index,
);
let outputs = evaluator.evaluate_cpu(&ctx);
for (port_name, value) in outputs {
for (name, slot_idx) in &step.output_slots {
if *name == port_name {
self.slots[*slot_idx] = Some(value);
break;
}
}
}
}
}
pub fn execute_gpu(&mut self, gpu: &mut BrushGpuContext) {
self.dispatch_gpu(gpu, |ev, ctx, gpu| ev.evaluate_gpu(ctx, gpu));
}
pub fn render_cursor_preview_pipeline(&mut self, gpu: &mut BrushGpuContext) {
self.dispatch_gpu(gpu, |ev, ctx, gpu| ev.render_cursor_preview(ctx, gpu));
}
fn dispatch_gpu<F>(&mut self, gpu: &mut BrushGpuContext, mut f: F)
where
F: FnMut(
&dyn BrushNodeEvaluator,
&EvalContext,
&mut BrushGpuContext,
) -> Vec<(String, ScalarValue)>,
{
let is_compiled = self.compiled.is_some();
if let Some(compiled) = &self.compiled {
gpu.dab_batch.compiled_brush = Some(compiled.clone());
gpu.dab_batch.slot_outputs = Some(self.build_slot_outputs());
}
let n = self.plan.steps.len();
for idx in 0..n {
let step = &self.plan.steps[idx];
if !step.is_gpu {
continue;
}
let Some(evaluator) = self.step_evaluators[idx].clone() else {
continue;
};
if is_compiled && !step.is_terminal {
continue;
}
gather_inputs_into(
&self.slots,
&step.input_slots,
step.node_id,
&self.node_data,
&mut self.inputs_scratch,
);
let ctx = build_eval_ctx(
step,
&step.input_slots,
&self.inputs_scratch,
&self.node_data,
self.stroke_seed,
self.dab_index,
);
let mut outputs = evaluator.evaluate_cpu(&ctx);
let gpu_outputs = f(evaluator.as_ref(), &ctx, gpu);
outputs.extend(gpu_outputs);
for (port_name, value) in outputs {
for (name, slot_idx) in &step.output_slots {
if *name == port_name {
self.slots[*slot_idx] = Some(value);
break;
}
}
}
}
}
pub fn begin_stroke(&mut self, gpu: &mut BrushGpuContext) {
gpu.dab_batch.clear();
let registry = crate::brush::registry();
self.dispatch_lifecycle(gpu, false, |type_id, ev, ctx, gpu| {
let lifecycle = registry
.get(type_id)
.map(|r| r.lifecycle)
.unwrap_or(super::node::Lifecycle::None);
apply_lifecycle(lifecycle, gpu);
ev.begin_stroke(ctx, gpu);
});
}
pub fn commit(&mut self, gpu: &mut BrushGpuContext) {
self.dispatch_lifecycle(gpu, true, |_id, ev, ctx, gpu| ev.commit(ctx, gpu));
}
pub fn flush_dabs(&mut self, gpu: &mut BrushGpuContext) {
self.dispatch_lifecycle(gpu, false, |_id, ev, ctx, gpu| ev.flush_dabs(ctx, gpu));
}
fn dispatch_lifecycle<F>(
&mut self,
gpu: &mut BrushGpuContext,
gather_from_slots: bool,
mut f: F,
) where
F: FnMut(&str, &dyn BrushNodeEvaluator, &EvalContext, &mut BrushGpuContext),
{
let n = self.plan.steps.len();
for idx in 0..n {
let step = &self.plan.steps[idx];
if !step.is_gpu {
continue;
}
let Some(evaluator) = self.step_evaluators[idx].clone() else {
continue;
};
let (input_slots_view, input_values_view): (&[InputSlot], &[Option<ScalarValue>]) =
if gather_from_slots {
gather_inputs_into(
&self.slots,
&step.input_slots,
step.node_id,
&self.node_data,
&mut self.inputs_scratch,
);
(&step.input_slots, &self.inputs_scratch)
} else {
(&[], &[])
};
let ctx = build_eval_ctx(
step,
input_slots_view,
input_values_view,
&self.node_data,
self.stroke_seed,
self.dab_index,
);
f(&step.type_id, evaluator.as_ref(), &ctx, gpu);
}
}
pub fn read_slot(&self, slot: usize) -> Option<ScalarValue> {
self.slots.get(slot).copied().flatten()
}
pub fn last_dab_size(&self) -> Option<[f32; 2]> {
let slot = self.dab_size_slot?;
let size = self.read_slot(slot)?.as_vec2();
(size[0] > 0.0 && size[1] > 0.0).then_some(size)
}
pub fn find_output_slot(&self, type_id: &str, port_name: &str) -> Option<usize> {
self.plan
.steps
.iter()
.find(|s| s.type_id == type_id)
.and_then(|s| {
s.output_slots
.iter()
.find(|(name, _)| name == port_name)
.map(|(_, slot)| *slot)
})
}
pub fn find_node_output_slot(&self, node_id: NodeId, port_name: &str) -> Option<usize> {
self.plan
.steps
.iter()
.find(|s| s.node_id == node_id)
.and_then(|s| {
s.output_slots
.iter()
.find(|(name, _)| name == port_name)
.map(|(_, slot)| *slot)
})
}
pub fn plan(&self) -> &ExecutionPlan {
&self.plan
}
pub fn clear_slots(&mut self) {
for slot in self.slots.iter_mut() {
*slot = None;
}
}
pub fn graph_has_cursor_preview_wire(&self) -> bool {
self.plan.steps.iter().any(|s| {
s.input_slots
.iter()
.any(|slot| slot.port_name == "brush_preview")
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remap_identity_when_ranges_match() {
for v in [0.0, 0.25, 0.5, 0.75, 1.0] {
assert!((remap_scalar(v, (0.0, 1.0), (0.0, 1.0)) - v).abs() < 1e-6);
}
}
#[test]
fn remap_unit_to_seed_range() {
assert!((remap_scalar(0.0, (0.0, 1.0), (0.0, 1024.0)) - 0.0).abs() < 1e-4);
assert!((remap_scalar(0.5, (0.0, 1.0), (0.0, 1024.0)) - 512.0).abs() < 1e-4);
assert!((remap_scalar(1.0, (0.0, 1.0), (0.0, 1024.0)) - 1024.0).abs() < 1e-4);
}
#[test]
fn remap_bipolar_to_seed_range() {
assert!((remap_scalar(-1.0, (-1.0, 1.0), (0.0, 1024.0)) - 0.0).abs() < 1e-4);
assert!((remap_scalar(0.0, (-1.0, 1.0), (0.0, 1024.0)) - 512.0).abs() < 1e-4);
assert!((remap_scalar(1.0, (-1.0, 1.0), (0.0, 1024.0)) - 1024.0).abs() < 1e-4);
}
#[test]
fn remap_unit_to_bipolar_radians() {
use std::f32::consts::TAU;
assert!((remap_scalar(0.0, (0.0, 1.0), (-TAU, TAU)) - (-TAU)).abs() < 1e-4);
assert!((remap_scalar(0.5, (0.0, 1.0), (-TAU, TAU))).abs() < 1e-4);
assert!((remap_scalar(1.0, (0.0, 1.0), (-TAU, TAU)) - TAU).abs() < 1e-4);
}
#[test]
fn remap_degenerate_source_collapses_to_dst_min() {
assert_eq!(remap_scalar(0.5, (1.0, 1.0), (0.0, 1024.0)), 0.0);
assert_eq!(remap_scalar(0.5, (1.0, 1.0), (-5.0, 5.0)), -5.0);
}
#[test]
fn remap_outside_src_range_is_not_clamped() {
let v = remap_scalar(1.5, (0.0, 1.0), (0.0, 100.0));
assert!((v - 150.0).abs() < 1e-4);
}
}