Skip to content

Commit 490e291

Browse files
committed
Cleanup texture upload / download
1 parent 5bbd32a commit 490e291

File tree

3 files changed

+154
-77
lines changed

3 files changed

+154
-77
lines changed

node-graph/interpreted-executor/src/node_registry.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
6161
convert_node!(from: DVec2, to: String),
6262
convert_node!(from: IVec2, to: String),
6363
convert_node!(from: DAffine2, to: String),
64+
convert_node!(from: Table<Raster<CPU>>, to: Table<Raster<CPU>>, converter: &WgpuExecutor),
65+
convert_node!(from: Table<Raster<CPU>>, to: Table<Raster<GPU>>, converter: &WgpuExecutor),
66+
convert_node!(from: Table<Raster<GPU>>, to: Table<Raster<GPU>>, converter: &WgpuExecutor),
67+
convert_node!(from: Table<Raster<GPU>>, to: Table<Raster<CPU>>, converter: &WgpuExecutor),
6468
// =============
6569
// MONITOR NODES
6670
// =============
@@ -401,9 +405,10 @@ mod node_registry_macros {
401405
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
402406
|mut args| {
403407
Box::pin(async move {
408+
let mut args = args.drain(..);
404409
let node = graphene_core::ops::ConvertNode::new(
405-
graphene_std::any::downcast_node::<Context, $from>(args.pop().expect("Construct node did not get first argument")),
406-
graphene_std::any::downcast_node::<Context, $convert>(args.pop().expect("Convert node did not get converter argument")),
410+
graphene_std::any::downcast_node::<Context, $from>(args.next().expect("Convert node did not get first argument")),
411+
graphene_std::any::downcast_node::<Context, $convert>(args.next().expect("Convert node did not get converter argument")),
407412
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>))
408413
);
409414
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node);

node-graph/preprocessor/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
6767
1 => {
6868
let input = inputs.iter().next().unwrap();
6969
let input_ty = input.nested_type();
70+
let mut inputs = vec![NodeInput::import(input.clone(), i)];
7071

7172
let into_node_identifier = ProtoNodeIdentifier {
7273
name: format!("graphene_core::ops::IntoNode<{}>", input_ty.clone()).into(),
@@ -80,13 +81,14 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
8081
into_node_identifier
8182
} else if into_node_registry.keys().any(|ident| ident.name.as_ref() == convert_node_identifier.name.as_ref()) {
8283
generated_nodes += 1;
84+
inputs.push(NodeInput::value(TaggedValue::None, false));
8385
convert_node_identifier
8486
} else {
8587
identity_node.clone()
8688
};
8789

8890
DocumentNode {
89-
inputs: vec![NodeInput::import(input.clone(), i)],
91+
inputs,
9092
implementation: DocumentNodeImplementation::ProtoNode(proto_node),
9193
visible: true,
9294
..Default::default()

node-graph/wgpu-executor/src/texture_upload.rs

Lines changed: 144 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,84 @@ use graphene_core::transform::Footprint;
1010
use wgpu::util::{DeviceExt, TextureDataOrder};
1111
use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
1212

13+
/// Uploads CPU image data to a GPU texture
14+
///
15+
/// Creates a new WGPU texture with RGBA8UnormSrgb format and uploads the provided
16+
/// image data. The texture is configured for binding, copying, and source operations.
17+
fn upload_to_texture(device: &std::sync::Arc<wgpu::Device>, queue: &std::sync::Arc<wgpu::Queue>, image: &Raster<CPU>) -> wgpu::Texture {
18+
let rgba8_data: Vec<SRGBA8> = image.data.iter().map(|x| (*x).into()).collect();
19+
20+
device.create_texture_with_data(
21+
queue,
22+
&TextureDescriptor {
23+
label: Some("upload_texture node texture"),
24+
size: Extent3d {
25+
width: image.width,
26+
height: image.height,
27+
depth_or_array_layers: 1,
28+
},
29+
mip_level_count: 1,
30+
sample_count: 1,
31+
dimension: TextureDimension::D2,
32+
format: TextureFormat::Rgba8UnormSrgb,
33+
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
34+
view_formats: &[],
35+
},
36+
TextureDataOrder::LayerMajor,
37+
bytemuck::cast_slice(rgba8_data.as_slice()),
38+
)
39+
}
40+
41+
/// Downloads GPU texture data to a CPU buffer
42+
///
43+
/// Creates a buffer and adds a copy operation from the texture to the buffer
44+
/// using the provided command encoder. Returns dimensions and the buffer for
45+
/// later mapping and data extraction.
46+
fn download_to_buffer(device: &std::sync::Arc<wgpu::Device>, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> (u32, u32, wgpu::Buffer) {
47+
let width = texture.width();
48+
let height = texture.height();
49+
let bytes_per_pixel = 4; // RGBA8
50+
let buffer_size = (width * height * bytes_per_pixel) as u64;
51+
52+
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
53+
label: Some("texture_download_buffer"),
54+
size: buffer_size,
55+
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
56+
mapped_at_creation: false,
57+
});
58+
59+
encoder.copy_texture_to_buffer(
60+
wgpu::TexelCopyTextureInfo {
61+
texture,
62+
mip_level: 0,
63+
origin: wgpu::Origin3d::ZERO,
64+
aspect: wgpu::TextureAspect::All,
65+
},
66+
wgpu::TexelCopyBufferInfo {
67+
buffer: &output_buffer,
68+
layout: wgpu::TexelCopyBufferLayout {
69+
offset: 0,
70+
bytes_per_row: Some(width * bytes_per_pixel),
71+
rows_per_image: Some(height),
72+
},
73+
},
74+
Extent3d {
75+
width,
76+
height,
77+
depth_or_array_layers: 1,
78+
},
79+
);
80+
(width, height, output_buffer)
81+
}
82+
83+
/// Passthrough conversion for GPU tables - no conversion needed
1384
impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
1485
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table<Raster<GPU>> {
1586
self
1687
}
1788
}
89+
90+
/// Converts CPU raster table to GPU by uploading each image to a texture
1891
impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
1992
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table<Raster<GPU>> {
2093
let device = &executor.context.device;
@@ -23,28 +96,7 @@ impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
2396
.iter()
2497
.map(|row| {
2598
let image = row.element;
26-
let rgba8_data: Vec<SRGBA8> = image.data.iter().map(|x| (*x).into()).collect();
27-
28-
let texture = device.create_texture_with_data(
29-
queue,
30-
&TextureDescriptor {
31-
label: Some("upload_texture node texture"),
32-
size: Extent3d {
33-
width: image.width,
34-
height: image.height,
35-
depth_or_array_layers: 1,
36-
},
37-
mip_level_count: 1,
38-
sample_count: 1,
39-
dimension: TextureDimension::D2,
40-
format: TextureFormat::Rgba8UnormSrgb,
41-
// I don't know what usages are actually necessary
42-
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
43-
view_formats: &[],
44-
},
45-
TextureDataOrder::LayerMajor,
46-
bytemuck::cast_slice(rgba8_data.as_slice()),
47-
);
99+
let texture = upload_to_texture(device, queue, image);
48100

49101
TableRow {
50102
element: Raster::new_gpu(texture),
@@ -59,72 +111,51 @@ impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
59111
table
60112
}
61113
}
114+
115+
/// Converts single CPU raster to GPU by uploading to texture
116+
impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> {
117+
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> {
118+
let device = &executor.context.device;
119+
let queue = &executor.context.queue;
120+
let texture = upload_to_texture(device, queue, &self);
121+
122+
queue.submit([]);
123+
Raster::new_gpu(texture)
124+
}
125+
}
126+
127+
/// Passthrough conversion for CPU tables - no conversion needed
62128
impl<'i> Convert<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
63129
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table<Raster<CPU>> {
64130
self
65131
}
66132
}
133+
134+
/// Converts GPU raster table to CPU by downloading texture data in one go
135+
///
136+
/// then asynchronously maps all buffers and processes the results.
67137
impl<'i> Convert<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
68138
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table<Raster<CPU>> {
69139
let device = &executor.context.device;
70140
let queue = &executor.context.queue;
71141

72-
// Create a single command encoder for all copy operations
73142
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
74143
label: Some("batch_texture_download_encoder"),
75144
});
76145

77-
// Collect all buffer and texture info for batch processing
78146
let mut buffers_and_info = Vec::new();
79147

80148
for row in self.iter() {
81149
let gpu_raster = row.element;
82150
let texture = gpu_raster.data();
83151

84-
// Get texture dimensions
85-
let width = texture.width();
86-
let height = texture.height();
87-
let bytes_per_pixel = 4; // RGBA8
88-
let buffer_size = (width * height * bytes_per_pixel) as u64;
89-
90-
// Create a buffer to copy texture data to
91-
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
92-
label: Some("texture_download_buffer"),
93-
size: buffer_size,
94-
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
95-
mapped_at_creation: false,
96-
});
97-
98-
// Add copy operation to the batch encoder
99-
encoder.copy_texture_to_buffer(
100-
wgpu::TexelCopyTextureInfo {
101-
texture,
102-
mip_level: 0,
103-
origin: wgpu::Origin3d::ZERO,
104-
aspect: wgpu::TextureAspect::All,
105-
},
106-
wgpu::TexelCopyBufferInfo {
107-
buffer: &output_buffer,
108-
layout: wgpu::TexelCopyBufferLayout {
109-
offset: 0,
110-
bytes_per_row: Some(width * bytes_per_pixel),
111-
rows_per_image: Some(height),
112-
},
113-
},
114-
Extent3d {
115-
width,
116-
height,
117-
depth_or_array_layers: 1,
118-
},
119-
);
152+
let (width, height, output_buffer) = download_to_buffer(device, &mut encoder, texture);
120153

