From b132cf19b4ee7d42cb73b8a7ee3b3176eda37b7a Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 14 Sep 2025 11:00:20 +0200 Subject: [PATCH 1/9] Add upload texture trait --- node-graph/gcore/src/registry.rs | 2 +- .../src/shader_nodes/per_pixel_adjust.rs | 2 +- .../wgpu-executor/src/texture_upload.rs | 90 +++++++++++-------- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index d6a1788d3f..82d4ddb739 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -9,7 +9,7 @@ use std::sync::{LazyLock, Mutex}; pub use graphene_core_shaders::registry::types; // Translation struct between macro and definition -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct NodeMetadata { pub display_name: &'static str, pub category: Option<&'static str>, diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 5708697497..5937b77081 100644 --- a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -128,7 +128,7 @@ impl PerPixelAdjustCodegen<'_> { #(pub #uniform_members),* } }; - let uniform_struct_shader_struct_derive = crate::buffer_struct::derive_buffer_struct_struct(&self.crate_ident, &uniform_struct)?; + let uniform_struct_shader_struct_derive = crate::buffer_struct::derive_buffer_struct_struct(self.crate_ident, &uniform_struct)?; let image_params = self .params diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_upload.rs index f970fdec32..942cb355c0 100644 --- a/node-graph/wgpu-executor/src/texture_upload.rs +++ b/node-graph/wgpu-executor/src/texture_upload.rs @@ -6,47 +6,61 @@ use graphene_core::table::{Table, TableRow}; use wgpu::util::{DeviceExt, TextureDataOrder}; use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; -#[node_macro::node(category(""))] -pub async fn upload_texture<'a: 'n>(_: impl Ctx, input: Table>, executor: &'a WgpuExecutor) -> Table> { - let device = &executor.context.device; - let queue = &executor.context.queue; - let table = input - .iter() - .map(|row| { - let image = row.element; - let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); +pub trait UploadTexture { + fn upload_texture(self, executor: &WgpuExecutor) -> Table>; +} + +impl UploadTexture for Table> { + fn upload_texture(self, _executor: &WgpuExecutor) -> Table> { + self + } +} +impl UploadTexture for Table> { + fn upload_texture(self, executor: &WgpuExecutor) -> Table> { + let device = &executor.context.device; + let queue = &executor.context.queue; + let table = self + .iter() + .map(|row| { + let image = row.element; + let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); - let texture = device.create_texture_with_data( - queue, - &TextureDescriptor { - label: Some("upload_texture node texture"), - size: Extent3d { - width: image.width, - height: image.height, - depth_or_array_layers: 1, + let texture = device.create_texture_with_data( + queue, + &TextureDescriptor { + label: Some("upload_texture node texture"), + size: Extent3d { + width: image.width, + height: image.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + // I don't know what usages are actually necessary + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, + view_formats: &[], }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - // I don't know what usages are actually necessary - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, - view_formats: &[], - }, - TextureDataOrder::LayerMajor, - bytemuck::cast_slice(rgba8_data.as_slice()), - ); + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(rgba8_data.as_slice()), + ); - TableRow { - element: Raster::new_gpu(texture), - transform: *row.transform, - alpha_blending: *row.alpha_blending, - source_node_id: *row.source_node_id, - } - }) - .collect(); + TableRow { + element: Raster::new_gpu(texture), + transform: *row.transform, + alpha_blending: *row.alpha_blending, + source_node_id: *row.source_node_id, + } + }) + .collect(); - queue.submit([]); + queue.submit([]); + table + } +} - table +#[node_macro::node(category(""))] +pub async fn upload_texture<'a: 'n, T: UploadTexture>(_: impl Ctx, #[implementations(Table>, Table>)] input: T, executor: &'a WgpuExecutor) -> Table> { + input.upload_texture(executor) } From f2915bdd97127401a5b2280cd242191a9428a132 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 14 Sep 2025 12:52:28 +0200 Subject: [PATCH 2/9] Make convert trait use explicit converter --- node-graph/gcore/src/ops.rs | 16 +++++++------- .../interpreted-executor/src/node_registry.rs | 20 ++++++++++++------ .../wgpu-executor/src/texture_upload.rs | 21 ++++++++++--------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 79de790bbb..c2eaa2661b 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -49,16 +49,16 @@ fn into<'i, T: 'i + Send + Into, O: 'i + Send>(_: impl Ctx, value: T, _out_ty /// The [`Convert`] trait allows for conversion between Rust primitive numeric types. /// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types. -pub trait Convert: Sized { +pub trait Convert: Sized { /// Converts this type into the (usually inferred) output type. #[must_use] - fn convert(self) -> T; + fn convert(self, converter: C) -> T; } -impl Convert for T { +impl Convert for T { /// Converts this type into a `String` using its `ToString` implementation. #[inline] - fn convert(self) -> String { + fn convert(self, _converter: ()) -> String { self.to_string() } } @@ -66,8 +66,8 @@ impl Convert for T { /// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types. macro_rules! impl_convert { ($from:ty, $to:ty) => { - impl Convert<$to> for $from { - fn convert(self) -> $to { + impl Convert<$to, ()> for $from { + fn convert(self, _: ()) -> $to { self as $to } } @@ -105,8 +105,8 @@ impl_convert!(isize); impl_convert!(usize); #[node_macro::node(skip_impl)] -fn convert<'i, T: 'i + Send + Convert, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData) -> O { - value.convert() +fn convert<'i, T: 'i + Send + Convert, O: 'i + Send, C: 'i>(_: impl Ctx, value: T, converter: C, _out_ty: PhantomData) -> O { + value.convert(converter) } #[cfg(test)] diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 081320042f..726bec7ea0 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -394,21 +394,29 @@ mod node_registry_macros { x }}; (from: $from:ty, to: $to:ty) => { + convert_node!(from: $from, to: $to, converter: ()) + }; + (from: $from:ty, to: $to:ty, converter: $convert:ty) => { ( ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]), |mut args| { Box::pin(async move { - let node = graphene_core::ops::ConvertNode::new(graphene_std::any::downcast_node::(args.pop().unwrap()), -graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) ); + let node = graphene_core::ops::ConvertNode::new( + graphene_std::any::downcast_node::(args.pop().unwrap()), + graphene_std::any::downcast_node::(args.pop().unwrap()), + graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) + ); let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); Box::new(any) as TypeErasedBox }) }, { - let node = graphene_core::ops::ConvertNode::new(graphene_std::any::PanicNode:: + Send>>>::new(), - -graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) ); - let params = vec![fn_type_fut!(Context, $from)]; + let node = graphene_core::ops::ConvertNode::new( + graphene_std::any::PanicNode:: + Send>>>::new(), + graphene_std::any::PanicNode:: + Send>>>::new(), + graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) + ); + let params = vec![fn_type_fut!(Context, $from), fn_type_fut!(Context, $convert)]; let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); node_io }, diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_upload.rs index 942cb355c0..b883c4d5bb 100644 --- a/node-graph/wgpu-executor/src/texture_upload.rs +++ b/node-graph/wgpu-executor/src/texture_upload.rs @@ -1,22 +1,19 @@ use crate::WgpuExecutor; use graphene_core::Ctx; use graphene_core::color::SRGBA8; +use graphene_core::ops::Convert; use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::table::{Table, TableRow}; use wgpu::util::{DeviceExt, TextureDataOrder}; use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; -pub trait UploadTexture { - fn upload_texture(self, executor: &WgpuExecutor) -> Table>; -} - -impl UploadTexture for Table> { - fn upload_texture(self, _executor: &WgpuExecutor) -> Table> { +impl<'i> Convert>, &'i WgpuExecutor> for Table> { + fn convert(self, _converter: &'i WgpuExecutor) -> Table> { self } } -impl UploadTexture for Table> { - fn upload_texture(self, executor: &WgpuExecutor) -> Table> { +impl<'i> Convert>, &'i WgpuExecutor> for Table> { + fn convert(self, executor: &'i WgpuExecutor) -> Table> { let device = &executor.context.device; let queue = &executor.context.queue; let table = self @@ -61,6 +58,10 @@ impl UploadTexture for Table> { } #[node_macro::node(category(""))] -pub async fn upload_texture<'a: 'n, T: UploadTexture>(_: impl Ctx, #[implementations(Table>, Table>)] input: T, executor: &'a WgpuExecutor) -> Table> { - input.upload_texture(executor) +pub async fn upload_texture<'a: 'n, T: Convert>, &'a WgpuExecutor>>( + _: impl Ctx, + #[implementations(Table>, Table>)] input: T, + executor: &'a WgpuExecutor, +) -> Table> { + input.convert(executor) } From 1954420cee78a93afb7e87ae57a494f5a0b079e5 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 18 Sep 2025 10:02:42 +0200 Subject: [PATCH 3/9] Add gpu texture download implementation --- .../interpreted-executor/src/node_registry.rs | 4 +- .../wgpu-executor/src/texture_upload.rs | 126 +++++++++++++++++- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 726bec7ea0..22fc28ab65 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -402,8 +402,8 @@ mod node_registry_macros { |mut args| { Box::pin(async move { let node = graphene_core::ops::ConvertNode::new( - graphene_std::any::downcast_node::(args.pop().unwrap()), - graphene_std::any::downcast_node::(args.pop().unwrap()), + graphene_std::any::downcast_node::(args.pop().expect("Construct node did not get first argument")), + graphene_std::any::downcast_node::(args.pop().expect("Convert node did not get converter argument")), graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) ); let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_upload.rs index b883c4d5bb..1402081903 100644 --- a/node-graph/wgpu-executor/src/texture_upload.rs +++ b/node-graph/wgpu-executor/src/texture_upload.rs @@ -1,19 +1,22 @@ use crate::WgpuExecutor; +use graphene_core::Color; use graphene_core::Ctx; use graphene_core::color::SRGBA8; use graphene_core::ops::Convert; +use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::table::{Table, TableRow}; +use graphene_core::transform::Footprint; use wgpu::util::{DeviceExt, TextureDataOrder}; use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; impl<'i> Convert>, &'i WgpuExecutor> for Table> { - fn convert(self, _converter: &'i WgpuExecutor) -> Table> { + async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table> { self } } impl<'i> Convert>, &'i WgpuExecutor> for Table> { - fn convert(self, executor: &'i WgpuExecutor) -> Table> { + async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table> { let device = &executor.context.device; let queue = &executor.context.queue; let table = self @@ -56,6 +59,123 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { table } } +impl<'i> Convert>, &'i WgpuExecutor> for Table> { + async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table> { + self + } +} +impl<'i> Convert>, &'i WgpuExecutor> for Table> { + async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table> { + let device = &executor.context.device; + let queue = &executor.context.queue; + + // Create a single command encoder for all copy operations + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("batch_texture_download_encoder"), + }); + + // Collect all buffer and texture info for batch processing + let mut buffers_and_info = Vec::new(); + + for row in self.iter() { + let gpu_raster = row.element; + let texture = gpu_raster.data(); + + // Get texture dimensions + let width = texture.width(); + let height = texture.height(); + let bytes_per_pixel = 4; // RGBA8 + let buffer_size = (width * height * bytes_per_pixel) as u64; + + // Create a buffer to copy texture data to + let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("texture_download_buffer"), + size: buffer_size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + // Add copy operation to the batch encoder + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &output_buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * bytes_per_pixel), + rows_per_image: Some(height), + }, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + buffers_and_info.push((output_buffer, width, height, *row.transform, *row.alpha_blending, *row.source_node_id)); + } + + // Submit all copy operations in a single batch + queue.submit([encoder.finish()]); + + // Now async map all buffers and collect futures + let mut map_futures = Vec::new(); + for (buffer, _width, _height, _transform, _alpha_blending, _source_node_id) in &buffers_and_info { + let buffer_slice = buffer.slice(..); + let (sender, receiver) = futures::channel::oneshot::channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + let _ = sender.send(result); + }); + map_futures.push(receiver); + } + + // Wait for all mapping operations to complete + let map_results = futures::future::try_join_all(map_futures).await.map_err(|_| "Failed to receive map result").unwrap(); + + // Process all mapped buffers + let mut table = Vec::new(); + for (i, (buffer, width, height, transform, alpha_blending, source_node_id)) in buffers_and_info.into_iter().enumerate() { + if let Err(e) = &map_results[i] { + panic!("Buffer mapping failed: {:?}", e); + } + + let data = buffer.slice(..).get_mapped_range(); + // Convert bytes directly to Color via SRGBA8 + let cpu_data: Vec = data + .chunks_exact(4) + .map(|chunk| { + // Create SRGBA8 from bytes, then convert to Color + Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3]) + }) + .collect(); + + drop(data); + buffer.unmap(); + let cpu_image = Image { + data: cpu_data, + width, + height, + base64_string: None, + }; + let cpu_raster = Raster::new_cpu(cpu_image); + + table.push(TableRow { + element: cpu_raster, + transform, + alpha_blending, + source_node_id, + }); + } + + table.into_iter().collect() + } +} #[node_macro::node(category(""))] pub async fn upload_texture<'a: 'n, T: Convert>, &'a WgpuExecutor>>( @@ -63,5 +183,5 @@ pub async fn upload_texture<'a: 'n, T: Convert>, &'a WgpuExecu #[implementations(Table>, Table>)] input: T, executor: &'a WgpuExecutor, ) -> Table> { - input.convert(executor) + input.convert(Footprint::DEFAULT, executor).await } From 5bbd32a2912d1e6f0ec79bf27e0ec5eb4329df2a Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 18 Sep 2025 13:04:29 +0200 Subject: [PATCH 4/9] Add footprint to convert trait --- node-graph/gcore/src/ops.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index c2eaa2661b..806ff61eb3 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -1,5 +1,6 @@ -use crate::Node; use graphene_core_shaders::Ctx; + +use crate::{ExtractFootprint, Node, transform::Footprint}; use std::marker::PhantomData; // TODO: Rename to "Passthrough" @@ -52,13 +53,13 @@ fn into<'i, T: 'i + Send + Into, O: 'i + Send>(_: impl Ctx, value: T, _out_ty pub trait Convert: Sized { /// Converts this type into the (usually inferred) output type. #[must_use] - fn convert(self, converter: C) -> T; + fn convert(self, footprint: Footprint, converter: C) -> impl Future + Send; } -impl Convert for T { +impl Convert for T { /// Converts this type into a `String` using its `ToString` implementation. #[inline] - fn convert(self, _converter: ()) -> String { + async fn convert(self, _: Footprint, _converter: ()) -> String { self.to_string() } } @@ -67,7 +68,7 @@ impl Convert for T { macro_rules! impl_convert { ($from:ty, $to:ty) => { impl Convert<$to, ()> for $from { - fn convert(self, _: ()) -> $to { + async fn convert(self, _: Footprint, _: ()) -> $to { self as $to } } @@ -105,8 +106,8 @@ impl_convert!(isize); impl_convert!(usize); #[node_macro::node(skip_impl)] -fn convert<'i, T: 'i + Send + Convert, O: 'i + Send, C: 'i>(_: impl Ctx, value: T, converter: C, _out_ty: PhantomData) -> O { - value.convert(converter) +async fn convert<'i, T: 'i + Send + Convert, O: 'i + Send, C: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData) -> O { + value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await } #[cfg(test)] From 490e2912b053bcb432e35cf3d60dd7fec44a5270 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 18 Sep 2025 13:50:32 +0200 Subject: [PATCH 5/9] Cleanup texture upload / download --- .../interpreted-executor/src/node_registry.rs | 9 +- node-graph/preprocessor/src/lib.rs | 4 +- .../wgpu-executor/src/texture_upload.rs | 218 ++++++++++++------ 3 files changed, 154 insertions(+), 77 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 22fc28ab65..948b129df8 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -61,6 +61,10 @@ fn node_registry() -> HashMap>, to: Table>, converter: &WgpuExecutor), + convert_node!(from: Table>, to: Table>, converter: &WgpuExecutor), + convert_node!(from: Table>, to: Table>, converter: &WgpuExecutor), + convert_node!(from: Table>, to: Table>, converter: &WgpuExecutor), // ============= // MONITOR NODES // ============= @@ -401,9 +405,10 @@ mod node_registry_macros { ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]), |mut args| { Box::pin(async move { + let mut args = args.drain(..); let node = graphene_core::ops::ConvertNode::new( - graphene_std::any::downcast_node::(args.pop().expect("Construct node did not get first argument")), - graphene_std::any::downcast_node::(args.pop().expect("Convert node did not get converter argument")), + graphene_std::any::downcast_node::(args.next().expect("Convert node did not get first argument")), + graphene_std::any::downcast_node::(args.next().expect("Convert node did not get converter argument")), graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) ); let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index d9eec64e01..b0479344a3 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -67,6 +67,7 @@ pub fn generate_node_substitutions() -> HashMap { let input = inputs.iter().next().unwrap(); let input_ty = input.nested_type(); + let mut inputs = vec![NodeInput::import(input.clone(), i)]; let into_node_identifier = ProtoNodeIdentifier { name: format!("graphene_core::ops::IntoNode<{}>", input_ty.clone()).into(), @@ -80,13 +81,14 @@ pub fn generate_node_substitutions() -> HashMap, queue: &std::sync::Arc, image: &Raster) -> wgpu::Texture { + let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); + + device.create_texture_with_data( + queue, + &TextureDescriptor { + label: Some("upload_texture node texture"), + size: Extent3d { + width: image.width, + height: image.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(rgba8_data.as_slice()), + ) +} + +/// Downloads GPU texture data to a CPU buffer +/// +/// Creates a buffer and adds a copy operation from the texture to the buffer +/// using the provided command encoder. Returns dimensions and the buffer for +/// later mapping and data extraction. +fn download_to_buffer(device: &std::sync::Arc, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> (u32, u32, wgpu::Buffer) { + let width = texture.width(); + let height = texture.height(); + let bytes_per_pixel = 4; // RGBA8 + let buffer_size = (width * height * bytes_per_pixel) as u64; + + let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("texture_download_buffer"), + size: buffer_size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &output_buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * bytes_per_pixel), + rows_per_image: Some(height), + }, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + (width, height, output_buffer) +} + +/// Passthrough conversion for GPU tables - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for Table> { async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table> { self } } + +/// Converts CPU raster table to GPU by uploading each image to a texture impl<'i> Convert>, &'i WgpuExecutor> for Table> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table> { let device = &executor.context.device; @@ -23,28 +96,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { .iter() .map(|row| { let image = row.element; - let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); - - let texture = device.create_texture_with_data( - queue, - &TextureDescriptor { - label: Some("upload_texture node texture"), - size: Extent3d { - width: image.width, - height: image.height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - // I don't know what usages are actually necessary - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, - view_formats: &[], - }, - TextureDataOrder::LayerMajor, - bytemuck::cast_slice(rgba8_data.as_slice()), - ); + let texture = upload_to_texture(device, queue, image); TableRow { element: Raster::new_gpu(texture), @@ -59,72 +111,51 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { table } } + +/// Converts single CPU raster to GPU by uploading to texture +impl<'i> Convert, &'i WgpuExecutor> for Raster { + async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { + let device = &executor.context.device; + let queue = &executor.context.queue; + let texture = upload_to_texture(device, queue, &self); + + queue.submit([]); + Raster::new_gpu(texture) + } +} + +/// Passthrough conversion for CPU tables - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for Table> { async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table> { self } } + +/// Converts GPU raster table to CPU by downloading texture data in one go +/// +/// then asynchronously maps all buffers and processes the results. impl<'i> Convert>, &'i WgpuExecutor> for Table> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table> { let device = &executor.context.device; let queue = &executor.context.queue; - // Create a single command encoder for all copy operations let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("batch_texture_download_encoder"), }); - // Collect all buffer and texture info for batch processing let mut buffers_and_info = Vec::new(); for row in self.iter() { let gpu_raster = row.element; let texture = gpu_raster.data(); - // Get texture dimensions - let width = texture.width(); - let height = texture.height(); - let bytes_per_pixel = 4; // RGBA8 - let buffer_size = (width * height * bytes_per_pixel) as u64; - - // Create a buffer to copy texture data to - let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("texture_download_buffer"), - size: buffer_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - // Add copy operation to the batch encoder - encoder.copy_texture_to_buffer( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyBufferInfo { - buffer: &output_buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(width * bytes_per_pixel), - rows_per_image: Some(height), - }, - }, - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); + let (width, height, output_buffer) = download_to_buffer(device, &mut encoder, texture); buffers_and_info.push((output_buffer, width, height, *row.transform, *row.alpha_blending, *row.source_node_id)); } - // Submit all copy operations in a single batch queue.submit([encoder.finish()]); - // Now async map all buffers and collect futures let mut map_futures = Vec::new(); for (buffer, _width, _height, _transform, _alpha_blending, _source_node_id) in &buffers_and_info { let buffer_slice = buffer.slice(..); @@ -135,25 +166,19 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { map_futures.push(receiver); } - // Wait for all mapping operations to complete - let map_results = futures::future::try_join_all(map_futures).await.map_err(|_| "Failed to receive map result").unwrap(); + let map_results = futures::future::try_join_all(map_futures) + .await + .map_err(|_| "Failed to receive map result") + .expect("Buffer mapping communication failed"); - // Process all mapped buffers let mut table = Vec::new(); for (i, (buffer, width, height, transform, alpha_blending, source_node_id)) in buffers_and_info.into_iter().enumerate() { if let Err(e) = &map_results[i] { - panic!("Buffer mapping failed: {:?}", e); + panic!("Buffer mapping failed: {e:?}"); } let data = buffer.slice(..).get_mapped_range(); - // Convert bytes directly to Color via SRGBA8 - let cpu_data: Vec = data - .chunks_exact(4) - .map(|chunk| { - // Create SRGBA8 from bytes, then convert to Color - Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3]) - }) - .collect(); + let cpu_data: Vec = data.chunks_exact(4).map(|chunk| Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])).collect(); drop(data); buffer.unmap(); @@ -177,6 +202,51 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { } } +/// Converts single GPU raster to CPU by downloading texture data +impl<'i> Convert, &'i WgpuExecutor> for Raster { + async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { + let device = &executor.context.device; + let queue = &executor.context.queue; + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("single_texture_download_encoder"), + }); + + let gpu_raster = &self; + let texture = gpu_raster.data(); + + let (width, height, output_buffer) = download_to_buffer(device, &mut encoder, texture); + + queue.submit([encoder.finish()]); + + let buffer_slice = output_buffer.slice(..); + let (sender, receiver) = futures::channel::oneshot::channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + let _ = sender.send(result); + }); + receiver.await.expect("Failed to receive map result").expect("Buffer mapping failed"); + + let data = output_buffer.slice(..).get_mapped_range(); + let cpu_data: Vec = data.chunks_exact(4).map(|chunk| Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])).collect(); + + drop(data); + output_buffer.unmap(); + let cpu_image = Image { + data: cpu_data, + width, + height, + base64_string: None, + }; + + Raster::new_cpu(cpu_image) + } +} + +/// Node for uploading textures from CPU to GPU. This Is now deprecated and +/// we should use the Convert node in the future. +/// +/// Accepts either individual rasters or tables of rasters and converts them +/// to GPU format using the WgpuExecutor's device and queue. #[node_macro::node(category(""))] pub async fn upload_texture<'a: 'n, T: Convert>, &'a WgpuExecutor>>( _: impl Ctx, From df222222603d6384461994cb95db82f5a5415474 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 18 Sep 2025 15:45:22 +0000 Subject: [PATCH 6/9] Download wgpu textures aligned --- .../wgpu-executor/src/texture_upload.rs | 88 ++++++++++++++----- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_upload.rs index 677e2ed3ab..979901575c 100644 --- a/node-graph/wgpu-executor/src/texture_upload.rs +++ b/node-graph/wgpu-executor/src/texture_upload.rs @@ -43,11 +43,14 @@ fn upload_to_texture(device: &std::sync::Arc, queue: &std::sync::A /// Creates a buffer and adds a copy operation from the texture to the buffer /// using the provided command encoder. Returns dimensions and the buffer for /// later mapping and data extraction. -fn download_to_buffer(device: &std::sync::Arc, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> (u32, u32, wgpu::Buffer) { +fn download_to_buffer(device: &std::sync::Arc, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> (wgpu::Buffer, u32, u32, u32, u32) { let width = texture.width(); let height = texture.height(); let bytes_per_pixel = 4; // RGBA8 - let buffer_size = (width * height * bytes_per_pixel) as u64; + let unpadded_bytes_per_row = width * bytes_per_pixel; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; + let buffer_size = padded_bytes_per_row as u64 * height as u64; let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("texture_download_buffer"), @@ -67,7 +70,7 @@ fn download_to_buffer(device: &std::sync::Arc, encoder: &mut wgpu: buffer: &output_buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, - bytes_per_row: Some(width * bytes_per_pixel), + bytes_per_row: Some(padded_bytes_per_row), rows_per_image: Some(height), }, }, @@ -77,7 +80,7 @@ fn download_to_buffer(device: &std::sync::Arc, encoder: &mut wgpu: depth_or_array_layers: 1, }, ); - (width, height, output_buffer) + (output_buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row) } /// Passthrough conversion for GPU tables - no conversion needed @@ -149,21 +152,42 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { let gpu_raster = row.element; let texture = gpu_raster.data(); - let (width, height, output_buffer) = download_to_buffer(device, &mut encoder, texture); + let (output_buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row) = download_to_buffer(device, &mut encoder, texture); - buffers_and_info.push((output_buffer, width, height, *row.transform, *row.alpha_blending, *row.source_node_id)); + buffers_and_info.push(( + output_buffer, + width, + height, + unpadded_bytes_per_row, + padded_bytes_per_row, + *row.transform, + *row.alpha_blending, + *row.source_node_id, + )); } queue.submit([encoder.finish()]); let mut map_futures = Vec::new(); - for (buffer, _width, _height, _transform, _alpha_blending, _source_node_id) in &buffers_and_info { + let mut buffer_sclices_and_info = Vec::new(); + for (buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row, transform, alpha_blending, source_node_id) in &buffers_and_info { let buffer_slice = buffer.slice(..); let (sender, receiver) = futures::channel::oneshot::channel(); buffer_slice.map_async(wgpu::MapMode::Read, move |result| { let _ = sender.send(result); }); map_futures.push(receiver); + buffer_sclices_and_info.push(( + buffer, + buffer_slice, + width, + height, + unpadded_bytes_per_row, + padded_bytes_per_row, + transform, + alpha_blending, + source_node_id, + )); } let map_results = futures::future::try_join_all(map_futures) @@ -172,29 +196,39 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { .expect("Buffer mapping communication failed"); let mut table = Vec::new(); - for (i, (buffer, width, height, transform, alpha_blending, source_node_id)) in buffers_and_info.into_iter().enumerate() { + for (i, (buffer, buffer_slice, width, height, unpadded_bytes_per_row, padded_bytes_per_row, transform, alpha_blending, source_node_id)) in buffer_sclices_and_info.into_iter().enumerate() { if let Err(e) = &map_results[i] { panic!("Buffer mapping failed: {e:?}"); } - let data = buffer.slice(..).get_mapped_range(); - let cpu_data: Vec = data.chunks_exact(4).map(|chunk| Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])).collect(); + let view = buffer_slice.get_mapped_range(); + + let row_stride = *padded_bytes_per_row as usize; + let row_bytes = *unpadded_bytes_per_row as usize; + let mut cpu_data: Vec = Vec::with_capacity((width * height) as usize); + for row in 0..*height as usize { + let start = row * row_stride; + let row_slice = &view[start..start + row_bytes]; + for px in row_slice.chunks_exact(4) { + cpu_data.push(Color::from_rgba8_srgb(px[0], px[1], px[2], px[3])); + } + } - drop(data); + drop(view); buffer.unmap(); let cpu_image = Image { data: cpu_data, - width, - height, + width: *width, + height: *height, base64_string: None, }; let cpu_raster = Raster::new_cpu(cpu_image); table.push(TableRow { element: cpu_raster, - transform, - alpha_blending, - source_node_id, + transform: *transform, + alpha_blending: *alpha_blending, + source_node_id: *source_node_id, }); } @@ -215,22 +249,32 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { let gpu_raster = &self; let texture = gpu_raster.data(); - let (width, height, output_buffer) = download_to_buffer(device, &mut encoder, texture); + let (buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row) = download_to_buffer(device, &mut encoder, texture); queue.submit([encoder.finish()]); - let buffer_slice = output_buffer.slice(..); + let buffer_slice = buffer.slice(..); let (sender, receiver) = futures::channel::oneshot::channel(); buffer_slice.map_async(wgpu::MapMode::Read, move |result| { let _ = sender.send(result); }); receiver.await.expect("Failed to receive map result").expect("Buffer mapping failed"); - let data = output_buffer.slice(..).get_mapped_range(); - let cpu_data: Vec = data.chunks_exact(4).map(|chunk| Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])).collect(); + let view = buffer_slice.get_mapped_range(); + + let row_stride = padded_bytes_per_row as usize; + let row_bytes = unpadded_bytes_per_row as usize; + let mut cpu_data: Vec = Vec::with_capacity((width * height) as usize); + for row in 0..height as usize { + let start = row * row_stride; + let row_slice = &view[start..start + row_bytes]; + for px in row_slice.chunks_exact(4) { + cpu_data.push(Color::from_rgba8_srgb(px[0], px[1], px[2], px[3])); + } + } - drop(data); - output_buffer.unmap(); + drop(view); + buffer.unmap(); let cpu_image = Image { data: cpu_data, width, From 4d1a7bf56d53b92705ec7763c3c8d16600fa1899 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 18 Sep 2025 23:51:43 +0000 Subject: [PATCH 7/9] abstract texture download into converter helper --- .../wgpu-executor/src/texture_upload.rs | 249 ++++++++---------- 1 file changed, 109 insertions(+), 140 deletions(-) diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_upload.rs index 979901575c..1b6aacdedb 100644 --- a/node-graph/wgpu-executor/src/texture_upload.rs +++ b/node-graph/wgpu-executor/src/texture_upload.rs @@ -38,49 +38,100 @@ fn upload_to_texture(device: &std::sync::Arc, queue: &std::sync::A ) } -/// Downloads GPU texture data to a CPU buffer +/// Converts a Raster texture to Raster by downloading the underlying texture data. /// -/// Creates a buffer and adds a copy operation from the texture to the buffer -/// using the provided command encoder. Returns dimensions and the buffer for -/// later mapping and data extraction. -fn download_to_buffer(device: &std::sync::Arc, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> (wgpu::Buffer, u32, u32, u32, u32) { - let width = texture.width(); - let height = texture.height(); - let bytes_per_pixel = 4; // RGBA8 - let unpadded_bytes_per_row = width * bytes_per_pixel; - let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; - let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; - let buffer_size = padded_bytes_per_row as u64 * height as u64; - - let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("texture_download_buffer"), - size: buffer_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - encoder.copy_texture_to_buffer( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyBufferInfo { - buffer: &output_buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(padded_bytes_per_row), - rows_per_image: Some(height), +/// Assumptions: +/// - 2D texture, mip level 0 +/// - 4 bytes-per-pixel RGBA8 +/// - Texture has COPY_SRC usage +struct RasterGpuToRasterCpuConverter { + buffer: wgpu::Buffer, + width: u32, + height: u32, + unpadded_bytes_per_row: u32, + padded_bytes_per_row: u32, +} +impl RasterGpuToRasterCpuConverter { + fn new(device: &std::sync::Arc, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { + let texture = data_gpu.data(); + let width = texture.width(); + let height = texture.height(); + let bytes_per_pixel = 4; // RGBA8 + let unpadded_bytes_per_row = width * bytes_per_pixel; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; + let buffer_size = padded_bytes_per_row as u64 * height as u64; + + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("texture_download_buffer"), + size: buffer_size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, }, - }, - Extent3d { + wgpu::TexelCopyBufferInfo { + buffer: &buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row), + rows_per_image: Some(height), + }, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + Self { + buffer, width, height, - depth_or_array_layers: 1, - }, - ); - (output_buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row) + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } + + async fn convert(self) -> Result, wgpu::BufferAsyncError> { + let buffer_slice = self.buffer.slice(..); + let (sender, receiver) = futures::channel::oneshot::channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + let _ = sender.send(result); + }); + receiver.await.expect("Failed to receive map result")?; + + let view = buffer_slice.get_mapped_range(); + + let row_stride = self.padded_bytes_per_row as usize; + let row_bytes = self.unpadded_bytes_per_row as usize; + let mut cpu_data: Vec = Vec::with_capacity((self.width * self.height) as usize); + for row in 0..self.height as usize { + let start = row * row_stride; + let row_slice = &view[start..start + row_bytes]; + for px in row_slice.chunks_exact(4) { + cpu_data.push(Color::from_rgba8_srgb(px[0], px[1], px[2], px[3])); + } + } + + drop(view); + self.buffer.unmap(); + let cpu_image = Image { + data: cpu_data, + width: self.width, + height: self.height, + base64_string: None, + }; + + Ok(Raster::new_cpu(cpu_image)) + } } /// Passthrough conversion for GPU tables - no conversion needed @@ -146,48 +197,25 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { label: Some("batch_texture_download_encoder"), }); - let mut buffers_and_info = Vec::new(); + let mut converters = Vec::new(); + let mut rows_meta = Vec::new(); - for row in self.iter() { + for row in self.into_iter() { let gpu_raster = row.element; - let texture = gpu_raster.data(); - - let (output_buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row) = download_to_buffer(device, &mut encoder, texture); - - buffers_and_info.push(( - output_buffer, - width, - height, - unpadded_bytes_per_row, - padded_bytes_per_row, - *row.transform, - *row.alpha_blending, - *row.source_node_id, - )); + converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, gpu_raster)); + rows_meta.push(TableRow { + element: (), + transform: row.transform, + alpha_blending: row.alpha_blending, + source_node_id: row.source_node_id, + }); } queue.submit([encoder.finish()]); let mut map_futures = Vec::new(); - let mut buffer_sclices_and_info = Vec::new(); - for (buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row, transform, alpha_blending, source_node_id) in &buffers_and_info { - let buffer_slice = buffer.slice(..); - let (sender, receiver) = futures::channel::oneshot::channel(); - buffer_slice.map_async(wgpu::MapMode::Read, move |result| { - let _ = sender.send(result); - }); - map_futures.push(receiver); - buffer_sclices_and_info.push(( - buffer, - buffer_slice, - width, - height, - unpadded_bytes_per_row, - padded_bytes_per_row, - transform, - alpha_blending, - source_node_id, - )); + for converter in converters { + map_futures.push(converter.convert()); } let map_results = futures::future::try_join_all(map_futures) @@ -196,39 +224,12 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { .expect("Buffer mapping communication failed"); let mut table = Vec::new(); - for (i, (buffer, buffer_slice, width, height, unpadded_bytes_per_row, padded_bytes_per_row, transform, alpha_blending, source_node_id)) in buffer_sclices_and_info.into_iter().enumerate() { - if let Err(e) = &map_results[i] { - panic!("Buffer mapping failed: {e:?}"); - } - - let view = buffer_slice.get_mapped_range(); - - let row_stride = *padded_bytes_per_row as usize; - let row_bytes = *unpadded_bytes_per_row as usize; - let mut cpu_data: Vec = Vec::with_capacity((width * height) as usize); - for row in 0..*height as usize { - let start = row * row_stride; - let row_slice = &view[start..start + row_bytes]; - for px in row_slice.chunks_exact(4) { - cpu_data.push(Color::from_rgba8_srgb(px[0], px[1], px[2], px[3])); - } - } - - drop(view); - buffer.unmap(); - let cpu_image = Image { - data: cpu_data, - width: *width, - height: *height, - base64_string: None, - }; - let cpu_raster = Raster::new_cpu(cpu_image); - + for (element, row) in map_results.into_iter().zip(rows_meta.into_iter()) { table.push(TableRow { - element: cpu_raster, - transform: *transform, - alpha_blending: *alpha_blending, - source_node_id: *source_node_id, + element, + transform: row.transform, + alpha_blending: row.alpha_blending, + source_node_id: row.source_node_id, }); } @@ -246,43 +247,11 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { label: Some("single_texture_download_encoder"), }); - let gpu_raster = &self; - let texture = gpu_raster.data(); - - let (buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row) = download_to_buffer(device, &mut encoder, texture); + let converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self); queue.submit([encoder.finish()]); - let buffer_slice = buffer.slice(..); - let (sender, receiver) = futures::channel::oneshot::channel(); - buffer_slice.map_async(wgpu::MapMode::Read, move |result| { - let _ = sender.send(result); - }); - receiver.await.expect("Failed to receive map result").expect("Buffer mapping failed"); - - let view = buffer_slice.get_mapped_range(); - - let row_stride = padded_bytes_per_row as usize; - let row_bytes = unpadded_bytes_per_row as usize; - let mut cpu_data: Vec = Vec::with_capacity((width * height) as usize); - for row in 0..height as usize { - let start = row * row_stride; - let row_slice = &view[start..start + row_bytes]; - for px in row_slice.chunks_exact(4) { - cpu_data.push(Color::from_rgba8_srgb(px[0], px[1], px[2], px[3])); - } - } - - drop(view); - buffer.unmap(); - let cpu_image = Image { - data: cpu_data, - width, - height, - base64_string: None, - }; - - Raster::new_cpu(cpu_image) + converter.convert().await.expect("Failed to download texture data") } } From 259d2d35be3506848d9b35816cf8726abae0fcd6 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 18 Sep 2025 23:59:45 +0000 Subject: [PATCH 8/9] rename module not only doing uploads anymore conversion looks like a ok name --- .../portfolio/document/node_graph/document_node_definitions.rs | 2 +- node-graph/wgpu-executor/src/lib.rs | 2 +- .../src/{texture_upload.rs => texture_conversion.rs} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename node-graph/wgpu-executor/src/{texture_upload.rs => texture_conversion.rs} (100%) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 8d70d0842b..ff12d80e0d 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -980,7 +980,7 @@ fn static_nodes() -> Vec { DocumentNode { inputs: vec![NodeInput::import(concrete!(Table>), 0), NodeInput::node(NodeId(0), 0)], call_argument: generic!(T), - implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_upload::upload_texture::IDENTIFIER), + implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_conversion::upload_texture::IDENTIFIER), ..Default::default() }, DocumentNode { diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index 4a1e7f3e36..6e8503e22f 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -1,6 +1,6 @@ mod context; pub mod shader_runtime; -pub mod texture_upload; +pub mod texture_conversion; use crate::shader_runtime::ShaderRuntime; use anyhow::Result; diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_conversion.rs similarity index 100% rename from node-graph/wgpu-executor/src/texture_upload.rs rename to node-graph/wgpu-executor/src/texture_conversion.rs From 497baa648deb95a746b04243108e8a5cc3db5835 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 25 Sep 2025 10:41:21 +0200 Subject: [PATCH 9/9] Remove into_iter call and intermediate vector allocation --- .../wgpu-executor/src/texture_conversion.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/node-graph/wgpu-executor/src/texture_conversion.rs b/node-graph/wgpu-executor/src/texture_conversion.rs index 1b6aacdedb..2da453e327 100644 --- a/node-graph/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/wgpu-executor/src/texture_conversion.rs @@ -200,7 +200,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { let mut converters = Vec::new(); let mut rows_meta = Vec::new(); - for row in self.into_iter() { + for row in self { let gpu_raster = row.element; converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, gpu_raster)); rows_meta.push(TableRow { @@ -223,17 +223,16 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { .map_err(|_| "Failed to receive map result") .expect("Buffer mapping communication failed"); - let mut table = Vec::new(); - for (element, row) in map_results.into_iter().zip(rows_meta.into_iter()) { - table.push(TableRow { + map_results + .into_iter() + .zip(rows_meta.into_iter()) + .map(|(element, row)| TableRow { element, transform: row.transform, alpha_blending: row.alpha_blending, source_node_id: row.source_node_id, - }); - } - - table.into_iter().collect() + }) + .collect() } }