From 9f8fcada19f837e64fe7c608ae62ab151292e43f Mon Sep 17 00:00:00 2001 From: Timon Date: Sun, 21 Jun 2026 23:39:23 +0000 Subject: [PATCH] WIP --- Cargo.lock | 15 ++ Cargo.toml | 1 + node-graph/nodes/brush-next/Cargo.toml | 23 ++ node-graph/nodes/brush-next/src/lib.rs | 1 + .../nodes/brush-next/src/render_brush.rs | 230 ++++++++++++++++++ .../nodes/brush-next/src/render_brush.wgsl | 50 ++++ node-graph/nodes/gstd/Cargo.toml | 1 + node-graph/nodes/gstd/src/lib.rs | 1 + 8 files changed, 322 insertions(+) create mode 100644 node-graph/nodes/brush-next/Cargo.toml create mode 100644 node-graph/nodes/brush-next/src/lib.rs create mode 100644 node-graph/nodes/brush-next/src/render_brush.rs create mode 100644 node-graph/nodes/brush-next/src/render_brush.wgsl diff --git a/Cargo.lock b/Cargo.lock index 96b5bd89ed..3563f4748d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,20 @@ dependencies = [ "cfg_aliases", ] +[[package]] +name = "brush-next-nodes" +version = "0.0.0" +dependencies = [ + "bytemuck", + "core-types", + "dyn-any", + "glam", + "node-macro", + "raster-types", + "wgpu", + "wgpu-executor", +] + [[package]] name = "brush-nodes" version = "0.1.0" @@ -2113,6 +2127,7 @@ version = "0.1.0" dependencies = [ "base64", "blending-nodes", + "brush-next-nodes", "brush-nodes", "bytemuck", "core-types", diff --git a/Cargo.toml b/Cargo.toml index 3c6e406979..9f42d322e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ vector-types = { path = "node-graph/libraries/vector-types" } graphic-types = { path = "node-graph/libraries/graphic-types" } rendering = { path = "node-graph/libraries/rendering" } brush-nodes = { path = "node-graph/nodes/brush" } +brush-next-nodes = { path = "node-graph/nodes/brush-next" } blending-nodes = { path = "node-graph/nodes/blending" } graphene-core = { path = "node-graph/nodes/gcore" } graphic-nodes = { path = "node-graph/nodes/graphic" } diff --git a/node-graph/nodes/brush-next/Cargo.toml b/node-graph/nodes/brush-next/Cargo.toml new file mode 100644 index 0000000000..a8789f33d4 --- /dev/null +++ b/node-graph/nodes/brush-next/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "brush-next-nodes" +description = "Next-generation GPU brush rendering nodes for Graphene" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[features] +default = [] + +[dependencies] +# Local dependencies +dyn-any = { workspace = true } +core-types = { workspace = true } +raster-types = { workspace = true, features = ["wgpu"] } +wgpu-executor = { workspace = true } +node-macro = { workspace = true } + +# Workspace dependencies +glam = { workspace = true } +wgpu = { workspace = true } +bytemuck = { workspace = true } diff --git a/node-graph/nodes/brush-next/src/lib.rs b/node-graph/nodes/brush-next/src/lib.rs new file mode 100644 index 0000000000..a49a03c595 --- /dev/null +++ b/node-graph/nodes/brush-next/src/lib.rs @@ -0,0 +1 @@ +pub mod render_brush; diff --git a/node-graph/nodes/brush-next/src/render_brush.rs b/node-graph/nodes/brush-next/src/render_brush.rs new file mode 100644 index 0000000000..0c87711070 --- /dev/null +++ b/node-graph/nodes/brush-next/src/render_brush.rs @@ -0,0 +1,230 @@ +use bytemuck::{Pod, Zeroable}; +use core_types::list::{Item, List}; +use core_types::{ATTR_TRANSFORM, Ctx, ProtoNodeIdentifier}; +use glam::{DAffine2, DVec2, UVec2, Vec2}; +use raster_types::{GPU, Raster}; +use std::marker::PhantomData; +use std::sync::Arc; +use wgpu::util::DeviceExt; +use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; + +const WGPU_EXECUTOR_IDENTIFIER: ProtoNodeIdentifier = ProtoNodeIdentifier::new("graphene_std::platform_application_io::WgpuExecutorNode"); + +#[node_macro::node(category("Raster: Brush"))] +pub async fn render_brush_next<'a: 'n>( + _ctx: impl Ctx, + #[scope(brush_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, + start: DVec2, + end: DVec2, + #[default(8)] stamps: u32, + #[default(16.)] radius: f64, +) -> List> { + let pad = DVec2::splat(radius); + let bbox_min = start.min(end) - pad; + let bbox_max = start.max(end) + pad; + let bbox_size = (bbox_max - bbox_min).max(DVec2::ONE); + let size = bbox_size.ceil().as_uvec2().max(UVec2::ONE); + + let args = BrushPipelineArgs { + start: start - bbox_min, + end: end - bbox_min, + stamps, + radius: radius as f32, + size, + _marker: PhantomData, + }; + let texture = pipeline.run::(&args).await; + let raster = Raster::::new_gpu(texture.as_ref().clone()); + let transform = DAffine2::from_translation(bbox_min) * DAffine2::from_scale(DVec2::new(size.x as f64, size.y as f64)); + List::new_from_item(Item::new_from_element(raster).with_attribute(ATTR_TRANSFORM, transform)) +} + +#[node_macro::node(category(""), inject_scope)] +async fn brush_pipeline<'a: 'n>(_ctx: impl Ctx, #[scope(WGPU_EXECUTOR_IDENTIFIER)] executor: &'a WgpuExecutor, #[data] pipeline: WgpuPipelineCache) -> WgpuPipelineCache { + executor.pipeline_init::(pipeline); + pipeline.clone() +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +struct Stamp { + center_px: [f32; 2], + radius_px: f32, + _pad: f32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +struct Globals { + viewport_size: [f32; 2], + _pad: [f32; 2], +} + +pub struct BrushPipeline { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, +} + +pub struct BrushPipelineArgs<'a> { + pub start: DVec2, + pub end: DVec2, + pub stamps: u32, + pub radius: f32, + pub size: UVec2, + pub _marker: PhantomData<&'a ()>, +} + +impl AsyncWgpuPipeline for BrushPipeline { + type Args<'a> = BrushPipelineArgs<'a>; + type Out = Arc; + + fn create(executor: &WgpuExecutor) -> Self { + let device = &executor.context().device; + let shader = device.create_shader_module(wgpu::include_wgsl!("render_brush.wgsl")); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("brush_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("brush_pipeline_layout"), + bind_group_layouts: &[Some(&bind_group_layout)], + immediate_size: 0, + }); + + let instance_layout = wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32, + }, + ], + }; + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("brush_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[instance_layout], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + Self { pipeline, bind_group_layout } + } + + async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + let &BrushPipelineArgs { start, end, stamps, radius, size, .. } = args; + + let size = size.max(UVec2::ONE); + let output = executor.request_texture(size).await; + + let device = &executor.context().device; + let queue = &executor.context().queue; + + let count = stamps.max(1); + let mut instances = Vec::with_capacity(count as usize); + for i in 0..count { + let t = if count == 1 { 0.0 } else { i as f64 / (count - 1) as f64 }; + let center = start.lerp(end, t); + instances.push(Stamp { + center_px: [center.x as f32, center.y as f32], + radius_px: radius, + _pad: 0.0, + }); + } + + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("brush_instance_buffer"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX, + }); + + let globals = Globals { + viewport_size: Vec2::new(size.x as f32, size.y as f32).to_array(), + _pad: [0.0; 2], + }; + + let globals_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("brush_globals_buffer"), + contents: bytemuck::bytes_of(&globals), + usage: wgpu::BufferUsages::UNIFORM, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("brush_bind_group"), + layout: &self.bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: globals_buffer.as_entire_binding(), + }], + }); + + let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("brush_encoder") }); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("brush_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + ..Default::default() + }); + + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.set_vertex_buffer(0, instance_buffer.slice(..)); + pass.draw(0..3, 0..count); + } + + queue.submit([encoder.finish()]); + + output + } +} diff --git a/node-graph/nodes/brush-next/src/render_brush.wgsl b/node-graph/nodes/brush-next/src/render_brush.wgsl new file mode 100644 index 0000000000..93d83bc80e --- /dev/null +++ b/node-graph/nodes/brush-next/src/render_brush.wgsl @@ -0,0 +1,50 @@ +struct Globals { + viewport_size: vec2, + _pad: vec2, +}; + +@group(0) @binding(0) +var globals: Globals; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) center_px: vec2, + @location(1) radius_px: f32, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) vertex_index: u32, + @location(0) center_px: vec2, + @location(1) radius_px: f32, +) -> VertexOutput { + let viewport = globals.viewport_size; + + let offset_x = f32(i32(vertex_index) - 1) * 2.0; + let offset_y = f32(i32(vertex_index & 1u) * 2 - 1) * 2.0; + + let pos_px = center_px + vec2(offset_x, offset_y) * radius_px; + + let clip = vec2( + 2.0 * pos_px.x / viewport.x - 1.0, + 1.0 - 2.0 * pos_px.y / viewport.y, + ); + + var out: VertexOutput; + out.clip_position = vec4(clip, 0.0, 1.0); + out.center_px = center_px; + out.radius_px = radius_px; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let frag_px = in.clip_position.xy; + let d = distance(frag_px, in.center_px); + if (d > in.radius_px) { + discard; + } + let t = d / in.radius_px; + let alpha = 1.0 - smoothstep(0.0, 1.0, t); + return vec4(0.0, 0.0, 0.0, alpha); +} diff --git a/node-graph/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index 2385c9a9b6..b091d23fb5 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -49,6 +49,7 @@ rendering = { workspace = true } graphene-application-io = { workspace = true } raster-nodes = { workspace = true } brush-nodes = { workspace = true } +brush-next-nodes = { workspace = true } graphene-core = { workspace = true } graphic-nodes = { workspace = true } repeat-nodes = { workspace = true } diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 7236bb8c63..860746923e 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -6,6 +6,7 @@ pub mod render_node; pub mod render_pixel_preview; pub mod text; pub use blending_nodes; +pub use brush_next_nodes as brush_next; pub use brush_nodes as brush; pub use core_types::*; pub use graphene_application_io as application_io;