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
4 changes: 3 additions & 1 deletion crates/nexus_mpm2d/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[package]
name = "nexus_mpm2d"
authors = { workspace = true }
description = "Cross-platform 2D GPU-accelerated rigid-body physics."
description = "Cross-platform 2D GPU-accelerated MPM physics."
repository = { workspace = true }
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
readme = { workspace = true }
exclude = { workspace = true }

[lib]
name = "nexus_mpm2d"
Expand Down
2 changes: 2 additions & 0 deletions crates/nexus_mpm3d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ repository = { workspace = true }
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
readme = { workspace = true }
exclude = { workspace = true }

[lib]
name = "nexus_mpm3d"
Expand Down
2 changes: 2 additions & 0 deletions crates/nexus_mpm_shaders2d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ repository = { workspace = true }
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
readme = { workspace = true }
exclude = { workspace = true }
links = "nexus_mpm_shaders2d"
build = "build.rs"

Expand Down
2 changes: 2 additions & 0 deletions crates/nexus_mpm_shaders3d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ repository = { workspace = true }
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
readme = { workspace = true }
exclude = { workspace = true }
links = "nexus_mpm_shaders3d"
build = "build.rs"

Expand Down
183 changes: 167 additions & 16 deletions publish.sh
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
#! /bin/bash
#
# Publishes every nexus crate to crates.io with a single
# `cargo publish --workspace` invocation:
# Publishes every nexus crate to crates.io:
#
# - nexus2d / nexus3d
# - nexus_rbd2d / nexus_rbd3d
# - nexus_rbd_shaders2d / nexus_rbd_shaders3d
# - nexus_mpm2d / nexus_mpm3d
# - nexus_mpm_shaders2d / nexus_mpm_shaders3d
# - nexus_viewer2d / nexus_viewer3d
#
# Cargo computes the dependency order itself and waits for each crate to become
# available on the registry before publishing the ones that depend on it. The
# example crates and the python-binding crate are marked `publish = false`, so
# `--workspace` skips them.
# Publishing happens in waves (see `WAVES` below); within a wave cargo computes
# the dependency order itself and waits for each crate to become available on
# the registry before publishing the ones that depend on it. The example crates
# and the python-binding crate are marked `publish = false` and are simply not
# listed.
#
# Why this script exists
# ----------------------
# Each 2d/3d crate pair shares a single source tree at the repo root,
# referenced from each manifest as `path = "../../<shared>/lib.rs"`:
#
# - nexus2d / nexus3d -> src
# - nexus_rbd2d / nexus_rbd3d -> src_rbd
# - nexus2d / nexus3d -> src
# - nexus_rbd2d / nexus_rbd3d -> src_rbd
# - nexus_rbd_shaders2d / nexus_rbd_shaders3d -> src_rbd_shaders
# - nexus_viewer2d / nexus_viewer3d -> src_viewer
# - nexus_mpm2d / nexus_mpm3d -> src_mpm
# - nexus_mpm_shaders2d / nexus_mpm_shaders3d -> src_mpm_shaders
# - nexus_viewer2d / nexus_viewer3d -> src_viewer
#
# Those paths point outside the crate directory, which `cargo publish` refuses
# to package.
Expand All @@ -35,12 +39,37 @@
# trap restores the manifests and removes the symlinks on exit (including on
# error or Ctrl-C), leaving the tree exactly as it was.
#
# Extra arguments are forwarded to `cargo publish`, e.g.:
# ./publish.sh --dry-run
# Why the waves
# -------------
# When several crates are published at once, cargo verifies them against a
# temporary local registry (`target/package/tmp-registry`) holding the siblings
# it is about to upload. That overlay is only configured for the verification
# build itself: the *nested* cargo that a build script spawns does not see it.
#
# The host crates compile their shaders through `khal-builder`, which runs
# `cargo gpu build` on the packaged shader crate; that nested cargo resolves the
# shader crate's own dependencies against the real crates.io. So any nexus crate
# a shader crate depends on must already be live on crates.io before the crates
# that compile that shader can be verified, which the waves below guarantee.
#
# "Live" means served by the index, not merely uploaded: between waves the
# script polls index.crates.io itself, because `cargo publish` only warns (it
# does not fail) when a crate has not propagated within its own timeout.
#
# Re-running after a failure is safe: crates already published at this version
# are skipped, and the leftovers of the failed run are purged first (see
# `purge_publish_cache`).
#
# Extra arguments are forwarded to every `cargo publish` invocation, e.g.:
# ./publish.sh --dry-run --no-verify
# ./publish.sh --token "$CARGO_TOKEN"
#
# Requires cargo >= 1.90 (for `cargo publish --workspace`). The shader crates
# compile their SPIR-V in build.rs during the verification build, so the
# Note that a plain `--dry-run` only rehearses the first wave: nothing is
# actually uploaded, so the later waves fail to verify for the reason described
# above. Add `--no-verify` to rehearse the packaging of the whole set.
#
# Requires cargo >= 1.90 (for publishing several crates at once). The shader
# crates compile their SPIR-V in build.rs during the verification build, so the
# rust-gpu toolchain must be installed (`cargo gpu install`).

