spectral / spectral-render /src /renderer.rs
stevenkhan's picture
Upload spectral-render/src/renderer.rs
7077890 verified
use std::sync::Arc;
use wgpu;
use bytemuck::{Pod, Zeroable};
use spectral_core::Terminal;
use spectral_font::{GlyphAtlas, GlyphKey};
use crate::shaders::{BG_VERTEX, BG_FRAGMENT, TEXT_VERTEX, TEXT_FRAGMENT};
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct BgInstance {
pub col: u32, pub row: u32,
pub bg_r: u32, pub bg_g: u32, pub bg_b: u32, pub bg_a: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct TextInstance {
pub col: f32, pub row: f32,
pub bearing_x: f32, pub bearing_y: f32,
pub glyph_w: f32, pub glyph_h: f32,
pub atlas_x: f32, pub atlas_y: f32,
pub atlas_w: f32, pub atlas_h: f32,
pub fg_r: u32, pub fg_g: u32, pub fg_b: u32,
pub flags: u32, pub thickening: f32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Uniforms {
pub cell_width: f32, pub cell_height: f32, pub atlas_size: f32,
pub screen_cols: u32, pub screen_rows: u32,
_pad: u32, _pad2: u32,
}
pub struct Renderer {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub surface: wgpu::Surface<'static>,
pub surface_config: wgpu::SurfaceConfiguration,
pub bg_pipeline: wgpu::RenderPipeline,
pub text_pipeline: wgpu::RenderPipeline,
pub bg_instance_buf: wgpu::Buffer,
pub text_instance_buf: wgpu::Buffer,
pub uniform_buf: wgpu::Buffer,
pub atlas_texture: wgpu::Texture,
pub atlas_view: wgpu::TextureView,
pub atlas_sampler: wgpu::Sampler,
pub bg_bind_group: wgpu::BindGroup,
pub text_uniform_bind_group: wgpu::BindGroup,
pub text_texture_bind_group: wgpu::BindGroup,
pub glyph_atlas: GlyphAtlas,
pub bg_instances: Vec<BgInstance>,
pub text_instances: Vec<TextInstance>,
pub needs_atlas_upload: bool,
}
impl Renderer {
pub async fn new(window: Arc<winit::window::Window>, term_cols: u32, term_rows: u32, atlas_size: u16) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(), ..Default::default()
});
let surface = instance.create_surface(window).unwrap();
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface), force_fallback_adapter: false,
}).await.unwrap();
let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
label: Some("Spectral Device"),
memory_hints: wgpu::MemoryHints::Performance,
}, None).await.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps.formats.iter().copied()
.find(|f| f.is_srgb()).unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format, width: 1280, height: 720,
present_mode: wgpu::PresentMode::AutoNoVsync,
desired_maximum_frame_latency: 1,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
};
surface.configure(&device, &config);
let glyph_atlas = GlyphAtlas::new(atlas_size);
let max_instances = (term_cols * term_rows) as usize;
let atlas_size_px = 2048u32;
let bg_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("bg_instances"),
size: (max_instances * std::mem::size_of::<BgInstance>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let text_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("text_instances"),
size: (max_instances * std::mem::size_of::<TextInstance>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("uniforms"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("atlas_texture"),
size: wgpu::Extent3d { width: atlas_size_px, height: atlas_size_px, depth_or_array_layers: 1 },
mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("atlas_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("uniform_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0, visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false, min_binding_size: None,
}, count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1, visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false, min_binding_size: None,
}, count: None,
},
],
});
let text_texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("text_texture_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 2, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
}, count: None,
},
],
});
let bg_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("bg_pipeline_layout"),
bind_group_layouts: &[&uniform_layout], push_constant_ranges: &[],
});
let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("text_pipeline_layout"),
bind_group_layouts: &[&uniform_layout, &text_texture_layout], push_constant_ranges: &[],
});
let bg_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("bg_shader"), source: wgpu::ShaderSource::Wgsl(BG_VERTEX.into()),
});
let bg_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("bg_frag"), source: wgpu::ShaderSource::Wgsl(BG_FRAGMENT.into()),
});
let text_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text_shader"), source: wgpu::ShaderSource::Wgsl(TEXT_VERTEX.into()),
});
let text_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text_frag"), source: wgpu::ShaderSource::Wgsl(TEXT_FRAGMENT.into()),
});
let bg_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("bg_pipeline"),
layout: Some(&bg_pipeline_layout),
vertex: wgpu::VertexState {
module: &bg_shader, entry_point: Some("main"),
buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &bg_frag, entry_point: Some("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::default(), depth_stencil: None,
multisample: wgpu::MultisampleState::default(), multiview: None, cache: None,
});
let text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("text_pipeline"),
layout: Some(&text_pipeline_layout),
vertex: wgpu::VertexState {
module: &text_shader, entry_point: Some("main"),
buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &text_frag, entry_point: Some("main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format, blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(), depth_stencil: None,
multisample: wgpu::MultisampleState::default(), multiview: None, cache: None,
});
let bg_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bg_bind_group"), layout: &uniform_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: bg_instance_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: uniform_buf.as_entire_binding() },
],
});
let text_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("text_uniform_bind_group"), layout: &uniform_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: text_instance_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: uniform_buf.as_entire_binding() },
],
});
let text_texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("text_texture_bind_group"), layout: &text_texture_layout,
entries: &[
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&atlas_sampler) },
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&atlas_view) },
],
});
Self {
device, queue, surface, surface_config: config,
bg_pipeline, text_pipeline,
bg_instance_buf, text_instance_buf, uniform_buf,
atlas_texture, atlas_view, atlas_sampler,
bg_bind_group, text_uniform_bind_group, text_texture_bind_group,
glyph_atlas,
bg_instances: Vec::with_capacity(max_instances),
text_instances: Vec::with_capacity(max_instances),
needs_atlas_upload: false,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 { return; }
self.surface_config.width = width;
self.surface_config.height = height;
self.surface.configure(&self.device, &self.surface_config);
}
pub fn update_atlas(&mut self) {
if !self.needs_atlas_upload { return; }
for (page_idx, page_data) in self.glyph_atlas.texture_data.iter().enumerate() {
let page_size = self.glyph_atlas.page_size as u32;
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &self.atlas_texture, mip_level: 0,
origin: wgpu::Origin3d { x: 0, y: 0, z: page_idx as u32 },
aspect: wgpu::TextureAspect::All,
},
page_data,
wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(page_size * 4), rows_per_image: Some(page_size) },
wgpu::Extent3d { width: page_size, height: page_size, depth_or_array_layers: 1 },
);
}
self.needs_atlas_upload = false;
}
pub fn update_instances(&mut self, terminal: &Terminal) {
self.bg_instances.clear();
self.text_instances.clear();
let cell_width = 8.0f32;
let cell_height = 16.0f32;
let thickening = if terminal.config.font_thicken { terminal.config.font_thicken_strength } else { 0.0 };
for (row_idx, line) in terminal.grid.lines.iter().enumerate() {
for (col_idx, cell) in line.cells.iter().enumerate() {
self.bg_instances.push(BgInstance {
col: col_idx as u32, row: row_idx as u32,
bg_r: ((cell.bg >> 16) & 0xFF) as u32,
bg_g: ((cell.bg >> 8) & 0xFF) as u32,
bg_b: (cell.bg & 0xFF) as u32,
bg_a: ((cell.bg >> 24) & 0xFF) as u32,
});
if cell.ch == ' ' || cell.ch == '\0' { continue; }
let key = GlyphKey { font_id: 0, glyph_id: cell.ch as u16, size_bucket: (cell_height * 4.0) as u16, style_flags: 0 };
if let Some((_page, slot)) = self.glyph_atlas.get(&key) {
self.text_instances.push(TextInstance {
col: col_idx as f32, row: row_idx as f32,
bearing_x: 0.0, bearing_y: 0.0,
glyph_w: slot.w as f32, glyph_h: slot.h as f32,
atlas_x: slot.x as f32, atlas_y: slot.y as f32,
atlas_w: slot.w as f32, atlas_h: slot.h as f32,
fg_r: ((cell.fg >> 16) & 0xFF) as u32,
fg_g: ((cell.fg >> 8) & 0xFF) as u32,
fg_b: (cell.fg & 0xFF) as u32,
flags: cell.flags.bits() as u32,
thickening,
});
}
}
}
if !self.bg_instances.is_empty() {
self.queue.write_buffer(&self.bg_instance_buf, 0, bytemuck::cast_slice(&self.bg_instances));
}
if !self.text_instances.is_empty() {
self.queue.write_buffer(&self.text_instance_buf, 0, bytemuck::cast_slice(&self.text_instances));
}
let uniforms = Uniforms {
cell_width, cell_height,
atlas_size: self.glyph_atlas.page_size as f32,
screen_cols: terminal.grid.cols as u32,
screen_rows: terminal.grid.rows as u32,
_pad: 0, _pad2: 0,
};
self.queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(&uniforms));
}
pub fn render(&mut self) {
let output = match self.surface.get_current_texture() { Ok(t) => t, Err(_) => return };
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("render_encoder") });
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("bg_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view, resolve_target: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None,
});
pass.set_pipeline(&self.bg_pipeline);
pass.set_bind_group(0, &self.bg_bind_group, &[]);
pass.draw(0..6, 0..self.bg_instances.len() as u32);
}
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("text_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view, resolve_target: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None,
});
pass.set_pipeline(&self.text_pipeline);
pass.set_bind_group(0, &self.text_uniform_bind_group, &[]);
pass.set_bind_group(1, &self.text_texture_bind_group, &[]);
pass.draw(0..6, 0..self.text_instances.len() as u32);
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
}
}