Stub in basic wgpu framework; just draw triangle right now

Start again using WGPU with both native and WebAssembly. Just draws a
triangle right now.
This commit is contained in:
Matthew Gordon 2024-07-12 17:27:27 -03:00
parent 5e107d78f2
commit 5dfdd3e927
11 changed files with 2011 additions and 1386 deletions

2675
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,30 @@ name = "pteropus"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
serde_json = "1.0"
tokio = {version="1.35", features=["rt-multi-thread", "macros"]}
tokio-util = {version="0.7", features=["io"]}
reqwest = {version="0.11", features=["json", "stream"]}
proj = "0.27"
geo-types = "0.7"
clap = {version="4.4", features=["derive"]}
anyhow = "1.0"
futures = "0.3"
bytes = "1.5"
las = {version="0.8", features=["laz"]}
image = "0.24"
winit = { version = "0.30.3", features = ["rwh_06"] }
log = "0.4.22"
[target.'cfg(target_arch = "x86_64")'.dependencies]
wgpu = "0.20.1"
env_logger = "0.11.3"
futures = { version = "0.3.30", features = ["executor"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wgpu = { version = "0.20.1", features = ["webgl"]}
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.42"
console_log = "1.0"
console_error_panic_hook = "0.1.7"
web-sys = { version = "0.3", features = [
"Document",
"Window",
"Element",
]}
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.34"

168
src/app/mod.rs Normal file
View File

@ -0,0 +1,168 @@
use crate::mvu::{Event, MvuApp, Size2i};
use {
std::borrow::Cow,
wgpu::{Device, Instance, Queue, RenderPipeline, Surface, SurfaceConfiguration},
};
#[derive(Copy, Clone)]
pub struct Model {}
impl Model {
pub fn new() -> Model {
Model {}
}
}
struct Context {
config: SurfaceConfiguration,
surface: Surface<'static>,
device: Device,
render_pipeline: RenderPipeline,
queue: Queue,
}
#[derive(Default)]
pub struct App {
context: Option<Context>,
}
impl App {
pub fn new() -> Self {
Self::default()
}
}
impl MvuApp<Model> for App {
async fn init(&mut self, instance: &Instance, surface: Surface<'static>, size: Size2i) {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: Some(&surface),
})
.await
.expect("Failed to find an appropriate adapter");
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits()),
},
None,
)
.await
.expect("Failed to create device");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let swapchain_capabilities = surface.get_capabilities(&adapter);
let swapchain_format = swapchain_capabilities.formats[0];
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
compilation_options: Default::default(),
targets: &[Some(swapchain_format.into())],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
let config = surface
.get_default_config(&adapter, size.width, size.height)
.unwrap();
surface.configure(&device, &config);
self.context = Some(Context {
config,
surface,
device,
render_pipeline,
queue,
});
}
async fn resize(&mut self, new_size: Size2i) {
if let Some(Context {
config,
surface,
device,
render_pipeline: _,
queue: _,
}) = &mut self.context
{
config.width = new_size.width.max(1);
config.height = new_size.height.max(1);
surface.configure(device, config);
}
}
async fn update(&self, model: Model, _event: Event) -> Model {
model
}
async fn view(&mut self, _model: Model) -> Result<(), Box<dyn std::error::Error>> {
if let Some(Context {
config: _,
surface,
device,
render_pipeline,
queue,
}) = &self.context
{
let frame = surface
.get_current_texture()
.expect("Failed to acquire next swap chain texture");
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(render_pipeline);
rpass.draw(0..3, 0..1);
}
queue.submit(Some(encoder.finish()));
frame.present();
}
Ok(())
}
}

11
src/app/shader.wgsl Normal file
View File

@ -0,0 +1,11 @@
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(in_vertex_index) - 1);
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

21
src/lib.rs Normal file
View File

@ -0,0 +1,21 @@
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_arch = "wasm32")]
pub use wasm::run;
#[cfg(target_arch = "x86_64")]
mod native;
#[cfg(target_arch = "x86_64")]
pub use native::run;
mod mvu;
mod app;
use app::{App,Model};
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
pub fn main() {
run(App::new(), Model::new());
}

