Extract pipelines from WgpuExecutor into scope-provided pipeline nodes (#4210)

* Implement generic pipeline caching

* Fix WIP

* Fix

* Fix

* Fix

* Fix bench

* Migrations

* Review
This commit is contained in:
Timon
2026-06-19 21:20:25 +00:00
committed by GitHub
parent 3bf32443ed
commit 674b3a213f
29 changed files with 630 additions and 444 deletions

View File

@@ -1,56 +0,0 @@
struct CompositeUniforms {
transform_x: vec2<f32>,
transform_y: vec2<f32>,
transform_translation: vec2<f32>,
rect_min: vec2<f32>,
rect_max: vec2<f32>,
viewport_size: vec2<f32>,
pattern_origin: vec2<f32>,
checker_size: f32,
_pad: f32,
};
@group(0) @binding(0)
var<uniform> uniforms: CompositeUniforms;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) document_position: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
let document_corners = array<vec2<f32>, 6>(
uniforms.rect_min,
vec2<f32>(uniforms.rect_max.x, uniforms.rect_min.y),
vec2<f32>(uniforms.rect_min.x, uniforms.rect_max.y),
vec2<f32>(uniforms.rect_min.x, uniforms.rect_max.y),
vec2<f32>(uniforms.rect_max.x, uniforms.rect_min.y),
uniforms.rect_max,
);
let document_position = document_corners[vertex_index];
let transformed = uniforms.transform_x * document_position.x + uniforms.transform_y * document_position.y + uniforms.transform_translation;
let normalized = transformed / uniforms.viewport_size;
let clip = vec2<f32>(normalized.x * 2.0 - 1.0, 1.0 - normalized.y * 2.0);
var out: VertexOutput;
out.position = vec4<f32>(clip, 0.0, 1.0);
out.document_position = document_position;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let tile = floor((in.document_position - uniforms.pattern_origin) / uniforms.checker_size);
let parity = i32(tile.x + tile.y) & 1;
let luminance = select(1.0, 0.8, parity == 1);
let fw = fwidthFine(in.document_position);
let coverage_max = 1.0 - smoothstep(uniforms.rect_max - fw, uniforms.rect_max, in.document_position);
let coverage_min = smoothstep(uniforms.rect_min, uniforms.rect_min + fw, in.document_position);
let coverage = coverage_max * coverage_min;
let alpha = coverage.x * coverage.y;
return vec4<f32>(vec3<f32>(luminance), alpha);
}

View File

@@ -1,45 +0,0 @@
struct CompositeUniforms {
transform_x: vec2<f32>,
transform_y: vec2<f32>,
transform_translation: vec2<f32>,
rect_min: vec2<f32>,
rect_max: vec2<f32>,
viewport_size: vec2<f32>,
pattern_origin: vec2<f32>,
checker_size: f32,
_pad: f32,
};
@group(0) @binding(0)
var<uniform> uniforms: CompositeUniforms;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) document_position: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
let positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(-1.0, 3.0),
vec2<f32>( 3.0, -1.0),
);
let position = positions[vertex_index];
let screen_position = vec2<f32>((position.x + 1.0) * 0.5 * uniforms.viewport_size.x, (1.0 - position.y) * 0.5 * uniforms.viewport_size.y);
let document_position = uniforms.transform_x * screen_position.x + uniforms.transform_y * screen_position.y + uniforms.transform_translation;
var out: VertexOutput;
out.position = vec4<f32>(position, 0.0, 1.0);
out.document_position = document_position;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let tile = floor((in.document_position - uniforms.pattern_origin) / uniforms.checker_size);
let parity = i32(tile.x + tile.y) & 1;
let luminance = vec3<f32>(select(1.0, 0.8, parity == 1));
return vec4<f32>(luminance, 1.0);
}

View File

@@ -1,35 +0,0 @@
@group(0) @binding(0)
var foreground_sampler: sampler;
@group(0) @binding(1)
var foreground_texture: texture_2d<f32>;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) tex_coord: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
let positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(-1.0, 3.0),
vec2<f32>( 3.0, -1.0),
);
let tex_coords = array<vec2<f32>, 3>(
vec2<f32>(0.0, 1.0),
vec2<f32>(0.0, -1.0),
vec2<f32>(2.0, 1.0),
);
var vertex_out: VertexOutput;
vertex_out.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
vertex_out.tex_coord = tex_coords[vertex_index];
return vertex_out;
}
@fragment
fn fs_main(fragment_in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(foreground_texture, foreground_sampler, fragment_in.tex_coord);
}