121154
buffers_and_info.push((output_buffer, width, height, *row.transform, *row.alpha_blending, *row.source_node_id));
122155
}
123156

124-
// Submit all copy operations in a single batch
125157
queue.submit([encoder.finish()]);
126158

127-
// Now async map all buffers and collect futures
128159
let mut map_futures = Vec::new();
129160
for (buffer, _width, _height, _transform, _alpha_blending, _source_node_id) in &buffers_and_info {
130161
let buffer_slice = buffer.slice(..);
@@ -135,25 +166,19 @@ impl<'i> Convert<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
135166
map_futures.push(receiver);
136167
}
137168

138-
// Wait for all mapping operations to complete
139-
let map_results = futures::future::try_join_all(map_futures).await.map_err(|_| "Failed to receive map result").unwrap();
169+
let map_results = futures::future::try_join_all(map_futures)
170+
.await
171+
.map_err(|_| "Failed to receive map result")
172+
.expect("Buffer mapping communication failed");
140173

141-
// Process all mapped buffers
142174
let mut table = Vec::new();
143175
for (i, (buffer, width, height, transform, alpha_blending, source_node_id)) in buffers_and_info.into_iter().enumerate() {
144176
if let Err(e) = &map_results[i] {
145-
panic!("Buffer mapping failed: {:?}", e);
177+
panic!("Buffer mapping failed: {e:?}");
146178
}
147179

148180
let data = buffer.slice(..).get_mapped_range();
149-
// Convert bytes directly to Color via SRGBA8
150-
let cpu_data: Vec<Color> = data
151-
.chunks_exact(4)
152-
.map(|chunk| {
153-
// Create SRGBA8 from bytes, then convert to Color
154-
Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])
155-
})
156-
.collect();
181+
let cpu_data: Vec<Color> = data.chunks_exact(4).map(|chunk| Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])).collect();
157182