View File

@ -1,229 +1,3 @@
use {
clap::{Parser, Subcommand},
geo_types::Point,
image::{ImageBuffer, Luma},
las::Read as LasRead,
proj::Proj,
std::path::PathBuf,
tokio::io::AsyncReadExt,
};
mod geonb;
mod numeric_types;
use numeric_types::{Float, Integer, ToFloat};
#[derive(Parser)]
#[command(author, version, about, long_about=None)]
struct Args {
/// Print extra debug info when initializing graphics
#[arg(long)]
debug_init: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Download data from GeoNB
GeoNB {
#[command(subcommand)]
command: GeoNBCommand,
},
/// Create a grid DEM from point data
GridPoints {
#[arg(long, short = 'i')]
laz_filename: PathBuf,
#[arg(long)]
grid_cell_size: f64,
},
}
#[derive(Subcommand)]
enum GeoNBCommand {
/// Download the LIDAR tile that includes a location
DownloadLidarTile {
/// Latitude to fetch LIDAR tile at
#[arg(long, allow_hyphen_values = true)]
latitude: f64,
/// Longitude to fetch LIDAR tile at
#[arg(long, allow_hyphen_values = true)]
longitude: f64,
},
}
struct Grid<T> {
width: usize,
height: usize,
data: Vec<T>,
}
impl<T> Grid<T> {
fn calculate_index(&self, i: usize, j: usize) -> usize {
assert!(i < self.width);
assert!(j < self.height);
i + j * self.width
}
}
impl<T: Default + Copy> Grid<T> {
fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
data: vec![T::default(); width * height],
}
}
}
impl<T: Copy> Grid<T> {
fn set_cell_value(&mut self, i: usize, j: usize, value: T) {
let index = self.calculate_index(i, j);
self.data[index] = value;
}
fn get_cell_value(&self, i: usize, j: usize) -> T {
let index = self.calculate_index(i, j);
self.data[index]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_cell_value_returns_zero_after_new() {
let width = 11;
let height = 13;
let target: Grid<i32> = Grid::new(width, height);
for i in 0..width {
for j in 0..height {
assert_eq!(0, target.get_cell_value(i, j));
}
}
}
#[test]
fn get_cell_value_returns_values_set_by_set_cell_value() {
fn unique_value_for_cell(i: usize, j: usize) -> i32 {
let high_bits = i << 12;
let low_bits = j;
assert_eq!(0, high_bits & low_bits);
let bits = low_bits | high_bits;
assert_eq!(bits, bits & 0x7fffffff);
bits as i32
}
let width = 11;
let height = 13;
let mut target: Grid<i32> = Grid::new(width, height);
for i in 0..width {
for j in 0..height {
target.set_cell_value(i, j, unique_value_for_cell(i, j));
assert_eq!(unique_value_for_cell(i, j), target.get_cell_value(i, j));
}
}
for i in 0..width {
for j in 0..height {
assert_eq!(unique_value_for_cell(i, j), target.get_cell_value(i, j));
}
}
}
}
fn integerize<F, I>(grid_in: Grid<F>, min: F, max: F) -> Grid<I>
where
I: Integer + ToFloat<F>,
F: Float,
{
let mut grid_out = Grid::new(grid_in.width, grid_in.height);
let range = max - min;
for i in 0..(grid_in.width as usize) {
for j in 0..(grid_in.height as usize) {
grid_out.set_cell_value(
i,
j,
(((grid_in.get_cell_value(i, j) - min) / range) * (I::MAX).to_float())
.to_int_floor(),
);
}
}
grid_out
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let args = Args::parse();
match args.command {
Command::GeoNB {
command:
GeoNBCommand::DownloadLidarTile {
latitude,
longitude,
},
} => {
let location = Proj::new_known_crs("+proj=longlat +datum=WGS84", "EPSG:2953", None)
.unwrap()
.convert(Point::new(longitude, latitude))
.unwrap();
println!("{:?}", location);
let mut las_reader =
tokio::io::BufReader::new(geonb::get_lidar_tile_around_point(location).await?);
let mut las_bytes = Vec::new();
let mut buffer = [0_u8; 4096];
let mut byte_count = 0;
loop {
let num_bytes = las_reader.read(&mut buffer).await?;
if num_bytes == 0 {
break;
}
byte_count += num_bytes;
print!("{} bytes read\r", byte_count);
las_bytes.extend_from_slice(&buffer[0..num_bytes]);
}
println!();
let mut las_reader = las::Reader::new(std::io::Cursor::new(las_bytes))?;
for wrapped_point in las_reader.points().take(10) {
let point = wrapped_point.unwrap();
println!("Point coordinates: ({}, {}, {})", point.x, point.y, point.z);
}
Ok(())
}
Command::GridPoints {
laz_filename,
grid_cell_size,
} => {
let mut las_reader = las::Reader::from_path(laz_filename).unwrap();
let bounds = las_reader.header().bounds();
let num_cells_x = ((bounds.max.x - bounds.min.x) / grid_cell_size).ceil() as usize;
let num_cells_y = ((bounds.max.y - bounds.min.y) / grid_cell_size).ceil() as usize;
let mut height_grid = Grid::new(num_cells_y, num_cells_x);
let mut weight_grid = Grid::new(num_cells_y, num_cells_x);
println!("Creating {} by {} grid.", num_cells_y, num_cells_x);
for wrapped_point in las_reader.points() {
let point = wrapped_point.unwrap();
let bin_i = ((point.x - bounds.min.x) / grid_cell_size).floor() as usize;
let bin_j = ((point.y - bounds.min.y) / grid_cell_size).floor() as usize;
let height: f64 = height_grid.get_cell_value(bin_i, bin_j);
let weight: f64 = weight_grid.get_cell_value(bin_i, bin_j);
height_grid.set_cell_value(
bin_i,
bin_j,
(height * weight + point.z) / (weight + 1.0),
);
weight_grid.set_cell_value(bin_i, bin_j, weight + 1.0);
}
let grid_u8: Grid<u8> = integerize(height_grid, bounds.min.z, bounds.max.z);
let image: ImageBuffer<Luma<u8>, _> =
ImageBuffer::from_raw(num_cells_x as u32, num_cells_y as u32, grid_u8.data)
.unwrap();
image.save("test.png")?;
Ok(())
}
}
fn main() {
pteropus::main();
}

22
src/mvu/mod.rs Normal file
View File

@ -0,0 +1,22 @@
use wgpu::{Instance, Surface};
pub enum Event {
MouseButtonPressed,
}
#[derive(Debug, Clone, Copy)]
pub struct Size2i {
pub width: u32,
pub height: u32,
}
#[allow(async_fn_in_trait)]
// https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html#where-the-gaps-lie
pub trait MvuApp<M>
where
M: Copy,
{
async fn init(&mut self, instance: &Instance, surface: Surface<'static>, new_size: Size2i);
async fn resize(&mut self, size: Size2i);
async fn update(&self, model: M, event: Event) -> M;
async fn view(&mut self, model: M) -> Result<(), Box<dyn std::error::Error>>;
}

75
src/native/mod.rs Normal file
View File

@ -0,0 +1,75 @@
use std::sync::Arc;
use crate::mvu::{MvuApp, Size2i};
use {
futures::executor::block_on,
winit::{
application::ApplicationHandler,
dpi::PhysicalSize,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
},
};
struct AppHost<A, M> {
window: Option<Arc<Window>>,
app: A,
model: M,
}
impl<A, M> AppHost<A, M> {
fn new(app: A, model: M) -> Self {
Self {
window: None,
app,
model,
}
}
}
impl<A, M> ApplicationHandler for AppHost<A, M>
where
A: MvuApp<M>,
M: Copy,
{
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes())
.unwrap(),
);
let instance = wgpu::Instance::default();
let surface = instance.create_surface(Arc::clone(&window)).unwrap();
let PhysicalSize { width, height } = window.inner_size();
self.window = Some(window);
block_on(self.app.init(&instance, surface, Size2i { width, height }));
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::Resized(new_size) => block_on(self.app.resize(Size2i {
width: new_size.width,
height: new_size.height,
})),
WindowEvent::CloseRequested => {
println!("The close button was pressed; stopping");
event_loop.exit();
}
WindowEvent::RedrawRequested => block_on(self.app.view(self.model)).unwrap(),
_ => (),
}
}
}
pub fn run<A, M>(app: A, model: M)
where
A: MvuApp<M>,
M: Copy,
{
env_logger::init();
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let mut app_host = AppHost::new(app, model);
event_loop.run_app(&mut app_host).unwrap();
}

