Skip to content

Commit 96e09ff

Browse files
authored
Merge pull request #65 from Bloom-Engine/audit/architecture-fixes
Architecture overhaul: FFI unification, thread safety, fixed-timestep physics, golden tests, LOD, 256 lights
2 parents d86db22 + 94c1e92 commit 96e09ff

97 files changed

Lines changed: 19915 additions & 22489 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ jobs:
7171
working-directory: native/shared
7272
run: cargo test --release
7373

74+
# ---------------------------------------------------------------------------
75+
# FFI parity: every function in package.json's perry.nativeLibrary manifest
76+
# must be exported with matching arity by every platform crate (hand-written
77+
# or via define_core_ffi!/define_physics_ffi!). This is the check whose
78+
# absence let Android ship 60 functions behind (#59) and Windows ship the
79+
# whole scene-graph surface as silent no-op stubs. Cheap (pure text parse),
80+
# runs everywhere, gates merges.
81+
# ---------------------------------------------------------------------------
82+
ffi-parity:
83+
runs-on: ubuntu-22.04
84+
steps:
85+
- uses: actions/checkout@v4
86+
- name: validate-ffi
87+
run: node tools/validate-ffi.js
88+
- name: file line limit (2000, ratcheting baseline)
89+
run: node tools/check-file-lines.js
90+
7491
# ---------------------------------------------------------------------------
7592
# Lint: rustfmt + clippy. Advisory while the codebase is still in flux.
7693
# Flip `continue-on-error: false` once the lint baseline is clean.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,6 @@ tools/unreal_reference/
7171
tools/dump_dds/target/
7272
tools/dump_dds/Cargo.lock
7373
examples/intel-sponza/assets/outdoor.hdr
74+
75+
# Golden-test failure artifacts (written next to the golden on mismatch)
76+
*.actual.png

docs/migration-0.5.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Migrating to Bloom 0.5
2+
3+
0.5 makes the API consistent in three places where conventions silently
4+
diverged. Each change is breaking on purpose — the old inconsistencies
5+
caused invisible bugs (colors that rendered white, rotations that were
6+
60× too fast). All engine examples are already migrated and serve as
7+
references.
8+
9+
## Surface colors are 0–255 everywhere
10+
11+
`setSceneNodeColor`, `setOutlineColor`, and the color part of
12+
`setSceneNodeWaterMaterial` previously took 0–1 floats — the only places
13+
in the API that did. They now take 0–255 like every `draw*` call and the
14+
`Colors` presets.
15+
16+
```ts
17+
// before // after
18+
setSceneNodeColor(node, 0.75, 0.75, 0.7); setSceneNodeColor(node, 191, 191, 179);
19+
setSceneNodeColor(node, c.r/255, c.g/255, …) setSceneNodeColor(node, c.r, c.g, c.b, c.a); // Colors presets now just work
20+
```
21+
22+
Symptom of unmigrated code: scene nodes render almost black (values
23+
divided twice).
24+
25+
**Unchanged:** light colors (`addDirectionalLight`, `addPointLight`)
26+
stay 0–1 floats with a separate intensity — that's the radiometric
27+
convention (Unity and Unreal do the same), and light color × intensity
28+
can meaningfully exceed 1.0. The `*.world.json` format also keeps 0–1
29+
tints (serialized data is versioned separately); the loader converts.
30+
31+
## Angles are degrees everywhere
32+
33+
`drawModelRotated`'s `rotY` was radians; `Camera2D.rotation` was degrees.
34+
Everything user-facing is now degrees (the raylib convention).
35+
36+
```ts
37+
// before // after
38+
drawModelRotated(m, p, 1.0, Math.PI / 2, t); drawModelRotated(m, p, 1.0, 90, t);
39+
```
40+
41+
Symptom of unmigrated code: models spin ~57× faster than intended.
42+
43+
**Unchanged:** physics angular velocity stays radians/sec (SI, matches
44+
Jolt), and quaternions are quaternions.
45+
46+
## `Texture.handle` (was `Texture.id`)
47+
48+
`Texture` was the only resource type whose handle field was named `id`;
49+
`Sound`, `Music`, `Font`, and `Model` all use `handle`.
50+
51+
```ts
52+
// before // after
53+
myAtlas.id myAtlas.handle
54+
```
55+
56+
## Also in 0.5 (non-breaking)
57+
58+
- `physics.step(world, dt)` is now fixed-timestep with an accumulator
59+
and returns the interpolation alpha; `physics.stepVariable` is the
60+
old exact-dt behavior. See docs/physics.md "Stepping".
61+
- Stale handles (use-after-free/destroy) now fail lookups instead of
62+
aliasing whatever object reused the slot.
63+
- `*Raw` function variants are documented `@internal` — they exist only
64+
as a compiler workaround and will be removed.
65+
- Coordinate system is now documented at the top of the physics and
66+
scene modules: right-handed, Y-up, meters, SI units.

