use crate::ecs::text::components::TextVertex;
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec4};
pub struct TextPass {
pub render_pipeline: wgpu::RenderPipeline,
pub texture_bind_group_layout: wgpu::BindGroupLayout,
pub text_bind_group_layout: wgpu::BindGroupLayout,
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub uniform_buffer: wgpu::Buffer,
pub text_uniform_buffer: wgpu::Buffer,
pub character_color_buffer: wgpu::Buffer,
pub uniform_bind_group: wgpu::BindGroup,
pub text_bind_groups: Vec<wgpu::BindGroup>,
pub texture_bind_groups: Vec<wgpu::BindGroup>,
pub text_instances: Vec<TextInstance>,
vertex_buffer_capacity: usize,
index_buffer_capacity: usize,
uniform_buffer_instances: usize,
character_color_buffer_capacity: usize,
cached_font_texture_views: Vec<wgpu::TextureView>,
}
#[derive(Clone)]
pub struct TextInstance {
pub vertex_offset: u32,
pub index_offset: u32,
pub index_count: u32,
pub transform: Mat4,
pub color: Vec4,
pub outline_color: Vec4,
pub outline_width: f32,
pub smoothing: f32,
pub font_index: usize,
pub billboard: bool,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TextUniforms {
pub model: Mat4,
pub color: Vec4,
pub outline_color: Vec4,
pub outline_width: f32,
pub smoothing: f32,
pub _padding: [f32; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SceneUniforms {
pub view: Mat4,
pub projection: Mat4,
pub camera_position: Vec4,
}
const INITIAL_CHARACTER_COLOR_CAPACITY: usize = 10000;
impl TextPass {
pub fn new(
device: &wgpu::Device,
color_format: wgpu::TextureFormat,
depth_format: wgpu::TextureFormat,
) -> Self {
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text Uniform Bind Group Layout"),
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 text_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text Instance Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<TextUniforms>() as u64,
),
},
count: None,
}],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Text Texture 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: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Text Pipeline Layout"),
bind_group_layouts: &[
&uniform_bind_group_layout,
&text_bind_group_layout,
&texture_bind_group_layout,
],
push_constant_ranges: &[],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Text Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Text Render Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<TextVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Uint32,
},
],
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: depth_format,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Greater,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
});
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Vertex Buffer"),
size: (std::mem::size_of::<TextVertex>() * 10000) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Index Buffer"),
size: (std::mem::size_of::<u32>() * 30000) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Scene Uniform Buffer"),
size: std::mem::size_of::<SceneUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_size = std::mem::size_of::<TextUniforms>();
let alignment = device.limits().min_uniform_buffer_offset_alignment as usize;
let aligned_size = (uniform_size + alignment - 1) & !(alignment - 1);
let text_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Instance Uniform Buffer"),
size: (aligned_size * 100) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let character_color_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Character Color Buffer"),
size: (std::mem::size_of::<[f32; 4]>() * INITIAL_CHARACTER_COLOR_CAPACITY) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Text Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Text Instance Bind Group"),
layout: &text_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &text_uniform_buffer,
offset: 0,
size: wgpu::BufferSize::new(aligned_size as u64),
}),
}],
});
Self {
render_pipeline,
texture_bind_group_layout,
text_bind_group_layout,
vertex_buffer,
index_buffer,
uniform_buffer,
text_uniform_buffer,
character_color_buffer,
uniform_bind_group,
text_bind_groups: vec![text_bind_group],
texture_bind_groups: Vec::new(),
text_instances: Vec::new(),
vertex_buffer_capacity: 10000,
index_buffer_capacity: 30000,
uniform_buffer_instances: 100,
character_color_buffer_capacity: INITIAL_CHARACTER_COLOR_CAPACITY,
cached_font_texture_views: Vec::new(),
}
}
fn ensure_vertex_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.vertex_buffer_capacity {
let new_capacity = (required * 2).max(1000);
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Vertex Buffer (Resized)"),
size: (std::mem::size_of::<TextVertex>() * new_capacity) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.vertex_buffer_capacity = new_capacity;
}
}
fn ensure_index_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.index_buffer_capacity {
let new_capacity = (required * 2).max(3000);
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Index Buffer (Resized)"),
size: (std::mem::size_of::<u32>() * new_capacity) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.index_buffer_capacity = new_capacity;
}
}
fn ensure_uniform_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.uniform_buffer_instances {
let uniform_size = std::mem::size_of::<TextUniforms>();
let alignment = device.limits().min_uniform_buffer_offset_alignment as usize;
let aligned_size = (uniform_size + alignment - 1) & !(alignment - 1);
let new_capacity = (required * 2).max(10);
self.text_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Instance Uniform Buffer (Resized)"),
size: (aligned_size * new_capacity) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Text Instance Bind Group (Resized)"),
layout: &self.text_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.text_uniform_buffer,
offset: 0,
size: wgpu::BufferSize::new(aligned_size as u64),
}),
}],
});
self.text_bind_groups = vec![text_bind_group];
self.uniform_buffer_instances = new_capacity;
}
}
fn ensure_character_color_buffer_capacity(&mut self, device: &wgpu::Device, required: usize) {
if required > self.character_color_buffer_capacity {
let new_capacity = (required * 2).max(1000);
self.character_color_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Text Character Color Buffer (Resized)"),
size: (std::mem::size_of::<[f32; 4]>() * new_capacity) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.character_color_buffer_capacity = new_capacity;
self.rebuild_texture_bind_groups(device);
}
}
pub fn update_texture_bind_groups(
&mut self,
device: &wgpu::Device,
font_textures: &[(wgpu::Texture, wgpu::TextureView)],
) {
self.cached_font_texture_views.clear();
for (_texture, texture_view) in font_textures {
self.cached_font_texture_views.push(texture_view.clone());
}
self.rebuild_texture_bind_groups(device);
}
fn rebuild_texture_bind_groups(&mut self, device: &wgpu::Device) {
self.texture_bind_groups.clear();
for texture_view in &self.cached_font_texture_views {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Text Texture Bind Group"),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.character_color_buffer.as_entire_binding(),
},
],
});
self.texture_bind_groups.push(bind_group);
}
}
}
impl PassNode<World> for TextPass {
fn name(&self) -> &'static str {
"text_pass"
}
fn reads(&self) -> Vec<&str> {
vec![]
}
fn writes(&self) -> Vec<&str> {
vec![]
}
fn reads_writes(&self) -> Vec<&str> {
vec!["color", "depth"]
}
fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
self.text_instances.clear();
let mut all_vertices = Vec::new();
let mut all_indices = Vec::new();
let mut all_character_colors: Vec<[f32; 4]> = Vec::new();
let mut character_color_offset = 0u32;
let view_matrix = if let Some(camera_matrices) =
crate::ecs::camera::queries::query_active_camera_matrices(world)
{
let camera_position = nalgebra_glm::vec4(0.0, 0.0, 0.0, 1.0);
let scene_uniforms = SceneUniforms {
view: camera_matrices.view,
projection: camera_matrices.projection,
camera_position,
};
queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&[scene_uniforms]),
);
Some(camera_matrices.view)
} else {
None
};
let entities = world.query_entities(
crate::ecs::world::TEXT
| crate::ecs::world::VISIBILITY
| crate::ecs::world::GLOBAL_TRANSFORM,
);
for entity in entities {
let visible = world
.get_visibility(entity)
.map(|v| v.visible)
.unwrap_or(false);
if !visible {
continue;
}
let text = world.get_text(entity);
let global_transform = world.get_global_transform(entity);
let character_colors = world.get_text_character_colors(entity);
if let (Some(text), Some(global_transform)) = (text, global_transform)
&& let Some(mesh) = &text.cached_mesh
{
let vertex_offset = all_vertices.len() as u32;
let index_offset = all_indices.len() as u32;
for &index in &mesh.indices {
all_indices.push(index + vertex_offset);
}
let text_content = world
.resources
.text_cache
.get_text(text.text_index)
.map(|s| s.to_string())
.unwrap_or_default();
let char_count = text_content.chars().filter(|c| *c != '\n').count();
for vertex in &mesh.vertices {
let adjusted_vertex = TextVertex {
position: vertex.position,
tex_coords: vertex.tex_coords,
character_index: vertex.character_index + character_color_offset,
_padding: 0,
};
all_vertices.push(adjusted_vertex);
}
for character_index in 0..char_count {
let color = if let Some(colors) = character_colors {
colors
.colors
.get(character_index)
.and_then(|c| *c)
.map(|c| [c.x, c.y, c.z, c.w])
.unwrap_or([0.0, 0.0, 0.0, 0.0])
} else {
[0.0, 0.0, 0.0, 0.0]
};
all_character_colors.push(color);
}
character_color_offset += char_count as u32;
self.text_instances.push(TextInstance {
vertex_offset,
index_offset,
index_count: mesh.indices.len() as u32,
transform: global_transform.0,
color: text.properties.color,
outline_color: text.properties.outline_color,
outline_width: text.properties.outline_width,
smoothing: text.properties.smoothing,
font_index: text.font_index,
billboard: text.billboard,
});
}
}
if all_character_colors.is_empty() {
all_character_colors.push([0.0, 0.0, 0.0, 0.0]);
}
self.ensure_character_color_buffer_capacity(device, all_character_colors.len());
queue.write_buffer(
&self.character_color_buffer,
0,
bytemuck::cast_slice(&all_character_colors),
);
if !all_vertices.is_empty() {
self.ensure_vertex_buffer_capacity(device, all_vertices.len());
queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&all_vertices));
}
if !all_indices.is_empty() {
self.ensure_index_buffer_capacity(device, all_indices.len());
queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&all_indices));
}
let uniform_size = std::mem::size_of::<TextUniforms>();
let alignment = device.limits().min_uniform_buffer_offset_alignment as usize;
let aligned_size = (uniform_size + alignment - 1) & !(alignment - 1);
self.ensure_uniform_buffer_capacity(device, self.text_instances.len());
for (i, instance) in self.text_instances.iter().enumerate() {
if i >= self.uniform_buffer_instances {
break;
}
let model = if instance.billboard {
if let Some(view) = view_matrix {
let position = nalgebra_glm::vec3(
instance.transform[(0, 3)],
instance.transform[(1, 3)],
instance.transform[(2, 3)],
);
let scale_x = nalgebra_glm::length(&nalgebra_glm::vec3(
instance.transform[(0, 0)],
instance.transform[(1, 0)],
instance.transform[(2, 0)],
));
let scale_y = nalgebra_glm::length(&nalgebra_glm::vec3(
instance.transform[(0, 1)],
instance.transform[(1, 1)],
instance.transform[(2, 1)],
));
let scale_z = nalgebra_glm::length(&nalgebra_glm::vec3(
instance.transform[(0, 2)],
instance.transform[(1, 2)],
instance.transform[(2, 2)],
));
let right = nalgebra_glm::vec3(view[(0, 0)], view[(0, 1)], view[(0, 2)]);
let up = nalgebra_glm::vec3(view[(1, 0)], view[(1, 1)], view[(1, 2)]);
let forward = nalgebra_glm::vec3(-view[(2, 0)], -view[(2, 1)], -view[(2, 2)]);
#[rustfmt::skip]
let billboard_transform = Mat4::new(
right.x * scale_x, up.x * scale_y, forward.x * scale_z, position.x,
right.y * scale_x, up.y * scale_y, forward.y * scale_z, position.y,
right.z * scale_x, up.z * scale_y, forward.z * scale_z, position.z,
0.0, 0.0, 0.0, 1.0,
);
billboard_transform
} else {
instance.transform
}
} else {
instance.transform
};
let text_uniforms = TextUniforms {
model,
color: instance.color,
outline_color: instance.outline_color,
outline_width: instance.outline_width,
smoothing: instance.smoothing,
_padding: [0.0; 2],
};
let offset = i * aligned_size;
queue.write_buffer(
&self.text_uniform_buffer,
offset as u64,
bytemuck::cast_slice(&[text_uniforms]),
);
}
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, World>,
) -> Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
crate::render::wgpu::rendergraph::RenderGraphError,
> {
let (color_view, color_load, color_store) = context.get_color_attachment("color")?;
let (depth_view, depth_load, depth_store) = context.get_depth_attachment("depth")?;
let mut render_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Text Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: color_load,
store: color_store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: depth_store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if !self.text_instances.is_empty() {
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
for (i, instance) in self.text_instances.iter().enumerate() {
if i >= self.uniform_buffer_instances {
tracing::warn!(
"Skipping text instance {} - exceeds uniform buffer capacity of {}",
i,
self.uniform_buffer_instances
);
break;
}
if instance.font_index >= self.texture_bind_groups.len() {
tracing::warn!(
"Skipping text instance with invalid font index: {}",
instance.font_index
);
continue;
}
let index_end = instance.index_offset.saturating_add(instance.index_count);
if instance.vertex_offset >= self.vertex_buffer_capacity as u32
|| index_end > self.index_buffer_capacity as u32
{
tracing::warn!("Skipping text instance with out-of-bounds indices");
continue;
}
let uniform_size = std::mem::size_of::<TextUniforms>();
let alignment =
context.device.limits().min_uniform_buffer_offset_alignment as usize;
let aligned_size = (uniform_size + alignment - 1) & !(alignment - 1);
let offset = (i as u32) * (aligned_size as u32);
render_pass.set_bind_group(1, &self.text_bind_groups[0], &[offset]);
render_pass.set_bind_group(2, &self.texture_bind_groups[instance.font_index], &[]);
render_pass.draw_indexed(instance.index_offset..index_end, 0, 0..1);
}
}
drop(render_pass);
Ok(context.into_sub_graph_commands())
}
}