diff --git a/Cargo.toml b/Cargo.toml index e580050..648525c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,10 @@ members = [ "layout21raw", "layout21tetris", "layout21utils", + "layout21wgpu", "lef21", ] +resolver = "2" # Inherited Package Attributes # Thanks https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table! diff --git a/layout21raw/src/geom.rs b/layout21raw/src/geom.rs index 841e3a2..a0b95ae 100644 --- a/layout21raw/src/geom.rs +++ b/layout21raw/src/geom.rs @@ -100,6 +100,32 @@ pub struct Path { pub points: Vec, pub width: usize, } +impl Path { + /// Convert a Manhattan path into a vector of rectangles. + /// Returns `None` if the path is not Manhattan. + pub fn rects(&self) -> Option> { + let (points, width) = (&self.points, self.width); + let width = Int::try_from(width).unwrap(); // FIXME: probably store these signed, check them on creation + let mut rects: Vec = Vec::with_capacity(self.points.len()); + for k in 0..points.len() - 1 { + let rect = if points[k].x == points[k + 1].x { + Rect { + p0: Point::new(points[k].x - width / 2, points[k].y), + p1: Point::new(points[k].x + width / 2, points[k + 1].y), + } + } else if points[k].y == points[k + 1].y { + Rect { + p0: Point::new(points[k].x, points[k].y - width / 2), + p1: Point::new(points[k + 1].x, points[k].y + width / 2), + } + } else { + return None; // Non-Manhattan Path + }; + rects.push(rect); + } + Some(rects) + } +} /// # Polygon /// /// Closed n-sided polygon with arbitrary number of vertices. @@ -314,33 +340,15 @@ impl ShapeTrait for Path { /// Boolean indication of whether the [Shape] contains [Point] `pt`. /// Containment is *inclusive* for all [Shape] types. /// [Point]s on their boundary, which generally include all points specifying the shape itself, are regarded throughout as "inside" the shape. + /// + /// Note checks for [`Path`] are valid solely for Manhattan paths, i.e. those with segments solely running vertically or horizontally. fn contains(&self, pt: &Point) -> bool { - // Break into segments, and check for intersection with each - // Probably not the most efficient way to do this, but a start. - // Only "Manhattan paths", i.e. those with segments solely running vertically or horizontally, are supported. // FIXME: even with this method, there are some small pieces at corners which we'll miss. // Whether these are relevant in real life, tbd. - let (points, width) = (&self.points, self.width); - let width = Int::try_from(width).unwrap(); // FIXME: probably store these signed, check them on creation - for k in 0..points.len() - 1 { - let rect = if points[k].x == points[k + 1].x { - Rect { - p0: Point::new(points[k].x - width / 2, points[k].y), - p1: Point::new(points[k].x + width / 2, points[k + 1].y), - } - } else if points[k].y == points[k + 1].y { - Rect { - p0: Point::new(points[k].x, points[k].y - width / 2), - p1: Point::new(points[k + 1].x, points[k].y + width / 2), - } - } else { - unimplemented!("Unsupported Non-Manhattan Path") - }; - if rect.contains(pt) { - return true; - } + match self.rects() { + None => false, // FIXME! non-Manhattan paths + Some(rects) => rects.iter().any(|r| r.contains(pt)), } - false } fn to_poly(&self) -> Polygon { unimplemented!("Path::to_poly") diff --git a/layout21wgpu/Cargo.toml b/layout21wgpu/Cargo.toml new file mode 100644 index 0000000..1f0d484 --- /dev/null +++ b/layout21wgpu/Cargo.toml @@ -0,0 +1,41 @@ +[package] +description = "Layout21 WGPU" +name = "layout21wgpu" + +# Shared layout21 attributes +authors.workspace = true +categories.workspace = true +documentation.workspace = true +edition.workspace = true +exclude.workspace = true +homepage.workspace = true +include.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true +workspace = "../" + +[dependencies] +# Local workspace dependencies +layout21protos = {path = "../layout21protos", version = "3.0.0-pre.3"} +layout21raw = {path = "../layout21raw", version = "3.0.0-pre.3"} +layout21utils = {path = "../layout21utils", version = "3.0.0-pre.3"} + +# Crates.io +wgpu = "0.15" +derive_builder = "0.9" +derive_more = "0.99.16" +num-integer = "0.1" +num-traits = "0.2" +serde = {version = "1.0", features = ["derive"]} +serde_derive = "1.0.88" +slotmap = {version = "1.0", features = ["serde"]} +winit = "0.27.5" +rand = "0.8.5" +log = "0.4.17" +bytemuck = { version = "1.4", features = ["derive"] } +env_logger = "0.10.0" +pollster = "0.2.5" +lyon = "1.0.1" diff --git a/layout21wgpu/src/buffers.rs b/layout21wgpu/src/buffers.rs new file mode 100644 index 0000000..aec0fb7 --- /dev/null +++ b/layout21wgpu/src/buffers.rs @@ -0,0 +1,5 @@ + +use lyon::tessellation::geometry_builder::VertexBuffers; +use crate::Vertex; + +pub type Buffers = VertexBuffers; diff --git a/layout21wgpu/src/color.rs b/layout21wgpu/src/color.rs new file mode 100644 index 0000000..c358330 --- /dev/null +++ b/layout21wgpu/src/color.rs @@ -0,0 +1,29 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Color(pub [f32; 3]); + +pub const COLORS: [Color; 7] = [ + Color([1.0, 0.0, 0.0]), // red + Color([0.0, 1.0, 0.0]), // green + Color([0.0, 0.0, 1.0]), // blue + Color([1.0, 1.0, 0.0]), // + Color([1.0, 0.0, 1.0]), // + Color([0.0, 1.0, 1.0]), // + Color([1.0, 1.0, 1.0]), // white +]; +#[derive(Debug)] +pub(crate) struct ColorWheel { + index: usize, +} +impl ColorWheel { + pub fn new() -> Self { + Self { index: 0 } + } + pub fn next(&mut self) -> Color { + let color = COLORS[self.index]; + self.index = (self.index + 1) % COLORS.len(); + color + } +} diff --git a/layout21wgpu/src/gpu.rs b/layout21wgpu/src/gpu.rs new file mode 100644 index 0000000..ce5ee78 --- /dev/null +++ b/layout21wgpu/src/gpu.rs @@ -0,0 +1,194 @@ +//! +//! # GPU Stuff +//! +//! All the WGPU machinery lives here. +//! Many, many terms of art fly around GPU world; get used to it. +//! + +use wgpu::util::DeviceExt; +use winit::window::Window; + +// Local Imports +use crate::{Buffers, Vertex}; + +/// # GPU Stuff +/// +/// *Microooo-processors*. Idunno what they are, you dunno what they are, CASH. +/// +pub struct GpuStuff { + pub surface: wgpu::Surface, + pub device: wgpu::Device, + pub queue: wgpu::Queue, + pub config: wgpu::SurfaceConfiguration, + pub size: winit::dpi::PhysicalSize, + pub pipeline: wgpu::RenderPipeline, + pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, +} +impl GpuStuff { + /// Create new [`GpuStuff`]. + /// + /// Much of the machinery and terminology lives here in the configuration phase. + /// Once this gets done, things really do calm down to just the vertex and index buffers, + /// and writing triangles to them. + /// + pub async fn new(window: &Window, buffers: &Buffers) -> Self { + let size = window.inner_size(); + let backends = wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all); + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + }); + let surface = unsafe { instance.create_surface(window) }.unwrap(); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + }, + None, // Trace path + ) + .await + .unwrap(); + let swapchain_capabilities = surface.get_capabilities(&adapter); + let swapchain_format = swapchain_capabilities.formats[0]; + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: Vec::new(), + }; + surface.configure(&device, &config); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(&buffers.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents: bytemuck::cast_slice(&buffers.indices), + usage: wgpu::BufferUsages::INDEX, + }); + + Self { + surface, + device, + queue, + config, + size, + pipeline, + vertex_buffer, + index_buffer, + } + } + + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { + if new_size.width > 0 && new_size.height > 0 { + self.size = new_size; + self.config.width = new_size.width; + self.config.height = new_size.height; + self.surface.configure(&self.device, &self.config); + } + } + + /// Render the current frame + pub fn render(&self, buffers: &Buffers) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.2, + g: 0.247, + b: 0.314, + a: 1.0, + }), + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed( + 0..buffers.indices.len() as u32, // indices + 0, // base_vertex + 0..1, // instances + ); + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } +} diff --git a/layout21wgpu/src/layout.rs b/layout21wgpu/src/layout.rs new file mode 100644 index 0000000..faafca3 --- /dev/null +++ b/layout21wgpu/src/layout.rs @@ -0,0 +1,221 @@ +use std::collections::HashMap; + +use lyon::{ + geom::Box2D, + math::{point, Transform}, + path::polygon::Polygon, + tessellation::{ + geometry_builder::{BuffersBuilder, VertexBuffers}, + FillOptions, FillTessellator, FillVertex, FillVertexConstructor, + }, +}; + +use layout21protos::{self, conv as proto_converters}; +use layout21raw as raw; +use layout21utils::Ptr; + +// Local imports +use crate::{Color, ColorWheel, Vertex}; + +/// +/// # Tessellate `layout_display` to triangles ready to render on the GPU. +/// +pub fn tessellate(layout_display: &LayoutDisplay, size: &Size) -> VertexBuffers { + let cell1 = layout_display.cell.read().unwrap(); + let layout = cell1.layout.as_ref().unwrap(); + let bbox = &layout_display.bbox; + let layer_colors = &layout_display.layer_colors; + + let screen_transform = fit(&bbox, size); + let transform = &screen_transform.transform; + + let mut tessellator = FillTessellator::new(); + let mut buffers: VertexBuffers = VertexBuffers::new(); + + // Closures to convert between lyon and layout21 shapes, applying the transform + let get_lyon_point = |p: &raw::Point| transform.transform_point(point(p.x as f32, p.y as f32)); + let get_lyon_rect = |r: &raw::Rect| Box2D::new(get_lyon_point(&r.p0), get_lyon_point(&r.p1)); + + for elem in &layout.elems { + let color = layer_colors.get(&elem.layer).unwrap().clone(); + + let shape = &elem.inner; + match shape { + raw::Shape::Rect(r) => { + tessellator + .tessellate_rectangle( + &get_lyon_rect(r), + &FillOptions::DEFAULT, + &mut BuffersBuilder::new(&mut buffers, WithColor::new(color)), + ) + .unwrap(); + } + raw::Shape::Path(p) => { + // Operate on manhattan paths only, for now + // If the path is non-manhattan, we'll just skip it + let path_rects = p.rects(); + if path_rects.is_none() { + continue; + } + let path_rects = path_rects.unwrap(); + + // Tessellate each rectangle in the path + for r in path_rects.iter() { + let lyon_rect = get_lyon_rect(r); + tessellator + .tessellate_rectangle( + &lyon_rect, + &FillOptions::DEFAULT, + &mut BuffersBuilder::new(&mut buffers, WithColor::new(color)), + ) + .unwrap(); + } + } + raw::Shape::Polygon(p) => { + let points: Vec<_> = p.points.iter().map(|p| get_lyon_point(p)).collect(); + let lyon_polygon: Polygon<_> = Polygon { + points: &points, + closed: true, + }; + tessellator + .tessellate_polygon( + lyon_polygon, + &FillOptions::DEFAULT, + &mut BuffersBuilder::new(&mut buffers, WithColor::new(color)), + ) + .unwrap(); + } + } + } + + buffers +} + +/// Screen/ window size +#[derive(Debug)] +pub struct Size { + pub width: T, + pub height: T, +} +/// Get the transform to fit the bounding box in the screen +fn fit(bbox: &raw::BoundBox, size: &Size) -> ScreenTransformState { + let xspan = (bbox.p1.x - bbox.p0.x) as f64; + let yspan = (bbox.p1.y - bbox.p0.y) as f64; + + // Sort out which dimension to scale to + let zoom = if yspan * size.width as f64 > xspan * size.height as f64 { + 2.0 / yspan // scale to height + } else { + 2.0 / xspan // scale to width + }; + let zoom = (0.9 * zoom) as f32; // leave a bit of padding + + // Get the center of the bounding box + let xmid = (bbox.p1.x + bbox.p0.x) as f32 / 2.0; + let ymid = (bbox.p1.y + bbox.p0.y) as f32 / 2.0; + + // Provide a panning coordinate which scales/ zooms it into GPU coordinates + let pan = (-zoom * xmid, -zoom * ymid); + + ScreenTransformState::new(zoom, pan) +} + +/// The state of zooming and panning the screen +#[derive(Debug)] +pub struct ScreenTransformState { + pub zoom: f32, + pub pan: (f32, f32), + pub transform: Transform, +} +impl ScreenTransformState { + pub fn new(zoom: f32, pan: (f32, f32)) -> Self { + Self { + zoom, + pan, + transform: Transform::identity() + .pre_translate((pan.0, pan.1).into()) + .pre_scale(zoom, zoom), + } + } + pub fn identity() -> Self { + Self { + zoom: 1.0, + pan: (0.0, 0.0), + transform: Transform::identity(), + } + } + pub fn update(&mut self, zoom: f32, pan: (f32, f32)) { + self.zoom = zoom; + self.pan = pan; + self.transform = Transform::identity() + .pre_translate((pan.0, pan.1).into()) + .pre_scale(zoom, zoom); + } +} + +#[derive(Debug)] +pub struct LayoutDisplay { + // Source layout data + pub lib: raw::Library, + pub cell: Ptr, + + // Derived at load time + pub bbox: raw::BoundBox, + pub layer_colors: HashMap, +} +impl LayoutDisplay { + pub fn from_proto() -> Self { + let proto_lib: layout21protos::Library = + proto_converters::open(&resource("sky130_fd_sc_hd__dfxtp_1.pb")).unwrap(); + let rawlib = raw::Library::from_proto(proto_lib, None).unwrap(); + let cell = rawlib.cells[0].clone(); + Self::build(rawlib, cell) + } + pub fn build(rawlib: raw::Library, cell: Ptr) -> Self { + let cell1 = cell.read().unwrap(); + let layout = cell1.layout.as_ref().unwrap(); + + let bbox: raw::BoundBox = layout.bbox(); + + let mut layer_colors: HashMap = HashMap::new(); + let mut color_wheel = ColorWheel::new(); + for elem in &layout.elems { + layer_colors + .entry(elem.layer) + .or_insert_with(|| color_wheel.next()); + } + + Self { + lib: rawlib, + cell: cell.clone(), + bbox, + layer_colors, + } + } +} + +pub struct WithColor { + pub color: Color, +} +impl WithColor { + fn new(color: Color) -> Self { + Self { color } + } +} +impl FillVertexConstructor for WithColor { + fn new_vertex(&mut self, vertex: FillVertex) -> Vertex { + Vertex { + position: vertex.position().to_array(), + color: self.color.clone(), + } + } +} + +/// Grab the full path of resource-file `fname` +fn resource(rname: &str) -> String { + format!( + "{}/../layout21converters/resources/{}", + env!("CARGO_MANIFEST_DIR"), + rname + ) +} diff --git a/layout21wgpu/src/lib.rs b/layout21wgpu/src/lib.rs new file mode 100644 index 0000000..49abf67 --- /dev/null +++ b/layout21wgpu/src/lib.rs @@ -0,0 +1,23 @@ +//! +//! # Layout21 WGPU +//! + +// Internal modules +mod color; +use crate::color::{Color, ColorWheel}; + +mod gpu; +use crate::gpu::GpuStuff; + +mod vertex; +use crate::vertex::Vertex; + +mod buffers; +use crate::buffers::Buffers; + +mod layout; +use crate::layout::{tessellate, LayoutDisplay, Size}; + +// Primary public export: the run function +mod run; +pub use crate::run::run; diff --git a/layout21wgpu/src/main.rs b/layout21wgpu/src/main.rs new file mode 100644 index 0000000..b05b64e --- /dev/null +++ b/layout21wgpu/src/main.rs @@ -0,0 +1,5 @@ +use layout21wgpu::run; + +fn main() { + run() +} diff --git a/layout21wgpu/src/run.rs b/layout21wgpu/src/run.rs new file mode 100644 index 0000000..fea2ca2 --- /dev/null +++ b/layout21wgpu/src/run.rs @@ -0,0 +1,93 @@ +use log::error; +use winit::{ + event::*, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowBuilder}, +}; + +// Local imports +use crate::{tessellate, GpuStuff, LayoutDisplay, Size, Buffers}; + +/// # Application State +pub struct State { + gpu: GpuStuff, + layout: LayoutDisplay, + buffers: Buffers, +} +impl State { + async fn new(window: &Window) -> Self { + let layout = LayoutDisplay::from_proto(); + let size = window.inner_size(); + let size: Size = Size { + width: size.width, + height: size.height, + }; + let buffers = tessellate(&layout, &size); + let gpu = GpuStuff::new(window, &buffers).await; + Self { + gpu, + layout, + buffers, + } + } +} + +#[allow(unused_variables)] +fn handle_input(event: &WindowEvent) -> bool { + false +} + +pub fn run() { + env_logger::init(); + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + window.set_title(&*format!("{}", "Layout21 Viewer")); + let mut state = pollster::block_on(State::new(&window)); + + error!("START!!!"); + + event_loop.run(move |event, _, control_flow| match event { + Event::WindowEvent { + ref event, + window_id, + } if window_id == window.id() => { + if !handle_input(event) { + match event { + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Pressed, + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + }, + .. + } => *control_flow = ControlFlow::Exit, + WindowEvent::Resized(physical_size) => { + state.gpu.resize(*physical_size); + window.request_redraw(); + } + WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + state.gpu.resize(**new_inner_size); + } + _ => {} + } + } + } + Event::RedrawRequested(_) => { + error!("REDRAW!!!"); + match state.gpu.render(&state.buffers) { + Ok(_) => {} + Err(wgpu::SurfaceError::Lost) => state.gpu.resize(state.gpu.size), + Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, + Err(e) => eprintln!("{:?}", e), + } + } + Event::MainEventsCleared => { + // error!("REQUESTING!!!"); + // window.request_redraw(); + } + _ => {} + }); +} diff --git a/layout21wgpu/src/shader.wgsl b/layout21wgpu/src/shader.wgsl new file mode 100644 index 0000000..5da115f --- /dev/null +++ b/layout21wgpu/src/shader.wgsl @@ -0,0 +1,22 @@ +struct VertexInput { + @location(0) pos: vec2, + @location(1) color: vec3, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, +}; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.color = vec4(in.color, 0.3); + out.position = vec4(in.pos, 0.0, 1.0); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return in.color; +} \ No newline at end of file diff --git a/layout21wgpu/src/vertex.rs b/layout21wgpu/src/vertex.rs new file mode 100644 index 0000000..59cde91 --- /dev/null +++ b/layout21wgpu/src/vertex.rs @@ -0,0 +1,23 @@ +use bytemuck::{Pod, Zeroable}; + +// Local imports +use crate::Color; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Vertex { + pub position: [f32; 2], + pub color: Color, +} +impl Vertex { + pub const ATTRIBUTES: [wgpu::VertexAttribute; 2] = + wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x3]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBUTES, + } + } +}