Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ jobs:
# `ring` (under iroh) compiles C for wasm32, which needs clang and llvm-ar.
- name: clang
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends clang llvm
- run: cargo check -p client --target wasm32-unknown-unknown --locked
# Both browser builds, without the dev features, as `web/build.sh dist both` makes them.
- name: check (webgl2)
run: cargo check -p client --target wasm32-unknown-unknown --locked --no-default-features
env:
CC_wasm32_unknown_unknown: clang
AR_wasm32_unknown_unknown: llvm-ar
- name: check (webgpu)
run: cargo check -p client --target wasm32-unknown-unknown --locked --no-default-features --features webgpu
env:
CC_wasm32_unknown_unknown: clang
AR_wasm32_unknown_unknown: llvm-ar
3 changes: 2 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ jobs:
# `ring` (under iroh) compiles C for wasm32, which needs clang and llvm-ar.
- name: clang
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends clang llvm
- run: ./web/build.sh dist
# The WebGL2 build and the WebGPU one: the page picks by what the browser has.
- run: ./web/build.sh dist both
env:
LLVM: /usr
- uses: actions/upload-pages-artifact@v5
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ tracy = ["bevy/trace_tracy"]
# Names the commands and components in Bevy's error and warning messages (instead of
# "Enable the debug feature to see the name"): `--features debug`.
debug = ["bevy/debug"]
# The browser build draws through WebGPU instead of WebGL2 (`web/build.sh release webgpu`). Bevy
# picks the backend as it is compiled: a browser without WebGPU gets no picture from this build.
webgpu = ["bevy/webgpu"]

[dependencies]
bevy = { version = "0.20.0", features = [
Expand Down
2 changes: 2 additions & 0 deletions macos/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<string>ctr-rs</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.racing-games</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
Expand Down
40 changes: 31 additions & 9 deletions web/build.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#!/bin/sh
# Usage: web/build.sh [release|dev|dist], into target/web. Needs wasm-bindgen-cli at Cargo.lock's
# version, wasm-opt for dist, and an LLVM clang that targets wasm (brew install llvm, or LLVM=<prefix>).
# Usage: web/build.sh [release|dev|dist] [webgl2|webgpu|both], into target/web. Needs
# wasm-bindgen-cli at Cargo.lock's version, wasm-opt for dist, and an LLVM clang that targets wasm
# (brew install llvm, or LLVM=<prefix>). The page draws through WebGPU where the browser has it and
# that build was made, and through WebGL2 elsewhere; with webgpu alone, a browser without WebGPU
# shows nothing.
set -eu
cd "$(dirname "$0")/.."

Expand All @@ -27,12 +30,31 @@ else
features=
names=
fi
cargo build -p client --target wasm32-unknown-unknown --profile "$profile" $features
wasm-bindgen --target web --no-typescript $names --out-dir target/web/play --out-name client \
"target/wasm32-unknown-unknown/$dir/client.wasm"
if [ "$profile" = dist ]; then
wasm-opt -O3 target/web/play/client_bg.wasm -o target/web/play/client_bg.wasm
fi
backends="${2:-webgl2}"
case "$backends" in
webgl2 | webgpu) ;;
both) backends="webgl2 webgpu" ;;
*) echo "unknown backend: $backends (webgl2, webgpu or both)" >&2; exit 1 ;;
esac

rm -rf target/web/play
for backend in $backends; do
if [ "$backend" = webgpu ]; then
with=--features=webgpu
else
with=
fi
cargo build -p client --target wasm32-unknown-unknown --profile "$profile" $features $with
wasm-bindgen --target web --no-typescript $names --out-dir target/web/play \
--out-name "client-$backend" "target/wasm32-unknown-unknown/$dir/client.wasm"
if [ "$profile" = dist ]; then
wasm-opt -O3 "target/web/play/client-${backend}_bg.wasm" \
-o "target/web/play/client-${backend}_bg.wasm"
fi
done
cp web/index.html target/web/
cp web/play.html target/web/play/index.html
# The page picks among the builds made here (`BUILT` in it).
built=$(printf '"%s", ' $backends)
sed "s/^const BUILT = \[\"webgl2\"\];/const BUILT = [${built%, }];/" web/play.html \
>target/web/play/index.html
cp -R web/fonts target/web/
60 changes: 54 additions & 6 deletions web/play.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ <h1><a href="../">CTR-RS</a></h1>
};

