Files
OneCAD-Simulation/crates/gpu/src/view.rs
T
jyvikoandClaude Opus 5.5 248e74ca9e gpu: the viewport's pipelines move to a crate of their own (fieldcraft-gpu)
ui-egui keeps only the egui_wgpu paint callback; the shaders, batching and
pipelines are fieldcraft-gpu's view module, so an offscreen picture can draw
with the window's pipelines.

Refs #422

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 20:19:00 -04:00

555 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The viewport's GPU drawing, with no window toolkit: lit triangles and screen-space wide lines
//! with a depth buffer. The desktop viewport draws through it in an `egui_wgpu` paint callback;
//! [`crate::capture`] draws through it offscreen. Nodes are drawn as zero-length lines, which the
//! line shader turns into squares. A picture is up to three layers (the scene, the setup's
//! symbols and the overlay, the selection highlight), each uploaded on its own: the overlay's
//! faces pass the depth test over the faces they repeat. Every layer's `on_top` lines are drawn
//! last, with no depth test. Translucent faces (a datum plane's fill) are drawn after every
//! opaque face, blended, depth-tested without writing depth, as the CPU renderer draws them.
//!
//! A large mesh does not fit one vertex buffer (wgpu's default `max_buffer_size` is 256 MiB):
//! geometry is split into batches of at most [`MAX_BATCH_BYTES`] and the device's limit, each a
//! whole number of primitives, and every batch is drawn.
//!
//! Adapted from SolidCraft's `crates/ui-egui/src/gpu.rs` (MIT OR Apache-2.0), see
//! ATTRIBUTION.md.
use std::ops::Range;
use std::sync::{Arc, Mutex};
use fieldcraft_render::{Rgb, Scene};
/// Triangle vertex: position (3 × f32), normal (3 × f32), sRGB colour (4 × u8).
pub const TRI_SIZE: usize = 28;
/// Line instance: a, b (3 × f32 each), colour (4 × u8), width in physical pixels (f32).
pub const LINE_SIZE: usize = 32;
const UNIFORM_SIZE: u64 = 112;
/// Largest vertex buffer the viewport creates, whatever the device allows.
pub const MAX_BATCH_BYTES: u64 = 128 << 20;
/// The last error the GPU device reported, waiting for the UI to show it.
pub type GpuErrors = Arc<Mutex<Option<String>>>;
/// Scene geometry in the GPU's vertex formats, waiting to be uploaded.
#[derive(Default, Clone, Debug, PartialEq)]
pub struct GpuScene {
pub tris: Vec<u8>,
/// Translucent triangles ([`Scene::fills`]), in the triangle format with their alpha.
pub fills: Vec<u8>,
pub lines: Vec<u8>,
/// Lines drawn over everything ([`Scene::on_top`]).
pub top_lines: Vec<u8>,
}
impl GpuScene {
/// The scene's triangles, lines and points; widths and sizes scaled by `pixels_per_point`
/// (the scene gives them in points).
pub fn from_scene(scene: &Scene, pixels_per_point: f32) -> GpuScene {
let mut g = GpuScene { tris: Vec::with_capacity(scene.tris.len() * 3 * TRI_SIZE), ..GpuScene::default() };
for t in &scene.tris {
for p in t.p {
push_vertex(&mut g.tris, p.to_f32(), t.normal.to_f32(), t.color, 255);
}
}
for f in &scene.fills {
let alpha = if f.alpha.is_finite() { (f.alpha.clamp(0.0, 1.0) * 255.0).round() as u8 } else { 0 };
for p in f.tri.p {
push_vertex(&mut g.fills, p.to_f32(), f.tri.normal.to_f32(), f.tri.color, alpha);
}
}
for l in &scene.lines {
g.line(l.a.to_f32(), l.b.to_f32(), l.color, l.width as f32 * pixels_per_point);
}
for p in &scene.points {
g.line(p.p.to_f32(), p.p.to_f32(), p.color, p.size as f32 * pixels_per_point);
}
for l in &scene.on_top {
push_line(&mut g.top_lines, l.a.to_f32(), l.b.to_f32(), l.color, l.width as f32 * pixels_per_point);
}
g
}
fn line(&mut self, a: [f32; 3], b: [f32; 3], c: Rgb, width: f32) {
push_line(&mut self.lines, a, b, c, width);
}
}
/// A triangle vertex into `buf`.
fn push_vertex(buf: &mut Vec<u8>, p: [f32; 3], n: [f32; 3], c: Rgb, alpha: u8) {
for v in p.iter().chain(&n) {
buf.extend_from_slice(&v.to_le_bytes());
}
buf.extend_from_slice(&[c.0, c.1, c.2, alpha]);
}
/// A line instance into `buf`.
fn push_line(buf: &mut Vec<u8>, a: [f32; 3], b: [f32; 3], c: Rgb, width: f32) {
for v in a.iter().chain(&b) {
buf.extend_from_slice(&v.to_le_bytes());
}
buf.extend_from_slice(&[c.0, c.1, c.2, 255]);
buf.extend_from_slice(&width.to_le_bytes());
}
/// One frame's camera.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Frame {
/// Column-major view-projection.
pub view_proj: [[f32; 4]; 4],
/// Direction toward the eye (for lighting).
pub back: [f32; 3],
/// Eye position and 1 for a perspective camera (0: orthographic).
pub eye: [f32; 4],
/// Viewport size in physical pixels.
pub size_px: [f32; 2],
}
impl Frame {
/// The frame of camera `cam` over a `w` × `h` pixel viewport, clipping at `radius` around
/// its target (the scene's [`Scene::radius`]).
pub fn of(cam: &fieldcraft_render::Camera, w: f32, h: f32, radius: f64) -> Frame {
let aspect = f64::from(w) / f64::from(h).max(1.0);
let eye = cam.eye();
Frame {
view_proj: cam.view_proj(aspect, radius).to_f32(),
back: cam.back().to_f32(),
eye: [eye.x as f32, eye.y as f32, eye.z as f32, if cam.fov > 0.0 { 1.0 } else { 0.0 }],
size_px: [w, h],
}
}
}
struct Batch {
buffer: wgpu::Buffer,
count: u32,
}
/// Uploaded geometry: triangle and line batches.
#[derive(Default)]
pub struct Layer {
tris: Vec<Batch>,
fills: Vec<Batch>,
lines: Vec<Batch>,
top_lines: Vec<Batch>,
}
impl Layer {
/// Replace the layer's geometry with `sc`. The old buffers go first, so the new ones don't
/// have to fit beside them.
pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, sc: &GpuScene) {
*self = Layer::default();
let max = device.limits().max_buffer_size;
self.tris = upload(device, queue, "fc_tris", &sc.tris, TRI_SIZE, 3, max);
self.fills = upload(device, queue, "fc_fills", &sc.fills, TRI_SIZE, 3, max);
self.lines = upload(device, queue, "fc_lines", &sc.lines, LINE_SIZE, 1, max);
self.top_lines = upload(device, queue, "fc_top_lines", &sc.top_lines, LINE_SIZE, 1, max);
}
}
fn paint_tri_batches(batches: &[Batch], pass: &mut wgpu::RenderPass<'_>, tri: &wgpu::RenderPipeline) {
if !batches.is_empty() {
pass.set_pipeline(tri);
for b in batches {
pass.set_vertex_buffer(0, b.buffer.slice(..));
pass.draw(0..b.count, 0..1);
}
}
}
fn paint_line_batches(batches: &[Batch], pass: &mut wgpu::RenderPass<'_>, line: &wgpu::RenderPipeline) {
if !batches.is_empty() {
pass.set_pipeline(line);
for b in batches {
pass.set_vertex_buffer(0, b.buffer.slice(..));
pass.draw(0..6, 0..b.count);
}
}
}
/// The viewport's pipelines and camera uniform, for one target configuration.
pub struct Pipelines {
tri: wgpu::RenderPipeline,
/// The overlay's faces: pulled toward the eye and depth-tested without writing depth.
overlay_tri: wgpu::RenderPipeline,
/// Translucent faces: blended, depth-tested without writing depth.
fill: wgpu::RenderPipeline,
line: wgpu::RenderPipeline,
/// Lines over everything: no depth test.
top_line: wgpu::RenderPipeline,
uniform: wgpu::Buffer,
bind: wgpu::BindGroup,
linear_out: bool,
}
pub(crate) const SHADER: &str = r#"
struct U {
vp: mat4x4<f32>,
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
screen: vec4<f32>, // viewport width, height (px), depth bias
eye: vec4<f32>, // eye position, w = 1 for perspective
};
@group(0) @binding(0) var<uniform> u: U;
fn to_linear(c: vec3<f32>) -> vec3<f32> {
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
}
fn out_color(c: vec4<f32>) -> vec4<f32> {
if (u.back.w > 0.5) {
return vec4<f32>(to_linear(c.rgb), c.a);
}
return c;
}
struct TOut {
@builtin(position) pos: vec4<f32>,
@location(0) n: vec3<f32>,
@location(1) c: vec4<f32>,
};
@vertex
fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
var o: TOut;
o.pos = u.vp * vec4<f32>(p, 1.0);
o.n = n;
o.c = c;
return o;
}
/// Two-sided lighting: a key light above the eye, a fill from the eye and a small highlight
/// (fieldcraft_render::shade computes the same on the CPU).
@fragment
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
let v = normalize(u.back.xyz);
var n = normalize(i.n);
if (dot(n, v) < 0.0) { n = -n; }
let key = normalize(v * 0.8 + vec3<f32>(-0.3, 0.2, 0.6));
let diff = max(dot(n, key), 0.0);
let fill = max(dot(n, v), 0.0);
let h = normalize(key + v);
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
let k = 0.42 + 0.38 * diff + 0.25 * fill;
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
}
/// Clip-space z of `p` (clipped to `c`) pulled `s` depth biases toward the eye, so edges and
/// nodes on a face win the depth test against it.
fn biased_z(p: vec3<f32>, c: vec4<f32>, s: f32) -> f32 {
if (u.eye.w > 0.5) {
let q = u.vp * vec4<f32>(mix(p, u.eye.xyz, u.screen.z * 8.0 * s), 1.0);
return q.z / max(q.w, 1e-6) * c.w;
}
return c.z - u.screen.z * s * c.w;
}
/// An overlay face: as `vs_tri`, half a depth bias toward the eye, so it covers the scene face it
/// repeats (or the contour bands that split it).
@vertex
fn vs_overlay_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
var o: TOut;
let clip = u.vp * vec4<f32>(p, 1.0);
o.pos = vec4<f32>(clip.xy, biased_z(p, clip, 0.5), clip.w);
o.n = n;
o.c = c;
return o;
}
struct LOut {
@builtin(position) pos: vec4<f32>,
@location(0) c: vec4<f32>,
};
@vertex
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) w: f32) -> LOut {
var pa = a;
var pb = b;
var ca = u.vp * vec4<f32>(a, 1.0);
var cb = u.vp * vec4<f32>(b, 1.0);
// An end behind the near plane has no screen point: cut the segment at the plane.
if (ca.z < 0.0 && cb.z > 0.0) {
let k = ca.z / (ca.z - cb.z);
ca = mix(ca, cb, k);
pa = mix(a, b, k);
} else if (cb.z < 0.0 && ca.z > 0.0) {
let k = cb.z / (cb.z - ca.z);
cb = mix(cb, ca, k);
pb = mix(b, a, k);
}
let half = u.screen.xy * 0.5;
let sa = ca.xy / max(ca.w, 1e-6) * half;
let sb = cb.xy / max(cb.w, 1e-6) * half;
var dir = sb - sa;
let len = length(dir);
// A zero-length line (a node) becomes a square `w` pixels across.
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
let nrm = vec2<f32>(-dir.y, dir.x);
// Two triangles: corners (t, side).
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
var ss = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
let t = ts[vi];
let s = ss[vi];
let base = select(ca, cb, t > 0.5);
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
var o: LOut;
o.pos = vec4<f32>(base.xy + off / half * base.w, biased_z(select(pa, pb, t > 0.5), base, 1.0), base.w);
o.c = c;
return o;
}
@fragment
fn fs_line(i: LOut) -> @location(0) vec4<f32> {
return out_color(i.c);
}
"#;
fn tri_layout() -> wgpu::VertexBufferLayout<'static> {
const A: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Unorm8x4];
wgpu::VertexBufferLayout { array_stride: TRI_SIZE as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &A }
}
fn line_layout() -> wgpu::VertexBufferLayout<'static> {
const A: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Unorm8x4, 3 => Float32];
wgpu::VertexBufferLayout { array_stride: LINE_SIZE as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &A }
}
/// The target configuration: colour format, depth format and MSAA sample count.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GpuTarget {
pub format: wgpu::TextureFormat,
pub depth: Option<wgpu::TextureFormat>,
pub samples: u32,
}
impl Pipelines {
/// The pipelines for target `t`.
pub fn new(device: &wgpu::Device, t: GpuTarget) -> Self {
let module =
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("fc_viewport"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("fc_viewport"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
count: None,
}],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("fc_viewport"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("fc_viewport_u"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("fc_viewport"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
let depth = |compare: wgpu::CompareFunction, write: bool| -> Option<wgpu::DepthStencilState> {
t.depth.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: Some(write),
depth_compare: Some(compare),
stencil: Default::default(),
bias: Default::default(),
})
};
let pipeline = |label: &str,
vs: &str,
fs: &str,
buf: wgpu::VertexBufferLayout<'static>,
ds: Option<wgpu::DepthStencilState>,
blend: Option<wgpu::BlendState>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState { module: &module, entry_point: Some(vs), buffers: &[Some(buf)], compilation_options: Default::default() },
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: ds,
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(fs),
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend, write_mask: wgpu::ColorWrites::ALL })],
compilation_options: Default::default(),
}),
multiview_mask: None,
cache: None,
})
};
Pipelines {
tri: pipeline("fc_tris", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Less, true), None),
overlay_tri: pipeline("fc_overlay_tris", "vs_overlay_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), None),
fill: pipeline(
"fc_fills",
"vs_tri",
"fs_tri",
tri_layout(),
depth(wgpu::CompareFunction::Less, false),
Some(wgpu::BlendState::ALPHA_BLENDING),
),
line: pipeline(
"fc_lines",
"vs_line",
"fs_line",
line_layout(),
depth(wgpu::CompareFunction::LessEqual, false),
Some(wgpu::BlendState::ALPHA_BLENDING),
),
top_line: pipeline(
"fc_top_lines",
"vs_line",
"fs_line",
line_layout(),
depth(wgpu::CompareFunction::Always, false),
Some(wgpu::BlendState::ALPHA_BLENDING),
),
uniform,
bind,
linear_out: t.format.is_srgb(),
}
}
/// Write this frame's camera.
pub fn write_frame(&self, queue: &wgpu::Queue, frame: &Frame) {
queue.write_buffer(&self.uniform, 0, &uniform_bytes(frame, self.linear_out));
}
/// Draw the scene, the symbols and the overlay into `pass`: faces first, the overlay's over
/// the scene's, then the translucent faces, then every line, so the mesh edges stay on
/// highlighted faces, then the lines over everything (as the CPU renderer draws them).
pub fn paint(&self, pass: &mut wgpu::RenderPass<'_>, scene: &Layer, symbols: &Layer, overlay: &Layer) {
pass.set_bind_group(0, &self.bind, &[]);
paint_tri_batches(&scene.tris, pass, &self.tri);
paint_tri_batches(&symbols.tris, pass, &self.tri);
paint_tri_batches(&overlay.tris, pass, &self.overlay_tri);
let layers = [scene, symbols, overlay];
for layer in layers {
paint_tri_batches(&layer.fills, pass, &self.fill);
}
for layer in layers {
paint_line_batches(&layer.lines, pass, &self.line);
}
for layer in layers {
paint_line_batches(&layer.top_lines, pass, &self.top_line);
}
}
}
/// Byte ranges of `len` bytes of vertex data, `stride` bytes per vertex and `per_primitive`
/// vertices per primitive, for buffers of at most `max_buffer` bytes (and [`MAX_BATCH_BYTES`]).
/// Each range holds whole primitives; a trailing partial primitive is dropped. Empty when not
/// even one primitive fits.
pub fn batch_ranges(len: usize, stride: usize, per_primitive: usize, max_buffer: u64) -> Vec<Range<usize>> {
let Some(unit) = stride.checked_mul(per_primitive).filter(|&u| u > 0) else { return Vec::new() };
let limit = usize::try_from(max_buffer.min(MAX_BATCH_BYTES)).unwrap_or(usize::MAX);
let chunk = limit / unit * unit;
if chunk == 0 {
return Vec::new();
}
let end = len / unit * unit;
(0..end).step_by(chunk).map(|start| start..end.min(start.saturating_add(chunk))).collect()
}
/// Upload vertex data as batches (see [`batch_ranges`]); a failed allocation is reported to the
/// device's error handler, not a panic.
fn upload(device: &wgpu::Device, queue: &wgpu::Queue, label: &str, bytes: &[u8], stride: usize, per_primitive: usize, max_buffer: u64) -> Vec<Batch> {
batch_ranges(bytes.len(), stride, per_primitive, max_buffer)
.into_iter()
.filter_map(|r| {
let data = bytes.get(r)?;
let count = u32::try_from(data.len() / stride).ok()?;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: data.len() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, data);
Some(Batch { buffer, count })
})
.collect()
}
/// Uniform bytes for a frame.
fn uniform_bytes(f: &Frame, linear: bool) -> Vec<u8> {
let mut v: Vec<f32> = f.view_proj.iter().flatten().copied().collect();
v.extend([f.back[0], f.back[1], f.back[2], if linear { 1.0 } else { 0.0 }]);
v.extend([f.size_px[0].max(1.0), f.size_px[1].max(1.0), 2e-4, 0.0]);
v.extend(f.eye);
v.iter().flat_map(|x| x.to_le_bytes()).collect()
}
#[cfg(test)]
mod tests {
use fieldcraft_render::{SceneLine, ScenePoint, SceneTri, Vec3};
use wgpu::naga;
use super::*;
/// The viewport shader parses and validates (a bad shader would only fail at run time).
#[test]
fn shader_is_valid() {
let module = naga::front::wgsl::parse_str(SHADER).unwrap_or_else(|e| panic!("{}", e.emit_to_string(SHADER)));
naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty()).validate(&module).unwrap();
}
#[test]
fn batches_hold_whole_primitives_within_the_buffer_limit() {
let tri = TRI_SIZE * 3;
// Small scenes are one batch; nothing, or less than one primitive, is no batch.
assert_eq!(batch_ranges(10 * tri, TRI_SIZE, 3, 256 << 20), vec![0..10 * tri]);
assert!(batch_ranges(0, TRI_SIZE, 3, 256 << 20).is_empty());
assert!(batch_ranges(tri - 1, TRI_SIZE, 3, 256 << 20).is_empty());
// A trailing partial triangle is dropped.
assert_eq!(batch_ranges(2 * tri + 5, TRI_SIZE, 3, 256 << 20), vec![0..2 * tri]);
// A buffer limit smaller than one primitive, or a zero stride, gives nothing.
assert!(batch_ranges(10 * tri, TRI_SIZE, 3, (tri - 1) as u64).is_empty());
assert!(batch_ranges(10 * tri, 0, 3, 256 << 20).is_empty());
assert!(batch_ranges(usize::MAX, usize::MAX, 3, u64::MAX).is_empty());
// Ten triangles under a limit of four: 4 + 4 + 2.
let r = batch_ranges(10 * tri, TRI_SIZE, 3, (4 * tri + 7) as u64);
assert_eq!(r, vec![0..4 * tri, 4 * tri..8 * tri, 8 * tri..10 * tri]);
// A few million triangles under the 256 MiB default: every batch is within
// MAX_BATCH_BYTES, a whole number of triangles, and together they cover the data.
for (len, stride, per, max) in
[(5_000_000 * tri, TRI_SIZE, 3, 256u64 << 20), (9_000_000 * LINE_SIZE, LINE_SIZE, 1, u64::MAX), (777 * tri, TRI_SIZE, 3, 1000)]
{
let r = batch_ranges(len, stride, per, max);
assert!(r.len() > 1, "{len}");
let mut at = 0;
for b in &r {
assert_eq!(b.start, at);
assert!(b.len() as u64 <= max.min(MAX_BATCH_BYTES) && b.len() % (stride * per) == 0 && !b.is_empty());
at = b.end;
}
assert_eq!(at, len / (stride * per) * (stride * per));
}
}
#[test]
fn scene_bytes_match_the_vertex_layouts() {
let scene = Scene {
tris: vec![SceneTri { p: [Vec3::ZERO, Vec3::X, Vec3::Y], normal: Vec3::Z, color: Rgb(1, 2, 3) }],
lines: vec![SceneLine { a: Vec3::ZERO, b: Vec3::Z, color: Rgb(4, 5, 6), width: 1.5 }],
points: vec![ScenePoint { p: Vec3::X, color: Rgb(7, 8, 9), size: 4.0 }],
on_top: vec![SceneLine { a: Vec3::ZERO, b: Vec3::X, color: Rgb(1, 1, 1), width: 2.0 }],
..Default::default()
};
let g = GpuScene::from_scene(&scene, 2.0);
assert_eq!(g.tris.len(), 3 * TRI_SIZE);
assert_eq!(g.lines.len(), 2 * LINE_SIZE);
assert_eq!(g.top_lines.len(), LINE_SIZE);
assert_eq!(&g.tris[24..28], &[1, 2, 3, 255]);
// Widths are in physical pixels.
assert_eq!(f32::from_le_bytes([g.lines[28], g.lines[29], g.lines[30], g.lines[31]]), 3.0);
assert_eq!(f32::from_le_bytes([g.lines[60], g.lines[61], g.lines[62], g.lines[63]]), 8.0);
let f = Frame { view_proj: [[0.0; 4]; 4], back: [0.0; 3], eye: [0.0; 4], size_px: [1.0, 1.0] };
assert_eq!(uniform_bytes(&f, true).len() as u64, UNIFORM_SIZE);
}
}