diff --git a/CLAUDE.md b/CLAUDE.md index eae42468..8c672361 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with the Bloom Engine co ## Project Overview -Bloom is a native TypeScript game engine compiled by [Perry](../../perry/perry) (a TypeScript AOT compiler). It provides a simple, function-based API for 2D/3D games that compiles to Metal, DirectX 12, Vulkan, OpenGL, and WebGPU. +Bloom is a native TypeScript game engine compiled by [Perry](https://github.com/PerryTS/perry) (a TypeScript AOT compiler). It provides a simple, function-based API for 2D/3D games that compiles to Metal, DirectX 12, Vulkan, OpenGL, and WebGPU. ## Build Commands @@ -117,7 +117,9 @@ String parameters are `i64` on native (Perry StringHeader pointers) and NaN-boxe (`bloom_mesh_scratch_*`) like createMesh does. - Engine TS in `src/` is compiled by Perry too, so Perry codegen quirks apply here as well (no reachable `throw`, explicit object keys in - returns — the shooter's `docs/perry-quirks.md` is the reference list). + returns — the shooter's + [`docs/perry-quirks.md`](https://github.com/Bloom-Engine/shooter/blob/main/docs/perry-quirks.md) + is the reference list). ### Runtime/debug behavior worth knowing @@ -179,5 +181,7 @@ The web crate exposes `_str` variants (accepting `&str`) and `_bytes` variants ( - `docs/crash-triage-windows.md` — native-fault runbook (the engine self-reports crashes since 2026-07). - The Bloom Shooter (`../shooter`) is the flagship consumer; its - `CLAUDE.md` + `docs/perry-quirks.md` document the Perry-side rules + `CLAUDE.md` plus the shooter's + [`docs/perry-quirks.md`](https://github.com/Bloom-Engine/shooter/blob/main/docs/perry-quirks.md) + document the Perry-side rules games (and engine `src/` TS) must follow. diff --git a/README.md b/README.md index 6150ee0a..93c36922 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ Bloom compiles your game to Metal, DirectX 12, Vulkan, OpenGL, and WebGPU — on > API on raylib's — in our view one of the best API designs in gamedev. Bloom is an > independent implementation, not a port — [how Bloom relates to raylib »](#how-bloom-relates-to-raylib) +> **Release status:** this branch documents the upcoming 0.5 API. The latest stable +> npm release is 0.4.16; shared quick-start code works on both, while breaking +> field and convention changes are listed in the +> [0.5 migration guide](docs/migration-0.5.md). + ## Install ```bash @@ -23,13 +28,13 @@ pnpm add @bloomengine/engine yarn add @bloomengine/engine ``` -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. +The npm package ships the TypeScript API, Rust sources, and the [JoltPhysics](https://github.com/jrouwe/JoltPhysics) C++ fallback. It also installs compatible prebuilt Jolt libraries through `@bloomengine/jolt-prebuilt`, so supported targets normally avoid the C++ build without requiring a separate download. You'll also need: - **Perry** — the TypeScript AOT compiler that turns your game into a native binary or WASM module. It also drives the engine's native build. - **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. -- For web builds only: [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) (`cargo install wasm-pack`). +- For web builds only: [wasm-pack](https://crates.io/crates/wasm-pack) (`cargo install wasm-pack`). ## Quick Start @@ -65,15 +70,15 @@ runGame((dt) => { Build for web: ```bash -./native/web/build.sh main.ts +npm exec -- bloom-web main.ts --output dist/web cd dist/web && python3 -m http.server 8080 ``` ## Features -- **Simple API** — Functions, not classes. The entire API fits on a cheatsheet. ([design rationale](docs/design-api.md)) +- **Simple API** — A function-first gameplay API with plain data handles. ([design rationale](docs/design-api.md)) - **True native** — Compiles to Metal, DirectX 12, Vulkan, OpenGL, and WebGPU via wgpu. -- **Ship everywhere** — macOS, Windows, Linux, iOS, tvOS, Android, and Web from one codebase. +- **Ship everywhere** — macOS, Windows, Linux, iOS, tvOS, watchOS, visionOS, Android, and Web from one codebase. - **Unified 2D/3D** — Shapes, textures, text, 3D models, and audio in one engine. - **Coherent quality tiers** — Resolution, TAA, upscale filtering, sharpening, and effects move together from 0.50-scale Off to native-resolution Ultra. @@ -84,7 +89,7 @@ cd dist/web && python3 -m http.server 8080 Bloom's public API is heavily inspired by [raylib](https://github.com/raysan5/raylib). raylib's API is, in our opinion, one of the best in the gamedev space — a flat library -of plain functions, no classes, small enough to learn from a cheatsheet — so we model +of plain functions, no gameplay object hierarchy, small enough to learn from a cheatsheet — so we model ours on it. You'll recognize the shape immediately: `initWindow`, `beginDrawing`, `clearBackground`, `drawText`, and modules named core / shapes / textures / text / audio / models. @@ -113,6 +118,7 @@ setting the bar. ([full design rationale](docs/design-api.md)) | **VFX** | `@bloomengine/engine/vfx` | GPU particle systems + decals | | **World** | `@bloomengine/engine/world` | `.world.json` loading, validation, instantiation ([docs](docs/world-format.md)) | | **Mobile** | `@bloomengine/engine/mobile` | Virtual joystick/buttons, touch-input helpers | +| **Quality** | `@bloomengine/engine/quality` | Deterministic quality-run and capture helpers | ## Platforms @@ -124,6 +130,7 @@ setting the bar. ([full design rationale](docs/design-api.md)) | iOS | Metal | Touch + gamepad | | tvOS | Metal | Siri Remote + gamepad | | watchOS | SwiftUI Canvas (2D) + SceneKit (3D) | Digital Crown + taps ([docs](docs/watchos-target.md)) | +| visionOS | Metal | Spatial input | | Android | Vulkan / OpenGL ES | Touch + gamepad | | **Web** | **WebGPU / WebGL** | **Keyboard + mouse + touch + gamepad** | @@ -138,6 +145,12 @@ src/ TypeScript API audio/ Sound + music models/ 3D models math/ Vectors, matrices, easing + mobile/ Touch controls + scene/ Retained scene graph + physics/ Jolt physics + world/ World-format runtime + vfx/ Particles and decals + quality/ Qualification helpers native/ Rust implementations shared/ Cross-platform core (wgpu, fontdue, gltf) @@ -148,6 +161,8 @@ native/ Rust implementations linux/ Vulkan/OpenGL + X11 + ALSA android/ Vulkan/OpenGL ES + NativeActivity + AAudio web/ WebGPU/WebGL + Canvas + Web Audio (WASM) + watchos/ SwiftUI Canvas + SceneKit + visionos/ Metal + UIKit examples/ pong/ Complete working example (~170 lines) @@ -158,8 +173,8 @@ examples/ Install Node.js, Python 3.11 or newer, the stable Rust toolchain with `rustfmt`, `clippy`, and the `wasm32-unknown-unknown` target. The full and web lanes also require -[wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) and Chrome or -Chromium for the real-browser WebGPU smoke. Native builds need the platform +[wasm-pack](https://crates.io/crates/wasm-pack) and a current WebGPU-capable +browser for the real-browser smoke. Native builds need the platform dependencies listed in `.github/workflows/test.yml` (CMake and a C++ compiler everywhere, X11/audio development packages on Linux, and the MSVC developer environment on Windows). @@ -195,7 +210,7 @@ command inventory cannot silently differ from local development. ## Types -Plain interfaces, no classes: +Gameplay-facing resource and math types are plain interfaces: ```typescript interface Vec2 { x: number; y: number } @@ -203,7 +218,7 @@ interface Vec3 { x: number; y: number; z: number } interface Color { r: number; g: number; b: number; a: number } interface Rect { x: number; y: number; width: number; height: number } interface Camera2D { offset: Vec2; target: Vec2; rotation: number; zoom: number } -interface Camera3D { position: Vec3; target: Vec3; up: Vec3; fovy: number; projection: number } +interface Camera3D { position: Vec3; target: Vec3; up: Vec3; fovy: number; projection: "perspective" | "orthographic" } interface Texture { handle: number; width: number; height: number } interface Sound { handle: number } interface Model { handle: number } @@ -240,18 +255,25 @@ const character = loadModel("assets/models/character.glb"); const anim = loadModelAnimation("assets/models/character.glb"); // In your game loop: -updateModelAnimation(anim, 0, getTime(), 1.0, 0, 0, 0); +updateModelAnimation(anim, 0, getTime(), 1.0, 0, 0, 0, 0); drawModel(character, { x: 0, y: 0, z: 0 }, 1.0, Colors.WHITE); ``` Key functions: - `loadModel(path)` -- loads GLB with skin data (JOINTS_0, WEIGHTS_0) - `loadModelAnimation(path)` -- loads skeleton + animation channels from GLB -- `updateModelAnimation(handle, animIndex, time, scale, px, py, pz)` -- samples animation, computes joint matrices +- `updateModelAnimation(handle, animIndex, time, scale, px, py, pz, rotY)` -- samples animation, computes joint matrices - `drawModel(model, position, scale, tint)` -- renders with GPU skinning For the full pipeline (Blender export, pitfalls, architecture), see [docs/skeletal-animation.md](docs/skeletal-animation.md). +## Documentation languages + +The API reference and repository documentation are maintained in English. The +website's marketing pages are localized, but the language switcher labels the +technical reference as English-only rather than presenting unreviewed machine +translations as authoritative documentation. + ## Made with Bloom **[Bloom Jump](https://apps.apple.com/us/app/bloom-jump/id6761447092)** — our first shipped game and a proof point for the engine. A free retro pixel platformer with five hand-crafted levels, 60 FPS, and an original chiptune soundtrack, built entirely with Bloom from one TypeScript codebase running natively on every target. diff --git a/bloom-renderer-spec-v2.md b/bloom-renderer-spec-v2.md index 6de9ed40..4c97fdfb 100644 --- a/bloom-renderer-spec-v2.md +++ b/bloom-renderer-spec-v2.md @@ -7,9 +7,10 @@ philosophy lives on in `README.md` and the raylib-modeled `src/` API). Both were removed 2026-07-06; git history has them. -## Status vs. plan (as-built, 2026-07-16) +## Historical status snapshot (2026-07-16) -This document is the *plan*; the code has made choices where the plan +This section is retained as a dated snapshot, not current as-built +documentation. This document is the *plan*; the code has made choices where the plan offered options, and diverged where reality was cheaper: - **API/backends:** wgpu 29 (DX12/Metal/Vulkan/WebGPU through one diff --git a/docs/design-api.md b/docs/design-api.md index 0cdd0535..0814fa23 100644 --- a/docs/design-api.md +++ b/docs/design-api.md @@ -1,8 +1,14 @@ # API Design — Functions, Not Classes -Bloom's public API is a flat collection of free functions operating on plain-data -interfaces. There are no classes, no inheritance trees, no `this`-bound methods, -and no lifecycle base types to extend. This document records *why*. +Bloom's gameplay-facing API is a flat collection of free functions operating on +plain-data interfaces. There are no gameplay resource classes, inheritance +trees, `this`-bound engine methods, or lifecycle base types to extend. This +document records *why*. + +The `quality` tooling submodule is the deliberate exception: its inert +`QualityRun` orchestration helper is a class for qualification scripts. It does +not represent an engine resource or cross the Perry FFI boundary described +below. The short version: the industry's most-cited performance voices, the architectural trend in every major engine, and the practical constraints of our Perry FFI all @@ -10,7 +16,8 @@ point the same direction. Classes would be fighting three fights at once. ## The stated rationale (README) -> **Simple API** — Functions, not classes. The entire API fits on a cheatsheet. +> **Simple API** — Gameplay through functions and plain handles. The runtime API +> fits on a cheatsheet. That one-liner captures the user-facing benefit. The rest of this document captures the engineering reasons behind it. @@ -95,12 +102,13 @@ aesthetic call — it's where the industry has been migrating for a decade. ## The Bloom-specific reason: the Perry FFI boundary -Bloom compiles TypeScript through [Perry](../../perry/perry) (our AOT compiler) +Bloom compiles TypeScript through [Perry](https://github.com/PerryTS/perry) (our AOT compiler) and hands data across an FFI boundary to platform-specific Rust crates. The boundary has a specific shape, documented in `CLAUDE.md` and `package.json`: -- **~465 `bloom_*` FFI functions** declared in `package.json` under - `perry.nativeLibrary.functions`. +- **The versioned `bloom_*` FFI surface** is declared in `package.json` under + `perry.nativeLibrary.functions`; CI derives its count and validates every + platform against that manifest. - **Native platforms** use `#[no_mangle] extern "C"` — a C ABI. - **Web** uses `#[wasm_bindgen]`; Perry's runtime decodes NaN-boxed args (`wrapFfiForI64`) and the JS glue routes strings to `_str` variants. @@ -168,7 +176,8 @@ This section is deliberately here to keep the doc honest. explicitly unloaded. TypeScript has no destructors, and the FFI boundary would not respect them even if it did. - **No "smart" object APIs that discover methods via IDE autocomplete.** You - navigate by module (`bloom/textures`, `bloom/audio`) and function name. + navigate by module (`@bloomengine/engine/textures`, + `@bloomengine/engine/audio`) and function name. The [cheatsheet](../README.md#modules) is the map. We've judged these acceptable — and in several cases desirable — given the diff --git a/docs/ios-target.md b/docs/ios-target.md index 61877363..26683494 100644 --- a/docs/ios-target.md +++ b/docs/ios-target.md @@ -85,9 +85,10 @@ reads slot 0 — released, but still holding its last coordinates — as if it w live, which presents as a finger frozen where it left the glass. Scan `0..getMaxTouchPoints()` and skip slots that `isTouchActive(i)` rejects. -Gamepad is **not** implemented on iOS (`GCController` is never polled), despite -the framework being linked. `isGamepadAvailable()` returns false. tvOS has the -code to copy if this is ever needed. +Gamepad input polls the first connected extended controller through +`GCController`. MFi, Xbox, and PlayStation-compatible controllers populate six +axes (two sticks and two triggers), face/shoulder buttons, and the D-pad. The +mapping is compile-verified and still needs broader on-device coverage. ## Renderer notes @@ -103,9 +104,9 @@ code to copy if this is ever needed. ## Known gaps -- **No CI build.** No workflow compiles `native/ios/`; the only iOS gate is - `tools/validate-ffi.js`, which parses `lib.rs` for symbol names and proves - nothing about whether the crate compiles or runs. +- **Compile-only CI.** The mobile-target matrix compiles `native/ios/` for an + arm64 device and both arm64/x86_64 simulators. It does not run the renderer or + input paths on an iOS device, so device smoke coverage remains manual. - **EN-024** — iOS reports pixels where macOS reports points, so `getScreenWidth()` and 2D HUD coordinates do not carry across Apple targets. Games currently compensate themselves (scale the 2D pass through a `beginMode2D` zoom). diff --git a/docs/migration-0.5.md b/docs/migration-0.5.md index d6f03ffe..91c8d6fa 100644 --- a/docs/migration-0.5.md +++ b/docs/migration-0.5.md @@ -1,5 +1,9 @@ # Migrating to Bloom 0.5 +> **Preview:** this guide describes the next public API/ABI. The npm package is +> still on the 0.4 release line; use this document when testing the 0.5 branch or +> preparing an upgrade. + 0.5 makes the API consistent in three places where conventions silently diverged. Each change is breaking on purpose — the old inconsistencies caused invisible bugs (colors that rendered white, rotations that were diff --git a/docs/physics.md b/docs/physics.md index 0e56735a..ad308ec2 100644 --- a/docs/physics.md +++ b/docs/physics.md @@ -76,7 +76,7 @@ Matches or exceeds UE5's built-in physics surface. | 2 | **Character controller** (`CharacterVirtual` — slope + stair handling) | ✅ | ✅ | | 2 | **Soft bodies** — cloth, rope, jelly (per-vertex pinning via `invMass=0`) | ✅ | ✅ | | 2 | **Wheeled vehicles** — 4-wheel, ray collision tester, engine + differential | ✅ | ✅ | -| 2 | **Ragdolls** (EN-025) — built at runtime from the skinned skeleton; capsule-per-bone + limited six-DOF joints | ✅ (via `createRagdoll()` in `bloom/models`, `native/shared/src/ragdoll.rs`) | — | +| 2 | **Ragdolls** (EN-025) — built at runtime from the skinned skeleton; capsule-per-bone + limited six-DOF joints | ✅ (via `createRagdoll()` in `@bloomengine/engine/models`, `native/shared/src/ragdoll.rs`) | — | Six-DOF constraints exist in the shim (`bj_constraint_six_dof`) but are internal-only — ragdoll articulation uses a locked-translation wrapper; there @@ -90,7 +90,7 @@ damping setters on web, raycast world-space normals (currently returns (0,1,0) ## TypeScript API quick-start ```typescript -import * as physics from '@bloom/physics'; +import * as physics from '@bloomengine/engine/physics'; // 1. Create a world (once, on game start). const world = physics.createWorld({ gravity: { x: 0, y: -9.81, z: 0 } }); diff --git a/docs/quality-presets.md b/docs/quality-presets.md index a8e9b762..dd0d955d 100644 --- a/docs/quality-presets.md +++ b/docs/quality-presets.md @@ -11,7 +11,7 @@ together: | Low | 0.67 | Off | Catmull-Rom | 0.25 | Bloom | | Medium | 0.75 | On | Catmull-Rom | 0.40 | Shadows, SSAO, bloom | | High | 0.85 | On | Catmull-Rom | 0.45 | Medium + SSR, SSGI, subtle chromatic aberration | -| Ultra | 1.00 | On | Native | 0.50 | Full effect stack | +| Ultra | 1.00 | On | Native | 0.85 | Full effect stack | `setQualityPreset()` applies the row as one operation. Call individual setters afterward to override it: diff --git a/docs/watchos-target.md b/docs/watchos-target.md index db54e68f..3c8effcd 100644 --- a/docs/watchos-target.md +++ b/docs/watchos-target.md @@ -52,7 +52,7 @@ Limitations. watchOS builds go through Perry. The engine's watch crate (`native/watchos`) and Perry's runtime are tier-3 Rust targets built with nightly `-Z build-std`. See -the Perry [watchOS platform docs](../../../perry/perry/docs/src/platforms/watchos.md) +the Perry [watchOS platform docs](https://github.com/PerryTS/perry/blob/main/docs/src/platforms/watchos.md) for the full toolchain setup; the engine-specific parts are: - Compile the game with **`--features watchos-swift-app`** so the engine's @@ -95,6 +95,10 @@ SwiftUI shell owns the run loop and calls into the game thread. watchOS has no keyboard or pointer. Two input sources are bridged: ```typescript +import { + getCrownRotation, getPlatform, getTouchCount, isWatch, Platform, +} from "@bloomengine/engine/core"; + const turn = getCrownRotation(); // Digital Crown delta (radians) since last call const touches = getTouchCount(); // taps on the watch face ``` @@ -105,7 +109,9 @@ const touches = getTouchCount(); // taps on the watch face - **Taps** — surfaced through the same touch API as iOS (`getTouchCount()` / `getTouchX/Y()`), so `isWatch()` branches can treat any tap as e.g. "jump". -Use `isWatch()` (or `getPlatform() === Platform.WATCH`) to gate watch input. +Use `isWatch()` (or `getPlatform() === Platform.WATCHOS`) to gate watch input. +The watch-specific helpers are exported from `@bloomengine/engine/core`; they +are not part of the root barrel export. ## 2D Camera diff --git a/docs/web-target.md b/docs/web-target.md index f48970e3..d1e26040 100644 --- a/docs/web-target.md +++ b/docs/web-target.md @@ -25,14 +25,14 @@ Both game logic and rendering run in WebAssembly. A thin JS glue layer (`native/ ### Prerequisites -- [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/): `cargo install wasm-pack` -- [Perry compiler](../../perry/perry): built from source +- [wasm-pack](https://crates.io/crates/wasm-pack): `cargo install wasm-pack` +- [Perry compiler](https://github.com/PerryTS/perry): built from source - wasm-opt (optional): `cargo install wasm-opt` ### Quick Build ```bash -./native/web/build.sh path/to/game/main.ts +npm exec -- bloom-web path/to/game/main.ts --output dist/web ``` This runs: @@ -122,12 +122,11 @@ if (getPlatform() === Platform.WEB) { ## Browser Support -- **Chrome 113+**: WebGPU (best performance) -- **Firefox 141+**: WebGPU -- **Safari**: WebGPU in Technology Preview; WebGL fallback available -- **Edge 113+**: WebGPU - -The wgpu backend supports both WebGPU and WebGL. WebGL is used automatically as a fallback on browsers without WebGPU support. +Use a current release of Chrome, Edge, Firefox, or Safari. WebGPU rollout +versions differ by operating system and hardware, so the authoritative support +matrix is maintained by [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API#browser_compatibility), +not duplicated here. The wgpu backend automatically uses WebGL as a fallback +when WebGPU is unavailable. ## How It Works @@ -145,4 +144,8 @@ Perry compiles game TypeScript to one WASM module. Bloom's Rust backend compiles ### Shared Code -About two-thirds of Bloom's Rust code is in `native/shared/` — the renderer, audio mixer, text renderer, model loader, scene graph. This code compiles identically for native and WASM. Only the platform layer (~3300 lines across `native/web/src/`: `lib.rs`, `input_ffi.rs`, `material_ffi.rs`, `physics_ffi.rs`, `render_settings.rs`) is web-specific. +About two-thirds of Bloom's Rust code is in `native/shared/` — the renderer, +audio mixer, text renderer, model loader, and scene graph. This code compiles +identically for native and WASM. The platform layer under `native/web/src/` is +web-specific; its size is intentionally not duplicated here because it changes +with the FFI surface. diff --git a/docs/world-format.md b/docs/world-format.md index 0d27866f..a4f9dcef 100644 --- a/docs/world-format.md +++ b/docs/world-format.md @@ -29,7 +29,7 @@ format. Summary: Colors in world files are **0–1 floats** everywhere. (The runtime scene API takes 0–255; the shared helpers convert — never convert twice.) -Loading is `loadWorld(path)` from `bloom/world`: read → parse → migrate → +Loading is `loadWorld(path)` from `@bloomengine/engine/world`: read → parse → migrate → validate. It throws on malformed files and **migrates old schema versions automatically** (v1 worlds carrying `userData.kind === "point_light"` entities get them lifted into `lights[]`). @@ -89,8 +89,9 @@ losslessly — it just renders model-less placeholder boxes (no catalog). The editor's Play button saves the current level to a scratch world file and runs your `playCommand` with `--world ` appended, from your project root. To opt in: accept that flag and load the given world instead of your -default. That's the whole contract. (The shooter's `worldFromArgs` in -`src/world-runtime.ts` is a 9-line reference.) +default. That's the whole contract. The shooter's +[`worldFromArgs`](https://github.com/Bloom-Engine/shooter/blob/main/src/world-runtime.ts) +is a compact reference implementation. ## 5. Consuming worlds at runtime @@ -99,7 +100,7 @@ Two proven shapes: **Generic path** (shortest; the world-viewer example is exactly this): ```ts -import { loadWorld, instantiateWorld, applyWorldEnvironment } from 'bloom/world'; +import { loadWorld, instantiateWorld, applyWorldEnvironment } from '@bloomengine/engine/world'; const world = loadWorld('assets/worlds/level1.world.json'); const result = instantiateWorld(world, { diff --git a/native/web/build.sh b/native/web/build.sh index a9977ad7..d24aa986 100755 --- a/native/web/build.sh +++ b/native/web/build.sh @@ -2,7 +2,7 @@ # Build Bloom Engine for Web # # Usage: -# ./native/web/build.sh [game.ts] [--output dist/] +# bloom-web [game.ts] [--output dist/] # # Steps: # 1. Build bloom_web.wasm via wasm-pack @@ -18,9 +18,49 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ENGINE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +CALLER_DIR="$(pwd)" WEB_CRATE="$SCRIPT_DIR" -OUTPUT_DIR="${2:-$ENGINE_DIR/dist/web}" -GAME_FILE="$1" +OUTPUT_DIR="" +GAME_FILE="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --output) + if [ "$#" -lt 2 ]; then + echo "ERROR: --output requires a directory" + exit 2 + fi + OUTPUT_DIR="$2" + shift 2 + ;; + --output=*) + OUTPUT_DIR="${1#*=}" + shift + ;; + -h|--help) + echo "Usage: bloom-web [game.ts] [--output dist/]" + exit 0 + ;; + -*) + echo "ERROR: unknown option: $1" + exit 2 + ;; + *) + if [ -n "$GAME_FILE" ]; then + echo "ERROR: only one game entry file may be supplied" + exit 2 + fi + GAME_FILE="$1" + shift + ;; + esac +done + +if [ -z "$OUTPUT_DIR" ]; then + OUTPUT_DIR="$CALLER_DIR/dist/web" +elif [ "${OUTPUT_DIR#/}" = "$OUTPUT_DIR" ]; then + OUTPUT_DIR="$CALLER_DIR/$OUTPUT_DIR" +fi # Resolve the game file to an absolute path NOW, while still in the caller's # working directory — the build cd's into the web crate before compiling, so a diff --git a/npm/jolt-prebuilt/README.md b/npm/jolt-prebuilt/README.md index 4a54d7e0..2a3f9b07 100644 --- a/npm/jolt-prebuilt/README.md +++ b/npm/jolt-prebuilt/README.md @@ -1,6 +1,8 @@ # @bloomengine/jolt-prebuilt -Prebuilt JoltPhysics + bloom_jolt static libraries for every (os, arch) Bloom Engine targets. +Prebuilt JoltPhysics + bloom_jolt static libraries for Bloom Engine's published +target matrix. Targets without a published archive use the engine package's C++ +source fallback. ## Why this exists @@ -33,10 +35,15 @@ Each variant directory contains `libJolt.a` (or `Jolt.lib` on Windows) and `libb ## How `@bloomengine/engine` finds it -The engine's `native/shared/build.rs` walks up from `CARGO_MANIFEST_DIR` looking for `node_modules/@bloomengine/jolt-prebuilt/lib/-/`. If found, it links the prebuilt archives and skips cmake entirely. If not found (or the env var `BLOOM_JOLT_FROM_SOURCE=1` is set), it falls back to building Jolt from the C++ source bundled in `@bloomengine/engine` — the existing dev workflow. +The engine's `native/shared/build.rs` walks up from `CARGO_MANIFEST_DIR` looking +for `node_modules/@bloomengine/jolt-prebuilt/lib/-/`. If found, it +links the prebuilt archives and skips cmake entirely. If not found (or the env +var `BLOOM_JOLT_FROM_SOURCE=1` is set), it builds Jolt from the C++ source +bundled in `@bloomengine/engine`. ## Build / publish Built by `.github/workflows/release.yml` on each tag push — a matrix job per platform produces the libraries on the appropriate native runner (`macos-14` for Apple targets, `ubuntu-22.04` for Linux/Android, `windows-latest` for Windows) and uploads them as artifacts. A final assembly job collects every artifact into this package's `lib/` tree and publishes via OIDC trusted publishing. -The published version always matches the corresponding `@bloomengine/engine` version they were built against. +This package is versioned independently. `@bloomengine/engine` pins the +compatible prebuilt version in its dependencies. diff --git a/package.json b/package.json index 40378461..681ff39a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "Bloom Engine: native TypeScript game engine compiled by Perry", "main": "src/index.ts", "types": "src/index.ts", + "bin": { + "bloom-web": "native/web/build.sh" + }, "exports": { ".": "./src/index.ts", "./core": "./src/core/index.ts", @@ -29,6 +32,19 @@ "native/shared/src/**", "native/shared/shaders/**", "native/shared/assets/**", + "crates/bloom-geometry-format/Cargo.toml", + "crates/bloom-geometry-format/Cargo.lock", + "crates/bloom-geometry-format/src/**", + "crates/bloom-scene-format/Cargo.toml", + "crates/bloom-scene-format/Cargo.lock", + "crates/bloom-scene-format/src/**", + "native/third_party/bloom_jolt/CMakeLists.txt", + "native/third_party/bloom_jolt/include/**", + "native/third_party/bloom_jolt/src/**", + "native/third_party/JoltPhysics/Build/**", + "native/third_party/JoltPhysics/Jolt/**", + "native/third_party/JoltPhysics/Jolt.cmake", + "native/third_party/JoltPhysics/LICENSE", "native/macos/Cargo.toml", "native/macos/Cargo.lock", "native/macos/src/**", @@ -62,6 +78,7 @@ "native/web/Cargo.lock", "native/web/src/**", "native/web/build.sh", + "native/web/splice_game.py", "native/web/index.html", "native/web/bloom_glue.js", "native/web/jolt_bridge.js" diff --git a/scripts/ci-check.sh b/scripts/ci-check.sh index 08fdee95..df6b9076 100755 --- a/scripts/ci-check.sh +++ b/scripts/ci-check.sh @@ -208,6 +208,8 @@ run_component() { node tools/check-ci-contract.js hr "FFI/schema parity" node tools/validate-ffi.js + hr "documentation and package contracts" + node tools/validate-docs.js hr "file-size ratchet" node tools/check-file-lines.js ;; diff --git a/tools/check-ci-contract.js b/tools/check-ci-contract.js index ef91d1ba..1227c272 100755 --- a/tools/check-ci-contract.js +++ b/tools/check-ci-contract.js @@ -14,7 +14,7 @@ const expectedLanes = new Map([ ["full", ["contracts", "lint", "shared-tests", "wasm-check", "quality-contract", "example-inventory", "host-build", "wasm-build"]], ["web", ["wasm-check", "wasm-build", "browser-smoke"]], ["cross", ["target-check"]], - ["hardware", ["example-compile", "quality-check", "quality-faults", "quality-run"]], + ["hardware", ["example-compile", "quality-check", "quality-faults", "quality-run", "virtual-geometry-stress"]], ]); const listing = spawnSync("bash", ["scripts/ci-check.sh", "--list"], { @@ -60,10 +60,11 @@ const workflowCommands = [ "./scripts/ci-check.sh --hardware --component quality-check", "./scripts/ci-check.sh --hardware --component quality-faults", "./scripts/ci-check.sh --hardware --component quality-run", + "./scripts/ci-check.sh --hardware --component virtual-geometry-stress", ]; for (const command of workflowCommands) { - const workflow = command.includes("quality-") || command.includes("example-compile") + const workflow = command.includes("quality-") || command.includes("example-compile") || command.includes("virtual-geometry") ? qualityWorkflow : testWorkflow; if (!workflow.includes(command)) { diff --git a/tools/validate-docs.js b/tools/validate-docs.js new file mode 100644 index 00000000..7075756b --- /dev/null +++ b/tools/validate-docs.js @@ -0,0 +1,138 @@ +#!/usr/bin/env node + +// Fast documentation/package contract checks. Keep this dependency-free so it +// can run in the required `contracts` CI component. + +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const root = path.resolve(__dirname, ".."); +const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8"); +let failures = 0; + +function fail(message) { + console.error(`FAIL ${message}`); + failures += 1; +} + +function walkMarkdown(relative = "") { + const absolute = path.join(root, relative); + const entries = fs.readdirSync(absolute, { withFileTypes: true }); + const result = []; + for (const entry of entries) { + const child = path.join(relative, entry.name); + if (entry.isDirectory()) { + if ([".git", "node_modules", "target"].includes(entry.name)) continue; + if (child === "native/third_party" || child === "native/tvos/metal-patched") continue; + result.push(...walkMarkdown(child)); + } else if (entry.name.endsWith(".md")) { + result.push(child); + } + } + return result; +} + +const markdownFiles = walkMarkdown(); +for (const relative of markdownFiles) { + const source = read(relative); + const links = source.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g); + for (const match of links) { + let target = match[1].trim().replace(/^<|>$/g, ""); + target = target.replace(/\s+["'][^"']*["']$/, ""); + if (!target || target.startsWith("#") || target.startsWith("/")) continue; + if (/^(?:https?:|mailto:|tel:)/.test(target)) continue; + target = target.split("#", 1)[0].split("?", 1)[0]; + try { + target = decodeURIComponent(target); + } catch { + fail(`${relative}: malformed link target ${match[1]}`); + continue; + } + const resolved = path.resolve(path.dirname(path.join(root, relative)), target); + if (!resolved.startsWith(`${root}${path.sep}`) && resolved !== root) { + fail(`${relative}: repository link escapes the checkout: ${match[1]}`); + } else if (!fs.existsSync(resolved)) { + fail(`${relative}: missing link target ${match[1]}`); + } + } +} + +const currentDocs = markdownFiles.filter((relative) => + relative === "README.md" || + (relative.startsWith("docs/") && + !relative.startsWith("docs/evidence/") && + !relative.startsWith("docs/perf/") && + !relative.startsWith("docs/pt/") && + !relative.startsWith("docs/rfc/") && + relative !== "docs/tickets.md") +); +const currentText = currentDocs.map((relative) => `${relative}\n${read(relative)}`).join("\n"); +for (const [label, pattern] of [ + ["removed Colors.RAYWHITE constant", /Colors\.RAYWHITE/], + ["removed Platform.WATCH constant", /Platform\.WATCH\b/], + ["numeric Camera3D projection", /projection\s*:\s*(?:number|[01](?:\.0)?\b)/], + ["legacy @bloom package import", /from\s+['"]@bloom\//], + ["undocumented local bloom import alias", /from\s+['"]bloom(?:\/|['"])/], + ["retired wasm-pack installer URL", /rustwasm\.github\.io\/wasm-pack\/installer/], +]) { + if (pattern.test(currentText)) fail(`current docs contain ${label}`); +} + +const colorsSource = read("src/core/colors.ts"); +const colorKeys = new Set( + [...colorsSource.matchAll(/^\s{2}([A-Z][A-Z0-9_]+):\s+Color\./gm)].map((match) => match[1]), +); +for (const match of currentText.matchAll(/Colors\.([A-Z][A-Z0-9_]+)/g)) { + if (!colorKeys.has(match[1])) fail(`current docs reference unknown Colors.${match[1]}`); +} + +const qualityDocs = read("docs/quality-presets.md"); +const qualitySource = read("native/shared/src/renderer/quality_preset.rs"); +const ultraDocs = qualityDocs.match(/^\| Ultra \|[^\n]*\| ([0-9.]+) \| Full effect stack \|$/m)?.[1]; +const ultraSource = qualitySource.match(/render_scale:\s*1\.0,[\s\S]*?composite_sharpen:\s*([0-9.]+)/)?.[1]; +if (!ultraDocs || ultraDocs !== ultraSource) { + fail(`Ultra sharpen docs (${ultraDocs || "missing"}) do not match source (${ultraSource || "missing"})`); +} + +const packageJson = JSON.parse(read("package.json")); +if (packageJson.bin?.["bloom-web"] !== "native/web/build.sh") { + fail("package.json does not expose the bloom-web command"); +} + +const pack = spawnSync("npm", ["pack", "--dry-run", "--json"], { + cwd: root, + encoding: "utf8", +}); +if (pack.status !== 0) { + fail(`npm pack --dry-run failed: ${pack.stderr.trim()}`); +} else { + let packed = []; + try { + packed = JSON.parse(pack.stdout)[0].files.map((entry) => entry.path); + } catch (error) { + fail(`could not parse npm pack inventory: ${error.message}`); + } + for (const required of [ + "native/web/build.sh", + "native/web/splice_game.py", + "crates/bloom-geometry-format/Cargo.toml", + "crates/bloom-scene-format/Cargo.toml", + "native/third_party/bloom_jolt/CMakeLists.txt", + "native/third_party/JoltPhysics/Build/CMakeLists.txt", + "native/third_party/JoltPhysics/Jolt/Jolt.h", + ]) { + if (!packed.includes(required)) fail(`npm package omits ${required}`); + } +} + +const help = spawnSync("bash", ["native/web/build.sh", "--help"], { + cwd: root, + encoding: "utf8", +}); +if (help.status !== 0 || !help.stdout.includes("--output")) { + fail("bloom-web help/argument parsing is not usable"); +} + +console.log(`${markdownFiles.length} Markdown files checked; ${failures} failures`); +process.exit(failures === 0 ? 0 : 1);