View File

@ -1,94 +0,0 @@
use std::ops::{Add, Div, Mul, Sub};
pub trait FromFloatFloor<F> {
fn from_float_floor(f: F) -> Self;
}
pub trait ToFloat<F> {
fn to_float(self) -> F;
}
pub trait Integer:
Copy
+ Default
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Mul<Self, Output = Self>
+ Div<Self, Output = Self>
+ FromFloatFloor<f32>
+ FromFloatFloor<f64>
+ ToFloat<f32>
+ ToFloat<f64>
{
const MIN: Self;
const MAX: Self;
}
macro_rules! impl_int {
( $t:ident ) => {
impl FromFloatFloor<f32> for $t {
fn from_float_floor(f: f32) -> Self {
f as $t
}
}
impl FromFloatFloor<f64> for $t {
fn from_float_floor(f: f64) -> Self {
f as $t
}
}
impl ToFloat<f32> for $t {
fn to_float(self) -> f32 {
self as f32
}
}
impl ToFloat<f64> for $t {
fn to_float(self) -> f64 {
self as f64
}
}
impl Integer for $t {
const MIN: $t = $t::MIN;
const MAX: $t = $t::MAX;
}
};
}
impl_int!(u8);
impl_int!(i8);
impl_int!(u16);
impl_int!(i16);
impl_int!(u32);
impl_int!(i32);
impl_int!(u64);
impl_int!(i64);
pub trait Unsigned {}
impl Unsigned for u8 {}
impl Unsigned for u16 {}
impl Unsigned for u32 {}
impl Unsigned for u64 {}
pub trait Float:
Copy
+ Default
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Mul<Self, Output = Self>
+ Div<Self, Output = Self>
{
fn to_int_floor<I: Integer>(self) -> I;
}
macro_rules! impl_float {
( $t:ident ) => {
impl Float for $t {
fn to_int_floor<I: Integer>(self) -> I {
I::from_float_floor(self)
}
}
};
}
impl_float!(f32);
impl_float!(f64);