View File

@@ -1,344 +0,0 @@
use glam::{Affine2, Vec2};
use wgpu::util::DeviceExt;
pub struct BackgroundCompositor {
checker_rect_pipeline: wgpu::RenderPipeline,
checker_viewport_pipeline: wgpu::RenderPipeline,
fullscreen_pipeline: wgpu::RenderPipeline,
checker_bind_group_layout: wgpu::BindGroupLayout,
fullscreen_bind_group_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
}
impl BackgroundCompositor {
pub fn new(device: &wgpu::Device) -> Self {
let format = wgpu::TextureFormat::Rgba8Unorm;
let checker_rect_shader = device.create_shader_module(wgpu::include_wgsl!("checker_rect.wgsl"));
let checker_viewport_shader = device.create_shader_module(wgpu::include_wgsl!("checker_viewport.wgsl"));
let fullscreen_shader = device.create_shader_module(wgpu::include_wgsl!("fullscreen.wgsl"));
let checker_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("background_checker_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let checker_rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("background_checker_rect_pipeline_layout"),
bind_group_layouts: &[Some(&checker_bind_group_layout)],
immediate_size: 0,
});
let checker_viewport_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("background_checker_viewport_pipeline_layout"),
bind_group_layouts: &[Some(&checker_bind_group_layout)],
immediate_size: 0,
});
let fullscreen_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("background_fullscreen_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
],
});
let fullscreen_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("background_fullscreen_pipeline_layout"),
bind_group_layouts: &[Some(&fullscreen_bind_group_layout)],
immediate_size: 0,
});
let checker_rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("background_checker_rect_pipeline"),
layout: Some(&checker_rect_pipeline_layout),
vertex: wgpu::VertexState {
module: &checker_rect_shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &checker_rect_shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let checker_viewport_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("background_checker_viewport_pipeline"),
layout: Some(&checker_viewport_pipeline_layout),
vertex: wgpu::VertexState {
module: &checker_viewport_shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &checker_viewport_shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let fullscreen_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
};
let fullscreen_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("background_fullscreen_pipeline"),
layout: Some(&fullscreen_pipeline_layout),
vertex: wgpu::VertexState {
module: &fullscreen_shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &fullscreen_shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(fullscreen_blend),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("background_fullscreen_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
Self {
checker_rect_pipeline,
checker_viewport_pipeline,
fullscreen_pipeline,
checker_bind_group_layout,
fullscreen_bind_group_layout,
sampler,
}
}
pub fn composite(&self, context: &crate::WgpuContext, foreground: &wgpu::Texture, output: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) {
if zoom <= 0. {
return;
}
let device = &context.device;
let queue = &context.queue;
let checker_size_doc = 8. / zoom;
let screen_to_document = document_to_screen.inverse();
let viewport_size = output.size();
let viewport_size = Vec2::new(viewport_size.width as f32, viewport_size.height as f32);
let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
let foreground_view = foreground.create_view(&wgpu::TextureViewDescriptor::default());
let checker_draws = if backgrounds.is_empty() {
vec![(
3,
self.create_checker_bind_group(device, CompositeUniforms::fullscreen(viewport_size, screen_to_document, checker_size_doc)),
)]
} else {
backgrounds
.iter()
.filter_map(|background| {
let a = background.location.as_vec2();
let b = (background.location + background.dimensions).as_vec2();
let min = a.min(b);
let max = a.max(b);
if max.x <= min.x || max.y <= min.y {
return None;
}
let uniforms = CompositeUniforms::rect(min, max, document_to_screen, viewport_size, checker_size_doc);
Some((6, self.create_checker_bind_group(device, uniforms)))
})
.collect()
};
let fullscreen_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("background_fullscreen_bind_group"),
layout: &self.fullscreen_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&foreground_view),
},
],
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("background_encoder") });
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("background_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &output_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if backgrounds.is_empty() {
pass.set_pipeline(&self.checker_viewport_pipeline);
for (vertex_count, bind_group) in &checker_draws {
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..*vertex_count, 0..1);
}
} else {
pass.set_pipeline(&self.checker_rect_pipeline);
for (vertex_count, bind_group) in &checker_draws {
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..*vertex_count, 0..1);
}
}
pass.set_pipeline(&self.fullscreen_pipeline);
pass.set_bind_group(0, &fullscreen_bind_group, &[]);
pass.draw(0..3, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
}
fn create_checker_bind_group(&self, device: &wgpu::Device, uniforms: CompositeUniforms) -> wgpu::BindGroup {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("background_checker_uniforms"),
contents: bytemuck::bytes_of(&uniforms),
usage: wgpu::BufferUsages::UNIFORM,
});
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("background_checker_bind_group"),
layout: &self.checker_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
})
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct CompositeUniforms {
transform_x: [f32; 2],
transform_y: [f32; 2],
transform_translation: [f32; 2],
rect_min: [f32; 2],
rect_max: [f32; 2],
viewport_size: [f32; 2],
pattern_origin: [f32; 2],
checker_size: f32,
_pad: f32,
}
impl CompositeUniforms {
fn fullscreen(viewport_size: Vec2, screen_to_document: Affine2, checker_size_doc: f32) -> Self {
Self::new(screen_to_document, Vec2::ZERO, Vec2::ZERO, viewport_size, Vec2::ZERO, checker_size_doc)
}
fn rect(rect_min: Vec2, rect_max: Vec2, document_to_screen: Affine2, viewport_size: Vec2, checker_size_doc: f32) -> Self {
Self::new(document_to_screen, rect_min, rect_max, viewport_size, rect_min, checker_size_doc)
}
fn new(transform: Affine2, rect_min: Vec2, rect_max: Vec2, viewport_size: Vec2, pattern_origin: Vec2, checker_size: f32) -> Self {
Self {
transform_x: transform.matrix2.x_axis.to_array(),
transform_y: transform.matrix2.y_axis.to_array(),
transform_translation: transform.translation.to_array(),
rect_min: rect_min.to_array(),
rect_max: rect_max.to_array(),
viewport_size: viewport_size.to_array(),
pattern_origin: pattern_origin.to_array(),
checker_size,
_pad: 0.,
}
}
}