docs/physics.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,50 @@ const ball = physics.createBody(world, ballShape, {
107107
// 4. Call optimizeBroadphase once after initial body setup.
108108
physics.optimizeBroadphase(world);
109109

110-
// 5. In your game loop:
111-
physics.step(world, 1 / 60);
110+
// 5. In your game loop — pass the real frame delta; step() runs the
111+
// simulation at a fixed rate internally (see "Stepping" below):
112+
physics.step(world, deltaTime);
112113
const pos = physics.getBodyPosition(ball);
113114
// ... read positions and render sprites / meshes at those transforms
114115
```
115116

117+
## Stepping
118+
119+
`physics.step(world, deltaTime)` is **fixed-timestep**: it accumulates the
120+
wall-clock delta and advances the solver in whole steps of 1/60 s
121+
(configurable via `setFixedTimestep(world, hz, maxSteps?)`). Variable-size
122+
solver steps feed frame hitches straight into the constraint solver —
123+
tunneling and joint explosions on any slow frame — so the accumulator is
124+
the default, with two protections baked in:
125+
126+
- A single frame's contribution is clamped to 0.25 s (debugger pauses and
127+
OS hitches produce one slowed-down frame, not minutes of catch-up).
128+
- At most `maxSteps` (default 4) fixed steps run per frame; surplus
129+
backlog is dropped. The simulation slows down instead of spiraling.
130+
131+
`step` returns the **interpolation alpha**: how far the carried remainder
132+
sits between the last two physics states. Two ways to use it:
133+
134+
```typescript
135+
// Easiest: let the engine blend body transforms for rendering.
136+
physics.setInterpolation(world, true);
137+
runGame((dt) => {
138+
physics.step(world, dt);
139+
const p = physics.getBodyPosition(ball); // already smoothed
140+
});
141+
142+
// Manual: interpolate game-side state with the same alpha.
143+
const alpha = physics.step(world, dt); // or physics.getStepAlpha(world)
144+
```
145+
146+
With interpolation on, `getBodyPosition`/`getBodyRotation` return the
147+
blended state; physics queries (raycasts, overlaps, contacts) always see
148+
the raw simulation state.
149+
150+
Exact-dt stepping is still available as `physics.stepVariable(world, dt)`
151+
for code that drives its own accumulator — it trades stability for
152+
control.
153+
116154
## Character controllers (Tier 2)
117155

118156
For player movement, use `CharacterVirtual` — Jolt's kinematic controller with

examples/pbr-spheres/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ for (let row = 0; row < GRID_N; row = row + 1) {
195195

196196
const node = createSceneNode();
197197
attachModelToNode(node, sphereHandle, 0);
198-
setSceneNodeColor(node, BASE_R, BASE_G, BASE_B);
198+
setSceneNodeColor(node, BASE_R * 255, BASE_G * 255, BASE_B * 255);
199199
setSceneNodePbr(node, roughness, metallic);
200200
setSceneNodeCastShadow(node, false);
201201
setSceneNodeReceiveShadow(node, false);

examples/renderer-test/main.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ function placeSphere(
231231
roughness: number, metalness: number,
232232
): number {
233233
const node = placeNode(sphereHandle, 0, px, py, pz, scale, scale, scale);
234-
setSceneNodeColor(node, cr, cg, cb);
234+
setSceneNodeColor(node, cr * 255, cg * 255, cb * 255);
235235
setSceneNodePbr(node, roughness, metalness);
236236
return node;
237237
}
@@ -243,7 +243,7 @@ function placeCube(
243243
roughness: number, metalness: number,
244244
): number {
245245
const node = placeNode(cubeHandle, 0, px, py, pz, sx, sy, sz);
246-
setSceneNodeColor(node, cr, cg, cb);
246+
setSceneNodeColor(node, cr * 255, cg * 255, cb * 255);
247247
setSceneNodePbr(node, roughness, metalness);
248248
// Thin horizontal slabs (floors) should receive but not cast
249249
// shadows — otherwise they fill the shadow map with their own
@@ -374,7 +374,7 @@ function setupWater(): void {
374374
updateSceneNodeGeometry(waterNode, wv, wi);
375375
const wm = mat4Translate(mat4Identity(), { x: 0, y: 0.2, z: cz });
376376
setSceneNodeTransform(waterNode, wm);
377-
setSceneNodeWaterMaterial(waterNode, 0.15, 1.5, 0.1, 0.3, 0.5, 0.6);
377+
setSceneNodeWaterMaterial(waterNode, 0.15, 1.5, 26, 77, 128, 153);
378378
setSceneNodeReceiveShadow(waterNode, true);
379379

380380
// Rocks / objects sticking out of water
@@ -457,7 +457,7 @@ function setupThinGeometry(): void {
457457
const x = cx - 5.5 + i * 1.0;
458458
const node = createSceneNode();
459459
attachModelToNode(node, cubeHandle, 0);
460-
setSceneNodeColor(node, 0.6, 0.6, 0.62);
460+
setSceneNodeColor(node, 153, 153, 158);
461461
setSceneNodePbr(node, 0.3, 1.0);
462462
setSceneNodeCastShadow(node, true);
463463
setSceneNodeReceiveShadow(node, true);

examples/scene-graph/interactive.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ let selectedWallId: string | null = null;
5252
const floorHandle = createSceneNode();
5353
const floorPolygon = [-10, -10, 10, -10, 10, 10, -10, 10];
5454
extrudePolygon(floorHandle, floorPolygon, 0.02);
55-
setSceneNodeColor(floorHandle, 0.85, 0.85, 0.82, 1.0);
55+
setSceneNodeColor(floorHandle, 217, 217, 209, 255);
5656
setSceneNodePbr(floorHandle, 0.7, 0.0);
5757

5858
// Handle → wall ID lookup (for picking)
@@ -112,9 +112,9 @@ function wallSystem(dt: number): void {
112112

113113
// Color based on selection
114114
if (wall.id === selectedWallId) {
115-
setSceneNodeColor(wall.handle, 0.3, 0.6, 1.0, 1.0);
115+
setSceneNodeColor(wall.handle, 77, 153, 255, 255);
116116
} else {
117-
setSceneNodeColor(wall.handle, 0.95, 0.95, 0.92, 1.0);
117+
setSceneNodeColor(wall.handle, 242, 242, 235, 255);
118118
}
119119
setSceneNodePbr(wall.handle, 0.8, 0.0);
120120

examples/scene-graph/main.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ const floorIdx: number[] = [0, 1, 2, 0, 2, 3];
149149
updateSceneNodeGeometry(floor, floorVerts, floorIdx);
150150

151151
// Set materials
152-
setSceneNodeColor(wall1, 0.95, 0.95, 0.92, 1.0);
153-
setSceneNodeColor(wall2, 0.92, 0.92, 0.88, 1.0);
154-
setSceneNodeColor(wall3, 0.90, 0.90, 0.86, 1.0);
155-
setSceneNodeColor(floor, 0.7, 0.7, 0.65, 1.0);
152+
setSceneNodeColor(wall1, 242, 242, 235, 255);
153+
setSceneNodeColor(wall2, 235, 235, 224, 255);
154+
setSceneNodeColor(wall3, 230, 230, 219, 255);
155+
setSceneNodeColor(floor, 179, 179, 166, 255);
156156

157157
// Set PBR properties
158158
setSceneNodePbr(wall1, 0.8, 0.0);

examples/scene-graph/room.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ function slabSystem(dt: number): void {
151151
extrudePolygon(handle, flat, slab.elevation);
152152

153153
// Gray floor material
154-
setSceneNodeColor(handle, 0.75, 0.75, 0.70, 1.0);
154+
setSceneNodeColor(handle, 191, 191, 179, 255);
155155
setSceneNodePbr(handle, 0.6, 0.0);
156156

157157
clearDirty(id);
@@ -203,7 +203,7 @@ function wallSystem(dt: number): void {
203203
}
204204

205205
// White wall material
206-
setSceneNodeColor(handle, 0.95, 0.95, 0.92, 1.0);
206+
setSceneNodeColor(handle, 242, 242, 235, 255);
207207
setSceneNodePbr(handle, 0.8, 0.0);
208208

209209
clearDirty(id);

examples/scene-graph/shadows.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ setDirectionalLight(0.5, 1.0, 0.3, 255, 240, 220, 0.7);
5151
const floor = createSceneNode();
5252
const floorPoly = [-5, -5, 5, -5, 5, 5, -5, 5];
5353
extrudePolygon(floor, floorPoly, 0.05);
54-
setSceneNodeColor(floor, 0.8, 0.78, 0.72, 1.0);
54+
setSceneNodeColor(floor, 204, 199, 184, 255);
5555
setSceneNodePbr(floor, 0.7, 0.0);
5656

5757
// Walls
@@ -69,7 +69,7 @@ function makeWall(sx: number, sz: number, ex: number, ez: number): void {
6969
sx - nx, sz - nz,
7070
];
7171
extrudePolygon(node, poly, 3.0);
72-
setSceneNodeColor(node, 0.95, 0.93, 0.88, 1.0);
72+
setSceneNodeColor(node, 242, 237, 224, 255);
7373
setSceneNodePbr(node, 0.85, 0.0);
7474
}
7575

@@ -88,7 +88,7 @@ function makeBox(cx: number, cy: number, cz: number, w: number, h: number, d: nu
8888
// Offset Y via transform
8989
const t = mat4Translate(mat4Identity(), 0, cy, 0);
9090
setSceneNodeTransform(node, t);
91-
setSceneNodeColor(node, r, g, b, 1.0);
91+
setSceneNodeColor(node, r * 255, g * 255, b * 255, 255);
9292
setSceneNodePbr(node, 0.6, 0.0);
9393
}
9494

0 commit comments

Comments
 (0)