Re-add upload texture (#2915)

* vello: code cleanup of resource overwrites

* upload_texture: upload cpu textures as SRGBA8

* vello: fix wgpu::Texture leak within vello's `context.resource_overrides` HashMap

* fix missing feature gate
This commit is contained in:
Firestar99 2025-07-24 12:51:03 +02:00 committed by GitHub
parent 0d43ad2ea0
commit 2d11d96b4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 161 additions and 24 deletions

1
Cargo.lock generated
View file

@ -5830,6 +5830,7 @@ name = "wgpu-executor"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"dyn-any",
"futures",
"glam",

View file

@ -1083,6 +1083,86 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("TODO"),
properties: None,
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
identifier: "Upload Texture",
category: "Debug: GPU",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::scope("editor-api")],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::IntoNode<&WgpuExecutor>")),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::node(NodeId(0), 0)],
manual_composition: Some(generic!(T)),
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_upload::upload_texture::IDENTIFIER),
..Default::default()
},
DocumentNode {
manual_composition: Some(generic!(T)),
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
output_names: vec!["Texture".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Extract Executor".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Upload Texture".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Cache".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Extract",
category: "Debug",

View file

@ -16,10 +16,12 @@ use graphene_core::uuid::{NodeId, generate_uuid};
use graphene_core::vector::VectorDataTable;
use graphene_core::vector::click_target::{ClickTarget, FreePoint};
use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode};
use graphene_core::{AlphaBlending, Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable};
use graphene_core::{Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable};
use num_traits::Zero;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::ops::Deref;
use std::sync::{Arc, LazyLock};
#[cfg(feature = "vello")]
use vello::*;
@ -145,7 +147,7 @@ impl Default for SvgRender {
#[derive(Clone, Debug, Default)]
pub struct RenderContext {
#[cfg(feature = "vello")]
pub resource_overrides: HashMap<u64, wgpu::Texture>,
pub resource_overrides: Vec<(peniko::Image, wgpu::Texture)>,
}
/// Static state used whilst rendering
@ -1014,6 +1016,8 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
}
}
const LAZY_ARC_VEC_ZERO_U8: LazyLock<Arc<Vec<u8>>> = LazyLock::new(|| Arc::new(Vec::new()));
impl GraphicElementRendered for RasterDataTable<GPU> {
fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) {
log::warn!("tried to render texture as an svg");
@ -1023,30 +1027,30 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams) {
use vello::peniko;
let mut render_stuff = |image: peniko::Image, instance_transform: DAffine2, blend_mode: AlphaBlending| {
let image_transform = transform * instance_transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
for instance in self.instance_ref_iter() {
let blend_mode = *instance.alpha_blending;
let layer = blend_mode != Default::default();
if layer {
let Some(bounds) = self.bounding_box(transform, true) else { return };
let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver);
if layer {
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
scene.push_layer(blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect);
}
let image = peniko::Image::new(
peniko::Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()),
peniko::ImageFormat::Rgba8,
instance.instance.data().width(),
instance.instance.data().height(),
)
.with_extend(peniko::Extend::Repeat);
let image_transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array()));
context.resource_overrides.push((image, instance.instance.data().clone()));
if layer {
scene.pop_layer()
}
};
for instance in self.instance_ref_iter() {
let image = peniko::Image::new(vec![].into(), peniko::ImageFormat::Rgba8, instance.instance.data().width(), instance.instance.data().height()).with_extend(peniko::Extend::Repeat);
let id = image.data.id();
context.resource_overrides.insert(id, instance.instance.data().clone());
render_stuff(image, *instance.transform, *instance.alpha_blending);
}
}

View file

@ -25,3 +25,4 @@ futures = { workspace = true }
web-sys = { workspace = true }
winit = { workspace = true }
vello = { workspace = true }
bytemuck = { workspace = true }

View file

@ -1,4 +1,5 @@
mod context;
pub mod texture_upload;
use anyhow::Result;
pub use context::Context;
@ -111,20 +112,19 @@ impl WgpuExecutor {
{
let mut renderer = self.vello_renderer.lock().await;
for (id, texture) in context.resource_overrides.iter() {
let texture = texture.clone();
for (image, texture) in context.resource_overrides.iter() {
let texture_view = wgpu::TexelCopyTextureInfoBase {
texture,
texture: texture.clone(),
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
};
renderer.override_image(
&vello::peniko::Image::new(vello::peniko::Blob::from_raw_parts(Arc::new(vec![]), *id), vello::peniko::ImageFormat::Rgba8, 0, 0),
Some(texture_view),
);
renderer.override_image(image, Some(texture_view));
}
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &target_texture.view, &render_params)?;
for (image, _) in context.resource_overrides.iter() {
renderer.override_image(image, None);
}
}
let surface_texture = surface_inner.get_current_texture()?;

View file

@ -0,0 +1,51 @@
use crate::WgpuExecutor;
use graphene_core::color::SRGBA8;
use graphene_core::instances::Instance;
use graphene_core::raster_types::{CPU, GPU, Raster, RasterDataTable};
use graphene_core::{Ctx, ExtractFootprint};
use wgpu::util::{DeviceExt, TextureDataOrder};
use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
#[node_macro::node(category(""))]
pub async fn upload_texture<'a: 'n>(_: impl ExtractFootprint + Ctx, input: RasterDataTable<CPU>, executor: &'a WgpuExecutor) -> RasterDataTable<GPU> {
let device = &executor.context.device;
let queue = &executor.context.queue;
let instances = input
.instance_ref_iter()
.map(|instance| {
let image = instance.instance;
let rgba8_data: Vec<SRGBA8> = 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()),
);
Instance {
instance: Raster::new_gpu(texture.into()),
transform: *instance.transform,
alpha_blending: *instance.alpha_blending,
source_node_id: *instance.source_node_id,
}
})
.collect();
queue.submit([]);
instances
}