From 41a712564d55f3e9d45c04d8933128d9b2f46e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 23 Nov 2023 02:32:59 -0500 Subject: [PATCH] Add WebGL support to web builds (#52) * Add new feature `webgl` for enabling WebGL in web builds wgpu doesn't allow supporting both WebGL and WebGPU in the same binary yet so we have to decide at compile time which one to use. (cherry picked from commit ac4d0d9521a5ce11165aa701b77e426a34428894) * Fix "Texture[1] does not exist" when using WebGL (cherry picked from commit 1f6f2340a82672b99f0bc21ef303b7572c187002) * Set min log level to info in web builds This is to avoid clogging the console with useless messages. (cherry picked from commit cc857b2927ca3fa8f1b7099dbb3ae610c81c2c2f) * Refactor tilemap shader to not use storage buffers It's not supported in WebGL and probably also not supported in OpenGL. (cherry picked from commit f951d1477e26a918a28fa3b17b92a785067441cb) * Pad sprite shader uniforms to 16 bytes (cherry picked from commit 92e9aca7bca02e3aa69483482677f5e59332f45d) * Add a feature test for WebGPU (cherry picked from commit 858fcb0b5fe8d66821d24ce4e5c52f596372dc8f) * Remove extra space from assets/main.js (cherry picked from commit 4cf619300f7f0ca203f77925dc1bd141ef3b678a) * Move WebGPU feature detection into a web worker Firefox Nightly apparently only supports WebGPU on the main thread and not in a web worker where we need it, so the test for WebGPU support now runs in a worker thread. (cherry picked from commit 5d6bbb24b906ea8d5a4615d1984f63ad9f56a38c) * Replace all double quotes in assets/main.js with single quotes (cherry picked from commit d9ec1b623afd5dfb3f736cbfea550e0ee705980d) --- Cargo.lock | 1 + Cargo.toml | 7 +++++ assets/main.js | 37 +++++++++++++++++++++++--- assets/webgpu-test-worker.js | 25 +++++++++++++++++ assets/worker.js | 11 ++++---- crates/graphics/src/sprite/graphic.rs | 2 ++ crates/graphics/src/sprite/shader.rs | 2 +- crates/graphics/src/sprite/sprite.wgsl | 1 + crates/graphics/src/tiles/autotiles.rs | 14 +++++----- crates/graphics/src/tiles/mod.rs | 4 +-- crates/graphics/src/tiles/shader.rs | 4 +-- crates/graphics/src/tiles/tilemap.wgsl | 9 ++++--- crates/graphics/src/viewport.rs | 4 +-- crates/web/src/web_worker_runner.rs | 15 +++++------ index.html | 1 + src/main.rs | 10 ++++--- 16 files changed, 110 insertions(+), 37 deletions(-) create mode 100644 assets/webgpu-test-worker.js diff --git a/Cargo.lock b/Cargo.lock index dde3dd39..5a5866ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2750,6 +2750,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "wgpu", "winres", ] diff --git a/Cargo.toml b/Cargo.toml index fcf8392a..8e89ad9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,9 +185,16 @@ tracing.workspace = true web-sys = { version = "0.3", features = ["Window"] } +# Enable wgpu's `webgl` feature if Luminol's `webgl` feature is enabled, +# enabling its WebGL backend and disabling its WebGPU backend +[target.'cfg(target_arch = "wasm32")'.dependencies.wgpu] +workspace = true +optional = true +features = ["webgl"] [features] steamworks = ["dep:steamworks", "crc"] +webgl = ["dep:wgpu"] [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/assets/main.js b/assets/main.js index f14e1a35..0bc1d068 100644 --- a/assets/main.js +++ b/assets/main.js @@ -14,7 +14,38 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -import wasm_bindgen, { luminol_main_start } from '/luminol.js'; -await wasm_bindgen(); -luminol_main_start(); +// Check if the user's browser supports WebGPU +console.log('Checking for WebGPU support in web workers…'); +const worker = new Worker('/webgpu-test-worker.js'); +const promise = new Promise(function (resolve) { + worker.onmessage = function (e) { + resolve(e.data); + }; +}); +worker.postMessage(null); +const gpu = await promise; +worker.terminate(); +if (gpu) { + console.log('WebGPU is supported. Using WebGPU backend if available.'); +} else { + console.log('No support detected. Using WebGL backend if available.'); +} + +// If WebGPU is supported, always use luminol.js +// If WebGPU is not supported, use luminol_webgl.js if it's available or fallback to luminol.js +let fallback = false; +let luminol; +if (gpu) { + luminol = await import('/luminol.js'); +} else { + try { + luminol = await import('/luminol_webgl.js'); + fallback = true; + } catch (e) { + luminol = await import('/luminol.js'); + } +} + +await luminol.default(fallback ? '/luminol_webgl_bg.wasm' : '/luminol_bg.wasm'); +luminol.luminol_main_start(fallback); diff --git a/assets/webgpu-test-worker.js b/assets/webgpu-test-worker.js new file mode 100644 index 00000000..f3fe6fd5 --- /dev/null +++ b/assets/webgpu-test-worker.js @@ -0,0 +1,25 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +onmessage = async function () { + let gpu = false; + try { + let adapter = await navigator.gpu?.requestAdapter(); + gpu = typeof GPUAdapter === 'function' && adapter instanceof GPUAdapter; + } catch (e) {} + postMessage(gpu); +} diff --git a/assets/worker.js b/assets/worker.js index 0df15da6..809d4d31 100644 --- a/assets/worker.js +++ b/assets/worker.js @@ -14,11 +14,12 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -import wasm_bindgen, { luminol_worker_start } from '/luminol.js'; onmessage = async function (e) { - if (e.data[0] === 'init') { - await wasm_bindgen(undefined, e.data[1]); - await luminol_worker_start(e.data[2]); - } + const [fallback, memory, canvas] = e.data; + + const luminol = await import(fallback ? '/luminol_webgl.js' : '/luminol.js'); + + await luminol.default(fallback ? '/luminol_webgl_bg.wasm' : '/luminol_bg.wasm', memory); + await luminol.luminol_worker_start(canvas); }; diff --git a/crates/graphics/src/sprite/graphic.rs b/crates/graphics/src/sprite/graphic.rs index 0d5fabfb..83acd179 100644 --- a/crates/graphics/src/sprite/graphic.rs +++ b/crates/graphics/src/sprite/graphic.rs @@ -36,6 +36,7 @@ struct Data { hue: f32, opacity: f32, opacity_multiplier: f32, + _padding: u32, } impl Graphic { @@ -51,6 +52,7 @@ impl Graphic { hue, opacity, opacity_multiplier: 1., + _padding: 0, }; let uniform = diff --git a/crates/graphics/src/sprite/shader.rs b/crates/graphics/src/sprite/shader.rs index 1414674b..5004e89a 100644 --- a/crates/graphics/src/sprite/shader.rs +++ b/crates/graphics/src/sprite/shader.rs @@ -62,7 +62,7 @@ fn create_shader( }, wgpu::PushConstantRange { stages: wgpu::ShaderStages::FRAGMENT, - range: 64..(64 + 4 + 4 + 4), + range: 64..(64 + 16), }, ], }) diff --git a/crates/graphics/src/sprite/sprite.wgsl b/crates/graphics/src/sprite/sprite.wgsl index c4f222c0..f3d4f04e 100644 --- a/crates/graphics/src/sprite/sprite.wgsl +++ b/crates/graphics/src/sprite/sprite.wgsl @@ -17,6 +17,7 @@ struct Graphic { hue: f32, opacity: f32, opacity_multiplier: f32, + _padding: u32, } #if USE_PUSH_CONSTANTS == true diff --git a/crates/graphics/src/tiles/autotiles.rs b/crates/graphics/src/tiles/autotiles.rs index e3fc6535..4821ac83 100644 --- a/crates/graphics/src/tiles/autotiles.rs +++ b/crates/graphics/src/tiles/autotiles.rs @@ -30,12 +30,14 @@ struct AutotilesUniform { bind_group: wgpu::BindGroup, } -#[repr(C)] +#[repr(C, align(16))] #[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Data { + autotile_frames: [u32; 7], + _array_padding: u32, ani_index: u32, autotile_region_width: u32, - autotile_frames: [u32; 7], + _end_padding: u64, } impl Autotiles { @@ -48,6 +50,8 @@ impl Autotiles { autotile_frames: atlas.autotile_frames, autotile_region_width: atlas.autotile_width, ani_index: 0, + _array_padding: 0, + _end_padding: 0, }; let uniform = @@ -56,9 +60,7 @@ impl Autotiles { &wgpu::util::BufferInitDescriptor { label: Some("tilemap autotile buffer"), contents: bytemuck::cast_slice(&[autotiles]), - usage: wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_DST - | wgpu::BufferUsages::UNIFORM, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, }, ); let bind_group = graphics_state.render_state.device.create_bind_group( @@ -120,7 +122,7 @@ pub fn create_bind_group_layout(render_state: &egui_wgpu::RenderState) -> wgpu:: binding: 0, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, + ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, diff --git a/crates/graphics/src/tiles/mod.rs b/crates/graphics/src/tiles/mod.rs index 57f86825..e3ff17d5 100644 --- a/crates/graphics/src/tiles/mod.rs +++ b/crates/graphics/src/tiles/mod.rs @@ -82,7 +82,7 @@ impl Tiles { #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] struct VertexPushConstant { viewport: [u8; 64], - autotiles: [u8; 36], + autotiles: [u8; 48], } render_pass.push_debug_group("tilemap tiles renderer"); @@ -113,7 +113,7 @@ impl Tiles { if self.use_push_constants { render_pass.set_push_constants( wgpu::ShaderStages::FRAGMENT, - 64 + 36, + 64 + 48, bytemuck::bytes_of::(&opacity), ); } diff --git a/crates/graphics/src/tiles/shader.rs b/crates/graphics/src/tiles/shader.rs index 653b4220..6e7af927 100644 --- a/crates/graphics/src/tiles/shader.rs +++ b/crates/graphics/src/tiles/shader.rs @@ -65,12 +65,12 @@ pub fn create_render_pipeline( // Viewport + Autotiles wgpu::PushConstantRange { stages: wgpu::ShaderStages::VERTEX, - range: 0..(64 + 36), + range: 0..(64 + 48), }, // Fragment wgpu::PushConstantRange { stages: wgpu::ShaderStages::FRAGMENT, - range: (64 + 36)..(64 + 36 + 4), + range: (64 + 48)..(64 + 48 + 4), }, ], }) diff --git a/crates/graphics/src/tiles/tilemap.wgsl b/crates/graphics/src/tiles/tilemap.wgsl index 07cbf26a..e00b12a9 100644 --- a/crates/graphics/src/tiles/tilemap.wgsl +++ b/crates/graphics/src/tiles/tilemap.wgsl @@ -21,9 +21,9 @@ struct Viewport { } struct Autotiles { + frame_counts: array, 2>, animation_index: u32, max_frame_count: u32, - frame_counts: array } #if USE_PUSH_CONSTANTS == true @@ -37,7 +37,7 @@ var push_constants: PushConstants; @group(1) @binding(0) var viewport: Viewport; @group(2) @binding(0) -var autotiles: Autotiles; +var autotiles: Autotiles; @group(3) @binding(0) var opacity: array, 1>; #endif @@ -89,12 +89,13 @@ fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { } if is_autotile { + let autotile_type = instance.tile_id / 48 - 1; // we get an error about non constant indexing without this. // not sure why #if USE_PUSH_CONSTANTS == true - let frame_count = push_constants.autotiles.frame_counts[instance.tile_id / 48 - 1]; + let frame_count = push_constants.autotiles.frame_counts[autotile_type / 4][autotile_type % 4]; #else - let frame_count = autotiles.frame_counts[instance.tile_id / 48 - 1]; + let frame_count = autotiles.frame_counts[autotile_type / 4][autotile_type % 4]; #endif let frame = autotiles.animation_index % frame_count; diff --git a/crates/graphics/src/viewport.rs b/crates/graphics/src/viewport.rs index b7667b25..e70a0156 100644 --- a/crates/graphics/src/viewport.rs +++ b/crates/graphics/src/viewport.rs @@ -42,9 +42,7 @@ impl Viewport { &wgpu::util::BufferInitDescriptor { label: Some("tilemap viewport buffer"), contents: bytemuck::cast_slice(&[proj]), - usage: wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_DST - | wgpu::BufferUsages::UNIFORM, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, }, ); let bind_group = graphics_state.render_state.device.create_bind_group( diff --git a/crates/web/src/web_worker_runner.rs b/crates/web/src/web_worker_runner.rs index a6bce979..9780ead0 100644 --- a/crates/web/src/web_worker_runner.rs +++ b/crates/web/src/web_worker_runner.rs @@ -113,7 +113,6 @@ struct WebWorkerRunnerState { } /// A runner for wgpu egui applications intended to be run in a web worker. -/// Currently only targets WebGPU, not WebGL. #[derive(Clone)] pub struct WebWorkerRunner { state: std::rc::Rc>, @@ -452,15 +451,15 @@ impl WebWorkerRunner { ) }; + // Create texture to render onto + // This variable needs to live for the entire remaining duration we use + // `state.render_state` or WebGL will break + let render_texture = state.surface.get_current_texture().unwrap(); + // Execute egui's render pass { let renderer = state.render_state.renderer.read(); - let view = state - .surface - .get_current_texture() - .unwrap() - .texture - .create_view(&Default::default()); + let view = render_texture.texture.create_view(&Default::default()); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, @@ -495,7 +494,7 @@ impl WebWorkerRunner { .into_iter() .chain(std::iter::once(encoder.finish())), ); - state.surface.get_current_texture().unwrap().present(); + render_texture.present(); self.time_lock.store( bindings::performance(&worker).now() / 1000. diff --git a/index.html b/index.html index d90251c9..edd3bc29 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ + diff --git a/src/main.rs b/src/main.rs index 1fc26dcc..3c774f87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -181,7 +181,7 @@ static WORKER_DATA: parking_lot::RwLock> = parking_lot::RwLoc #[cfg(target_arch = "wasm32")] #[wasm_bindgen] -pub fn luminol_main_start() { +pub fn luminol_main_start(fallback: bool) { let (panic_tx, mut panic_rx) = flume::unbounded::<()>(); wasm_bindgen_futures::spawn_local(async move { @@ -205,7 +205,11 @@ pub fn luminol_main_start() { })); // Redirect tracing to console.log and friends: - tracing_wasm::set_as_global_default(); + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_max_level(tracing::Level::INFO) + .build(), + ); // Redirect log (currently used by egui) to tracing tracing_log::LogTracer::init().expect("failed to initialize tracing-log"); @@ -260,7 +264,7 @@ pub fn luminol_main_start() { .expect("failed to spawn web worker"); let message = js_sys::Array::new(); - message.push(&JsValue::from("init")); + message.push(&JsValue::from(fallback)); message.push(&wasm_bindgen::memory()); message.push(&offscreen_canvas); let transfer = js_sys::Array::new();