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
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
46 changes: 34 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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** |

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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).
Expand Down Expand Up @@ -195,15 +210,15 @@ 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 }
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 }
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions bloom-renderer-spec-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 17 additions & 8 deletions docs/design-api.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
# 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
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions docs/ios-target.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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).
4 changes: 4 additions & 0 deletions docs/migration-0.5.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/physics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 } });
Expand Down
2 changes: 1 addition & 1 deletion docs/quality-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions docs/watchos-target.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```
Expand All @@ -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

Expand Down
Loading