// Sound only starts after a key or click, so the game's audio contexts are resumed on one.
// Must run before client.js is imported.
// Must run before the game (`client-*.js`) is imported.
const audio = [];
window.AudioContext = new Proxy(window.AudioContext, {
construct(target, args) {
Expand Down Expand Up @@ -190,16 +190,31 @@ <h1><a href="../">CTR-RS</a></h1>
}
</script>
<script type="module">
import init from "./client.js";
// The builds of the game here, one for each way of drawing: `web/build.sh` puts the ones it made
// in this list.
const BUILT = ["webgl2"];
// WebGPU where the browser has it (and an adapter to give), WebGL2 elsewhere.
const backend = (async () => {
if (BUILT.includes("webgpu")) {
try {
if ((await navigator.gpu?.requestAdapter({ powerPreference: "high-performance" })) != null) {
return "webgpu";
}
} catch (e) {
console.warn(`could not ask for WebGPU: ${e}`);
}
}
return BUILT.includes("webgl2") ? "webgl2" : BUILT[0];
})();

// The game's code is fetched and compiled from the start, while the player picks the disc and it is
// read; `init` falls back to fetching it itself if that fails.
const compiled = WebAssembly.compileStreaming(fetch(new URL("client_bg.wasm", import.meta.url))).catch(
(e) => {
const compiled = backend
.then((b) => WebAssembly.compileStreaming(fetch(new URL(`client-${b}_bg.wasm`, import.meta.url))))
.catch((e) => {
console.warn(`could not compile the game ahead: ${e}`);
return null;
},
);
});

const drop = document.getElementById("drop");
const status = document.getElementById("status");
Expand Down Expand Up @@ -468,6 +483,16 @@ <h1><a href="../">CTR-RS</a></h1>
});
status.textContent = "Starting";
try {
const drawsWith = await backend;
console.info(`ctr-rs: drawing through ${drawsWith}: client-${drawsWith}.js, of ${BUILT.join(", ")}`);
// The fast GPU of a machine with two. Bevy asks WebGPU for it, but wgpu creates the WebGL2
// context without asking: this one is made first, and a canvas hands wgpu the context it
// already has (with the attributes it was made with, so wgpu's own `antialias: false` too). A
// canvas with a WebGL2 context has no WebGPU one, so WebGPU is left to Bevy.
if (drawsWith === "webgl2") {
canvas.getContext("webgl2", { antialias: false, powerPreference: "high-performance" });
}
const { default: init } = await import(`./client-${drawsWith}.js`);
const module = await compiled;
await init(module === null ? undefined : { module_or_path: module });
} catch (e) {
Expand All @@ -482,13 +507,36 @@ <h1><a href="../">CTR-RS</a></h1>
drop.classList.add("hidden");
canvas.focus();
touchGame.hidden = !hasTouch;
keepAwake();
if (copy !== null) remember(copy).then(() => navigator.storage?.persist?.());
}

// The screen stays on while the game runs, which a race driven with a pad (no keys or mouse for the
// system to see) would not. The browser lets go of the lock when the page is hidden, so it is
// asked for again when the page is back; a crash lets go of it (`crashed`).
let wakeLock = null;
let askingWakeLock = false;
async function keepAwake() {
const running = drop.classList.contains("hidden") && crash === null;
if (!running || !navigator.wakeLock || document.visibilityState !== "visible") return;
if (askingWakeLock || (wakeLock !== null && !wakeLock.released)) return;
askingWakeLock = true;
try {
wakeLock = await navigator.wakeLock.request("screen");
if (crash !== null) wakeLock.release().catch(() => {});
} catch (e) {
console.warn(`could not keep the screen on: ${e}`);
} finally {
askingWakeLock = false;
}
}
document.addEventListener("visibilitychange", keepAwake);

let crash = null;
function crashed(message) {
if (crash !== null) return;
crash = message;
wakeLock?.release().catch(() => {});
const starting = !drop.classList.contains("hidden");
audio.forEach((context) => context.close().catch(() => {}));
document.activeElement?.blur();
Expand Down
Loading