View File

@@ -1,12 +1,11 @@
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>,
pub device: Device,
pub queue: Queue,
pub instance: Instance,
pub adapter: Adapter,
}
impl Context {
@@ -58,12 +57,7 @@ impl ContextBuilder {
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),
})
Some(Context { device, queue, adapter, instance })
}
}
impl ContextBuilder {
@@ -113,12 +107,7 @@ impl ContextBuilder {
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),
})
Some(Context { device, queue, adapter, instance })
}
async fn select_adapter<S>(&self, instance: &Instance, select: S) -> Option<Adapter>
where

View File

@@ -1,46 +1,59 @@
mod background; // TODO: Think about where to place this. Likely inlined in the node. Requires refactor of wgpu pipline usage.
mod context;
mod resample;
mod pipeline;
pub mod shader_runtime;
mod texture_cache;
pub mod texture_conversion;
use std::sync::Arc;
use crate::background::BackgroundCompositor;
use crate::resample::Resampler;
use crate::shader_runtime::ShaderRuntime;
use crate::texture_cache::TextureCache;
use anyhow::Result;
use core_types::Color;
use core_types::color::SRGBA8;
use futures::lock::Mutex;
use glam::{Affine2, UVec2};
use glam::UVec2;
use graphene_application_io::{ApplicationIo, EditorApi};
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
use wgpu::{Origin3d, TextureAspect};
pub use context::Context as WgpuContext;
pub use context::ContextBuilder as WgpuContextBuilder;
pub use pipeline::AsyncPipeline as AsyncWgpuPipeline;
pub use pipeline::Pipeline as WgpuPipeline;
pub use pipeline::PipelineCache as WgpuPipelineCache;
pub use rendering::RenderContext;
pub use wgpu::Backends as WgpuBackends;
pub use wgpu::Features as WgpuFeatures;
const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB
#[derive(dyn_any::DynAny)]
#[derive(dyn_any::DynAny, Clone)]
pub struct WgpuExecutor {
pub context: WgpuContext,
inner: Arc<WgpuExecutorInner>,
}
impl WgpuExecutor {
pub fn context(&self) -> &WgpuContext {
&self.inner.context
}
pub fn shader_runtime(&self) -> &ShaderRuntime {
&self.inner.shader_runtime
}
}
#[derive(dyn_any::DynAny)]
pub struct WgpuExecutorInner {
context: WgpuContext,
texture_cache: Mutex<TextureCache>,
vello_renderer: Mutex<Renderer>,
resampler: Resampler,
background_compositor: BackgroundCompositor,
pub shader_runtime: ShaderRuntime,
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()
f.debug_struct("WgpuExecutor").field("context", &self.context()).finish()
}
}
@@ -65,7 +78,7 @@ impl WgpuExecutor {
};
{
let mut renderer = self.vello_renderer.lock().await;
let mut renderer = self.inner.vello_renderer.lock().await;
for (image_brush, texture) in context.resource_overrides.iter() {
let texture_view = wgpu::TexelCopyTextureInfoBase {
texture: texture.clone(),
@@ -75,7 +88,7 @@ impl WgpuExecutor {
};
renderer.override_image(&image_brush.image, Some(texture_view));
}
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &texture_view, &render_params)?;
renderer.render_to_texture(&self.context().device, &self.context().queue, scene, &texture_view, &render_params)?;
for (image_brush, _) in context.resource_overrides.iter() {
renderer.override_image(&image_brush.image, None);
}
@@ -84,21 +97,12 @@ impl WgpuExecutor {
Ok(texture)
}
pub async fn resample_texture(&self, source: &wgpu::Texture, size: UVec2, transform: &glam::DAffine2) -> Arc<wgpu::Texture> {
let out = self.request_texture(size).await;
self.resampler.resample(&self.context, source, transform, &out);
out
}
pub async fn composite_background(&self, foreground: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) -> Arc<wgpu::Texture> {
let size = foreground.size();
let output = self.request_texture(UVec2::new(size.width, size.height)).await;
self.background_compositor.composite(&self.context, foreground, &output, backgrounds, document_to_screen, zoom);
output
pub fn pipeline_init<P: WgpuPipeline>(&self, pipeline: &WgpuPipelineCache) {
pipeline.init::<P>(self);
}
pub async fn request_texture(&self, size: UVec2) -> Arc<wgpu::Texture> {
self.texture_cache.lock().await.request_texture(&self.context.device, size)
self.inner.texture_cache.lock().await.request_texture(&self.context().device, size)
}
}
@@ -122,17 +126,15 @@ impl WgpuExecutor {
let texture_cache = TextureCache::new(TEXTURE_CACHE_SIZE);
let resampler = Resampler::new(&context.device);
let background_compositor = BackgroundCompositor::new(&context.device);
let shader_runtime = ShaderRuntime::new(&context);
Some(Self {
context,
texture_cache: texture_cache.into(),
vello_renderer: vello_renderer.into(),
resampler,
background_compositor,
shader_runtime,
inner: Arc::new(WgpuExecutorInner {
context,
texture_cache: texture_cache.into(),
vello_renderer: vello_renderer.into(),
shader_runtime,
}),
})
}
}

