mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add tool for visualizing crate hierarchy (#3315)
* Add tool for visualizing crate hierarchy * Update crate structure * Restructure crate viz and integrate crate into workspace * Remove transitive dependency edges * Move png / svg creation into the rust binary
This commit is contained in:
152
node-graph/libraries/wgpu-executor/src/context.rs
Normal file
152
node-graph/libraries/wgpu-executor/src/context.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::sync::Arc;
|
||||
use wgpu::{Adapter, Backends, Device, Features, Instance, Queue};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub device: Arc<Device>,
|
||||
pub queue: Arc<Queue>,
|
||||
pub instance: Arc<Instance>,
|
||||
pub adapter: Arc<Adapter>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn new() -> Option<Self> {
|
||||
ContextBuilder::new().build().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ContextBuilder {
|
||||
backends: Backends,
|
||||
features: Features,
|
||||
}
|
||||
impl ContextBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
backends: Backends::all(),
|
||||
features: Features::empty(),
|
||||
}
|
||||
}
|
||||
pub fn with_backends(mut self, backends: Backends) -> Self {
|
||||
self.backends = backends;
|
||||
self
|
||||
}
|
||||
pub fn with_features(mut self, features: Features) -> Self {
|
||||
self.features = features;
|
||||
self
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl ContextBuilder {
|
||||
pub async fn build(self) -> Option<Context> {
|
||||
self.build_with_adapter_selection_inner(None::<fn(&[Adapter]) -> Option<usize>>).await
|
||||
}
|
||||
pub async fn build_with_adapter_selection<S>(self, select: S) -> Option<Context>
|
||||
where
|
||||
S: Fn(&[Adapter]) -> Option<usize>,
|
||||
{
|
||||
self.build_with_adapter_selection_inner(Some(select)).await
|
||||
}
|
||||
pub async fn available_adapters_fmt(&self) -> impl std::fmt::Display {
|
||||
let instance = self.build_instance();
|
||||
fmt::AvailableAdaptersFormatter(instance.enumerate_adapters(self.backends))
|
||||
}
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
impl ContextBuilder {
|
||||
pub async fn build(self) -> Option<Context> {
|
||||
let instance = self.build_instance();
|
||||
let adapter = self.request_adapter(&instance).await?;
|
||||
let (device, queue) = self.request_device(&adapter).await?;
|
||||
Some(Context {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
adapter: Arc::new(adapter),
|
||||
instance: Arc::new(instance),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ContextBuilder {
|
||||
fn build_instance(&self) -> Instance {
|
||||
Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: self.backends,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
async fn request_adapter(&self, instance: &Instance) -> Option<Adapter> {
|
||||
let request_adapter_options = wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: None,
|
||||
force_fallback_adapter: false,
|
||||
};
|
||||
instance.request_adapter(&request_adapter_options).await.ok()
|
||||
}
|
||||
async fn request_device(&self, adapter: &Adapter) -> Option<(Device, Queue)> {
|
||||
let device_descriptor = wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
required_features: self.features,
|
||||
required_limits: adapter.limits(),
|
||||
memory_hints: Default::default(),
|
||||
trace: wgpu::Trace::Off,
|
||||
experimental_features: Default::default(),
|
||||
};
|
||||
adapter.request_device(&device_descriptor).await.ok()
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl ContextBuilder {
|
||||
async fn build_with_adapter_selection_inner<S>(self, select: Option<S>) -> Option<Context>
|
||||
where
|
||||
S: Fn(&[Adapter]) -> Option<usize>,
|
||||
{
|
||||
let instance = self.build_instance();
|
||||
|
||||
let selected_adapter = if let Some(select) = select {
|
||||
self.select_adapter(&instance, select)
|
||||
} else if cfg!(target_os = "windows") {
|
||||
self.select_adapter(&instance, |adapters: &[Adapter]| adapters.iter().position(|a| a.get_info().backend == wgpu::Backend::Dx12))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let adapter = if let Some(adapter) = selected_adapter { adapter } else { self.request_adapter(&instance).await? };
|
||||
|
||||
let (device, queue) = self.request_device(&adapter).await?;
|
||||
Some(Context {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
adapter: Arc::new(adapter),
|
||||
instance: Arc::new(instance),
|
||||
})
|
||||
}
|
||||
fn select_adapter<S>(&self, instance: &Instance, select: S) -> Option<Adapter>
|
||||
where
|
||||
S: Fn(&[Adapter]) -> Option<usize>,
|
||||
{
|
||||
let mut adapters = instance.enumerate_adapters(self.backends);
|
||||
let selected_index = select(&adapters)?;
|
||||
if selected_index >= adapters.len() {
|
||||
return None;
|
||||
}
|
||||
Some(adapters.remove(selected_index))
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod fmt {
|
||||
use super::*;
|
||||
|
||||
pub(super) struct AvailableAdaptersFormatter(pub(super) Vec<Adapter>);
|
||||
impl std::fmt::Display for AvailableAdaptersFormatter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (i, adapter) in self.0.iter().enumerate() {
|
||||
let info = adapter.get_info();
|
||||
writeln!(
|
||||
f,
|
||||
"[{}] {:?} {:?} (Name: {}, Driver: {}, Device: {})",
|
||||
i, info.backend, info.device_type, info.name, info.driver, info.device,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
178
node-graph/libraries/wgpu-executor/src/lib.rs
Normal file
178
node-graph/libraries/wgpu-executor/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
mod context;
|
||||
pub mod shader_runtime;
|
||||
pub mod texture_conversion;
|
||||
|
||||
use crate::shader_runtime::ShaderRuntime;
|
||||
use anyhow::Result;
|
||||
use core_types::Color;
|
||||
use dyn_any::StaticType;
|
||||
use futures::lock::Mutex;
|
||||
use glam::UVec2;
|
||||
use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle, SurfaceId};
|
||||
pub use rendering::RenderContext;
|
||||
use std::sync::Arc;
|
||||
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
||||
use wgpu::util::TextureBlitter;
|
||||
use wgpu::{Origin3d, TextureAspect};
|
||||
|
||||
pub use context::Context as WgpuContext;
|
||||
pub use context::ContextBuilder as WgpuContextBuilder;
|
||||
pub use wgpu::Backends as WgpuBackends;
|
||||
pub use wgpu::Features as WgpuFeatures;
|
||||
|
||||
#[derive(dyn_any::DynAny)]
|
||||
pub struct WgpuExecutor {
|
||||
pub context: WgpuContext,
|
||||
vello_renderer: Mutex<Renderer>,
|
||||
pub shader_runtime: ShaderRuntime,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuExecutor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("WgpuExecutor").field("context", &self.context).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &'a WgpuExecutor {
|
||||
fn from(editor_api: &'a EditorApi<T>) -> Self {
|
||||
editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub type WgpuSurface = Arc<SurfaceHandle<Surface>>;
|
||||
pub type WgpuWindow = Arc<SurfaceHandle<WindowHandle>>;
|
||||
|
||||
pub struct Surface {
|
||||
pub inner: wgpu::Surface<'static>,
|
||||
pub target_texture: Mutex<Option<TargetTexture>>,
|
||||
pub blitter: TextureBlitter,
|
||||
}
|
||||
|
||||
pub struct TargetTexture {
|
||||
texture: wgpu::Texture,
|
||||
view: wgpu::TextureView,
|
||||
size: UVec2,
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub type Window = web_sys::HtmlCanvasElement;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub type Window = Arc<dyn winit::window::Window>;
|
||||
|
||||
unsafe impl StaticType for Surface {
|
||||
type Static = Surface;
|
||||
}
|
||||
|
||||
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
impl WgpuExecutor {
|
||||
pub async fn render_vello_scene_to_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color) -> Result<wgpu::Texture> {
|
||||
let mut output = None;
|
||||
self.render_vello_scene_to_target_texture(scene, size, context, background, &mut output).await?;
|
||||
Ok(output.unwrap().texture)
|
||||
}
|
||||
|
||||
async fn render_vello_scene_to_target_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color, output: &mut Option<TargetTexture>) -> Result<()> {
|
||||
let size = size.max(UVec2::ONE);
|
||||
let target_texture = if let Some(target_texture) = output
|
||||
&& target_texture.size == size
|
||||
{
|
||||
target_texture
|
||||
} else {
|
||||
let texture = self.context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
format: VELLO_SURFACE_FORMAT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
*output = Some(TargetTexture { texture, view, size });
|
||||
output.as_mut().unwrap()
|
||||
};
|
||||
|
||||
let [r, g, b, a] = background.to_rgba8_srgb();
|
||||
let render_params = RenderParams {
|
||||
base_color: vello::peniko::Color::from_rgba8(r, g, b, a),
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
antialiasing_method: AaConfig::Msaa16,
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = self.vello_renderer.lock().await;
|
||||
for (image_brush, texture) in context.resource_overrides.iter() {
|
||||
let texture_view = wgpu::TexelCopyTextureInfoBase {
|
||||
texture: texture.clone(),
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
renderer.override_image(&image_brush.image, Some(texture_view));
|
||||
}
|
||||
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &target_texture.view, &render_params)?;
|
||||
for (image_brush, _) in context.resource_overrides.iter() {
|
||||
renderer.override_image(&image_brush.image, None);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn create_surface(&self, canvas: graphene_application_io::WasmSurfaceHandle) -> Result<SurfaceHandle<Surface>> {
|
||||
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas.surface))?;
|
||||
self.create_surface_inner(surface, canvas.window_id)
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn create_surface(&self, window: SurfaceHandle<Window>) -> Result<SurfaceHandle<Surface>> {
|
||||
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Window(Box::new(window.surface)))?;
|
||||
self.create_surface_inner(surface, window.window_id)
|
||||
}
|
||||
|
||||
pub fn create_surface_inner(&self, surface: wgpu::Surface<'static>, window_id: SurfaceId) -> Result<SurfaceHandle<Surface>> {
|
||||
let blitter = TextureBlitter::new(&self.context.device, VELLO_SURFACE_FORMAT);
|
||||
Ok(SurfaceHandle {
|
||||
window_id,
|
||||
surface: Surface {
|
||||
inner: surface,
|
||||
target_texture: Mutex::new(None),
|
||||
blitter,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WgpuExecutor {
|
||||
pub async fn new() -> Option<Self> {
|
||||
Self::with_context(WgpuContext::new().await?)
|
||||
}
|
||||
|
||||
pub fn with_context(context: WgpuContext) -> Option<Self> {
|
||||
let vello_renderer = Renderer::new(
|
||||
&context.device,
|
||||
RendererOptions {
|
||||
pipeline_cache: None,
|
||||
use_cpu: false,
|
||||
antialiasing_support: AaSupport::all(),
|
||||
num_init_threads: std::num::NonZeroUsize::new(1),
|
||||
},
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create Vello renderer: {:?}", e))
|
||||
.ok()?;
|
||||
|
||||
Some(Self {
|
||||
shader_runtime: ShaderRuntime::new(&context),
|
||||
context,
|
||||
vello_renderer: vello_renderer.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type WindowHandle = Arc<SurfaceHandle<Window>>;
|
||||
20
node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs
Normal file
20
node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::WgpuContext;
|
||||
use crate::shader_runtime::per_pixel_adjust_runtime::PerPixelAdjustShaderRuntime;
|
||||
|
||||
pub mod per_pixel_adjust_runtime;
|
||||
|
||||
pub const FULLSCREEN_VERTEX_SHADER_NAME: &str = "fullscreen_vertexfullscreen_vertex";
|
||||
|
||||
pub struct ShaderRuntime {
|
||||
context: WgpuContext,
|
||||
per_pixel_adjust: PerPixelAdjustShaderRuntime,
|
||||
}
|
||||
|
||||
impl ShaderRuntime {
|
||||
pub fn new(context: &WgpuContext) -> Self {
|
||||
Self {
|
||||
context: context.clone(),
|
||||
per_pixel_adjust: PerPixelAdjustShaderRuntime::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use crate::WgpuContext;
|
||||
use crate::shader_runtime::{FULLSCREEN_VERTEX_SHADER_NAME, ShaderRuntime};
|
||||
use core_types::shaders::buffer_struct::BufferStruct;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use futures::lock::Mutex;
|
||||
use raster_types::{GPU, Raster};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use wgpu::util::{BufferInitDescriptor, DeviceExt};
|
||||
use wgpu::{
|
||||
BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face,
|
||||
FragmentState, FrontFace, LoadOp, Operations, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor,
|
||||
ShaderModuleDescriptor, ShaderSource, ShaderStages, StoreOp, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureViewDescriptor, TextureViewDimension, VertexState,
|
||||
};
|
||||
|
||||
pub struct PerPixelAdjustShaderRuntime {
|
||||
// TODO: PerPixelAdjustGraphicsPipeline already contains the key as `name`
|
||||
pipeline_cache: Mutex<HashMap<String, PerPixelAdjustGraphicsPipeline>>,
|
||||
}
|
||||
|
||||
impl Default for PerPixelAdjustShaderRuntime {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PerPixelAdjustShaderRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pipeline_cache: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShaderRuntime {
|
||||
pub async fn run_per_pixel_adjust<T: BufferStruct>(&self, shaders: &Shaders<'_>, textures: Table<Raster<GPU>>, args: Option<&T>) -> Table<Raster<GPU>> {
|
||||
let mut cache = self.per_pixel_adjust.pipeline_cache.lock().await;
|
||||
let pipeline = cache
|
||||
.entry(shaders.fragment_shader_name.to_owned())
|
||||
.or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(&self.context, shaders));
|
||||
|
||||
let arg_buffer = args.map(|args| {
|
||||
let device = &self.context.device;
|
||||
device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some(&format!("{} arg buffer", pipeline.name.as_str())),
|
||||
usage: BufferUsages::STORAGE,
|
||||
contents: bytemuck::bytes_of(&T::write(*args)),
|
||||
})
|
||||
});
|
||||
pipeline.dispatch(&self.context, textures, arg_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Shaders<'a> {
|
||||
pub wgsl_shader: &'a str,
|
||||
pub fragment_shader_name: &'a str,
|
||||
pub has_uniform: bool,
|
||||
}
|
||||
|
||||
pub struct PerPixelAdjustGraphicsPipeline {
|
||||
name: String,
|
||||
has_uniform: bool,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl PerPixelAdjustGraphicsPipeline {
|
||||
pub fn new(context: &WgpuContext, info: &Shaders) -> Self {
|
||||
let device = &context.device;
|
||||
let name = info.fragment_shader_name.to_owned();
|
||||
|
||||
let fragment_name = &name;
|
||||
let fragment_name = &fragment_name[(fragment_name.find("::").unwrap() + 2)..];
|
||||
// TODO workaround to naga removing `:`
|
||||
let fragment_name = fragment_name.replace(":", "");
|
||||
let shader_module = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some(&format!("PerPixelAdjust {name} wgsl shader")),
|
||||
source: ShaderSource::Wgsl(Cow::Borrowed(info.wgsl_shader)),
|
||||
});
|
||||
|
||||
let entries: &[_] = if info.has_uniform {
|
||||
&[
|
||||
BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
&[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}]
|
||||
};
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some(&format!("PerPixelAdjust {name} PipelineLayout")),
|
||||
bind_group_layouts: &[&device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
label: Some(&format!("PerPixelAdjust {name} BindGroupLayout 0")),
|
||||
entries,
|
||||
})],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some(&format!("PerPixelAdjust {name} Pipeline")),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &shader_module,
|
||||
entry_point: Some(FULLSCREEN_VERTEX_SHADER_NAME),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: PrimitiveState {
|
||||
topology: PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: FrontFace::Ccw,
|
||||
cull_mode: Some(Face::Back),
|
||||
unclipped_depth: false,
|
||||
polygon_mode: PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(FragmentState {
|
||||
module: &shader_module,
|
||||
entry_point: Some(&fragment_name),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format: TextureFormat::Rgba8UnormSrgb,
|
||||
blend: None,
|
||||
write_mask: Default::default(),
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
Self {
|
||||
pipeline,
|
||||
name,
|
||||
has_uniform: info.has_uniform,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(&self, context: &WgpuContext, textures: Table<Raster<GPU>>, arg_buffer: Option<Buffer>) -> Table<Raster<GPU>> {
|
||||
assert_eq!(self.has_uniform, arg_buffer.is_some());
|
||||
let device = &context.device;
|
||||
let name = self.name.as_str();
|
||||
|
||||
let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some(&format!("{name} cmd encoder")),
|
||||
});
|
||||
let out = textures
|
||||
.iter()
|
||||
.map(|instance| {
|
||||
let tex_in = &instance.element.texture;
|
||||
let view_in = tex_in.create_view(&TextureViewDescriptor::default());
|
||||
let format = tex_in.format();
|
||||
|
||||
let entries: &[_] = if let Some(arg_buffer) = arg_buffer.as_ref() {
|
||||
&[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::Buffer(BufferBinding {
|
||||
buffer: arg_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::TextureView(&view_in),
|
||||
},
|
||||
]
|
||||
} else {
|
||||
&[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(&view_in),
|
||||
}]
|
||||
};
|
||||
let bind_group = device.create_bind_group(&BindGroupDescriptor {
|
||||
label: Some(&format!("{name} bind group")),
|
||||
// `get_bind_group_layout` allocates unnecessary memory, we could create it manually to not do that
|
||||
layout: &self.pipeline.get_bind_group_layout(0),
|
||||
entries,
|
||||
});
|
||||
|
||||
let tex_out = device.create_texture(&TextureDescriptor {
|
||||
label: Some(&format!("{name} texture out")),
|
||||
size: tex_in.size(),
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[format],
|
||||
});
|
||||
|
||||
let view_out = tex_out.create_view(&TextureViewDescriptor::default());
|
||||
let mut rp = cmd.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some(&format!("{name} render pipeline")),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view_out,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
// should be dont_care but wgpu doesn't expose that
|
||||
load: LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
rp.set_pipeline(&self.pipeline);
|
||||
rp.set_bind_group(0, Some(&bind_group), &[]);
|
||||
rp.draw(0..3, 0..1);
|
||||
|
||||
TableRow {
|
||||
element: Raster::new(GPU { texture: tex_out }),
|
||||
transform: *instance.transform,
|
||||
alpha_blending: *instance.alpha_blending,
|
||||
source_node_id: *instance.source_node_id,
|
||||
}
|
||||
})
|
||||
.collect::<Table<_>>();
|
||||
context.queue.submit([cmd.finish()]);
|
||||
out
|
||||
}
|
||||
}
|
||||
269
node-graph/libraries/wgpu-executor/src/texture_conversion.rs
Normal file
269
node-graph/libraries/wgpu-executor/src/texture_conversion.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use crate::WgpuExecutor;
|
||||
use core_types::Color;
|
||||
use core_types::Ctx;
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::ops::Convert;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use core_types::transform::Footprint;
|
||||
use raster_types::Image;
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use wgpu::util::{DeviceExt, TextureDataOrder};
|
||||
use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
|
||||
|
||||
/// Uploads CPU image data to a GPU texture
|
||||
///
|
||||
/// Creates a new WGPU texture with RGBA8UnormSrgb format and uploads the provided
|
||||
/// image data. The texture is configured for binding, copying, and source operations.
|
||||
fn upload_to_texture(device: &std::sync::Arc<wgpu::Device>, queue: &std::sync::Arc<wgpu::Queue>, image: &Raster<CPU>) -> wgpu::Texture {
|
||||
let rgba8_data: Vec<SRGBA8> = 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()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts a Raster<GPU> texture to Raster<CPU> by downloading the underlying texture data.
|
||||
///
|
||||
/// 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<wgpu::Device>, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster<GPU>) -> 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,
|
||||
},
|
||||
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,
|
||||
unpadded_bytes_per_row,
|
||||
padded_bytes_per_row,
|
||||
}
|
||||
}
|
||||
|
||||
async fn convert(self) -> Result<Raster<CPU>, 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<Color> = 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
|
||||
impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
|
||||
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table<Raster<GPU>> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts CPU raster table to GPU by uploading each image to a texture
|
||||
impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
|
||||
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table<Raster<GPU>> {
|
||||
let device = &executor.context.device;
|
||||
let queue = &executor.context.queue;
|
||||
let table = self
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let image = row.element;
|
||||
let texture = upload_to_texture(device, queue, image);
|
||||
|
||||
TableRow {
|
||||
element: Raster::new_gpu(texture),
|
||||
transform: *row.transform,
|
||||
alpha_blending: *row.alpha_blending,
|
||||
source_node_id: *row.source_node_id,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
queue.submit([]);
|
||||
table
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts single CPU raster to GPU by uploading to texture
|
||||
impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> {
|
||||
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> {
|
||||
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<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<CPU>> {
|
||||
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> Table<Raster<CPU>> {
|
||||
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<Table<Raster<CPU>>, &'i WgpuExecutor> for Table<Raster<GPU>> {
|
||||
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Table<Raster<CPU>> {
|
||||
let device = &executor.context.device;
|
||||
let queue = &executor.context.queue;
|
||||
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("batch_texture_download_encoder"),
|
||||
});
|
||||
|
||||
let mut converters = Vec::new();
|
||||
let mut rows_meta = Vec::new();
|
||||
|
||||
for row in self {
|
||||
let gpu_raster = row.element;
|
||||
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();
|
||||
for converter in converters {
|
||||
map_futures.push(converter.convert());
|
||||
}
|
||||
|
||||
let map_results = futures::future::try_join_all(map_futures)
|
||||
.await
|
||||
.map_err(|_| "Failed to receive map result")
|
||||
.expect("Buffer mapping communication failed");
|
||||
|
||||
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,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts single GPU raster to CPU by downloading texture data
|
||||
impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> {
|
||||
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<CPU> {
|
||||
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 converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self);
|
||||
|
||||
queue.submit([encoder.finish()]);
|
||||
|
||||
converter.convert().await.expect("Failed to download texture data")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Table<Raster<GPU>>, &'a WgpuExecutor>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(Table<Raster<CPU>>, Table<Raster<GPU>>)] input: T,
|
||||
executor: &'a WgpuExecutor,
|
||||
) -> Table<Raster<GPU>> {
|
||||
input.convert(Footprint::DEFAULT, executor).await
|
||||
}
|
||||
Reference in New Issue
Block a user