From 9f423001091496cac2a2c3ff4445a9e4d6fe8ad7 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 10:35:15 +0900 Subject: [PATCH] feat(editor): add ViewportRenderer blit pipeline and shader Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_editor/src/lib.rs | 3 + crates/voltex_editor/src/viewport_renderer.rs | 182 ++++++++++++++++++ crates/voltex_editor/src/viewport_shader.wgsl | 42 ++++ 3 files changed, 227 insertions(+) create mode 100644 crates/voltex_editor/src/viewport_renderer.rs create mode 100644 crates/voltex_editor/src/viewport_shader.wgsl diff --git a/crates/voltex_editor/src/lib.rs b/crates/voltex_editor/src/lib.rs index 1ff04dd..8b3fdb9 100644 --- a/crates/voltex_editor/src/lib.rs +++ b/crates/voltex_editor/src/lib.rs @@ -17,3 +17,6 @@ pub use orbit_camera::OrbitCamera; pub mod viewport_texture; pub use viewport_texture::{ViewportTexture, VIEWPORT_COLOR_FORMAT, VIEWPORT_DEPTH_FORMAT}; + +pub mod viewport_renderer; +pub use viewport_renderer::ViewportRenderer; diff --git a/crates/voltex_editor/src/viewport_renderer.rs b/crates/voltex_editor/src/viewport_renderer.rs new file mode 100644 index 0000000..8fa8df3 --- /dev/null +++ b/crates/voltex_editor/src/viewport_renderer.rs @@ -0,0 +1,182 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct RectUniform { + rect: [f32; 4], + screen: [f32; 2], + _pad: [f32; 2], +} + +pub struct ViewportRenderer { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, + uniform_buffer: wgpu::Buffer, +} + +impl ViewportRenderer { + pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Viewport Blit Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("viewport_shader.wgsl").into()), + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Viewport Blit BGL"), + 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, + }, + 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, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Viewport Blit PL"), + bind_group_layouts: &[&bind_group_layout], + immediate_size: 0, + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Viewport Blit 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: surface_format, + blend: Some(wgpu::BlendState::REPLACE), + 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, + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Viewport Sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Viewport Rect Uniform"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + ViewportRenderer { + pipeline, + bind_group_layout, + sampler, + uniform_buffer, + } + } + + pub fn render( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + target_view: &wgpu::TextureView, + viewport_color_view: &wgpu::TextureView, + screen_w: f32, + screen_h: f32, + rect_x: f32, + rect_y: f32, + rect_w: f32, + rect_h: f32, + ) { + let uniform = RectUniform { + rect: [rect_x, rect_y, rect_w, rect_h], + screen: [screen_w, screen_h], + _pad: [0.0; 2], + }; + queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniform])); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Viewport Blit BG"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: self.uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(viewport_color_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + ], + }); + + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Viewport Blit Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + rpass.set_scissor_rect( + rect_x.max(0.0) as u32, + rect_y.max(0.0) as u32, + rect_w.ceil().max(1.0) as u32, + rect_h.ceil().max(1.0) as u32, + ); + rpass.set_pipeline(&self.pipeline); + rpass.set_bind_group(0, &bind_group, &[]); + rpass.draw(0..6, 0..1); + } +} diff --git a/crates/voltex_editor/src/viewport_shader.wgsl b/crates/voltex_editor/src/viewport_shader.wgsl new file mode 100644 index 0000000..9154f1e --- /dev/null +++ b/crates/voltex_editor/src/viewport_shader.wgsl @@ -0,0 +1,42 @@ +struct RectUniform { + rect: vec4, + screen: vec2, + _pad: vec2, +}; + +@group(0) @binding(0) var u: RectUniform; +@group(0) @binding(1) var t_viewport: texture_2d; +@group(0) @binding(2) var s_viewport: sampler; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +@vertex +fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { + var positions = array, 6>( + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0), + ); + + let p = positions[idx]; + let px = u.rect.x + p.x * u.rect.z; + let py = u.rect.y + p.y * u.rect.w; + let ndc_x = (px / u.screen.x) * 2.0 - 1.0; + let ndc_y = 1.0 - (py / u.screen.y) * 2.0; + + var out: VertexOutput; + out.position = vec4(ndc_x, ndc_y, 0.0, 1.0); + out.uv = p; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(t_viewport, s_viewport, in.uv); +}