View File

@@ -0,0 +1,68 @@
use dyn_any::DynAny;
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use crate::WgpuExecutor;
pub type PipelineFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait Pipeline: Any + Send + Sync + Sized {
type Args<'a>;
type Out: Send;
fn create(executor: &WgpuExecutor) -> Self;
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out>;
}
pub trait AsyncPipeline: Any + Send + Sync + Sized {
type Args<'a>;
type Out: Send;
fn create(executor: &WgpuExecutor) -> Self;
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> impl Future<Output = Self::Out> + Send + 'a;
}
impl<P: AsyncPipeline> Pipeline for P {
type Args<'a> = <P as AsyncPipeline>::Args<'a>;
type Out = <P as AsyncPipeline>::Out;
fn create(executor: &WgpuExecutor) -> Self {
<P as AsyncPipeline>::create(executor)
}
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out> {
Box::pin(<P as AsyncPipeline>::run(self, executor, args))
}
}
#[derive(Default, Clone, DynAny)]
pub struct PipelineCache {
pipeline: Arc<OnceLock<Box<dyn Any + Send + Sync>>>,
executor: Arc<OnceLock<WgpuExecutor>>,
}
impl PipelineCache {
pub(super) fn init<P: Pipeline>(&self, executor: &WgpuExecutor) {
self.executor.get_or_init(|| executor.clone());
self.pipeline.get_or_init(|| Box::new(P::create(executor)));
}
pub async fn run<P: Pipeline>(&self, args: &P::Args<'_>) -> P::Out {
let executor = self.executor.get().expect("PipelineCache not initialized");
let entry = self.pipeline.get().expect("PipelineCache not initialized");
let pipeline = (&**entry)
.downcast_ref::<P>()
.unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::<P>(),));
pipeline.run(executor, args).await
}
}
impl std::fmt::Debug for PipelineCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PipelineCache").field("initialized", &self.pipeline.get().is_some()).finish()
}
}

