summaryrefslogtreecommitdiffstats
path: root/src/renderer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.rs')
-rw-r--r--src/renderer.rs303
1 files changed, 303 insertions, 0 deletions
diff --git a/src/renderer.rs b/src/renderer.rs
new file mode 100644
index 0000000..e3a2acd
--- /dev/null
+++ b/src/renderer.rs
@@ -0,0 +1,303 @@
+use crate::uniform::{UniformCache, UniformError};
+use rosc::OscType;
+
+const CANVAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
+
+const VERTEX_SHADER: &str = "\
+@group(0) @binding(0) var<uniform> _wgsl_resolution: vec2<f32>;
+
+struct VertexOutput {
+ @builtin(position) clip_position: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+ @location(1) resolution: vec2<f32>,
+};
+
+@vertex
+fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
+ var uv = vec2<f32>(f32(vi % 2u), f32(vi / 2u));
+ var out: VertexOutput;
+ out.clip_position = vec4<f32>(2.0 * uv - 1.0, 0.0, 1.0);
+ out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
+ out.resolution = _wgsl_resolution;
+ return out;
+}
+";
+
+const DEFAULT_FRAGMENT: &str = "\
+@fragment
+fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
+ let check = floor(uv * 10.0);
+ return vec4<f32>(vec3<f32>(fract((check.x + check.y) / 2.0)), 1.0);
+}
+";
+
+pub struct Renderer {
+ canvas_view: wgpu::TextureView,
+ vertex_module: wgpu::ShaderModule,
+ vertex_bgl: wgpu::BindGroupLayout,
+ vertex_bg: wgpu::BindGroup,
+ render_pipeline: wgpu::RenderPipeline,
+ uniform_cache: UniformCache,
+}
+
+impl Renderer {
+ pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self {
+ let canvas_view = device
+ .create_texture(&wgpu::TextureDescriptor {
+ label: Some("canvas"),
+ size: wgpu::Extent3d {
+ width,
+ height,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: CANVAS_FORMAT,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ })
+ .create_view(&Default::default());
+
+ let vertex_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("vertex_shader"),
+ source: wgpu::ShaderSource::Wgsl(VERTEX_SHADER.into()),
+ });
+
+ let resolution_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("resolution"),
+ size: 8,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let vertex_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: None,
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ }],
+ });
+
+ let vertex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: None,
+ layout: &vertex_bgl,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: resolution_buffer.as_entire_binding(),
+ }],
+ });
+
+ queue.write_buffer(
+ &resolution_buffer,
+ 0,
+ bytemuck::cast_slice(&[width as f32, height as f32]),
+ );
+
+ let mut uniform_cache = UniformCache::new();
+ let render_pipeline = build_pipeline(
+ device,
+ &vertex_module,
+ &vertex_bgl,
+ DEFAULT_FRAGMENT,
+ &mut uniform_cache,
+ )
+ .expect("default shader");
+
+ Self {
+ canvas_view,
+ vertex_module,
+ vertex_bgl,
+ vertex_bg,
+ render_pipeline,
+ uniform_cache,
+ }
+ }
+
+ pub fn canvas_view(&self) -> &wgpu::TextureView {
+ &self.canvas_view
+ }
+
+ pub fn load_shader(&mut self, device: &wgpu::Device, source: &str) -> Result<(), String> {
+ let pipeline = build_pipeline(
+ device,
+ &self.vertex_module,
+ &self.vertex_bgl,
+ source,
+ &mut self.uniform_cache,
+ )?;
+ self.render_pipeline = pipeline;
+ log::info!("shader loaded");
+ Ok(())
+ }
+
+ pub fn set_uniform(
+ &mut self,
+ path: &str,
+ args: &[OscType],
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ let mut parts = path.split('/');
+ let name = parts.next().ok_or("missing uniform name")?;
+
+ let mut uref = self.uniform_cache.get(name).ok_or("uniform not found")?;
+
+ for component in parts {
+ uref = uref.field(component)?;
+ }
+
+ let scalar = uref.leaf_scalar()?;
+ match scalar.kind {
+ naga::ScalarKind::Float => {
+ let values: Vec<f32> = args
+ .iter()
+ .map(|a| match a {
+ OscType::Float(f) => Ok(*f),
+ OscType::Double(d) => Ok(*d as f32),
+ OscType::Int(i) => Ok(*i as f32),
+ OscType::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
+ _ => Err(UniformError::TypeMismatch),
+ })
+ .collect::<Result<_, _>>()?;
+ uref.set_f32(&values)?;
+ }
+ naga::ScalarKind::Sint => {
+ let values: Vec<i32> = args
+ .iter()
+ .map(|a| match a {
+ OscType::Int(i) => Ok(*i),
+ OscType::Float(f) => Ok(*f as i32),
+ OscType::Double(d) => Ok(*d as i32),
+ OscType::Bool(b) => Ok(if *b { 1 } else { 0 }),
+ _ => Err(UniformError::TypeMismatch),
+ })
+ .collect::<Result<_, _>>()?;
+ uref.set_i32(&values)?;
+ }
+ naga::ScalarKind::Uint => {
+ let values: Vec<u32> = args
+ .iter()
+ .map(|a| match a {
+ OscType::Int(i) => Ok(*i as u32),
+ OscType::Float(f) => Ok(*f as u32),
+ OscType::Double(d) => Ok(*d as u32),
+ OscType::Bool(b) => Ok(if *b { 1 } else { 0 }),
+ _ => Err(UniformError::TypeMismatch),
+ })
+ .collect::<Result<_, _>>()?;
+ uref.set_u32(&values)?;
+ }
+ _ => return Err(Box::new(UniformError::TypeMismatch)),
+ }
+
+ Ok(())
+ }
+
+ pub fn render(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
+ self.uniform_cache.flush(queue);
+
+ let mut encoder =
+ device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
+
+ {
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: None,
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &self.canvas_view,
+ depth_slice: None,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ occlusion_query_set: None,
+ timestamp_writes: None,
+ multiview_mask: None,
+ });
+
+ pass.set_pipeline(&self.render_pipeline);
+ pass.set_bind_group(0, &self.vertex_bg, &[]);
+ if let Some(ref bg) = self.uniform_cache.bind_group {
+ pass.set_bind_group(1, bg, &[]);
+ }
+ pass.draw(0..4, 0..1);
+ }
+
+ queue.submit([encoder.finish()]);
+ }
+}
+
+fn build_pipeline(
+ device: &wgpu::Device,
+ vertex_module: &wgpu::ShaderModule,
+ vertex_bgl: &wgpu::BindGroupLayout,
+ fragment_source: &str,
+ cache: &mut UniformCache,
+) -> Result<wgpu::RenderPipeline, String> {
+ let module = naga::front::wgsl::parse_str(fragment_source)
+ .map_err(|e| format!("WGSL parse error: {e}"))?;
+
+ naga::valid::Validator::new(
+ naga::valid::ValidationFlags::all(),
+ naga::valid::Capabilities::all(),
+ )
+ .validate(&module)
+ .map_err(|e| format!("WGSL validation error: {e}"))?;
+
+ cache.refresh(module, device);
+
+ let fragment_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("fragment_shader"),
+ source: wgpu::ShaderSource::Wgsl(fragment_source.into()),
+ });
+
+ let mut bind_group_layouts: Vec<Option<&wgpu::BindGroupLayout>> = vec![Some(vertex_bgl)];
+ if let Some(ref bgl) = cache.bind_group_layout {
+ bind_group_layouts.push(Some(bgl));
+ }
+
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: None,
+ bind_group_layouts: &bind_group_layouts,
+ immediate_size: 0,
+ });
+
+ Ok(
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: None,
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: vertex_module,
+ entry_point: Some("vs_main"),
+ buffers: &[],
+ compilation_options: Default::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &fragment_module,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: CANVAS_FORMAT,
+ blend: Some(wgpu::BlendState::REPLACE),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: Default::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleStrip,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: Default::default(),
+ multiview_mask: None,
+ cache: None,
+ }),
+ )
+}