set -euo pipefail
Expand All @@ -57,10 +86,27 @@ CRATES=(
nexus_rbd3d:src_rbd
nexus_rbd_shaders2d:src_rbd_shaders
nexus_rbd_shaders3d:src_rbd_shaders
nexus_mpm2d:src_mpm
nexus_mpm3d:src_mpm
nexus_mpm_shaders2d:src_mpm_shaders
nexus_mpm_shaders3d:src_mpm_shaders
nexus_viewer2d:src_viewer
nexus_viewer3d:src_viewer
)

# Publication waves, published one after the other. A crate may only share a
# wave with a shader crate it compiles; every *other* nexus crate that shader
# crate depends on must be live on crates.io by then (see "Why the waves").
# Today the only such edge is nexus_mpm_shaders* -> nexus_rbd_shaders*, so two
# waves are enough. Adding a shader crate that depends on another nexus shader
# crate means adding a wave.
WAVES=(
"nexus_rbd_shaders2d nexus_rbd_shaders3d"
"nexus_rbd2d nexus_rbd3d nexus_mpm_shaders2d nexus_mpm_shaders3d nexus_mpm2d nexus_mpm3d nexus2d nexus3d nexus_viewer2d nexus_viewer3d"
)

VERSION="$(sed -n '/^\[workspace.package\]/,/^\[/ s/^version = "\(.*\)"/\1/p' Cargo.toml)"

# Refuse to run on a dirty tree: the only diff during publishing must be our own
# temporary edits, so the restore at the end is guaranteed to be correct.
if [ -n "$(git status --porcelain)" ]; then
Expand Down Expand Up @@ -106,6 +152,111 @@ for entry in "${CRATES[@]}"; do
link_shared "${entry%%:*}" "${entry#*:}"
done

# Publish the whole workspace. `--allow-dirty` is required because our temporary
# edits make the tree dirty; the clean-tree check above keeps that safe.
cargo publish --workspace --allow-dirty "$@"
# True if `$1` is already on crates.io at `$VERSION`. Since the waves upload
# incrementally, a run that fails halfway leaves the earlier waves published;
# skipping them makes the script re-runnable. An unreachable index means "not
# published", so a network hiccup can never silently skip a crate.
already_published() {
local crate="$1" prefix body
case "${#crate}" in
1 | 2) prefix="${#crate}" ;;
3) prefix="3/${crate:0:1}" ;;
*) prefix="${crate:0:2}/${crate:2:2}" ;;
esac
body="$(curl -sf "https://index.crates.io/$prefix/$crate" || true)"
case "$body" in
*"\"vers\":\"$VERSION\""*) return 0 ;;
*) return 1 ;;
esac
}

# Block until every crate named in `$@` is actually served by the index.
# `cargo publish` does wait for propagation on its own, but it gives up after a
# timeout with a warning rather than an error; the next wave then starts while
# crates.io still serves the previous version, and the nested cargo-gpu
# resolution fails exactly as if the wave order were wrong.
wait_for_index() {
local deadline=$((SECONDS + 900)) crate pending

while :; do
pending=""
for crate in "$@"; do
already_published "$crate" || pending="$pending $crate"
done

if [ -z "$pending" ]; then
# Our probe and cargo's own fetch may land on different CDN edges,
# so let the slower ones catch up before building against them.
sleep 20
return
fi

if [ "$SECONDS" -ge "$deadline" ]; then
echo "error: timed out waiting for crates.io to serve:$pending" >&2
exit 1
fi

echo "waiting for crates.io to serve:$pending"
sleep 10
done
}

# Cargo unpacks the temporary publish registry into `$CARGO_HOME/registry` and
# keys those copies by name and version alone, so a previous run of the same
# version leaves sources behind that the next run silently reuses, lockfile
# checksums included. Once a wave has been uploaded those checksums no longer
# match what crates.io serves, and the nested cargo-gpu resolution dies with
# "checksum for <crate> changed between lock files". Dropping the stale copies
# makes every run start from freshly packaged sources. Only our own crates are
# removed, and only from the `file://` overlay registries (their directory name
# starts with `-`, since such a URL has no host); crates.io is left alone.
purge_publish_cache() {
local registry_dir entry crate

rm -rf target/package

for registry_dir in "${CARGO_HOME:-$HOME/.cargo}"/registry/src/-* \
"${CARGO_HOME:-$HOME/.cargo}"/registry/cache/-*; do
[ -d "$registry_dir" ] || continue
for entry in "${CRATES[@]}"; do
crate="${entry%%:*}"
rm -rf "$registry_dir/$crate-$VERSION" "$registry_dir/$crate-$VERSION.crate"
done
done
}

