babylon-box3d 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/wasm/box3d_shim.c CHANGED
@@ -367,12 +367,200 @@ static int bxIsIdentity( b3Transform xf, b3Vec3 scale )
367
367
  xf.q.s == 1.0f && scale.x == 1.0f && scale.y == 1.0f && scale.z == 1.0f;
368
368
  }
369
369
 
370
+ // ---------------------------------------------------------------------------------------------
371
+ // Worker threads
372
+ // ---------------------------------------------------------------------------------------------
373
+ //
374
+ // The threaded build spreads a world step over several threads. Box3D can do that on its own, but its scheduler
375
+ // creates threads with the world and joins them when the world is destroyed, and joining is what breaks in a browser:
376
+ // a worker emscripten has not finished starting cannot get going while the main thread sits in pthread_join waiting
377
+ // for it, and the page stops there for good. Disposing a scene and building the next one in the same function is
378
+ // enough to reach that, so it is not a corner.
379
+ //
380
+ // The shim brings its own task system instead, which box3d takes through the world definition. The threads are
381
+ // created once, shared by every world and never joined, so worlds come and go without touching them. The design is
382
+ // the one box3d uses internally - a task ring, a semaphore, and a caller that runs pending tasks itself while it
383
+ // waits - and box3d still splits the work by worker count, so results do not change.
384
+
385
+ #ifdef __EMSCRIPTEN_PTHREADS__
386
+
387
+ #include <pthread.h>
388
+ #include <sched.h>
389
+ #include <semaphore.h>
390
+ #include <stdatomic.h>
391
+
392
+ // Emscripten pre-spawns BX_WORKER_POOL workers when the module loads (wasm/build.mjs passes the same number to
393
+ // PTHREAD_POOL_SIZE). Staying inside that is what keeps pthread_create from having to start a worker on the spot.
394
+ #ifndef BX_WORKER_POOL
395
+ #define BX_WORKER_POOL 8
396
+ #endif
397
+ #define BX_MAX_WORKERS ( BX_WORKER_POOL + 1 < B3_MAX_WORKERS ? BX_WORKER_POOL + 1 : B3_MAX_WORKERS )
398
+
399
+ // Tasks live for one step and the ring is rewound at the top of every step, so this only has to cover a single step.
400
+ // Box3D's own scheduler holds 256 with 32 workers.
401
+ #define BX_TASK_CAPACITY 512
402
+
403
+ enum bxTaskStatus
404
+ {
405
+ bxTaskFree = 0,
406
+ bxTaskPending = 1,
407
+ bxTaskClaimed = 2,
408
+ bxTaskComplete = 3,
409
+ };
410
+
411
+ typedef struct bxTask
412
+ {
413
+ b3TaskCallback* callback;
414
+ void* context;
415
+ atomic_int status;
416
+ } bxTask;
417
+
418
+ static bxTask s_tasks[BX_TASK_CAPACITY];
419
+ static atomic_int s_taskCount;
420
+ static sem_t s_taskSemaphore;
421
+ static int s_workerThreads;
422
+ static int s_poolStarted;
423
+
424
+ // Claims and runs one pending task. Returns 1 if it ran something.
425
+ static int bxRunOneTask( void )
426
+ {
427
+ int count = atomic_load( &s_taskCount );
428
+ if ( count > BX_TASK_CAPACITY )
429
+ {
430
+ count = BX_TASK_CAPACITY;
431
+ }
432
+ for ( int i = 0; i < count; ++i )
433
+ {
434
+ bxTask* task = s_tasks + i;
435
+ if ( atomic_load( &task->status ) != bxTaskPending )
436
+ {
437
+ continue;
438
+ }
439
+ int expected = bxTaskPending;
440
+ if ( atomic_compare_exchange_strong( &task->status, &expected, bxTaskClaimed ) == 0 )
441
+ {
442
+ continue;
443
+ }
444
+ task->callback( task->context );
445
+ atomic_store( &task->status, bxTaskComplete );
446
+ return 1;
447
+ }
448
+ return 0;
449
+ }
450
+
451
+ static void* bxWorkerMain( void* context )
452
+ {
453
+ ( (void)( context ) );
454
+ for ( ;; )
455
+ {
456
+ // only worker threads block here; the thread that calls the step never does
457
+ sem_wait( &s_taskSemaphore );
458
+ while ( bxRunOneTask() )
459
+ {
460
+ }
461
+ }
462
+ return NULL;
463
+ }
464
+
465
+ // Grows the shared pool. Called while creating a world, never during a step.
466
+ static int bxEnsureWorkers( int threads )
467
+ {
468
+ if ( threads > BX_WORKER_POOL )
469
+ {
470
+ threads = BX_WORKER_POOL;
471
+ }
472
+ if ( s_poolStarted == 0 )
473
+ {
474
+ if ( sem_init( &s_taskSemaphore, 0, 0 ) != 0 )
475
+ {
476
+ return 0;
477
+ }
478
+ s_poolStarted = 1;
479
+ }
480
+ while ( s_workerThreads < threads )
481
+ {
482
+ pthread_t thread;
483
+ if ( pthread_create( &thread, NULL, bxWorkerMain, NULL ) != 0 )
484
+ {
485
+ break;
486
+ }
487
+ // nothing ever joins these: they live as long as the module does
488
+ pthread_detach( thread );
489
+ s_workerThreads++;
490
+ }
491
+ // what this world may use, not how big the shared pool has grown for some other world
492
+ return s_workerThreads < threads ? s_workerThreads : threads;
493
+ }
494
+
495
+ static void* bxEnqueueTask( b3TaskCallback* task, void* taskContext, void* userContext, const char* name )
496
+ {
497
+ ( (void)( userContext ) );
498
+ ( (void)( name ) );
499
+ int slot = atomic_fetch_add( &s_taskCount, 1 );
500
+ if ( slot >= BX_TASK_CAPACITY )
501
+ {
502
+ // more tasks in one step than the ring holds: run it here and tell box3d there is nothing to wait for
503
+ task( taskContext );
504
+ return NULL;
505
+ }
506
+ bxTask* entry = s_tasks + slot;
507
+ entry->callback = task;
508
+ entry->context = taskContext;
509
+ // publishes the callback and context with it
510
+ atomic_store( &entry->status, bxTaskPending );
511
+ sem_post( &s_taskSemaphore );
512
+ return entry;
513
+ }
514
+
515
+ static void bxFinishTask( void* userTask, void* userContext )
516
+ {
517
+ ( (void)( userContext ) );
518
+ if ( userTask == NULL )
519
+ {
520
+ return;
521
+ }
522
+ bxTask* entry = userTask;
523
+ while ( atomic_load( &entry->status ) != bxTaskComplete )
524
+ {
525
+ // carry a share of the step rather than spinning on it, which is also what keeps this from blocking
526
+ if ( bxRunOneTask() == 0 )
527
+ {
528
+ sched_yield();
529
+ }
530
+ }
531
+ }
532
+
533
+ // Rewinds the ring for a step. Every task from the last step has completed, because finishTask waited for each one.
534
+ static void bxResetTasks( void )
535
+ {
536
+ atomic_store( &s_taskCount, 0 );
537
+ }
538
+
539
+ #else
540
+
541
+ #define BX_MAX_WORKERS 1
542
+
543
+ #endif
544
+
370
545
  // ---------------------------------------------------------------------------------------------