158183
drop(data);
159184
buffer.unmap();
@@ -177,6 +202,51 @@ impl<'i> Convert<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
177202
}
178203
}
179204

205+
/// Converts single GPU raster to CPU by downloading texture data
206+
impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> {
207+
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<CPU> {
208+
let device = &executor.context.device;
209+
let queue = &executor.context.queue;
210+
211+
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
212+
label: Some("single_texture_download_encoder"),
213+
});
214+
215+
let gpu_raster = &self;
216+
let texture = gpu_raster.data();
217+
218+
let (width, height, output_buffer) = download_to_buffer(device, &mut encoder, texture);
219+
220+
queue.submit([encoder.finish()]);
221+
222+
let buffer_slice = output_buffer.slice(..);
223+
let (sender, receiver) = futures::channel::oneshot::channel();
224+
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
225+
let _ = sender.send(result);
226+
});
227+
receiver.await.expect("Failed to receive map result").expect("Buffer mapping failed");
228+
229+
let data = output_buffer.slice(..).get_mapped_range();
230+
let cpu_data: Vec<Color> = data.chunks_exact(4).map(|chunk| Color::from_rgba8_srgb(chunk[0], chunk[1], chunk[2], chunk[3])).collect();
231+
232+
drop(data);
233+
output_buffer.unmap();
234+
let cpu_image = Image {
235+
data: cpu_data,
236+
width,
237+
height,
238+
base64_string: None,
239+
};
240+
241+
Raster::new_cpu(cpu_image)
242+
}
243+
}
244+
245+
/// Node for uploading textures from CPU to GPU. This Is now deprecated and
246+
/// we should use the Convert node in the future.
247+
///
248+
/// Accepts either individual rasters or tables of rasters and converts them
249+
/// to GPU format using the WgpuExecutor's device and queue.
180250
#[node_macro::node(category(""))]
181251
pub async fn upload_texture<'a: 'n, T: Convert<Table<Raster<GPU>>, &'a WgpuExecutor>>(
182252
_: impl Ctx,

0 commit comments

Comments
 (0)