purge_publish_cache

# `--dry-run` uploads nothing, so there is never anything to wait for.
dry_run=0
for arg in "$@"; do
if [ "$arg" = "--dry-run" ]; then
dry_run=1
fi
done

# `--allow-dirty` is required because our temporary edits make the tree dirty;
# the clean-tree check above keeps that safe.
for wave in "${WAVES[@]}"; do
pkgs=()
names=""
for crate in $wave; do
if already_published "$crate"; then
echo "skipping $crate $VERSION: already on crates.io"
else
pkgs+=(-p "$crate")
names="$names $crate"
fi
done

if [ "${#pkgs[@]}" -eq 0 ]; then
continue
fi

echo "publishing wave:$names"
cargo publish "${pkgs[@]}" --allow-dirty "$@"

if [ "$dry_run" -eq 0 ]; then
wait_for_index $names
fi
done
3 changes: 2 additions & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@
},
"engines": {
"node": ">=20.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
50 changes: 35 additions & 15 deletions website/scripts/build-demos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,35 +173,55 @@ write_index_html() {
}
.error {
color: #ff6b6b;
line-height: 1.5;
}
.error::after {
display: none;
}
.error a {
color: #ff6b6b;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="loading" id="loading">Loading WebAssembly...</div>
<script type="module">
import init from './pkg/example.js';

const loading = document.getElementById('loading');

if (!navigator.gpu) {
const showError = (html) => {
loading.className = 'loading error';
loading.textContent = 'WebGPU is not available in this browser. '
+ 'Nexus runs its physics as WebGPU compute shaders. '
+ 'On Firefox, enable dom.webgpu.enabled in about:config; '
+ 'on Chromium, enable "Unsafe WebGPU Support" in chrome://flags. '
+ 'Safari is currently not supported.';
loading.innerHTML = html;
};

// Safari matches every WebKit-based UA except the Chromium/Firefox ones,
// which advertise themselves as Safari too.
const isSafari = /^((?!chromium|chrome|crios|fxios|edgios|android).)*safari/i
.test(navigator.userAgent);

// Both checks run before the demo module is imported, so an unsupported
// browser never downloads the (large) WASM app.
if (isSafari) {
showError('Safari is currently not supported by nexus. '
+ 'Please open this demo in a recent Chromium-based browser or in Firefox.');
} else if (!navigator.gpu) {
showError('WebGPU is required: nexus runs its physics as WebGPU compute '
+ 'shaders, and WebGPU is not available in this browser. See '
+ '<a href="https://caniuse.com/webgpu" target="_blank" rel="noopener noreferrer">'
+ 'caniuse.com/webgpu</a> for browser support. On Firefox, enable '
+ 'dom.webgpu.enabled in about:config; on Chromium, enable '
+ '"Unsafe WebGPU Support" in chrome://flags.');
} else {
init().then(() => {
loading.style.display = 'none';
}).catch(err => {
console.error('WASM Error:', err);
loading.className = 'loading error';
loading.textContent = 'Error: ' + err.message;
});
import('./pkg/example.js')
.then(({default: init}) => init())
.then(() => {
loading.style.display = 'none';
})
.catch(err => {
console.error('WASM Error:', err);
loading.className = 'loading error';
loading.textContent = 'Error: ' + err.message;
});
}
</script>
</body>
Expand Down
28 changes: 21 additions & 7 deletions website/src/pages/demos.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,27 @@
}
}

/* WebGPU warning */
.webgpuWarning {
padding: 0.75rem 1rem;
background: var(--ifm-color-warning-contrast-background, #fff8e6);
color: var(--ifm-color-warning-contrast-foreground, #4d3800);
border-bottom: 1px solid var(--ifm-color-emphasis-300);
font-size: 0.9rem;
/* Shown instead of the viewer when the browser can't run the demos */
.unsupported {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.75rem;
padding: 2rem;
text-align: center;
}

.unsupported h2 {
margin: 0;
font-size: 1.5rem;
}

.unsupported p {
margin: 0;
max-width: 42rem;
color: var(--ifm-color-emphasis-700);
}

/* Viewer */
Expand Down
Loading
Loading