Skip to content

Commit

Permalink
Add WebGL support to web builds (#52)
Browse files Browse the repository at this point in the history
* 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 ac4d0d9)

* Fix "Texture[1] does not exist" when using WebGL

(cherry picked from commit 1f6f234)

* Set min log level to info in web builds

This is to avoid clogging the console with useless messages.

(cherry picked from commit cc857b2)

* 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 f951d14)

* Pad sprite shader uniforms to 16 bytes

(cherry picked from commit 92e9aca)

* Add a feature test for WebGPU

(cherry picked from commit 858fcb0)

* Remove extra space from assets/main.js

(cherry picked from commit 4cf6193)

* 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 5d6bbb2)

* Replace all double quotes in assets/main.js with single quotes

(cherry picked from commit d9ec1b6)
  • Loading branch information
white-axe authored Nov 23, 2023
1 parent 378d480 commit 41a7125
Show file tree
Hide file tree
Showing 16 changed files with 110 additions and 37 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 34 additions & 3 deletions assets/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,38 @@
//
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see <http://www.gnu.org/licenses/>.
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);
25 changes: 25 additions & 0 deletions assets/webgpu-test-worker.js
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

onmessage = async function () {
let gpu = false;
try {
let adapter = await navigator.gpu?.requestAdapter();
gpu = typeof GPUAdapter === 'function' && adapter instanceof GPUAdapter;
} catch (e) {}
postMessage(gpu);
}
11 changes: 6 additions & 5 deletions assets/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
//
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see <http://www.gnu.org/licenses/>.
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);
};
2 changes: 2 additions & 0 deletions crates/graphics/src/sprite/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ struct Data {
hue: f32,
opacity: f32,
opacity_multiplier: f32,
_padding: u32,
}

impl Graphic {
Expand All @@ -51,6 +52,7 @@ impl Graphic {
hue,
opacity,
opacity_multiplier: 1.,
_padding: 0,
};

let uniform =
Expand Down
2 changes: 1 addition & 1 deletion crates/graphics/src/sprite/shader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fn create_shader(
},
wgpu::PushConstantRange {
stages: wgpu::ShaderStages::FRAGMENT,
range: 64..(64 + 4 + 4 + 4),
range: 64..(64 + 16),
},
],
})
Expand Down
1 change: 1 addition & 0 deletions crates/graphics/src/sprite/sprite.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct Graphic {
hue: f32,
opacity: f32,
opacity_multiplier: f32,
_padding: u32,
}

#if USE_PUSH_CONSTANTS == true
Expand Down
14 changes: 8 additions & 6 deletions crates/graphics/src/tiles/autotiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 =
Expand All @@ -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(
Expand Down Expand Up @@ -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,
},
Expand Down
4 changes: 2 additions & 2 deletions crates/graphics/src/tiles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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::<f32>(&opacity),
);
}
Expand Down
4 changes: 2 additions & 2 deletions crates/graphics/src/tiles/shader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
],
})
Expand Down
9 changes: 5 additions & 4 deletions crates/graphics/src/tiles/tilemap.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ struct Viewport {
}

struct Autotiles {
frame_counts: array<vec4<u32>, 2>,
animation_index: u32,
max_frame_count: u32,
frame_counts: array<u32, 7>
}

#if USE_PUSH_CONSTANTS == true
Expand All @@ -37,7 +37,7 @@ var<push_constant> push_constants: PushConstants;
@group(1) @binding(0)
var<uniform> viewport: Viewport;
@group(2) @binding(0)
var<storage, read> autotiles: Autotiles;
var<uniform> autotiles: Autotiles;
@group(3) @binding(0)
var<uniform> opacity: array<vec4<f32>, 1>;
#endif
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions crates/graphics/src/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 7 additions & 8 deletions crates/web/src/web_worker_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::cell::RefCell<WebWorkerRunnerState>>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<link data-trunk rel="copy-file" href="assets/main.js" />
<link data-trunk rel="copy-file" href="assets/worker.js" />
<link data-trunk rel="copy-file" href="assets/webgpu-test-worker.js" />
<link data-trunk rel="copy-file" href="assets/coi-serviceworker.js" />
<link data-trunk rel="copy-file" href="assets/manifest.json" />
<link data-trunk rel="copy-file" href="assets/icon-1024.png" />
Expand Down
10 changes: 7 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ static WORKER_DATA: parking_lot::RwLock<Option<WorkerData>> = 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 {
Expand All @@ -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");
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 41a7125

Please sign in to comment.