38
src/wasm/mod.rs Normal file
View File

@ -0,0 +1,38 @@
use {
wasm_bindgen::prelude::*, wasm_bindgen_futures::spawn_local, web_sys::HtmlCanvasElement,
wgpu::SurfaceTarget,
};
use crate::mvu::{MvuApp, Size2i};
async fn run_async<A, M>(mut app: A, model: M)
where
A: MvuApp<M> + 'static,
M: Copy + 'static,
{
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let canvas_element = document.get_element_by_id("pteropus-canvas").unwrap();
let html_canvas: HtmlCanvasElement = canvas_element.dyn_into().unwrap();
let size = Size2i {
width: html_canvas.width(),
height: html_canvas.height(),
};
let instance = wgpu::Instance::default();
let surface_target = SurfaceTarget::Canvas(html_canvas);
let surface = instance.create_surface(surface_target).unwrap();
app.init(&instance, surface, size).await;
app.view(model).await.unwrap();
}
pub fn run<A, M>(app: A, model: M)
where
A: MvuApp<M> + 'static,
M: Copy + 'static,
{
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Info).expect("Couldn't initialize logger");
spawn_local(run_async(app, model));
}

25
web/index.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Pteropus</title>
<style>
#pteropus-canvas {
height: 100vh;
width: 100vh;
display: block;
}
</style>
</head>
<body>
<canvas id="pteropus-canvas"></canvas>
<script type="module">
import init from '../pkg/pteropus.js';
async function run() {
await init();
}
run();
</script>
</body>
</html>