Ported to WASM + WebGPU. (Untested.)
I was not able to test this because: * Firefox nightly does not support the WebGPU spec of wgpu 0.13 * Chrome Dev WebGPU does not work consistently on AMD+Linux * I don't feel like setting up a Windows VM or reverting wgpu Pushing onto a separate branch for whenever Firefox gets updated, so I can test it then.wasm
parent
7fb393ddda
commit
a0ae573258
|
@ -7,3 +7,6 @@ charset = utf-8
|
|||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js, json, html}]
|
||||
indent_size = 2
|
||||
|
|
|
@ -11,3 +11,13 @@
|
|||
!/Cargo.lock
|
||||
!/Cargo.toml
|
||||
!/LICENSE.txt
|
||||
|
||||
# www source code
|
||||
!/www/bootstrap.js
|
||||
!/www/index.js
|
||||
!/www/index.html
|
||||
|
||||
# www configuration
|
||||
!/www/package.json
|
||||
!/www/package-lock.json
|
||||
!/www/webpack.config.js
|
||||
|
|
|
@ -297,6 +297,26 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494"
|
||||
dependencies = [
|
||||
"log",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copyless"
|
||||
version = "0.1.5"
|
||||
|
@ -1179,11 +1199,16 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bytemuck",
|
||||
"claxon",
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"cpal",
|
||||
"fern",
|
||||
"image",
|
||||
"log",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
"winit",
|
||||
]
|
||||
|
@ -1434,18 +1459,6 @@ checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
|
|||
dependencies = [
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
41
Cargo.toml
41
Cargo.toml
|
@ -6,13 +6,14 @@ description = ""
|
|||
repository = "https://github.com/jamestmartin/pathland"
|
||||
license = "0BSD"
|
||||
publish = false
|
||||
autobins = false
|
||||
|
||||
[features]
|
||||
client = []
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
server = []
|
||||
|
||||
[dependencies]
|
||||
[[bin]]
|
||||
name = "pathland"
|
||||
path = "src/main.rs"
|
||||
|
||||
# brotli (compression format)
|
||||
#[dependencies.brotli]
|
||||
|
@ -39,11 +40,6 @@ version = "0.13.5"
|
|||
#[dependencies.directories]
|
||||
#version = "4.0"
|
||||
|
||||
# logging backend
|
||||
[dependencies.fern]
|
||||
version = "0.6.1"
|
||||
features = ["colored"]
|
||||
|
||||
# text rendering
|
||||
#[dependencies.fontdue]
|
||||
#version = "0.7.2"
|
||||
|
@ -85,12 +81,6 @@ features = ["std"]
|
|||
#[dependencies.serde]
|
||||
#version = "1.0"
|
||||
|
||||
# async runtime
|
||||
[dependencies.tokio]
|
||||
version = "1.19"
|
||||
# TODO: Is rt-multi-thread faster for our use case?
|
||||
features = ["rt", "macros"]
|
||||
|
||||
# TOML (configuration format)
|
||||
#[dependencies.toml_edit]
|
||||
#version = "0.14.4"
|
||||
|
@ -104,7 +94,26 @@ version = "0.13.1"
|
|||
version = "0.26.1"
|
||||
features = ["x11", "wayland"]
|
||||
|
||||
# logging backend
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.fern]
|
||||
version = "0.6.1"
|
||||
features = ["colored"]
|
||||
|
||||
# async runtime
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "1.19"
|
||||
# TODO: Is rt-multi-thread faster for our use case?
|
||||
features = ["rt"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
wasm-bindgen = "0.2.81"
|
||||
wasm-bindgen-futures = "0.4.31"
|
||||
web-sys = { version = "0.3.58", features = ["HtmlCanvasElement"] }
|
||||
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
|
|
|
@ -159,7 +159,7 @@ impl Graphics {
|
|||
VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
|
||||
step_mode: VertexStepMode::Vertex,
|
||||
attributes: &vertex_attr_array![0 => Float32x3, 1 => Float32x3]
|
||||
attributes: &vertex_attr_array![0 => Float32x2]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -224,7 +224,7 @@ impl Graphics {
|
|||
format: self.surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::Mailbox
|
||||
present_mode: PresentMode::AutoVsync
|
||||
});
|
||||
self.uniform_copy_buffer.slice(..).get_mapped_range_mut().copy_from_slice(bytemuck::cast_slice(&[Uniforms {
|
||||
dimensions: [self.desired_size.width as f32, self.desired_size.height as f32],
|
||||
|
@ -235,7 +235,11 @@ impl Graphics {
|
|||
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||
encoder.copy_buffer_to_buffer(&self.uniform_copy_buffer, 0, &self.uniform_buffer, 0, std::mem::size_of::<Uniforms>() as u64);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.uniform_copy_buffer.slice(..).map_async(MapMode::Write, |err| err.unwrap());
|
||||
self.uniform_copy_buffer.slice(..).map_async(MapMode::Write, |err| {
|
||||
if let Err(err) = err {
|
||||
log::error!("buffer async error: {}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn reconfigure_surface_if_stale(&mut self) {
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
mod audio;
|
||||
mod graphics;
|
||||
|
||||
use winit::window::WindowBuilder;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn main(canvas: web_sys::HtmlCanvasElement) {
|
||||
console_error_panic_hook::set_once();
|
||||
wasm_bindgen_futures::spawn_local(_main(move |wb| {
|
||||
log::info!("callback");
|
||||
use winit::platform::web::WindowBuilderExtWebSys;
|
||||
wb.with_canvas(Some(canvas))
|
||||
}));
|
||||
}
|
||||
|
||||
pub async fn _main<F>(wb_platform_specific: F)
|
||||
where F: FnOnce(WindowBuilder) -> WindowBuilder
|
||||
{
|
||||
setup_logger();
|
||||
log::info!("main");
|
||||
use winit::event_loop::EventLoop;
|
||||
// TODO: class and app id on unix
|
||||
//use winit::platform::unix::WindowBuilderExtUnix;
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
let window = wb_platform_specific(WindowBuilder::new())
|
||||
// Arbitrarily chosen as the minimum resolution the game is designed to support (for e.g. UI scaling).
|
||||
.with_min_inner_size(winit::dpi::LogicalSize { height: 360, width: 640 })
|
||||
.with_title("Pathland")
|
||||
.with_maximized(true)
|
||||
// TODO: hide window until first frame is drawn (default behavior on wayland)
|
||||
.with_visible(true)
|
||||
.with_decorations(true)
|
||||
.build(&event_loop)
|
||||
.expect("Failed to create window.");
|
||||
// TODO: window icon, fullscreen, IME position, cursor grab, cursor visibility
|
||||
let mut graphics = graphics::Graphics::setup(window).await;
|
||||
//let audio = audio::Audio::setup();
|
||||
|
||||
event_loop.run(move |event, target, control_flow| {
|
||||
use winit::event::*;
|
||||
*control_flow = winit::event_loop::ControlFlow::Wait;
|
||||
match event {
|
||||
Event::WindowEvent { window_id, event } => {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
WindowEvent::Destroyed => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
WindowEvent::Focused(focused) => {
|
||||
// TODO: handle focus/unfocus (e.g. pause, resume)
|
||||
},
|
||||
WindowEvent::Resized(new_size) => {
|
||||
graphics.window_resized(new_size)
|
||||
},
|
||||
WindowEvent::ScaleFactorChanged { new_inner_size: new_size, .. } => {
|
||||
graphics.window_resized(*new_size)
|
||||
},
|
||||
// TODO: handle user input
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Event::DeviceEvent { device_id, event } => {
|
||||
// TODO: handle user input
|
||||
},
|
||||
Event::MainEventsCleared => {
|
||||
// TODO: main event loop. queue simulation calculations, screen redrawing, etc.
|
||||
},
|
||||
Event::RedrawRequested(_) => {
|
||||
graphics.draw();
|
||||
},
|
||||
Event::LoopDestroyed => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
// TODO: What is suspending/resuming? Do I want to support it?
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn setup_logger() {
|
||||
use fern::Dispatch;
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::LevelFilter;
|
||||
|
||||
Dispatch::new()
|
||||
.chain(
|
||||
Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
ColoredLevelConfig::default().color(record.level()),
|
||||
message
|
||||
));
|
||||
})
|
||||
.level(LevelFilter::Warn)
|
||||
.level_for("pathland", LevelFilter::Info)
|
||||
.chain(std::io::stderr()))
|
||||
.chain(
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(LevelFilter::Debug)
|
||||
.level_for("pathland", LevelFilter::Trace)
|
||||
// FIXME: linux-specific path
|
||||
.chain(std::fs::OpenOptions::new().write(true).create(true).truncate(true).open("/tmp/pathland.log").unwrap()))
|
||||
.apply().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn setup_logger() {
|
||||
console_log::init().unwrap();
|
||||
}
|
108
src/main.rs
108
src/main.rs
|
@ -1,107 +1,19 @@
|
|||
mod audio;
|
||||
mod graphics;
|
||||
use winit::window::WindowBuilder;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
fn main() {
|
||||
tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(pathland::_main(wb_platform_specific));
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
setup_logger();
|
||||
|
||||
use winit::event_loop::EventLoop;
|
||||
#[cfg(unix)]
|
||||
fn wb_platform_specific(wb: WindowBuilder) -> WindowBuilder {
|
||||
use winit::platform::unix::WindowBuilderExtUnix;
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let window = winit::window::WindowBuilder::new()
|
||||
// Arbitrarily chosen as the minimum resolution the game is designed to support (for e.g. UI scaling).
|
||||
.with_min_inner_size(winit::dpi::LogicalSize { height: 360, width: 640 })
|
||||
.with_title("Pathland")
|
||||
.with_maximized(true)
|
||||
// TODO: hide window until first frame is drawn (default behavior on wayland)
|
||||
.with_visible(true)
|
||||
.with_decorations(true)
|
||||
wb
|
||||
.with_class("pathland".to_string(), "pathland".to_string())
|
||||
.with_app_id("pathland".to_string())
|
||||
.build(&event_loop)
|
||||
.expect("Failed to create window.");
|
||||
// TODO: window icon, fullscreen, IME position, cursor grab, cursor visibility
|
||||
let mut graphics = graphics::Graphics::setup(window).await;
|
||||
//let audio = audio::Audio::setup();
|
||||
log::info!("Took {} milliseconds", now.elapsed().as_millis());
|
||||
|
||||
event_loop.run(move |event, target, control_flow| {
|
||||
use winit::event::*;
|
||||
*control_flow = winit::event_loop::ControlFlow::Wait;
|
||||
match event {
|
||||
Event::WindowEvent { window_id, event } => {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
WindowEvent::Destroyed => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
WindowEvent::Focused(focused) => {
|
||||
// TODO: handle focus/unfocus (e.g. pause, resume)
|
||||
},
|
||||
WindowEvent::Resized(new_size) => {
|
||||
graphics.window_resized(new_size)
|
||||
},
|
||||
WindowEvent::ScaleFactorChanged { new_inner_size: new_size, .. } => {
|
||||
graphics.window_resized(*new_size)
|
||||
},
|
||||
// TODO: handle user input
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Event::DeviceEvent { device_id, event } => {
|
||||
// TODO: handle user input
|
||||
},
|
||||
Event::MainEventsCleared => {
|
||||
// TODO: main event loop. queue simulation calculations, screen redrawing, etc.
|
||||
},
|
||||
Event::RedrawRequested(_) => {
|
||||
graphics.draw();
|
||||
},
|
||||
Event::LoopDestroyed => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
// TODO: What is suspending/resuming? Do I want to support it?
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_logger() {
|
||||
use fern::Dispatch;
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::LevelFilter;
|
||||
|
||||
Dispatch::new()
|
||||
.chain(
|
||||
Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
ColoredLevelConfig::default().color(record.level()),
|
||||
message
|
||||
));
|
||||
})
|
||||
.level(LevelFilter::Warn)
|
||||
.level_for("pathland", LevelFilter::Info)
|
||||
.chain(std::io::stderr()))
|
||||
.chain(
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(LevelFilter::Debug)
|
||||
.level_for("pathland", LevelFilter::Trace)
|
||||
.chain(std::fs::OpenOptions::new().write(true).create(true).truncate(true).open("/tmp/pathland.log").unwrap()))
|
||||
.apply().unwrap();
|
||||
#[cfg(not(unix))]
|
||||
fn wb_platform_specific(wb: WindowBuilder) -> WindowBuilder {
|
||||
wb
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// A dependency graph that contains any wasm must all be imported
|
||||
// asynchronously. This `bootstrap.js` file does the single async import, so
|
||||
// that no one else needs to worry about it again.
|
||||
import("./index.js")
|
||||
.catch(e => console.error("Error importing `index.js`:", e));
|
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<title>pathland</title>
|
||||
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script src="bootstrap.js"></script>
|
|
@ -0,0 +1,4 @@
|
|||
import * as pathland from "pathland";
|
||||
|
||||
let canvas = document.getElementById("canvas");
|
||||
pathland.main(document.getElementById("canvas"));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "pathland",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"start": "webpack-dev-server"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jamestmartin/pathland"
|
||||
},
|
||||
"keywords": [
|
||||
"webassembly",
|
||||
"wasm",
|
||||
"rust",
|
||||
"webpack"
|
||||
],
|
||||
"dependencies": {
|
||||
"pathland": "file:../pkg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.9.3",
|
||||
"copy-webpack-plugin": "^5.1.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: "./bootstrap.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "bootstrap.js",
|
||||
hashFunction: "xxhash64",
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new CopyWebpackPlugin(['index.html'])
|
||||
],
|
||||
experiments: {
|
||||
syncWebAssembly: true
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue