use std::num::NonZeroU64;
use std::ops::Range;
use bitflags::bitflags;
use enumset::{EnumSet, enum_set};
use re_tracing::profile_function;
use smallvec::smallvec;
use super::{DrawData, DrawError, RenderContext, Renderer};
use crate::allocator::create_and_fill_uniform_buffer_batch;
use crate::draw_phases::{DrawPhase, OutlineMaskProcessor};
use crate::renderer::{DrawDataDrawable, DrawInstruction, DrawableCollectionViewInfo};
use crate::view_builder::ViewBuilder;
use crate::wgpu_resources::{
BindGroupDesc, BindGroupEntry, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle,
GpuRenderPipelineHandle, GpuRenderPipelinePoolAccessor, PipelineLayoutDesc, PoolError,
RenderPipelineDesc,
};
use crate::{
DebugLabel, DepthOffset, DrawableCollector, LineDrawableBuilder, OutlineMaskPreference,
PickingLayerObjectId, PickingLayerProcessor, include_shader_module,
};
pub mod gpu_data {
use super::LineStripFlags;
use crate::size::SizeHalf;
use crate::{Color32, PickingLayerObjectId, UnalignedColor32, wgpu_buffer_types};
#[repr(C, packed)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineVertex {
pub position: glam::Vec3,
pub strip_index: u32,
}
static_assertions::assert_eq_size!(LineVertex, glam::Vec4);
impl LineVertex {
pub const SENTINEL: Self = Self {
position: glam::vec3(f32::MAX, f32::MAX, f32::MAX),
strip_index: u32::MAX,
};
pub const NUM_SENTINEL_VERTICES: usize = 2;
}
#[repr(C, packed)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineStripInfo {
pub color: UnalignedColor32, pub stippling: u8,
pub flags: LineStripFlags,
pub radius: SizeHalf,
}
static_assertions::assert_eq_size!(LineStripInfo, [u32; 2]);
impl Default for LineStripInfo {
fn default() -> Self {
Self {
radius: crate::Size::new_ui_points(1.5).into(),
color: Color32::WHITE.into(),
stippling: 0,
flags: LineStripFlags::empty(),
}
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DrawDataUniformBuffer {
pub radius_boost_in_ui_points: wgpu_buffer_types::F32RowPadded,
pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 1],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BatchUniformBuffer {
pub world_from_obj: wgpu_buffer_types::Mat4,
pub outline_mask_ids: wgpu_buffer_types::UVec2,
pub picking_object_id: PickingLayerObjectId,
pub depth_offset: f32,
pub triangle_cap_length_factor: f32,
pub triangle_cap_width_factor: f32,
pub _padding: f32,
pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 6],
}
}
#[derive(Clone)]
struct LineStripBatch {
bind_group: GpuBindGroup,
vertex_range: Range<u32>,
active_phases: EnumSet<DrawPhase>,
}
#[derive(Clone)]
pub struct LineDrawData {
bind_group_all_lines: Option<GpuBindGroup>,
bind_group_all_lines_outline_mask: Option<GpuBindGroup>,
batches: Vec<LineStripBatch>,
}
impl DrawData for LineDrawData {
type Renderer = LineRenderer;
fn collect_drawables(
&self,
_view_info: &DrawableCollectionViewInfo,
collector: &mut DrawableCollector<'_>,
) {
for (batch_idx, batch) in self.batches.iter().enumerate() {
collector.add_drawable(
batch.active_phases,
DrawDataDrawable {
distance_sort_key: f32::MAX,
draw_data_payload: batch_idx as _,
},
);
}
}
}
bitflags! {
#[repr(C)]
#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineStripFlags : u8 {
const FLAG_CAP_END_TRIANGLE = 0b0000_0001;
const FLAG_CAP_END_ROUND = 0b0000_0010;
const FLAG_CAP_END_EXTEND_OUTWARDS = 0b0000_0100;
const FLAG_CAP_START_TRIANGLE = 0b0000_1000;
const FLAG_CAP_START_ROUND = 0b0001_0000;
const FLAG_CAP_START_EXTEND_OUTWARDS = 0b0010_0000;
const FLAG_COLOR_GRADIENT = 0b0100_0000;
const FLAG_FORCE_ORTHO_SPANNING = 0b1000_0000;
const FLAGS_OUTWARD_EXTENDING_ROUND_CAPS =
LineStripFlags::FLAG_CAP_START_ROUND.bits() |
LineStripFlags::FLAG_CAP_END_ROUND.bits() |
LineStripFlags::FLAG_CAP_START_EXTEND_OUTWARDS.bits() |
LineStripFlags::FLAG_CAP_END_EXTEND_OUTWARDS.bits();
}
}
pub struct LineBatchInfo {
pub label: DebugLabel,
pub world_from_obj: glam::Affine3A,
pub line_vertex_count: u32,
pub overall_outline_mask_ids: OutlineMaskPreference,
pub additional_outline_mask_ids_vertex_ranges: Vec<(Range<u32>, OutlineMaskPreference)>,
pub picking_object_id: PickingLayerObjectId,
pub depth_offset: DepthOffset,
pub triangle_cap_length_factor: f32,
pub triangle_cap_width_factor: f32,
}
impl Default for LineBatchInfo {
fn default() -> Self {
Self {
label: "unknown_line_batch".into(),
world_from_obj: glam::Affine3A::IDENTITY,
line_vertex_count: 0,
overall_outline_mask_ids: OutlineMaskPreference::NONE,
additional_outline_mask_ids_vertex_ranges: Vec::new(),
picking_object_id: PickingLayerObjectId::default(),
depth_offset: 0,
triangle_cap_length_factor: 4.0,
triangle_cap_width_factor: 2.0,
}
}
}
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum LineDrawDataError {
#[error("Line vertex refers to unknown line strip.")]
InvalidStripIndex,
#[error(transparent)]
PoolError(#[from] PoolError),
#[error(transparent)]
FailedTransferringDataToGpu(#[from] crate::allocator::CpuWriteGpuReadError),
#[error(transparent)]
DataTextureSourceWriteError(#[from] crate::allocator::DataTextureSourceWriteError),
}
impl LineDrawData {
pub fn new(line_builder: LineDrawableBuilder<'_>) -> Result<Self, LineDrawDataError> {
let LineDrawableBuilder {
ctx,
vertices_buffer,
batches,
strips_buffer,
picking_instance_ids_buffer,
radius_boost_in_ui_points_for_outlines,
} = line_builder;
let line_renderer = ctx.renderer::<LineRenderer>();
if strips_buffer.is_empty() || vertices_buffer.is_empty() {
return Ok(Self {
bind_group_all_lines: None,
bind_group_all_lines_outline_mask: None,
batches: Vec::new(),
});
}
let batches = if batches.is_empty() {
vec![LineBatchInfo {
label: "LineDrawData::fallback_batch".into(),
line_vertex_count: vertices_buffer.len() as _,
..Default::default()
}]
} else {
batches
};
const NUM_SENTINEL_VERTICES: usize = 2;
let max_texture_dimension_2d = ctx.device.limits().max_texture_dimension_2d;
let max_num_texels = max_texture_dimension_2d as usize * max_texture_dimension_2d as usize;
let max_num_vertices = max_num_texels - NUM_SENTINEL_VERTICES;
let position_texture = vertices_buffer.finish(
wgpu::TextureFormat::Rgba32Float,
"LineDrawData::position_texture",
)?;
let strip_data_texture = strips_buffer.finish(
wgpu::TextureFormat::Rg32Uint,
"LineDrawData::strip_data_texture",
)?;
let picking_instance_id_texture = picking_instance_ids_buffer.finish(
wgpu::TextureFormat::Rg32Uint,
"LineDrawData::picking_instance_id_texture",
)?;
let draw_data_uniform_buffer_bindings = create_and_fill_uniform_buffer_batch(
ctx,
"LineDrawData::DrawDataUniformBuffer".into(),
[
gpu_data::DrawDataUniformBuffer {
radius_boost_in_ui_points: 0.0.into(),
end_padding: Default::default(),
},
gpu_data::DrawDataUniformBuffer {
radius_boost_in_ui_points: radius_boost_in_ui_points_for_outlines.into(),
end_padding: Default::default(),
},
]
.into_iter(),
);
let bind_group_all_lines = ctx.gpu_resources.bind_groups.alloc(
&ctx.device,
&ctx.gpu_resources,
&BindGroupDesc {
label: "LineDrawData::bind_group_all_lines".into(),
entries: smallvec![
BindGroupEntry::DefaultTextureView(position_texture.handle),
BindGroupEntry::DefaultTextureView(strip_data_texture.handle),
BindGroupEntry::DefaultTextureView(picking_instance_id_texture.handle),
draw_data_uniform_buffer_bindings[0].clone(),
],
layout: line_renderer.bind_group_layout_all_lines,
},
);
let bind_group_all_lines_outline_mask = ctx.gpu_resources.bind_groups.alloc(
&ctx.device,
&ctx.gpu_resources,
&BindGroupDesc {
label: "LineDrawData::bind_group_all_lines_outline_mask".into(),
entries: smallvec![
BindGroupEntry::DefaultTextureView(position_texture.handle),
BindGroupEntry::DefaultTextureView(strip_data_texture.handle),
BindGroupEntry::DefaultTextureView(picking_instance_id_texture.handle),
draw_data_uniform_buffer_bindings[1].clone(),
],
layout: line_renderer.bind_group_layout_all_lines,
},
);
let mut batches_internal = Vec::with_capacity(batches.len());
{
fn uniforms_from_batch_info(
batch_info: &LineBatchInfo,
outline_mask_ids: [u8; 2],
) -> gpu_data::BatchUniformBuffer {
gpu_data::BatchUniformBuffer {
world_from_obj: batch_info.world_from_obj.into(),
outline_mask_ids: outline_mask_ids.into(),
picking_object_id: batch_info.picking_object_id,
depth_offset: batch_info.depth_offset as f32,
triangle_cap_length_factor: batch_info.triangle_cap_length_factor,
triangle_cap_width_factor: batch_info.triangle_cap_width_factor,
_padding: 0.0,
end_padding: Default::default(),
}
}
let uniform_buffer_bindings = create_and_fill_uniform_buffer_batch(
ctx,
"lines batch uniform buffers".into(),
batches.iter().map(|batch_info| {
uniforms_from_batch_info(
batch_info,
batch_info.overall_outline_mask_ids.0.unwrap_or_default(),
)
}),
);
let mut uniform_buffer_bindings_mask_only_batches =
create_and_fill_uniform_buffer_batch(
ctx,
"lines batch uniform buffers - mask only".into(),
batches
.iter()
.flat_map(|batch_info| {
batch_info
.additional_outline_mask_ids_vertex_ranges
.iter()
.map(|(_, mask)| {
uniforms_from_batch_info(batch_info, mask.0.unwrap_or_default())
})
})
.collect::<Vec<_>>()
.into_iter(),
)
.into_iter();
let mut start_vertex_for_next_batch = 0;
for (batch_info, uniform_buffer_binding) in
batches.iter().zip(uniform_buffer_bindings.into_iter())
{
let line_vertex_range_end = (start_vertex_for_next_batch
+ batch_info.line_vertex_count)
.min(max_num_vertices as u32);
let mut active_phases = enum_set![DrawPhase::Opaque | DrawPhase::PickingLayer];
if batch_info.overall_outline_mask_ids.is_some() {
active_phases.insert(DrawPhase::OutlineMask);
}
batches_internal.push(line_renderer.create_linestrip_batch(
ctx,
batch_info.label.clone(),
uniform_buffer_binding,
start_vertex_for_next_batch..line_vertex_range_end,
active_phases,
));
for (range, _) in &batch_info.additional_outline_mask_ids_vertex_ranges {
batches_internal.push(line_renderer.create_linestrip_batch(
ctx,
format!("{} strip-only {range:?}", batch_info.label).into(),
uniform_buffer_bindings_mask_only_batches.next().unwrap(),
range.clone(),
enum_set![DrawPhase::OutlineMask],
));
}
start_vertex_for_next_batch = line_vertex_range_end;
}
}
Ok(Self {
bind_group_all_lines: Some(bind_group_all_lines),
bind_group_all_lines_outline_mask: Some(bind_group_all_lines_outline_mask),
batches: batches_internal,
})
}
}
pub struct LineRenderer {
render_pipeline_color: GpuRenderPipelineHandle,
render_pipeline_picking_layer: GpuRenderPipelineHandle,
render_pipeline_outline_mask: GpuRenderPipelineHandle,
bind_group_layout_all_lines: GpuBindGroupLayoutHandle,
bind_group_layout_batch: GpuBindGroupLayoutHandle,
}
impl LineRenderer {
fn create_linestrip_batch(
&self,
ctx: &RenderContext,
label: DebugLabel,
uniform_buffer_binding: BindGroupEntry,
line_vertex_range: Range<u32>,
active_phases: EnumSet<DrawPhase>,
) -> LineStripBatch {
let bind_group = ctx.gpu_resources.bind_groups.alloc(
&ctx.device,
&ctx.gpu_resources,
&BindGroupDesc {
label,
entries: smallvec![uniform_buffer_binding],
layout: self.bind_group_layout_batch,
},
);
LineStripBatch {
bind_group,
vertex_range: (line_vertex_range.start * 6)..(line_vertex_range.end * 6),
active_phases,
}
}
}
impl Renderer for LineRenderer {
type RendererDrawData = LineDrawData;
fn create_renderer(ctx: &RenderContext) -> Self {
profile_function!();
let render_pipelines = &ctx.gpu_resources.render_pipelines;
let bind_group_layout_all_lines = ctx.gpu_resources.bind_group_layouts.get_or_create(
&ctx.device,
&BindGroupLayoutDesc {
label: "LineRenderer::bind_group_layout_all_lines".into(),
entries: vec![
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Uint,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Uint,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<
gpu_data::DrawDataUniformBuffer,
>() as _),
},
count: None,
},
],
},
);
let bind_group_layout_batch = ctx.gpu_resources.bind_group_layouts.get_or_create(
&ctx.device,
&BindGroupLayoutDesc {
label: "LineRenderer::bind_group_layout_batch".into(),
entries: vec![wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<
gpu_data::BatchUniformBuffer,
>() as _),
},
count: None,
}],
},
);
let pipeline_layout = ctx.gpu_resources.pipeline_layouts.get_or_create(
ctx,
&PipelineLayoutDesc {
label: "LineRenderer::pipeline_layout".into(),
entries: vec![
ctx.global_bindings.layout,
bind_group_layout_all_lines,
bind_group_layout_batch,
],
},
);
let shader_module = ctx
.gpu_resources
.shader_modules
.get_or_create(ctx, &include_shader_module!("../../shader/lines.wgsl"));
let render_pipeline_desc_color = RenderPipelineDesc {
label: "LineRenderer::render_pipeline_color".into(),
pipeline_layout,
vertex_entrypoint: "vs_main".into(),
vertex_handle: shader_module,
fragment_entrypoint: "fs_main".into(),
fragment_handle: shader_module,
vertex_buffers: smallvec![],
render_targets: smallvec![Some(ViewBuilder::MAIN_TARGET_ALPHA_TO_COVERAGE_COLOR_STATE)],
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(ViewBuilder::MAIN_TARGET_DEFAULT_DEPTH_STATE),
multisample: ViewBuilder::main_target_default_msaa_state(ctx.render_config(), true),
};
let render_pipeline_color =
render_pipelines.get_or_create(ctx, &render_pipeline_desc_color);
let render_pipeline_picking_layer = render_pipelines.get_or_create(
ctx,
&RenderPipelineDesc {
label: "LineRenderer::render_pipeline_picking_layer".into(),
fragment_entrypoint: "fs_main_picking_layer".into(),
render_targets: smallvec![Some(PickingLayerProcessor::PICKING_LAYER_FORMAT.into())],
depth_stencil: PickingLayerProcessor::PICKING_LAYER_DEPTH_STATE,
multisample: PickingLayerProcessor::PICKING_LAYER_MSAA_STATE,
..render_pipeline_desc_color.clone()
},
);
let render_pipeline_outline_mask = render_pipelines.get_or_create(
ctx,
&RenderPipelineDesc {
label: "LineRenderer::render_pipeline_outline_mask".into(),
pipeline_layout,
vertex_entrypoint: "vs_main".into(),
vertex_handle: shader_module,
fragment_entrypoint: "fs_main_outline_mask".into(),
fragment_handle: shader_module,
vertex_buffers: smallvec![],
render_targets: smallvec![Some(OutlineMaskProcessor::MASK_FORMAT.into())],
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: OutlineMaskProcessor::MASK_DEPTH_STATE,
multisample: OutlineMaskProcessor::mask_default_msaa_state(ctx.device_caps().tier),
},
);
Self {
render_pipeline_color,
render_pipeline_picking_layer,
render_pipeline_outline_mask,
bind_group_layout_all_lines,
bind_group_layout_batch,
}
}
fn draw(
&self,
render_pipelines: &GpuRenderPipelinePoolAccessor<'_>,
phase: DrawPhase,
pass: &mut wgpu::RenderPass<'_>,
draw_instructions: &[DrawInstruction<'_, Self::RendererDrawData>],
) -> Result<(), DrawError> {
let pipeline_handle = match phase {
DrawPhase::OutlineMask => self.render_pipeline_outline_mask,
DrawPhase::Opaque => self.render_pipeline_color,
DrawPhase::PickingLayer => self.render_pipeline_picking_layer,
_ => unreachable!("We were called on a phase we weren't subscribed to: {phase:?}"),
};
let pipeline = render_pipelines.get(pipeline_handle)?;
pass.set_pipeline(pipeline);
for DrawInstruction {
draw_data,
drawables,
} in draw_instructions
{
let bind_group_draw_data = match phase {
DrawPhase::OutlineMask => &draw_data.bind_group_all_lines_outline_mask,
DrawPhase::Opaque | DrawPhase::PickingLayer => &draw_data.bind_group_all_lines,
_ => unreachable!("We were called on a phase we weren't subscribed to: {phase:?}"),
};
let Some(bind_group_draw_data) = bind_group_draw_data else {
re_log::debug_panic!(
"Line data bind group for draw phase {phase:?} was not set despite being submitted for drawing."
);
continue;
};
pass.set_bind_group(1, bind_group_draw_data, &[]);
for drawable in *drawables {
let batch = &draw_data.batches[drawable.draw_data_payload as usize];
pass.set_bind_group(2, &batch.bind_group, &[]);
pass.draw(batch.vertex_range.clone(), 0..1);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Rgba;
use crate::view_builder::TargetConfiguration;
#[test]
fn empty_strips() {
re_log::setup_logging();
re_log::PanicOnWarnScope::new();
RenderContext::new_test().execute_test_frame(|ctx| {
let mut view = ViewBuilder::new(ctx, TargetConfiguration::default()).unwrap();
let empty = LineDrawableBuilder::new(ctx);
view.queue_draw(ctx, empty.into_draw_data().unwrap());
let mut empty_batch = LineDrawableBuilder::new(ctx);
empty_batch
.batch("empty batch")
.add_strip(std::iter::empty());
view.queue_draw(ctx, empty_batch.into_draw_data().unwrap());
let mut empty_batch_between_non_empty = LineDrawableBuilder::new(ctx);
empty_batch_between_non_empty
.batch("non-empty batch")
.add_strip([glam::Vec3::ZERO, glam::Vec3::ZERO].into_iter());
empty_batch_between_non_empty
.batch("empty batch")
.add_strip(std::iter::empty());
empty_batch_between_non_empty
.batch("non-empty batch")
.add_strip([glam::Vec3::ZERO, glam::Vec3::ZERO].into_iter());
view.queue_draw(ctx, empty_batch_between_non_empty.into_draw_data().unwrap());
[view.draw(ctx, Rgba::BLACK).unwrap()]
});
}
}