use crate::camera::Camera;
use crate::color::{solid_color_srgb, srgb_to_linear};
use crate::scene::{RenderScene, SolidDisplay};
use crate::style::{Emphasis, EmphasisState, FaceColorMode, RenderSettings, Rgba};
use std::collections::{HashMap, HashSet};
use wgpu::util::DeviceExt;
pub const SAMPLES: u32 = 4;
pub const COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
const EDGE_NUDGE: f32 = 2e-4;
const LINE_ART_PLACED_OUTLINE_PX: f32 = 1.5;
const LINE_ART_ADDING_OUTLINE_PX: f32 = 2.5;
const LINE_ART_OUTLINE_DEPTH: f32 = 2e-3;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Globals {
view_proj: [[f32; 4]; 4],
viewport: [f32; 4],
forward: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct ModelParams {
model: [f32; 16],
normal: [f32; 16],
}
const IDENTITY4: [f32; 16] = [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];
impl ModelParams {
fn of(pose: Option<&[f32; 16]>) -> Self {
let Some(m) = pose else { return Self { model: IDENTITY4, normal: IDENTITY4 } };
let a = |row: usize, col: usize| m[col * 4 + row] as f64;
let cof = |r0: usize, r1: usize, c0: usize, c1: usize| a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0);
let det = a(0, 0) * cof(1, 2, 1, 2) - a(0, 1) * cof(1, 2, 0, 2) + a(0, 2) * cof(1, 2, 0, 1);
let mut normal = IDENTITY4;
if det.abs() > 1e-30 {
let c = [
[cof(1, 2, 1, 2), -cof(1, 2, 0, 2), cof(1, 2, 0, 1)],
[-cof(0, 2, 1, 2), cof(0, 2, 0, 2), -cof(0, 2, 0, 1)],
[cof(0, 1, 1, 2), -cof(0, 1, 0, 2), cof(0, 1, 0, 1)],
];
for row in 0..3 {
for col in 0..3 {
normal[col * 4 + row] = (c[row][col] / det) as f32;
}
}
}
Self { model: *m, normal }
}
}
struct ModelArena {
buf: wgpu::Buffer,
bind: wgpu::BindGroup,
capacity: u32,
data: Vec<u8>,
free: Vec<u32>,
next: u32,
dirty: Option<(usize, usize)>,
fresh: bool,
}
const MODEL_STRIDE: u64 = 256;
fn model_binding(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, buf: &wgpu::Buffer, label: &str) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: buf,
offset: 0,
size: std::num::NonZeroU64::new(std::mem::size_of::<ModelParams>() as u64),
}),
}],
})
}
fn model_buffer(device: &wgpu::Device, slots: u32, label: &str) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: slots as u64 * MODEL_STRIDE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
impl ModelArena {
fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, capacity: u32) -> Self {
let capacity = capacity.max(16);
let buf = model_buffer(device, capacity, "solid poses");
let bind = model_binding(device, layout, &buf, "solid poses");
Self { buf, bind, capacity, data: vec![0; capacity as usize * MODEL_STRIDE as usize], free: Vec::new(), next: 0, dirty: None, fresh: true }
}
fn alloc(&mut self) -> u32 {
self.free.pop().unwrap_or_else(|| {
self.next += 1;
self.next - 1
})
}
fn release(&mut self, slot: u32) {
self.free.push(slot);
}
fn set(&mut self, slot: u32, pose: Option<&[f32; 16]>) {
let need = (slot as usize + 1) * MODEL_STRIDE as usize;
if self.data.len() < need {
self.data.resize(need.next_power_of_two(), 0);
}
let at = slot as usize * MODEL_STRIDE as usize;
let params = ModelParams::of(pose);
let bytes = bytemuck::bytes_of(¶ms);
self.data[at..at + bytes.len()].copy_from_slice(bytes);
let (lo, hi) = self.dirty.unwrap_or((at, at + bytes.len()));
self.dirty = Some((lo.min(at), hi.max(at + bytes.len())));
}
fn offset(slot: u32) -> u32 {
(slot as u64 * MODEL_STRIDE) as u32
}
fn flush(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout) {
let slots = (self.data.len() / MODEL_STRIDE as usize) as u32;
if slots > self.capacity {
self.capacity = slots;
self.buf = model_buffer(device, slots, "solid poses");
self.bind = model_binding(device, layout, &self.buf, "solid poses");
self.fresh = true;
}
if std::mem::take(&mut self.fresh) {
queue.write_buffer(&self.buf, 0, &self.data[..self.capacity as usize * MODEL_STRIDE as usize]);
} else if let Some((lo, hi)) = self.dirty {
let hi = hi.next_multiple_of(4);
queue.write_buffer(&self.buf, lo as u64, &self.data[lo..hi]);
}
self.dirty = None;
}
}
struct IdentityModel {
_buf: wgpu::Buffer,
bind: wgpu::BindGroup,
}
impl IdentityModel {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout) -> Self {
let buf = model_buffer(device, 1, "identity pose");
queue.write_buffer(&buf, 0, bytemuck::bytes_of(&ModelParams::of(None)));
let bind = model_binding(device, layout, &buf, "identity pose");
Self { _buf: buf, bind }
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct StyleParams {
color: [f32; 4],
params: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct MeshVertex {
position: [f32; 3],
normal: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct EdgeInstance {
p0: [f32; 3],
p1: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct PointInstance {
center: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct OverlayLineInstance {
p0: [f32; 3],
p1: [f32; 3],
color: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct OverlayTriVertex {
position: [f32; 3],
normal: [f32; 3],
color: [f32; 4],
}
struct StyleBuf {
buf: wgpu::Buffer,
bind: wgpu::BindGroup,
}
impl StyleBuf {
fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, label: &str) -> Self {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: std::mem::size_of::<StyleParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buf.as_entire_binding(),
}],
});
Self { buf, bind }
}
fn write(&self, queue: &wgpu::Queue, color: Rgba, params: [f32; 4]) {
queue.write_buffer(&self.buf, 0, bytemuck::bytes_of(&StyleParams { color, params }));
}
}
struct GlobalStyles {
face_selected: StyleBuf,
face_hovered: StyleBuf,
edge_base: StyleBuf,
edge_selected: StyleBuf,
edge_hovered: StyleBuf,
edge_hidden: StyleBuf,
edge_aux: StyleBuf,
boundary: StyleBuf,
point_base: StyleBuf,
point_selected: StyleBuf,
point_hovered: StyleBuf,
axis_x: StyleBuf,
axis_y: StyleBuf,
axis_z: StyleBuf,
overlay_line: StyleBuf,
line_art_placed_face: StyleBuf,
line_art_adding_face: StyleBuf,
line_art_placed_edge: StyleBuf,
line_art_adding_edge: StyleBuf,
line_art_placed_outline: StyleBuf,
line_art_adding_outline: StyleBuf,
}
struct FaceRange {
first_index: u32,
index_count: u32,
wire_first: u32,
wire_count: u32,
style: Option<u32>,
}
#[derive(Default)]
struct EdgeHasher(u64);
impl std::hash::Hasher for EdgeHasher {
fn write(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.write_u32(u32::from(byte));
}
}
fn write_u32(&mut self, value: u32) {
self.0 = (self.0 ^ u64::from(value)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
fn finish(&self) -> u64 {
self.0 ^ (self.0 >> 32)
}
}
type BuildEdgeHasher = std::hash::BuildHasherDefault<EdgeHasher>;
struct EdgeRange {
first_instance: u32,
instance_count: u32,
}
struct GpuSolid {
revision: u64,
shape: u64,
vertex_count: u32,
vertex_buf: wgpu::Buffer,
index_buf: wgpu::Buffer,
wire_index_buf: wgpu::Buffer,
wire_index_count: u32,
faces: Vec<FaceRange>,
edge_buf: Option<wgpu::Buffer>,
edges: Vec<EdgeRange>,
edge_instances: u32,
occludable_edge_instances: u32,
point_buf: Option<wgpu::Buffer>,
point_count: u32,
base_style: StyleBuf,
face_styles: Vec<StyleBuf>,
boundary: Option<BoundaryBuf>,
model_slot: u32,
holds_shape: bool,
}
struct BoundaryBuf {
emphasis_generation: u64,
revision: u64,
buf: Option<wgpu::Buffer>,
count: u32,
}
pub const WORK_PART_CONTEXT_RGBA: [f32; 4] = [0.45, 0.55, 0.65, 0.22];
#[derive(Default)]
pub struct GpuScene {
solids: HashMap<String, GpuSolid>,
order: Vec<String>,
settings_generation: u64,
axis_buf: Option<wgpu::Buffer>,
uploads: u64,
reposes: u64,
pose_writes: u64,
models: Option<ModelArena>,
}
impl GpuScene {
pub fn upload_count(&self) -> u64 {
self.uploads
}
pub fn repose_count(&self) -> u64 {
self.reposes
}
pub fn pose_write_count(&self) -> u64 {
self.pose_writes
}
}
struct CachedTargets {
width: u32,
height: u32,
msaa_view: wgpu::TextureView,
depth_view: wgpu::TextureView,
}
pub struct FrameParams<'a> {
pub camera: &'a Camera,
pub width: u32,
pub height: u32,
pub dpr: f32,
pub settings: &'a RenderSettings,
pub emphasis: &'a Emphasis,
pub world_per_pixel: f64,
pub overlay: Option<&'a crate::widgets::WidgetOverlay>,
}
#[cfg(not(target_arch = "wasm32"))]
pub fn create_headless_device() -> Result<(wgpu::Device, wgpu::Queue, String), String> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
let want = std::env::var("BREP_RENDER_ADAPTER").ok();
let adapter = if let Some(want) = &want {
let want_lower = want.to_lowercase();
pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()))
.into_iter()
.find(|adapter| adapter.get_info().name.to_lowercase().contains(&want_lower))
.ok_or_else(|| format!("no adapter matching BREP_RENDER_ADAPTER={want}"))?
} else {
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))
.map_err(|error| format!("no wgpu adapter available: {error}"))?
};
let info = adapter.get_info();
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("brep-render"),
..Default::default()
}))
.map_err(|error| format!("wgpu device request failed: {error}"))?;
Ok((device, queue, format!("{} ({:?})", info.name, info.backend)))
}
pub struct RenderCore {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
format: wgpu::TextureFormat,
globals_buf: wgpu::Buffer,
globals_bind: wgpu::BindGroup,
style_layout: wgpu::BindGroupLayout,
model_layout: wgpu::BindGroupLayout,
identity_model: IdentityModel,
mesh_pipeline: wgpu::RenderPipeline,
hull_pipeline: wgpu::RenderPipeline,
context_pipeline: wgpu::RenderPipeline,
context_style: StyleBuf,
context_selected: StyleBuf,
context_hovered: StyleBuf,
wire_pipeline: wgpu::RenderPipeline,
edge_visible_pipeline: wgpu::RenderPipeline,
edge_hidden_pipeline: wgpu::RenderPipeline,
point_pipeline: wgpu::RenderPipeline,
overlay_tri_pipeline: wgpu::RenderPipeline,
overlay_tri_nodepth_pipeline: wgpu::RenderPipeline,
overlay_line_pipeline: wgpu::RenderPipeline,
vc_globals_buf: wgpu::Buffer,
vc_globals_bind: wgpu::BindGroup,
styles: GlobalStyles,
targets: Option<CachedTargets>,
}
#[cfg(not(target_arch = "wasm32"))]
fn encode_png(rgb: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
brep_gizmos::raster::encode_png(rgb, width, height, png::ColorType::Rgb)
}
mod pipelines;
mod scene_sync;
mod frame;