371
546
  // World
372
547
  // ---------------------------------------------------------------------------------------------
373
548
 
374
- BX_EXPORT int bx_CreateWorld( float gx, float gy, float gz )
549
+ BX_EXPORT int bx_GetMaxWorkers( void )
375
550
  {
551
+ return BX_MAX_WORKERS;
552
+ }
553
+
554
+ BX_EXPORT int bx_CreateWorld( float gx, float gy, float gz, int workerCount )
555
+ {
556
+ if ( workerCount < 1 )
557
+ {
558
+ workerCount = 1;
559
+ }
560
+ if ( workerCount > BX_MAX_WORKERS )
561
+ {
562
+ workerCount = BX_MAX_WORKERS;
563
+ }
376
564
  for ( int i = 0; i < BX_MAX_WORLDS; ++i )
377
565
  {
378
566
  if ( B3_IS_NULL( s_worlds[i] ) )
@@ -380,6 +568,19 @@ BX_EXPORT int bx_CreateWorld( float gx, float gy, float gz )
380
568
  b3WorldDef def = b3DefaultWorldDef();
381
569
  def.gravity = bxVec3( gx, gy, gz );
382
570
  def.workerCount = 1;
571
+ #ifdef __EMSCRIPTEN_PTHREADS__
572
+ if ( workerCount > 1 )
573
+ {
574
+ // the shared pool, not box3d's own threads, and however many of them actually started
575
+ def.workerCount = (uint32_t)( bxEnsureWorkers( workerCount - 1 ) + 1 );
576
+ if ( def.workerCount > 1 )
577
+ {
578
+ def.enqueueTask = bxEnqueueTask;
579
+ def.finishTask = bxFinishTask;
580
+ def.userTaskContext = NULL;
581
+ }
582
+ }
583
+ #endif
383
584
  s_worlds[i] = b3CreateWorld( &def );
384
585
  return i + 1;
385
586
  }
@@ -387,6 +588,12 @@ BX_EXPORT int bx_CreateWorld( float gx, float gy, float gz )
387
588
  return 0;
388
589
  }
389
590
 
591
+ BX_EXPORT int bx_World_GetWorkerCount( int w )
592
+ {
593
+ b3WorldId worldId = bxGetWorld( w );
594
+ return B3_IS_NULL( worldId ) ? 0 : b3World_GetWorkerCount( worldId );
595
+ }
596
+
390
597
  BX_EXPORT void bx_DestroyWorld( int w )
391
598
  {
392
599
  b3WorldId worldId = bxGetWorld( w );
@@ -420,6 +627,9 @@ BX_EXPORT void bx_World_Step( int w, float dt, int subSteps )
420
627
  {
421
628
  return;
422
629
  }
630
+ #ifdef __EMSCRIPTEN_PTHREADS__
631
+ bxResetTasks();
632
+ #endif
423
633
  b3World_Step( worldId, dt, subSteps );
424
634
  }
425
635