View File

@@ -1,130 +0,0 @@
use crate::WgpuContext;
use glam::{DAffine2, Vec2};
pub struct Resampler {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
}
impl Resampler {
pub fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::include_wgsl!("resample_shader.wgsl"));
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("resample_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("resample_pipeline_layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
..Default::default()
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("resample_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
Resampler { pipeline, bind_group_layout }
}
pub fn resample(&self, context: &WgpuContext, source: &wgpu::Texture, transform: &DAffine2, output: &wgpu::Texture) {
let source_view = source.create_view(&wgpu::TextureViewDescriptor::default());
let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
let params_buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("resample_params"),
size: 32,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let params_data = [transform.matrix2.x_axis.as_vec2(), transform.matrix2.y_axis.as_vec2(), transform.translation.as_vec2(), Vec2::ZERO];
context.queue.write_buffer(&params_buffer, 0, bytemuck::cast_slice(&params_data));
let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("resample_bind_group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: params_buffer.as_entire_binding(),
},
],
});
let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("resample_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &output_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
context.queue.submit([encoder.finish()]);
}
}

View File

@@ -1,51 +0,0 @@
// =============
// VERTEX SHADER
// =============
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
vec2f(-1.0, -1.0),
vec2f(3.0, -1.0),
vec2f(-1.0, 3.0),
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy, 0.0, 1.0);
let coords = xy / 2. + 0.5;
out.tex_coords = vec2f(coords.x, 1. - coords.y);
return out;
}
// ===============
// FRAGMENT SHADER
// ===============
@group(0) @binding(0)
var t_source: texture_2d<f32>;
struct Params {
matrix: mat2x2<f32>,
translation: vec2<f32>,
_pad: vec2<f32>,
};
// We need to use a uniform buffer for the params because push constants are not supported on web
@group(0) @binding(1)
var<uniform> params: Params;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let position = params.matrix * in.tex_coords + params.translation;
let texel = vec2<i32>(floor(position));
let texture_size = vec2<i32>(textureDimensions(t_source));
if (texel.x >= 0 && texel.x < texture_size.x && texel.y >= 0 && texel.y < texture_size.y) {
return textureLoad(t_source, texel, 0);
}
return vec4<f32>(0.0);
}

View File

@@ -14,7 +14,7 @@ use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, 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 {
fn upload_to_texture(device: &wgpu::Device, queue: &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(
@@ -52,7 +52,7 @@ struct RasterGpuToRasterCpuConverter {
padded_bytes_per_row: u32,
}
impl RasterGpuToRasterCpuConverter {
fn new(device: &std::sync::Arc<wgpu::Device>, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster<GPU>) -> Self {
fn new(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster<GPU>) -> Self {
let texture = data_gpu.data();
let width = texture.width();
let height = texture.height();
@@ -100,7 +100,7 @@ impl RasterGpuToRasterCpuConverter {
}
}
async fn convert(self, device: &std::sync::Arc<wgpu::Device>) -> Result<Raster<CPU>, wgpu::BufferAsyncError> {
async fn convert(self, device: &wgpu::Device) -> 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| {
@@ -149,8 +149,8 @@ impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
/// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture
impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<GPU>> {
let device = &executor.context.device;
let queue = &executor.context.queue;
let device = &executor.context().device;
let queue = &executor.context().queue;
let list = self
.into_iter()
.map(|row| {
@@ -169,8 +169,8 @@ impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
/// 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 device = &executor.context().device;
let queue = &executor.context().queue;
let texture = upload_to_texture(device, queue, &self);
queue.submit([]);
@@ -188,8 +188,8 @@ impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
/// Converts a `List<Raster<GPU>>` to `List<Raster<CPU>>` by downloading texture data in one go then asynchronously maps all buffers and processes the results.
impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<CPU>> {
let device = &executor.context.device;
let queue = &executor.context.queue;
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"),
@@ -230,8 +230,8 @@ impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
/// 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 device = &executor.context().device;
let queue = &executor.context().queue;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("single_texture_download_encoder"),