Skip to content

Commit

Permalink
Merge pull request #21 from Birch-san/sample-determinism
Browse files Browse the repository at this point in the history
sample determinism
  • Loading branch information
ErikSom authored Jan 5, 2025
2 parents a31cd50 + 466d8fa commit 3348353
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 18 deletions.
29 changes: 28 additions & 1 deletion box2d3-wasm/csrc/glue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ emscripten::val getEventsArray(T* events, int count) {
return result;
}

template<typename T>
emscripten::val toBytes(const T& value) {
auto array = emscripten::val::global("Uint8Array").new_(sizeof(T));
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&value);
for (size_t i = 0; i < sizeof(T); i++) {
array.set(i, bytes[i]);
}
return array;
}

EMSCRIPTEN_BINDINGS(box2dcpp) {
class_<b2Vec2>("b2Vec2")
.constructor()
Expand Down Expand Up @@ -117,11 +127,13 @@ EMSCRIPTEN_BINDINGS(box2dcpp) {

constant("b2Rot_identity", b2Rot_identity);


class_<b2Transform>("b2Transform")
.constructor()
.property("p", &b2Transform::p, return_value_policy::reference())
.property("q", &b2Transform::q, return_value_policy::reference())
.function("ToBytes", +[](const b2Transform& self) -> emscripten::val {
return toBytes(self);
});
;

class_<b2Mat22>("b2Mat22")
Expand Down Expand Up @@ -1721,6 +1733,21 @@ EMSCRIPTEN_BINDINGS(box2d) {
function("b2AABB_Center", &b2AABB_Center);
function("b2AABB_Extents", &b2AABB_Extents);
function("b2AABB_Union", &b2AABB_Union);
function("b2Hash", +[](uint32_t hash, const emscripten::val& array) -> uint32_t {
if (!array.instanceof(emscripten::val::global("Uint8Array"))) {
throw std::runtime_error("Expected a Uint8Array.");
}
auto buffer = array["buffer"].as<emscripten::val>();
size_t length = array["length"].as<size_t>();
int byteOffset = array["byteOffset"].as<int>();
auto view = emscripten::val::global("Uint8Array").new_(buffer, byteOffset, length);
uint32_t result = hash;
for (size_t i = 0; i < length; i++) {
result = (result << 5) + result + view[i].as<uint8_t>();
}
return result;
}, allow_raw_pointers());
constant("B2_HASH_INIT", B2_HASH_INIT);

// ------------------------------------------------------------------------
// Random
Expand Down
170 changes: 170 additions & 0 deletions demo/samples/categories/determinism/fallingHinges.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import Sample from "../../sample.mjs";

import Keyboard from '../../../utils/keyboard.mjs';
import settings from '../../settings.mjs';

const e_columns = 4;
const e_rows = 30;

/*
Note, hash differs from C version due to different float precision (f32 vs f64)
But the wasm build should be deterministic cross-browser / cross-platform
*/

export default class FallingHinges extends Sample{
constructor(box2d, camera, debugDraw){
super(box2d, camera, debugDraw);

camera.center = {x: 0.0, y: 7.5 };
camera.zoom = 10;

settings.drawJoints = true;

this.m_bodies = new Array(e_columns * e_rows);
this.m_hash = 0;
this.m_sleepStep = -1;

const {
b2DefaultBodyDef,
b2CreateBody,
b2MakeBox,
b2DefaultShapeDef,
b2CreatePolygonShape,
b2DefaultRevoluteJointDef,
b2CreateRevoluteJoint,
b2MakeRoundedBox,
b2BodyType,
B2_PI,
b2MakeRot,
b2Body_GetTransform,
} = this.box2d;

{
const bodyDef = b2DefaultBodyDef();
bodyDef.position.Set(0.0, -1.0);
const groundId = b2CreateBody( this.m_worldId, bodyDef );

const box = b2MakeBox( 20.0, 1.0 );
const shapeDef = b2DefaultShapeDef();
b2CreatePolygonShape( groundId, shapeDef, box );
}

const h = 0.25;
const r = 0.1 * h;
const box = b2MakeRoundedBox( h - r, h - r, r );

const shapeDef = b2DefaultShapeDef();
shapeDef.friction = 0.3;

const offset = 0.4 * h;
const dx = 10.0 * h;
const xroot = -0.5 * dx * ( e_columns - 1.0 );

const jointDef = b2DefaultRevoluteJointDef();
jointDef.enableLimit = true;
jointDef.lowerAngle = -0.1 * B2_PI;
jointDef.upperAngle = 0.2 * B2_PI;
jointDef.enableSpring = true;
jointDef.hertz = 0.5;
jointDef.dampingRatio = 0.5;
jointDef.localAnchorA.Set(h, h );
jointDef.localAnchorB.Set(offset, -h);
jointDef.drawSize = 0.1;

let bodyIndex = 0;
let bodyCount = e_rows * e_columns;

for ( let j = 0; j < e_columns; j++ )
{
let x = xroot + j * dx;

let prevBodyId = null;

for ( let i = 0; i < e_rows; i++ )
{
const bodyDef = b2DefaultBodyDef();
bodyDef.type = b2BodyType.b2_dynamicBody;

bodyDef.position.Set(
x + offset * i,
h + 2.0 * h * i
);

// this tests the deterministic cosine and sine functions
bodyDef.rotation = b2MakeRot( 0.1 * i - 1.0 );

const bodyId = b2CreateBody( this.m_worldId, bodyDef );

if ((i & 1) == 0)
{
prevBodyId = bodyId;
}
else
{
jointDef.bodyIdA = prevBodyId;
jointDef.bodyIdB = bodyId;
b2CreateRevoluteJoint( this.m_worldId, jointDef );
prevBodyId = null;
}

b2CreatePolygonShape( bodyId, shapeDef, box );

console.assert( bodyIndex < bodyCount );
this.m_bodies[bodyIndex] = bodyId;

bodyIndex += 1;
}
}

this.CreateUI();
}

Despawn(){
Keyboard.HideTouchControls();
}

Step(){
const {
b2World_GetBodyEvents,
b2Body_GetTransform,
b2Hash,
B2_HASH_INIT,
} = this.box2d;

super.Step();

if(this.m_hash === 0){
const bodyEvents = b2World_GetBodyEvents( this.m_worldId );

if ( bodyEvents.moveCount == 0 )
{
let hash = B2_HASH_INIT;

const bodyCount = e_rows * e_columns;
for ( let i = 0; i < bodyCount; i++ )
{
const xf = b2Body_GetTransform( this.m_bodies[i] );
//printf( "%d %.9f %.9f %.9f %.9f\n", i, xf.p.x, xf.p.y, xf.q.c, xf.q.s );
hash = b2Hash( hash, xf.ToBytes() );
}

this.m_sleepStep = this.m_stepCount - 1;
this.m_hash = hash;
}
}
}

UpdateUI(DrawString, m_textLine){
DrawString(5, m_textLine, `sleep step = ${this.m_sleepStep}, hash = 0x${this.m_hash.toString(16)}`);
}

Destroy(){
super.Destroy();
this.Despawn();

if (this.pane){
this.pane.dispose();
this.pane = null;
}
}
}
3 changes: 3 additions & 0 deletions demo/samples/categories/list.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default {
"Determinism": {
"Falling Hinges": "./categories/determinism/fallingHinges.mjs"
},
"Events": {
"Body Move": "./categories/events/bodyMove.mjs",
"Contacts": "./categories/events/contacts.mjs",
Expand Down
42 changes: 27 additions & 15 deletions demo/samples/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import samples from './categories/list.mjs';
import settings from './settings.mjs';

const state = {
singleStep: false,
mouseDown: false,
mousePos: {x: 0, y: 0},
mouseDownPos: {x: 0, y: 0},
Expand Down Expand Up @@ -39,6 +38,7 @@ const camera = new Camera({autoResize: true, controls: true, canvas});
let debugDraw = null;

function loadSample(url) {
console.log('loading sample', url);
if(sample){
sample.Destroy();
sample = null;
Expand Down Expand Up @@ -83,6 +83,8 @@ function addUI(){

const PARAMS = {
pause: false,
'warm starting': settings.enableWarmStarting,
continuous: settings.enableContinuous,
};
pane = new Pane({
title: 'Main Settings',
Expand All @@ -104,10 +106,18 @@ function addUI(){
settings.pause = event.value;
});

main.addBinding(PARAMS, 'warm starting').on('change', (event) => {
settings.enableWarmStarting = event.value;
});

main.addBinding(PARAMS, 'continuous').on('change', (event) => {
settings.enableContinuous = event.value;
});

main.addButton({
title: 'single step',
}).on('click', () => {
state.singleStep = true;
settings.singleStep = true;
});

// debug draw settings
Expand Down Expand Up @@ -204,24 +214,26 @@ function update(timestamp) {
loadSample(sampleUrl);
}

if (deltaTime >= settings.maxFrameTime && sample) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if(Keyboard.IsPressed(Key.P)){
settings.pause = !settings.pause;
}

const start = performance.now();
if (!settings.pause || state.singleStep) {
sample.Step()
}
const end = performance.now();
if (deltaTime >= settings.maxFrameTime) {
ctx.clearRect(0, 0, canvas.width, canvas.height);

state.singleStep = false;
if(sample){
const start = performance.now();
sample.Step()
const end = performance.now();

DrawString(5, m_textLine, sampleName);
sample?.UpdateUI(DrawString, m_textLine);
DrawString(5, m_textLine, sampleName);
sample?.UpdateUI(DrawString, m_textLine);

frameTime = end - start;
frameTime = end - start;

lastFrameTime = timestamp - (deltaTime % settings.maxFrameTime);
frame++;
lastFrameTime = timestamp - (deltaTime % settings.maxFrameTime);
frame++;
}

Keyboard.Update();
}
Expand Down
22 changes: 20 additions & 2 deletions demo/samples/sample.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import settings, {DEFAULT_SETTINGS} from './settings.mjs';
export default class Sample{
constructor(box2d, camera, debugDraw){

Object.assign(settings, DEFAULT_SETTINGS);
// Object.assign(settings, DEFAULT_SETTINGS);

this.box2d = box2d;
this.debugDraw = debugDraw;
Expand Down Expand Up @@ -41,7 +41,25 @@ export default class Sample{
b2World_Step,
} = this.box2d;

const timeStep = settings.hertz > 0.0 ? 1.0 / settings.hertz : 0.0;
let timeStep = settings.hertz > 0.0 ? 1.0 / settings.hertz : 0.0;

if ( settings.pause )
{
if ( settings.singleStep )
{
settings.singleStep = false;
}
else
{
timeStep = 0.0;
}
}

function f32(n) {
return new Float32Array([n])[0];
}

timeStep = f32(timeStep);

b2World_EnableSleeping( this.m_worldId, settings.enableSleep );
b2World_EnableWarmStarting( this.m_worldId, settings.enableWarmStarting );
Expand Down
1 change: 1 addition & 0 deletions demo/samples/settings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const DEFAULT_SETTINGS = {
maxFrameTime: 1000 / 60,
hertz: 60,
pause: false,
singleStep: false,
enableWarmStarting: true,
enableContinuous: true,
subStepCount: 4,
Expand Down

0 comments on commit 3348353

Please sign in to comment.