← Back to blog

Where Bloom stands: design choices, progress, and what comes next

Bloom turned five weeks old this week. Three hundred and eleven commits in, we wanted to write down what we have, why we built it the way we did, and which pieces are still rough.

The pitch, in one sentence

Write your game in TypeScript, compile it ahead of time, and ship a real native binary on macOS, Windows, Linux, iOS and tvOS — or a WASM bundle for the web. No Electron, no WebView, no embedded JavaScript runtime in your shipped game.

That sentence has done a lot of work. It is also the reason most of our early design decisions look the way they do.

Why TypeScript

We did not pick TypeScript because we love JavaScript. We picked it because it is the most widely-used statically-typed language with a structural type system, an enormous tooling story, and a syntax that does not scare anyone away. Most of the people we want to make games with have shipped TypeScript before. Very few have shipped C++.

We also wanted a language that could be compiled ahead of time cleanly, without dragging a garbage collector and a bytecode interpreter into every shipped binary. That ruled out anything that requires a heavy runtime (Python, C#, JS-the-language). TypeScript — minus the dynamic-by-default bits — turned out to be a sweet spot.

Why Perry

Perry is the ahead-of-time compiler that turns your TypeScript into native code. The TS-to-binary path is the part that lets us promise “no runtime overhead.” Your game becomes a single binary that calls into a Rust core through a stable C ABI. There is no V8, no Bun, no JIT.

Picking Perry let us be ruthless about the language surface we exposed. The Bloom API is functions and plain interfaces — no classes, no decorators, no proxies, no eval. If a TypeScript feature does not compile cleanly to a native call, we do not use it in the API. The result is an API that fits on a cheatsheet and a build that fits in your head.

Why wgpu and not four bespoke renderers

Our first instinct was to write a Metal renderer, then a DirectX 12 renderer, then a Vulkan renderer. We talked ourselves out of it within a weekend. Four backends mean four shader languages, four resource models, four bug surfaces, and four places to forget about HiDPI.

Instead, the entire renderer is written once on top of wgpu. Shaders are WGSL. We get Metal on Apple platforms, DirectX 12 on Windows, Vulkan on Linux and Android, and WebGPU (with a WebGL fallback) in the browser — from one codebase. The cost is that we are bound to wgpu's feature set, and a few exotic things (mesh shaders, ray tracing) are off the table for now. We think that's a fair trade for a small team.

What's actually in the box today

We try not to put anything on the marketing site that does not work. Here is what is real, today:

  • Nine importable modulesbloom/core, bloom/shapes, bloom/textures, bloom/text, bloom/audio, bloom/models, bloom/math, bloom/physics and bloom/scene.
  • A real PBR renderer — substrate-style layered materials, cascaded shadow maps with caching for static geometry, ACES/AgX tone mapping, auto-exposure, bloom, depth of field, motion blur, SSGI, SSAO, TAA, and a CAS sharpen pass for fractional render scales.
  • GPU skeletal animation — glTF 2.0 import, four-bone linear blend skinning on the GPU, up to 128 joints per skeleton.
  • Jolt physics — rigid and soft bodies, character controllers, vehicles, raycasts, constraints, contact callbacks. The web target uses the JoltPhysics.js fallback so the same code runs in the browser.
  • Six target platforms — macOS, Windows, Linux, iOS, tvOS, and Web. Android is partially wired up but not yet shippable.
  • Hot reload — save a WGSL shader or a material JSON and the change is on screen in under a second during development. The file-watching code is stripped out of release builds.
  • Seventeen example projects — from a 170-line Pong to loading the Intel Sponza and Bistro scenes.

Some recent things we're proud of

The renderer has had a good month. A few highlights:

  • Auto-DRS. The renderer self-tunes its render scale to hit your target framerate, then runs an upscale plus RCAS sharpen pass so the image stays crisp. You set a target FPS; the engine does the rest.
  • Planar reflections. Real mirror-plane captures with oblique-clip and IBL fallback when the reflection budget is exhausted. Useful for water, polished floors, and shopfronts.
  • Texture-array splat mapping. Terrain and detail layers can now bind a texture array with proper mips, so you can paint several materials per tile without paying for one draw call per layer.
  • Imposter baker. A small CLI that bakes octahedral impostor atlases for distant LODs. We needed this the moment we started dropping forests into scenes.
  • Cross-platform HiDPI. Windows, Linux and Web finally share the same HiDPI handling that macOS and iOS already had. UI stays sharp on every display.

Things we're not pretending are done

We owe you the unflattering list too:

  • Android. The platform crate exists and most of the FFI surface is wired up, but the native-activity glue is not finished. We are not telling anyone to ship Android with Bloom yet.
  • watchOS. Shaders compile, the platform stub is there, but we are blocked on Perry's watchOS support before this is real.
  • Virtualized geometry. No Nanite-equivalent, no mesh shaders, no hardware ray tracing. This is on the roadmap, not in the binary.
  • Skeletal animation cap. 128 joints per skeleton, hard limit, because of the UBO size. Big rigs need workarounds today.
  • The scene graph is young. Transforms, visibility, shadows and material binding all work. Query systems and broader optimization passes do not exist yet.
  • User shaders. You can hand-write WGSL and load it through the material pipeline, but there is no in-engine shader graph and no runtime shader compilation from TypeScript.

The shape of the API

We borrowed heavily from Raylib for the immediate-mode parts of the API. If you have written a Raylib game, the muscle memory transfers:

import { initWindow, windowShouldClose, beginDrawing,
         endDrawing, clearBackground, drawText, Colors } from "bloom";

initWindow(800, 450, "Hello Bloom");

while (!windowShouldClose()) {
  beginDrawing();
  clearBackground(Colors.RAYWHITE);
  drawText("Hello, Bloom!", 190, 200, 20, Colors.DARKGRAY);
  endDrawing();
}

For 3D and physics work, the same philosophy applies: plain interfaces, pure functions, no hidden state machines. A camera is an object literal. A rigid body is a handle. The rendering pipeline is configured by calling functions in a specific order, not by registering listeners on an invisible orchestrator.

What's next

Our short list for the next few weeks:

  • Finish the Android native-activity glue and call Android shippable.
  • Push the scene-graph work past “basic” into actually useful: queries, frustum culling refinement, instancing helpers.
  • Land a first cut of the editor integration so you can iterate on a scene without restarting your game.
  • Open-source the example games, including a slightly bigger demo than Pong.
  • Write up the Perry compilation pipeline in detail. Several people have asked.

Try it

Bloom is open source under MIT. The whole repository is on GitHub. If you want to follow along without forking, the docs have a 12-line quick start, and the showcase page lists what we are building with it ourselves.

Either way: thanks for taking a look. We will keep posting here as bigger pieces land.