Skip to content

Commit 068511e

Browse files
authored
feat(renderer): SDF disk cache (closes #22) + npm packaging (#50)
* feat(renderer): on-disk cache for per-mesh SDFs (closes #22) Cold launches re-baked every per-mesh 32³ R32Float SDF from scratch even though the same content always produces the same voxel data. Sponza's first 9 frames spent ~8 bakes/frame on this path; second launch spent another 9. This change content-hashes (positions + indices) at GPU upload time, checks a platform-appropriate cache directory, and `queue.write_texture`s the cached voxel bytes directly when the file exists — bypassing the GPU dispatch entirely. Misses fall through to the existing bake; the renderer encodes a copy_texture_to_buffer alongside each dispatch and persists the readback to disk after the frame's main submit. The next launch hits and skips the bake. The cache is best-effort throughout: a corrupt entry, missing dir, or write failure silently re-bakes. wasm32 has no filesystem path so load returns None and store is gated out — web builds bake every launch as before. Cache layout: - macOS / iOS / tvOS / watchOS: ~/Library/Caches/bloom/sdf - Linux / Android: $XDG_CACHE_HOME/bloom/sdf - Windows: %LOCALAPPDATA%\bloom\cache\sdf - 16 B header (magic + version + voxel_res) + 128 KB R32Float payload Sponza disk footprint: 68 × 128 KB = 8.7 MB (matches the issue's budget). Disk reads happen synchronously at upload — a 128 KB read from local cache is sub-millisecond. The synchronous device.poll(Wait) on flush blocks for the bake submission to finish before persisting; this is a cold-launch-only stall (~9 frames) and the bake itself is the bottleneck on those frames anyway. Async pipelining is a follow-up if the cold-launch stall ever shows up in profiles. 8 new unit tests cover hash stability, change-detection on positions and indices, count-vs-value distinguishability, store/load round-trip, miss handling, size validation, and bad-magic rejection. cargo test 74/0 (was 66/0) on macOS, wasm32 cargo check clean. * chore(pkg): prep @bloomengine/engine for npm publish Rename from bloom to @bloomengine/engine (Perry FFI module ref updated to match) and add a files: allowlist so the tarball ships just TS sources, Rust crates, shaders/assets, the bloom_jolt shim, and JoltPhysics/Jolt — vendored at publish time rather than fetched at install so installs stay self-contained and reproducible. .npmignore is belt-and-suspenders against target/, pkg/, build/, and the multi-MB Jolt extras (Samples/Docs/UnitTests/Assets/…). scripts/prepack.sh refuses to publish if the Jolt submodule isn't initialised — deliberately not auto-initing so we don't silently publish stale refs. Root MIT LICENSE added so the legal terms travel with the package. * chore(pkg): fix GitHub URLs to match actual Bloom-Engine/engine remote * docs(pkg): npm install instructions and @bloomengine/engine imports Now that the package is published to npm, swap every documented import from "bloom" to "@bloomengine/engine" so the snippets actually resolve against an installed package. README also gains an Install section up front pointing at npm (plus bun/pnpm/yarn equivalents) and the toolchain prereqs (Perry + Rust, wasm-pack for web). * ci(release): publish @bloomengine/engine to npm on tag release Adds a publish-npm job to the existing tag-driven release workflow. Runs after github-release so a failed publish doesn't leave a release-but-no-package state, and after the await-tests gate so we never ship a tag that didn't pass CI. Idempotent: re-checks npm before publishing and skips cleanly if the version already exists (so workflow_dispatch on an old tag won't double-publish). Checks out submodules recursively because the prepack hook refuses to ship without JoltPhysics sources on disk, and uses --provenance for the npm attestation badge. Requires an NPM_TOKEN repo secret with publish rights on the @bloomengine scope. * ci(release): drop NPM_TOKEN, use npm trusted publishing via OIDC The package is now configured on npmjs.com with this workflow as a trusted publisher, so `id-token: write` is sufficient — npm publish exchanges the GitHub OIDC token for a short-lived credential. No long-lived NPM_TOKEN secret to rotate or leak, and provenance attestation is automatic.
1 parent 3720489 commit 068511e

12 files changed

Lines changed: 757 additions & 27 deletions

File tree

.github/workflows/release.yml

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ name: Release
22

33
# Fires on a version tag push (e.g. `v0.3.2`). Gates on the Tests workflow
44
# passing for the exact same commit, then creates/updates the GitHub Release
5-
# and (once wired up) would publish to npm.
5+
# and publishes the package to npm as @bloomengine/engine.
66
#
77
# The /release Claude Code skill in .claude/skills/release/ drives this end
88
# to end: it bumps the version in package.json, commits, tags, pushes, and
99
# waits for this workflow to go green.
10+
#
11+
# Authentication: npm trusted publishing. The @bloomengine/engine package is
12+
# configured on npmjs.com with this workflow (Bloom-Engine/engine →
13+
# .github/workflows/release.yml → job publish-npm) as a trusted publisher.
14+
# `id-token: write` on the publish job is enough — `npm publish` exchanges
15+
# the GitHub OIDC token for a short-lived publish credential, no NPM_TOKEN
16+
# secret needed. Provenance attestation is automatic under this flow.
1017

1118
on:
1219
push:
@@ -147,3 +154,71 @@ jobs:
147154
else
148155
echo "OK package.json version matches tag ($VERSION)"
149156
fi
157+
158+
# ---------------------------------------------------------------------------
159+
# Publish the package to npm as @bloomengine/engine. Runs after the GitHub
160+
# Release so a failure here doesn't leave a dangling release-but-no-package
161+
# state. Skips cleanly if the version is already on the registry, which
162+
# keeps re-runs idempotent (workflow_dispatch on an existing tag won't
163+
# double-publish or fail).
164+
#
165+
# We check out submodules recursively because scripts/prepack.sh refuses
166+
# to ship a tarball without the JoltPhysics sources materialised — the
167+
# package vendors them rather than relying on a postinstall git clone.
168+
# ---------------------------------------------------------------------------
169+
publish-npm:
170+
needs: github-release
171+
runs-on: ubuntu-latest
172+
permissions:
173+
contents: read
174+
id-token: write # required for npm provenance attestations
175+
steps:
176+
- uses: actions/checkout@v4
177+
with:
178+
submodules: recursive
179+
180+
- uses: actions/setup-node@v5
181+
with:
182+
node-version: "24"
183+
registry-url: "https://registry.npmjs.org"
184+
185+
- name: Resolve tag
186+
id: tag
187+
env:
188+
DISPATCH_TAG: ${{ github.event.inputs.tag }}
189+
run: |
190+
if [ -n "$DISPATCH_TAG" ]; then
191+
TAG="$DISPATCH_TAG"
192+
else
193+
TAG="${GITHUB_REF#refs/tags/}"
194+
fi
195+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
196+
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
197+
198+
- name: Verify package.json version matches tag
199+
env:
200+
VERSION: ${{ steps.tag.outputs.version }}
201+
TAG: ${{ steps.tag.outputs.tag }}
202+
run: |
203+
PKG_VERSION=$(node -p "require('./package.json').version")
204+
if [ "$PKG_VERSION" != "$VERSION" ]; then
205+
echo "::error::Tag $TAG ($VERSION) does not match package.json ($PKG_VERSION) — refusing to publish."
206+
exit 1
207+
fi
208+
209+
- name: Check if version already published
210+
id: check
211+
run: |
212+
PKG_NAME=$(node -p "require('./package.json').name")
213+
PKG_VERSION=$(node -p "require('./package.json').version")
214+
if npm view "$PKG_NAME@$PKG_VERSION" version >/dev/null 2>&1; then
215+
echo "$PKG_NAME@$PKG_VERSION is already on the registry — skipping publish."
216+
echo "skip=true" >> "$GITHUB_OUTPUT"
217+
else
218+
echo "$PKG_NAME@$PKG_VERSION not yet published — will publish."
219+
echo "skip=false" >> "$GITHUB_OUTPUT"
220+
fi
221+
222+
- name: Publish to npm
223+
if: steps.check.outputs.skip == 'false'
224+
run: npm publish --provenance --access public

.npmignore

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# The `files:` field in package.json is the source of truth for what
2+
# ships. This .npmignore is belt-and-suspenders — it strips artifacts
3+
# from globbed directories that `files:` pulls in, so we never ship
4+
# build output, IDE state, or local caches even by accident.
5+
6+
# Rust build output
7+
target/
8+
**/target/
9+
*.rlib
10+
*.rmeta
11+
12+
# wasm-pack output (built per consumer; not part of the package)
13+
native/web/pkg/
14+
15+
# Native build dirs
16+
native/third_party/bloom_jolt/build/
17+
native/third_party/JoltPhysics/Build/
18+
19+
# Jolt submodule extras we don't need at consumer build time.
20+
# We only ship JoltPhysics/Jolt/ (the actual sources our cmake builds
21+
# against) plus LICENSE + README. Everything else is samples, viewer,
22+
# docs, assets, tests — multi-MB and irrelevant for embedding.
23+
native/third_party/JoltPhysics/.git
24+
native/third_party/JoltPhysics/.github/
25+
native/third_party/JoltPhysics/Assets/
26+
native/third_party/JoltPhysics/Build/
27+
native/third_party/JoltPhysics/Docs/
28+
native/third_party/JoltPhysics/HelloWorld/
29+
native/third_party/JoltPhysics/JoltViewer/
30+
native/third_party/JoltPhysics/PerformanceTest/
31+
native/third_party/JoltPhysics/Samples/
32+
native/third_party/JoltPhysics/TestFramework/
33+
native/third_party/JoltPhysics/UnitTests/
34+
native/third_party/JoltPhysics/Doxyfile
35+
native/third_party/JoltPhysics/run_doxygen.bat
36+
native/third_party/JoltPhysics/sonar-project.properties
37+
native/third_party/JoltPhysics/ContributorAgreement.md
38+
39+
# Perry build artifacts
40+
*.ts.o
41+
*_ts.o
42+
.perry-cache/
43+
dist/
44+
45+
# OS / editor junk
46+
.DS_Store
47+
.vscode/
48+
.idea/
49+
*.swp
50+
51+
# Local-only state never meant for the registry
52+
.claude/
53+
node_modules/
54+
package-lock.json

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Bloom Engine
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,33 @@
55
Write TypeScript. Ship native games — and now the web too.
66
Bloom compiles your game to Metal, DirectX 12, Vulkan, OpenGL, and WebGPU — one codebase for every platform.
77

8+
## Install
9+
10+
```bash
11+
npm install @bloomengine/engine
12+
```
13+
14+
Or with your preferred package manager:
15+
16+
```bash
17+
bun add @bloomengine/engine
18+
pnpm add @bloomengine/engine
19+
yarn add @bloomengine/engine
20+
```
21+
22+
The npm package ships the TypeScript API alongside the engine's Rust sources and the bundled [JoltPhysics](https://github.com/jrouwe/JoltPhysics) C++ shim, so a single `install` is enough — there's no separate native download step.
23+
24+
You'll also need:
25+
26+
- **Perry** — the TypeScript AOT compiler that turns your game into a native binary or WASM module. It also drives the engine's native build.
27+
- **Rust toolchain** ([rustup.rs](https://rustup.rs)) — Perry invokes Cargo to compile the engine's platform crate the first time you build for each target.
28+
- For web builds only: [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) (`cargo install wasm-pack`).
29+
830
## Quick Start
931

1032
```typescript
1133
import { initWindow, windowShouldClose, beginDrawing,
12-
endDrawing, clearBackground, drawText, Colors } from "bloom";
34+
endDrawing, clearBackground, drawText, Colors } from "@bloomengine/engine";
1335

1436
initWindow(800, 450, "My Game");
1537

@@ -26,7 +48,7 @@ while (!windowShouldClose()) {
2648
Use `runGame()` for code that works on both native and web:
2749

2850
```typescript
29-
import { initWindow, runGame, clearBackground, drawText, Colors } from "bloom";
51+
import { initWindow, runGame, clearBackground, drawText, Colors } from "@bloomengine/engine";
3052

3153
initWindow(800, 450, "My Game");
3254

@@ -55,14 +77,14 @@ cd dist/web && python3 -m http.server 8080
5577

5678
| Module | Import | Description |
5779
|--------|--------|-------------|
58-
| **Core** | `bloom/core` | Window, game loop, input, timing |
59-
| **Shapes** | `bloom/shapes` | 2D drawing + collision detection |
60-
| **Textures** | `bloom/textures` | Image loading, sprite batching |
61-
| **Text** | `bloom/text` | TTF/OTF font loading and rendering |
62-
| **Audio** | `bloom/audio` | Sound effects + music streaming |
63-
| **Models** | `bloom/models` | 3D model loading (glTF, OBJ), skeletal animation |
64-
| **Math** | `bloom/math` | Vectors, matrices, quaternions, easing |
65-
| **Physics** | `bloom/physics` | Jolt-backed rigid + soft bodies, character, vehicles ([docs](docs/physics.md)) |
80+
| **Core** | `@bloomengine/engine/core` | Window, game loop, input, timing |
81+
| **Shapes** | `@bloomengine/engine/shapes` | 2D drawing + collision detection |
82+
| **Textures** | `@bloomengine/engine/textures` | Image loading, sprite batching |
83+
| **Text** | `@bloomengine/engine/text` | TTF/OTF font loading and rendering |
84+
| **Audio** | `@bloomengine/engine/audio` | Sound effects + music streaming |
85+
| **Models** | `@bloomengine/engine/models` | 3D model loading (glTF, OBJ), skeletal animation |
86+
| **Math** | `@bloomengine/engine/math` | Vectors, matrices, quaternions, easing |
87+
| **Physics** | `@bloomengine/engine/physics` | Jolt-backed rigid + soft bodies, character, vehicles ([docs](docs/physics.md)) |
6688

6789
## Platforms
6890

@@ -143,7 +165,7 @@ Bloom supports GPU-accelerated skeletal animation via glTF/GLB models. The pipel
143165

144166
```typescript
145167
import { loadModel, loadModelAnimation, updateModelAnimation, drawModel,
146-
getTime, Colors } from "bloom";
168+
getTime, Colors } from "@bloomengine/engine";
147169

148170
const character = loadModel("assets/models/character.glb");
149171
const anim = loadModelAnimation("assets/models/character.glb");

docs/skeletal-animation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Joint matrices are written to the GPU in `end_frame()` via `flush_joint_matrices
136136
### Loading
137137

138138
```typescript
139-
import { loadModel, loadModelAnimation, drawModel, updateModelAnimation } from "bloom";
139+
import { loadModel, loadModelAnimation, drawModel, updateModelAnimation } from "@bloomengine/engine";
140140

141141
// Load the mesh (vertices with skin data: JOINTS_0 + WEIGHTS_0)
142142
const model = loadModel("assets/models/character.glb");
@@ -179,7 +179,7 @@ drawModel(model, { x: playerX, y: playerY, z: playerZ }, 1.0, WHITE);
179179
```typescript
180180
import { initWindow, windowShouldClose, beginDrawing, endDrawing,
181181
clearBackground, loadModel, loadModelAnimation,
182-
updateModelAnimation, drawModel, getTime, Colors } from "bloom";
182+
updateModelAnimation, drawModel, getTime, Colors } from "@bloomengine/engine";
183183

184184
initWindow(800, 600, "Animation Demo");
185185

docs/web-target.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ python3 -m http.server 8080
5454
Browsers cannot run blocking `while` loops. Use `runGame()` instead:
5555

5656
```typescript
57-
import { initWindow, runGame, clearBackground, drawRect, Colors } from "bloom";
57+
import { initWindow, runGame, clearBackground, drawRect, Colors } from "@bloomengine/engine";
5858

5959
initWindow(800, 600, "My Game");
6060

@@ -113,7 +113,7 @@ if (fileExists("save.json")) {
113113
## Platform Detection
114114

115115
```typescript
116-
import { getPlatform, Platform } from "bloom";
116+
import { getPlatform, Platform } from "@bloomengine/engine";
117117

118118
if (getPlatform() === Platform.WEB) {
119119
// web-specific code

native/shared/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub mod postfx;
2020
pub mod custom_shaders;
2121
pub mod staging;
2222
pub mod profiler;
23+
pub mod sdf_cache;
2324
// Jolt C ABI + Rust wrapper live on native only. On wasm32 the web crate
2425
// routes bloom_physics_* calls through wasm_bindgen to JoltPhysics.js;
2526
// no Rust-side Jolt integration is needed.

0 commit comments

Comments
 (0)