use super::*;
impl ViewportGpuResources {
pub(crate) fn ensure_polyline_pipeline(&mut self, device: &wgpu::Device) {
if self.polyline_pipeline.is_some() {
return;
}
let pl_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("polyline_bgl"),
entries: &[
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: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("polyline_shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!(concat!(env!("OUT_DIR"), "/polyline.wgsl")).into(),
),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("polyline_pipeline_layout"),
bind_group_layouts: &[&self.camera_bind_group_layout, &pl_bgl],
push_constant_ranges: &[],
});
let pl_instance_layout = wgpu::VertexBufferLayout {
array_stride: 112,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}, wgpu::VertexAttribute {
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}, wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
}, wgpu::VertexAttribute {
offset: 36,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
}, wgpu::VertexAttribute {
offset: 48,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
}, wgpu::VertexAttribute {
offset: 52,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
}, wgpu::VertexAttribute {
offset: 56,
shader_location: 6,
format: wgpu::VertexFormat::Uint32,
}, wgpu::VertexAttribute {
offset: 60,
shader_location: 7,
format: wgpu::VertexFormat::Uint32,
}, wgpu::VertexAttribute {
offset: 64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
}, wgpu::VertexAttribute {
offset: 80,
shader_location: 9,
format: wgpu::VertexFormat::Float32x4,
}, wgpu::VertexAttribute {
offset: 96,
shader_location: 10,
format: wgpu::VertexFormat::Float32,
}, wgpu::VertexAttribute {
offset: 100,
shader_location: 11,
format: wgpu::VertexFormat::Float32,
}, wgpu::VertexAttribute {
offset: 104,
shader_location: 12,
format: wgpu::VertexFormat::Uint32,
}, ],
};
let sample_count = self.sample_count;
let make = |fmt: wgpu::TextureFormat| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("polyline_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[pl_instance_layout.clone()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: fmt,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: sample_count,
..Default::default()
},
multiview: None,
cache: None,
})
};
self.polyline_bgl = Some(pl_bgl);
self.polyline_pipeline = Some(DualPipeline {
ldr: make(self.target_format),
hdr: make(wgpu::TextureFormat::Rgba16Float),
});
self.ensure_polyline_wireframe_pipeline(device);
}
pub(crate) fn ensure_polyline_wireframe_pipeline(&mut self, device: &wgpu::Device) {
if self.polyline_wireframe_pipeline.is_some() {
return;
}
let wf_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("polyline_wireframe_bgl"),
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,
}],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("polyline_wireframe_shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!(concat!(env!("OUT_DIR"), "/polyline_wireframe.wgsl")).into(),
),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("polyline_wireframe_pipeline_layout"),
bind_group_layouts: &[&self.camera_bind_group_layout, &wf_bgl],
push_constant_ranges: &[],
});
let sample_count = self.sample_count;
let make = |fmt: wgpu::TextureFormat| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("polyline_wireframe_pipeline"),
layout: Some(&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: fmt,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::LineList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: sample_count,
..Default::default()
},
multiview: None,
cache: None,
})
};
self.polyline_wireframe_bgl = Some(wf_bgl);
self.polyline_wireframe_pipeline = Some(DualPipeline {
ldr: make(self.target_format),
hdr: make(wgpu::TextureFormat::Rgba16Float),
});
}
pub(crate) fn upload_polyline_per_frame(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
item: &crate::renderer::PolylineItem,
viewport_size: [f32; 2],
) -> PolylineGpuData {
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct SegInstance {
pos_a: [f32; 3], pos_b: [f32; 3], prev_pos: [f32; 3], next_pos: [f32; 3], scalar_a: f32, scalar_b: f32, has_prev: u32, has_next: u32, colour_a: [f32; 4], colour_b: [f32; 4], radius_a: f32, radius_b: f32, use_direct_colour: u32, _pad: u32, }
let use_direct = !item.node_colours.is_empty() || !item.edge_colours.is_empty();
let use_edge_scalars = item.scalars.is_empty() && !item.edge_scalars.is_empty();
let use_node_radii = !item.node_radii.is_empty();
let mut instances: Vec<SegInstance> = Vec::new();
let positions = &item.positions;
let npos = positions.len();
let strip_ranges: Vec<(usize, usize)> = if item.strip_lengths.is_empty() {
vec![(0, npos)]
} else {
let mut ranges = Vec::with_capacity(item.strip_lengths.len());
let mut off = 0usize;
for &l in &item.strip_lengths {
ranges.push((off, off + l as usize));
off += l as usize;
}
ranges
};
let mut seg_idx_global: usize = 0;
for &(strip_start, strip_end) in &strip_ranges {
let end = strip_end.min(npos);
for i in strip_start..end.saturating_sub(1) {
let j = i + 1;
let has_prev = i > strip_start;
let has_next = j + 1 < end;
let (scalar_a, scalar_b) = if use_edge_scalars {
let s = item
.edge_scalars
.get(seg_idx_global)
.copied()
.unwrap_or(0.0);
(s, s)
} else {
(
item.scalars.get(i).copied().unwrap_or(0.0),
item.scalars.get(j).copied().unwrap_or(0.0),
)
};
let (colour_a, colour_b) = if !item.node_colours.is_empty() {
(
item.node_colours.get(i).copied().unwrap_or([1.0; 4]),
item.node_colours.get(j).copied().unwrap_or([1.0; 4]),
)
} else if !item.edge_colours.is_empty() {
let c = item
.edge_colours
.get(seg_idx_global)
.copied()
.unwrap_or([1.0; 4]);
(c, c)
} else {
([1.0; 4], [1.0; 4])
};
let (radius_a, radius_b) = if use_node_radii {
(
item.node_radii.get(i).copied().unwrap_or(item.line_width),
item.node_radii.get(j).copied().unwrap_or(item.line_width),
)
} else {
(item.line_width, item.line_width)
};
instances.push(SegInstance {
pos_a: positions[i],
pos_b: positions[j],
prev_pos: if has_prev {
positions[i - 1]
} else {
positions[i]
},
next_pos: if has_next {
positions[j + 1]
} else {
positions[j]
},
scalar_a,
scalar_b,
has_prev: has_prev as u32,
has_next: has_next as u32,
colour_a,
colour_b,
radius_a,
radius_b,
use_direct_colour: use_direct as u32,
_pad: 0,
});
seg_idx_global += 1;
}
}
let seg_count = instances.len() as u32;
let seg_bytes: &[u8] = bytemuck::cast_slice(&instances);
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("polyline_vertex_buf"),
size: seg_bytes.len().max(112) as u64,
usage: wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if !seg_bytes.is_empty() {
queue.write_buffer(&vertex_buffer, 0, seg_bytes);
}
let scalar_source: &[f32] = if !item.scalars.is_empty() {
&item.scalars
} else {
&item.edge_scalars
};
let (has_scalar, scalar_min, scalar_max) = if !scalar_source.is_empty() {
let (min, max) = item.scalar_range.unwrap_or_else(|| {
let mn = scalar_source.iter().cloned().fold(f32::INFINITY, f32::min);
let mx = scalar_source
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
(mn, mx)
});
(1u32, min, max)
} else {
(0u32, 0.0f32, 1.0f32)
};
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct PolylineUniform {
model: [[f32; 4]; 4], default_colour: [f32; 4], line_width: f32, scalar_min: f32, scalar_max: f32, has_scalar: u32, viewport_width: f32, viewport_height: f32, _pad: [f32; 2], }
let uniform_data = PolylineUniform {
model: item.model,
default_colour: item.default_colour,
line_width: item.line_width,
scalar_min,
scalar_max,
has_scalar,
viewport_width: viewport_size[0].max(1.0),
viewport_height: viewport_size[1].max(1.0),
_pad: [0.0; 2],
};
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("polyline_uniform_buf"),
size: std::mem::size_of::<PolylineUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
let lut_view = self
.builtin_colourmap_ids
.and_then(|ids| {
let preset_id = item
.colourmap_id
.unwrap_or(ids[crate::resources::BuiltinColourmap::Viridis as usize]);
self.colourmap_views.get(preset_id.0)
})
.unwrap_or(&self.fallback_lut_view);
let lut_sampler = &self.material_sampler;
let bgl = self
.polyline_bgl
.as_ref()
.expect("ensure_polyline_pipeline not called");
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("polyline_bind_group"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(lut_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(lut_sampler),
},
],
});
let wireframe_bind_group = self.polyline_wireframe_bgl.as_ref().map(|bgl| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("polyline_wireframe_bind_group"),
layout: bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: vertex_buffer.as_entire_binding(),
}],
})
});
PolylineGpuData {
vertex_buffer,
segment_count: seg_count,
bind_group,
_uniform_buf: uniform_buf,
skip_clip: false,
wireframe: false,
wireframe_bind_group,
}
}
pub fn upload_polyline(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
item: &crate::renderer::PolylineItem,
) -> crate::resources::PolylineId {
self.ensure_polyline_pipeline(device);
let gpu = self.upload_polyline_per_frame(device, queue, item, [1.0, 1.0]);
self.polyline_store.insert(gpu)
}
pub fn drop_polyline(&mut self, id: crate::resources::PolylineId) -> bool {
self.polyline_store.remove(id)
}
pub fn replace_polyline(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
id: crate::resources::PolylineId,
item: &crate::renderer::PolylineItem,
) -> bool {
if !self.polyline_store.contains(id) {
return false;
}
self.ensure_polyline_pipeline(device);
let gpu = self.upload_polyline_per_frame(device, queue, item, [1.0, 1.0]);
self.polyline_store.replace(id, gpu)
}
pub fn begin_upload_polyline(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
item: crate::renderer::PolylineItem,
) -> crate::resources::JobId {
let slot = crate::resources::ResultSlot::<crate::resources::PolylineId>::new();
let slot_for_apply = slot.clone();
let device_for_apply = device.clone();
let queue_for_apply = queue.clone();
let id = {
let mut runner = self.jobs.lock().expect("upload job runner poisoned");
runner.submit_cpu(move |progress| {
progress.set(0.9);
Ok(crate::resources::upload_jobs::JobProduct::with_apply(
Box::new(move |resources: &mut ViewportGpuResources| {
let pid =
resources.upload_polyline(&device_for_apply, &queue_for_apply, &item);
slot_for_apply.set(pid);
}),
))
})
};
self.job_polyline_results
.lock()
.expect("polyline result map poisoned")
.insert(id, slot);
id
}
pub fn upload_result_polyline(
&mut self,
id: crate::resources::JobId,
) -> crate::error::ViewportResult<crate::resources::PolylineId> {
let mut map = self
.job_polyline_results
.lock()
.expect("polyline result map poisoned");
let slot = match map.get(&id) {
Some(s) => s.clone(),
None => {
return Err(crate::error::ViewportError::JobResultMissing {
reason: "unknown id or wrong upload type",
});
}
};
match slot.take() {
Some(pid) => {
map.remove(&id);
Ok(pid)
}
None => Err(crate::error::ViewportError::JobNotReady),
}
}
pub(crate) fn ensure_polyline_no_clip_pipeline(&mut self, device: &wgpu::Device) {
if self.polyline_no_clip_pipeline.is_some() {
return;
}
self.ensure_polyline_pipeline(device);
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("polyline_no_clip_shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!(concat!(env!("OUT_DIR"), "/polyline.wgsl")).into(),
),
});
let pl_bgl = self
.polyline_bgl
.as_ref()
.expect("polyline_bgl must exist after ensure_polyline_pipeline");
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("polyline_no_clip_pipeline_layout"),
bind_group_layouts: &[&self.camera_bind_group_layout, pl_bgl],
push_constant_ranges: &[],
});
let pl_instance_layout = wgpu::VertexBufferLayout {
array_stride: 112,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 36,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 48,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 52,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 56,
shader_location: 6,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
offset: 60,
shader_location: 7,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
offset: 64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 80,
shader_location: 9,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 96,
shader_location: 10,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 100,
shader_location: 11,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 104,
shader_location: 12,
format: wgpu::VertexFormat::Uint32,
},
],
};
let sample_count = self.sample_count;
let make = |fmt: wgpu::TextureFormat| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("polyline_no_clip_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[pl_instance_layout.clone()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main_no_clip"),
targets: &[Some(wgpu::ColorTargetState {
format: fmt,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: sample_count,
..Default::default()
},
multiview: None,
cache: None,
})
};
self.polyline_no_clip_pipeline = Some(DualPipeline {
ldr: make(self.target_format),
hdr: make(wgpu::TextureFormat::Rgba16Float),
});
}
pub(crate) fn ensure_polyline_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
if self.polyline_outline_mask_pipeline.is_some() {
return;
}
self.ensure_polyline_pipeline(device);
let pl_bgl = self
.polyline_bgl
.as_ref()
.expect("polyline_bgl must exist after ensure_polyline_pipeline");
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("polyline_outline_mask_pipeline_layout"),
bind_group_layouts: &[&self.camera_bind_group_layout, pl_bgl],
push_constant_ranges: &[],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("polyline_outline_mask_shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!(concat!(env!("OUT_DIR"), "/polyline_outline_mask.wgsl")).into(),
),
});
let pl_instance_layout = wgpu::VertexBufferLayout {
array_stride: 112,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 36,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 48,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 52,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 56,
shader_location: 6,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
offset: 60,
shader_location: 7,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
offset: 64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 80,
shader_location: 9,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 96,
shader_location: 10,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 100,
shader_location: 11,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
offset: 104,
shader_location: 12,
format: wgpu::VertexFormat::Uint32,
},
],
};
self.polyline_outline_mask_pipeline = Some(device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("polyline_outline_mask_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[pl_instance_layout],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::R8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
},
));
}
}
#[cfg(test)]
mod tests {
use crate::ViewportGpuResources;
use crate::renderer::PolylineItem;
use crate::resources::UploadStatus;
fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
}
fn sample_polyline() -> PolylineItem {
PolylineItem {
positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
strip_lengths: vec![3],
..Default::default()
}
}
#[test]
fn default_model_is_identity() {
let item = PolylineItem::default();
let expected = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
assert_eq!(item.model, expected);
}
#[test]
fn upload_polyline_returns_valid_handle() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources =
ViewportGpuResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let id = resources.upload_polyline(&device, &queue, &sample_polyline());
assert!(resources.polyline_store.contains(id));
assert!(resources.drop_polyline(id));
assert!(!resources.polyline_store.contains(id));
}
#[test]
fn replace_polyline_keeps_handle_stable() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources =
ViewportGpuResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let id = resources.upload_polyline(&device, &queue, &sample_polyline());
let mut updated = sample_polyline();
updated.line_width = 5.0;
assert!(resources.replace_polyline(&device, &queue, id, &updated));
assert!(resources.polyline_store.contains(id));
}
#[test]
fn begin_upload_polyline_drains_to_handle() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources =
ViewportGpuResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let job = resources.begin_upload_polyline(&device, &queue, sample_polyline());
let err = resources.upload_result_polyline(job).unwrap_err();
assert!(matches!(err, crate::error::ViewportError::JobNotReady));
for _ in 0..200 {
resources.process_uploads(&device, &queue);
match resources.upload_status(job) {
UploadStatus::Ready => break,
UploadStatus::Failed(e) => panic!("polyline upload failed: {e:?}"),
UploadStatus::Pending { .. } => {
std::thread::sleep(std::time::Duration::from_millis(5));
}
UploadStatus::Unknown => panic!("polyline job id disappeared"),
}
}
let id = resources.upload_result_polyline(job).expect("ready result");
assert!(resources.polyline_store.contains(id));
let err = resources.upload_result_polyline(job).unwrap_err();
assert!(matches!(
err,
crate::error::ViewportError::JobResultMissing { .. }
));
}
#[test]
fn non_identity_model_is_carried_on_item() {
let m = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[3.0, 4.0, 5.0, 1.0],
];
let item = PolylineItem {
model: m,
..PolylineItem::default()
};
assert_eq!(item.model[3], [3.0, 4.0, 5.0, 1.0]);
}
}