reze-engine 0.32.2 → 0.33.1

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.
Files changed (63) hide show
  1. package/README.md +1 -3
  2. package/dist/engine.d.ts +1 -11
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +2 -31
  5. package/dist/physics/contact.d.ts.map +1 -1
  6. package/dist/physics/contact.js +13 -3
  7. package/dist/physics/physics.d.ts +20 -0
  8. package/dist/physics/physics.d.ts.map +1 -1
  9. package/dist/physics/physics.js +44 -10
  10. package/dist/physics/world.d.ts.map +1 -1
  11. package/dist/physics/world.js +9 -2
  12. package/dist/pmx-loader.d.ts.map +1 -1
  13. package/dist/pmx-loader.js +29 -11
  14. package/package.json +1 -1
  15. package/src/engine.ts +3 -33
  16. package/src/physics/contact.ts +10 -3
  17. package/src/physics/physics.ts +35 -6
  18. package/src/physics/world.ts +9 -2
  19. package/src/pmx-loader.ts +28 -12
  20. package/dist/gpu-profile.d.ts +0 -19
  21. package/dist/gpu-profile.d.ts.map +0 -1
  22. package/dist/gpu-profile.js +0 -120
  23. package/dist/physics/physics.worker.d.ts +0 -2
  24. package/dist/physics/physics.worker.d.ts.map +0 -1
  25. package/dist/physics/physics.worker.js +0 -52
  26. package/dist/physics/profile.d.ts +0 -18
  27. package/dist/physics/profile.d.ts.map +0 -1
  28. package/dist/physics/profile.js +0 -44
  29. package/dist/physics/worker-physics.d.ts +0 -28
  30. package/dist/physics/worker-physics.d.ts.map +0 -1
  31. package/dist/physics/worker-physics.js +0 -91
  32. package/dist/shaders/materials/body.d.ts +0 -2
  33. package/dist/shaders/materials/body.d.ts.map +0 -1
  34. package/dist/shaders/materials/body.js +0 -95
  35. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  36. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  37. package/dist/shaders/materials/cloth_rough.js +0 -69
  38. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  39. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  40. package/dist/shaders/materials/cloth_smooth.js +0 -61
  41. package/dist/shaders/materials/default.d.ts +0 -2
  42. package/dist/shaders/materials/default.d.ts.map +0 -1
  43. package/dist/shaders/materials/default.js +0 -43
  44. package/dist/shaders/materials/eye.d.ts +0 -2
  45. package/dist/shaders/materials/eye.d.ts.map +0 -1
  46. package/dist/shaders/materials/eye.js +0 -60
  47. package/dist/shaders/materials/face.d.ts +0 -2
  48. package/dist/shaders/materials/face.d.ts.map +0 -1
  49. package/dist/shaders/materials/face.js +0 -95
  50. package/dist/shaders/materials/hair.d.ts +0 -2
  51. package/dist/shaders/materials/hair.d.ts.map +0 -1
  52. package/dist/shaders/materials/hair.js +0 -90
  53. package/dist/shaders/materials/metal.d.ts +0 -2
  54. package/dist/shaders/materials/metal.d.ts.map +0 -1
  55. package/dist/shaders/materials/metal.js +0 -77
  56. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  57. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  58. package/dist/shaders/materials/mmd_classic.js +0 -66
  59. package/dist/shaders/materials/stockings.d.ts +0 -2
  60. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  61. package/dist/shaders/materials/stockings.js +0 -122
  62. package/src/physics/physics.worker.ts +0 -66
  63. package/src/physics/worker-physics.ts +0 -126
package/src/engine.ts CHANGED
@@ -7,7 +7,6 @@ import { VMDLoader } from "./vmd-loader"
7
7
  import { CameraAnimation } from "./camera-animation"
8
8
  import { PmxLoader } from "./pmx-loader"
9
9
  import { RezePhysics } from "./physics"
10
- import { WorkerPhysics } from "./physics/worker-physics"
11
10
  import {
12
11
  createFetchAssetReader,
13
12
  createFileMapAssetReader,
@@ -347,11 +346,6 @@ export interface GizmoDragEvent {
347
346
  export type GizmoDragCallback = (event: GizmoDragEvent) => void
348
347
 
349
348
  export type EngineOptions = {
350
- /** Run each model's physics on a dedicated Web Worker (default true where
351
- * Workers exist). Pipelined one frame deep: main-thread physics cost drops
352
- * to two small copies and multi-model scenes pay for the SLOWEST model
353
- * instead of the sum. false = classic main-thread stepping. */
354
- physicsWorkers?: boolean
355
349
  world?: WorldOptions
356
350
  sun?: SunOptions
357
351
  camera?: CameraOptions
@@ -382,7 +376,6 @@ export interface EngineStats {
382
376
  jitter: number // ms — stddev of frame intervals (pacing evenness; high = janky at any mean fps)
383
377
  cpuAnimMs: number // ms/frame (EMA) — model updates: blending, IK, world matrices
384
378
  cpuPhysicsMs: number // ms/frame (EMA) — physics stepping across all instances
385
- cpuPhysicsWorkerMs: number // ms/frame (EMA) — the SLOWEST worker's step cost. Background-thread time (often an efficiency core), parallel to the frame: it only matters if it exceeds the frame budget.
386
379
  cpuRenderMs: number // ms/frame (EMA) — the rest of the render thread: uniforms, encoding, submit
387
380
  }
388
381
 
@@ -436,7 +429,7 @@ interface ModelInstance {
436
429
  pickPerInstanceBindGroup: GPUBindGroup
437
430
  pickDrawCalls: PickDrawCall[]
438
431
  hiddenMaterials: Set<string>
439
- physics: RezePhysics | WorkerPhysics | null
432
+ physics: RezePhysics | null
440
433
  vertexBufferNeedsUpdate: boolean
441
434
  gpuMorph: GpuMorph | null
442
435
  // Style groups applied to this model: group id → compiled install.
@@ -837,7 +830,6 @@ export class Engine {
837
830
  fps1PercentLow: 0,
838
831
  cpuAnimMs: 0,
839
832
  cpuPhysicsMs: 0,
840
- cpuPhysicsWorkerMs: 0,
841
833
  cpuRenderMs: 0,
842
834
  jitter: 0,
843
835
  }
@@ -847,7 +839,6 @@ export class Engine {
847
839
  private viewTransform!: ViewTransformOptions
848
840
 
849
841
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
850
- this.physicsWorkersEnabled = options?.physicsWorkers ?? true
851
842
  this.canvas = canvas
852
843
  const d = DEFAULT_ENGINE_OPTIONS
853
844
  this.world = {
@@ -3040,10 +3031,7 @@ export class Engine {
3040
3031
 
3041
3032
  dispose() {
3042
3033
  this.stopRenderLoop()
3043
- this.forEachInstance((inst) => {
3044
- inst.model.stopAnimation()
3045
- if (inst.physics instanceof WorkerPhysics) inst.physics.dispose()
3046
- })
3034
+ this.forEachInstance((inst) => inst.model.stopAnimation())
3047
3035
  if (Engine.instance === this) Engine.instance = null
3048
3036
  if (this.camera) this.camera.detachControl()
3049
3037
 
@@ -3120,7 +3108,6 @@ export class Engine {
3120
3108
  const inst = this.modelInstances.get(name)
3121
3109
  if (!inst) return
3122
3110
  inst.model.stopAnimation()
3123
- if (inst.physics instanceof WorkerPhysics) inst.physics.dispose()
3124
3111
  for (const path of inst.textureCacheKeys) {
3125
3112
  const tex = this.textureCache.get(path)
3126
3113
  if (!tex) continue
@@ -3294,9 +3281,6 @@ export class Engine {
3294
3281
  // else on the render thread. The first question of any perf report.
3295
3282
  private cpuAnimMs = 0
3296
3283
  private cpuPhysicsMs = 0
3297
- /** EMA of worker-side step cost (all instances) — the off-thread physics bill. */
3298
- private cpuPhysicsWorkerMs = 0
3299
- private physicsWorkersEnabled = true
3300
3284
  private cpuRenderMs = 0
3301
3285
  private frameAnimMsRaw = 0
3302
3286
  private framePhysicsMsRaw = 0
@@ -3304,7 +3288,6 @@ export class Engine {
3304
3288
  private updateInstances(deltaTime: number): void {
3305
3289
  let animMs = 0
3306
3290
  let physicsMs = 0
3307
- let workerMs = 0
3308
3291
  this.forEachInstance((inst) => {
3309
3292
  const tAnim = performance.now()
3310
3293
  const verticesChanged = inst.model.update(deltaTime, this.ikEnabled)
@@ -3330,7 +3313,6 @@ export class Engine {
3330
3313
  const tPhys = performance.now()
3331
3314
  inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
3332
3315
  physicsMs += performance.now() - tPhys
3333
- if (inst.physics instanceof WorkerPhysics) workerMs = Math.max(workerMs, inst.physics.stepMs)
3334
3316
  }
3335
3317
  if (inst.vertexBufferNeedsUpdate) this.updateVertexBuffer(inst)
3336
3318
  })
@@ -3339,7 +3321,6 @@ export class Engine {
3339
3321
  const EMA = 0.1
3340
3322
  this.cpuAnimMs += (animMs - this.cpuAnimMs) * EMA
3341
3323
  this.cpuPhysicsMs += (physicsMs - this.cpuPhysicsMs) * EMA
3342
- this.cpuPhysicsWorkerMs += (workerMs - this.cpuPhysicsWorkerMs) * EMA
3343
3324
  }
3344
3325
 
3345
3326
  private updateVertexBuffer(inst: ModelInstance): void {
@@ -3451,17 +3432,7 @@ export class Engine {
3451
3432
  this.device.queue.writeBuffer(indexBuffer, 0, indices)
3452
3433
 
3453
3434
  const rbs = model.getRigidbodies()
3454
- let physics: RezePhysics | WorkerPhysics | null = null
3455
- if (rbs.length > 0) {
3456
- if (this.physicsWorkersEnabled && WorkerPhysics.supported()) {
3457
- try {
3458
- physics = await WorkerPhysics.create(rbs, model.getJoints(), model.getBoneInverseBindMatrices())
3459
- } catch (e) {
3460
- console.warn("[reze] physics worker unavailable — falling back to main-thread physics:", e)
3461
- }
3462
- }
3463
- if (!physics) physics = new RezePhysics(rbs, model.getJoints())
3464
- }
3435
+ const physics = rbs.length > 0 ? new RezePhysics(rbs, model.getJoints()) : null
3465
3436
 
3466
3437
  const shadowBindGroup = this.device.createBindGroup({
3467
3438
  label: `${name}: shadow bind`,
@@ -5515,7 +5486,6 @@ export class Engine {
5515
5486
  this.stats.jitter = Math.round(stddev * 100) / 100
5516
5487
  this.stats.cpuAnimMs = Math.round(this.cpuAnimMs * 100) / 100
5517
5488
  this.stats.cpuPhysicsMs = Math.round(this.cpuPhysicsMs * 100) / 100
5518
- this.stats.cpuPhysicsWorkerMs = Math.round(this.cpuPhysicsWorkerMs * 100) / 100
5519
5489
  this.stats.cpuRenderMs = Math.round(this.cpuRenderMs * 100) / 100
5520
5490
  }
5521
5491
  }
@@ -929,8 +929,13 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
929
929
  return
930
930
  }
931
931
  if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Sphere) {
932
+ // Only flip a contact this call actually produced. The detector returns
933
+ // without emitting whenever the shapes are out of range, and flipping then
934
+ // reverses the normal of whatever unrelated contact happens to sit at the
935
+ // end of the pool — pushing those two bodies together instead of apart.
936
+ const before = pool.count
932
937
  detectSphereCapsule(store, b, a, pool)
933
- flipLastNormal(pool)
938
+ if (pool.count > before) flipLastNormal(pool)
934
939
  return
935
940
  }
936
941
  if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Capsule) {
@@ -942,8 +947,9 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
942
947
  return
943
948
  }
944
949
  if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Sphere) {
950
+ const before = pool.count
945
951
  detectSphereBox(store, b, a, pool)
946
- flipLastNormal(pool)
952
+ if (pool.count > before) flipLastNormal(pool)
947
953
  return
948
954
  }
949
955
  if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Box) {
@@ -951,8 +957,9 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
951
957
  return
952
958
  }
953
959
  if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Capsule) {
960
+ const before = pool.count
954
961
  detectCapsuleBox(store, b, a, pool)
955
- flipLastNormal(pool)
962
+ if (pool.count > before) flipLastNormal(pool)
956
963
  return
957
964
  }
958
965
  // Box-box left unimplemented.
@@ -266,10 +266,12 @@ export class RezePhysics {
266
266
  }
267
267
  if (this.timeAccum >= this.fixedTimeStep) {
268
268
  // Substep budget exhausted mid-catchup: drop the remaining time and
269
- // snap kinematic bodies the rest of the way (zero velocity) so they
270
- // don't start next frame lagging behind their bones.
269
+ // snap kinematic bodies the rest of the way so they don't start next
270
+ // frame lagging behind their bones. They keep the velocity their
271
+ // trajectory implies — the character did move, and cloth in contact
272
+ // needs to know that or it stops being dragged and starts juddering.
271
273
  this.timeAccum = 0
272
- this.snapKinematicToTargets(false)
274
+ this.snapKinematicToTargets(false, true)
273
275
  }
274
276
 
275
277
  // MMD mode-2 bone alignment for pinned (depth-1) bodies: position
@@ -470,7 +472,27 @@ export class RezePhysics {
470
472
  // Snap kinematic bodies straight to the frame target with zero velocity.
471
473
  // Used on teleports (onlyFlagged) and when the substep budget runs out
472
474
  // mid-catchup (all).
473
- private snapKinematicToTargets(onlyFlagged: boolean): void {
475
+ /**
476
+ * Put kinematic bodies exactly on their targets.
477
+ *
478
+ * `keepTrajectoryVelocity` decides what the cloth is then told about how
479
+ * those bodies got there, and the two callers want opposite answers.
480
+ *
481
+ * After a teleport the honest answer is "nothing" — a scrub is not motion
482
+ * and handing the solver the implied velocity is what used to fling cloth
483
+ * across the discontinuity.
484
+ *
485
+ * After the substep budget is exhausted it is the opposite. The body really
486
+ * did travel along its animated trajectory this frame; the simulation just
487
+ * could not afford to walk it there in steps. Zeroing there tells every
488
+ * dynamic body in contact that the character stopped dead, so cloth loses
489
+ * its drag and is teleported by the body instead of dragged by it — it stops
490
+ * flowing and starts juddering. That branch only fires on a device already
491
+ * running under its refresh rate, which is precisely where the simulation
492
+ * can least afford to also look broken. The trajectory velocity is already
493
+ * computed every frame in kinTargetVel; it just needs to survive.
494
+ */
495
+ private snapKinematicToTargets(onlyFlagged: boolean, keepTrajectoryVelocity = false): void {
474
496
  const N = this.store.count
475
497
  const positions = this.store.positions
476
498
  const orientations = this.store.orientations
@@ -480,6 +502,8 @@ export class RezePhysics {
480
502
  const boneIdx = this.store.boneIndex
481
503
  const tp = this.kinTargetPos
482
504
  const to = this.kinTargetOri
505
+ const tv = this.kinTargetVel
506
+ const tav = this.kinTargetAngVel
483
507
  const flags = this.teleportFlags
484
508
 
485
509
  for (let i = 0; i < N; i++) {
@@ -496,8 +520,13 @@ export class RezePhysics {
496
520
  orientations[i4 + 1] = to[i4 + 1]
497
521
  orientations[i4 + 2] = to[i4 + 2]
498
522
  orientations[i4 + 3] = to[i4 + 3]
499
- lv[i3 + 0] = 0; lv[i3 + 1] = 0; lv[i3 + 2] = 0
500
- av[i3 + 0] = 0; av[i3 + 1] = 0; av[i3 + 2] = 0
523
+ if (keepTrajectoryVelocity) {
524
+ lv[i3 + 0] = tv[i3 + 0]; lv[i3 + 1] = tv[i3 + 1]; lv[i3 + 2] = tv[i3 + 2]
525
+ av[i3 + 0] = tav[i3 + 0]; av[i3 + 1] = tav[i3 + 1]; av[i3 + 2] = tav[i3 + 2]
526
+ } else {
527
+ lv[i3 + 0] = 0; lv[i3 + 1] = 0; lv[i3 + 2] = 0
528
+ av[i3 + 0] = 0; av[i3 + 1] = 0; av[i3 + 2] = 0
529
+ }
501
530
  }
502
531
  }
503
532
 
@@ -93,8 +93,15 @@ export class World {
93
93
  // it because it doesn't go through the velocity channel. Inverse-mass
94
94
  // weighted so a kinematic body stays put and only the dynamic one
95
95
  // translates.
96
- const POS_CORRECTION_FACTOR = 0.4
97
- const POS_SLOP = 0.005
96
+ // Softer and later than a rigid-body engine would use, deliberately. MMD
97
+ // cloth is a dense chain of small bodies resting on the character, and
98
+ // aggressive penetration recovery feeds those contacts energy that the
99
+ // joint springs hand straight back — the tips never settle. Measured on a
100
+ // 688-body model: resting peak velocity 1.79 → 1.10, and 12.9 → 6.6 under
101
+ // an idle animation, for a slop of two hundredths of a unit (under 2 mm at
102
+ // MMD scale) that no eye finds.
103
+ const POS_CORRECTION_FACTOR = 0.15
104
+ const POS_SLOP = 0.02
98
105
  for (let ci = 0; ci < contacts.count; ci++) {
99
106
  const c = contacts.get(ci)
100
107
  if (c.depth <= POS_SLOP) continue
package/src/pmx-loader.ts CHANGED
@@ -84,10 +84,19 @@ export class PmxLoader {
84
84
  }
85
85
 
86
86
  private parseHeader() {
87
- if (this.getString(3) !== "PMX") throw new Error("Not a PMX file")
88
-
89
- // PMX: 1-byte alignment after signature
90
- this.getUint8()
87
+ // The signature is four bytes. PMX 2.x writes "PMX ", but exporters in the
88
+ // wild also write "Pmx " — and a strict uppercase compare rejected those
89
+ // models outright. Compare case-insensitively and name what was actually
90
+ // found when it is something else, so a PMD (the older format, signature
91
+ // "Pmd") says so instead of "not a PMX file".
92
+ const signature = this.getString(4)
93
+ if (signature.slice(0, 3).toUpperCase() !== "PMX") {
94
+ const head = signature.slice(0, 3).toUpperCase()
95
+ if (head === "PMD") {
96
+ throw new Error("This is a PMD file (MMD's older format). Convert it to PMX in PMXEditor first.")
97
+ }
98
+ throw new Error(`Not a PMX file (signature "${signature.replace(/[^\x20-\x7e]/g, "?")}")`)
99
+ }
91
100
 
92
101
  // PMX: version (float32)
93
102
  const version = this.getFloat32()
@@ -1128,7 +1137,9 @@ export class PmxLoader {
1128
1137
  }
1129
1138
 
1130
1139
  private getString(len: number) {
1131
- const bytes = new Uint8Array(this.view.buffer, this.offset, len)
1140
+ // byteOffset matters: a DataView built over a slice of a larger buffer has
1141
+ // its own origin, and reading the raw buffer without it lands elsewhere.
1142
+ const bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len)
1132
1143
  this.offset += len
1133
1144
  return String.fromCharCode(...bytes)
1134
1145
  }
@@ -1137,18 +1148,23 @@ export class PmxLoader {
1137
1148
  const len = this.getInt32()
1138
1149
  if (len <= 0) return ""
1139
1150
 
1140
- // Debug: log problematic string lengths
1141
- if (len > 1000 || len < -1000) {
1142
- throw new RangeError(`Suspicious string length: ${len} at offset ${this.offset - 4}`)
1143
- }
1144
-
1151
+ // No arbitrary length ceiling. A model's comment field carries credits and
1152
+ // terms of use and routinely runs past a thousand bytes — roughly five
1153
+ // hundred characters in UTF-16 so a "this looks too long" heuristic
1154
+ // rejected perfectly ordinary models from several distributors. The bounds
1155
+ // check below is the real guard: a length that would read past the end of
1156
+ // the file is corrupt, and any length that fits is the author's business.
1145
1157
  // Ensure we don't read beyond buffer bounds
1146
- if (this.offset + len > this.view.buffer.byteLength) {
1158
+ if (this.offset + len > this.view.byteLength) {
1147
1159
  throw new RangeError(`String length ${len} exceeds buffer bounds at offset ${this.offset - 4}`)
1148
1160
  }
1149
1161
 
1150
- const bytes = new Uint8Array(this.view.buffer, this.offset, len)
1162
+ let bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len)
1151
1163
  this.offset += len
1164
+ // UTF-16 needs whole code units. A stray odd length (seen from a few
1165
+ // exporters) made TextDecoder emit a replacement char and, worse, shifted
1166
+ // nothing — better to drop the orphan byte than to carry it into the name.
1167
+ if (this.encoding === 0 && len % 2 === 1) bytes = bytes.subarray(0, len - 1)
1152
1168
  return this.decoder.decode(bytes)
1153
1169
  }
1154
1170
 
@@ -1,19 +0,0 @@
1
- export declare class GpuTimestamp {
2
- enabled: boolean;
3
- readonly supported: boolean;
4
- private device;
5
- private querySet;
6
- private resolveBuffer;
7
- private readbackBuffer;
8
- private nextSlot;
9
- private frameLabels;
10
- private inFlight;
11
- private pendingLabels;
12
- constructor(device: GPUDevice);
13
- private ensureCreated;
14
- beginFrame(): void;
15
- wrap(desc: GPURenderPassDescriptor, label: string): GPURenderPassDescriptor;
16
- endFrame(encoder: GPUCommandEncoder): void;
17
- afterSubmit(): void;
18
- }
19
- //# sourceMappingURL=gpu-profile.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gpu-profile.d.ts","sourceRoot":"","sources":["../src/gpu-profile.ts"],"names":[],"mappings":"AAoBA,qBAAa,YAAY;IACvB,OAAO,UAAQ;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAQ;IAKxB,OAAO,CAAC,aAAa,CAAwB;gBAEjC,MAAM,EAAE,SAAS;IAK7B,OAAO,CAAC,aAAa;IAiBrB,UAAU,IAAI,IAAI;IAWlB,IAAI,CAAC,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAqB3E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAW1C,WAAW,IAAI,IAAI;CAwBpB"}
@@ -1,120 +0,0 @@
1
- // WebGPU timestamp-query profiling. Records pass durations into the same
2
- // profiler module as CPU phases (so the snapshot table mixes both with a
3
- // "gpu." prefix). Read-only — no effect on rendering output.
4
- //
5
- // Pattern (per spec):
6
- // 1. Each render pass declares timestampWrites { begin, end } slot indices.
7
- // 2. After all passes, encoder.resolveQuerySet writes uint64 ns timestamps
8
- // into a QUERY_RESOLVE buffer.
9
- // 3. We copy the resolve buffer into a MAP_READ readback buffer.
10
- // 4. Submit. After the GPU finishes, mapAsync resolves; we read durations
11
- // and record them.
12
- //
13
- // One readback buffer with an in-flight gate: if mapAsync hasn't resolved
14
- // from a previous frame, we skip the copy this frame (no contention, just
15
- // one frame of dropped sampling).
16
- import { profiler } from "./physics/profile";
17
- const MAX_PASSES_PER_FRAME = 64;
18
- export class GpuTimestamp {
19
- constructor(device) {
20
- this.enabled = false;
21
- this.querySet = null;
22
- this.resolveBuffer = null;
23
- this.readbackBuffer = null;
24
- this.nextSlot = 0;
25
- this.frameLabels = [];
26
- this.inFlight = false;
27
- // Set inside endFrame() when a copy was queued this frame; consumed by
28
- // afterSubmit() to kick mapAsync. mapAsync must run AFTER queue.submit —
29
- // calling it before submit puts the buffer into "mapping pending" state,
30
- // which makes the copy in the encoder illegal.
31
- this.pendingLabels = null;
32
- this.device = device;
33
- this.supported = device.features.has("timestamp-query");
34
- }
35
- ensureCreated() {
36
- if (this.querySet || !this.supported)
37
- return;
38
- const slots = MAX_PASSES_PER_FRAME * 2;
39
- this.querySet = this.device.createQuerySet({ type: "timestamp", count: slots });
40
- this.resolveBuffer = this.device.createBuffer({
41
- label: "gpu-timestamp-resolve",
42
- size: slots * 8,
43
- usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
44
- });
45
- this.readbackBuffer = this.device.createBuffer({
46
- label: "gpu-timestamp-readback",
47
- size: slots * 8,
48
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
49
- });
50
- }
51
- // Called once at the top of each render() before any pass wrap.
52
- beginFrame() {
53
- if (!this.enabled || !this.supported)
54
- return;
55
- this.ensureCreated();
56
- this.nextSlot = 0;
57
- this.frameLabels = [];
58
- }
59
- // Wrap a render-pass descriptor with timestampWrites for `label`. If
60
- // profiling is off / unsupported / out of slots, returns desc unchanged.
61
- // The same label called multiple times (e.g. each bloom mip) is fine —
62
- // the profiler accumulates by label.
63
- wrap(desc, label) {
64
- if (!this.enabled || !this.querySet)
65
- return desc;
66
- if (this.nextSlot + 2 > MAX_PASSES_PER_FRAME * 2)
67
- return desc;
68
- const begin = this.nextSlot;
69
- const end = this.nextSlot + 1;
70
- this.nextSlot += 2;
71
- this.frameLabels.push(label);
72
- return {
73
- ...desc,
74
- timestampWrites: {
75
- querySet: this.querySet,
76
- beginningOfPassWriteIndex: begin,
77
- endOfPassWriteIndex: end,
78
- },
79
- };
80
- }
81
- // Call on the encoder after all passes, BEFORE encoder.finish(). Adds
82
- // resolveQuerySet + copyBufferToBuffer commands. mapAsync is deferred to
83
- // afterSubmit() — calling it before submit transitions the readback buffer
84
- // into "mapping pending" state, which makes the queued copy illegal.
85
- endFrame(encoder) {
86
- if (!this.enabled || !this.querySet || this.frameLabels.length === 0)
87
- return;
88
- const count = this.frameLabels.length * 2;
89
- encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
90
- if (this.inFlight)
91
- return;
92
- encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, count * 8);
93
- this.pendingLabels = this.frameLabels.slice();
94
- }
95
- // Call AFTER device.queue.submit(). Kicks mapAsync if a copy was queued
96
- // this frame; the .then() reads durations and unmaps.
97
- afterSubmit() {
98
- const labels = this.pendingLabels;
99
- if (!labels)
100
- return;
101
- this.pendingLabels = null;
102
- this.inFlight = true;
103
- const readback = this.readbackBuffer;
104
- void readback.mapAsync(GPUMapMode.READ).then(() => {
105
- const arr = new BigInt64Array(readback.getMappedRange());
106
- for (let i = 0; i < labels.length; i++) {
107
- const start = arr[i * 2];
108
- const end = arr[i * 2 + 1];
109
- // Per spec, timestamps are nanoseconds.
110
- const ms = Number(end - start) / 1e6;
111
- if (ms >= 0 && ms < 1000)
112
- profiler.record("gpu." + labels[i], ms);
113
- }
114
- readback.unmap();
115
- this.inFlight = false;
116
- }, () => {
117
- this.inFlight = false;
118
- });
119
- }
120
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=physics.worker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"physics.worker.d.ts","sourceRoot":"","sources":["../../src/physics/physics.worker.ts"],"names":[],"mappings":""}
@@ -1,52 +0,0 @@
1
- // Physics worker: owns ONE model's RezePhysics world. The host posts the
2
- // frame's bone world matrices as a transferable buffer; the worker steps the
3
- // simulation (identical math to the main-thread path — same class, same code)
4
- // and returns the buffer with the dynamic bones' matrices written back, plus
5
- // its own step cost for the host's stats.
6
- //
7
- // The incoming buffer is COPIED into a persistent local pose (and back out)
8
- // rather than wrapped: transferred ArrayBuffers get a fresh identity on every
9
- // hop, so wrappers could never be cached against them — and two ~20KB copies
10
- // per frame are microseconds.
11
- import { RezePhysics } from "./physics";
12
- import { Mat4 } from "../math";
13
- import { RigidbodyType } from "./types";
14
- const ctx = self;
15
- let physics = null;
16
- let inverseBind = new Float32Array(0);
17
- let pose = new Float32Array(0);
18
- let mats = [];
19
- ctx.onmessage = (e) => {
20
- const msg = e.data;
21
- if (msg.cmd === "init") {
22
- physics = new RezePhysics(msg.rigidbodies, msg.joints);
23
- inverseBind = msg.inverseBind;
24
- const boneCount = inverseBind.length / 16;
25
- pose = new Float32Array(boneCount * 16);
26
- mats = new Array(boneCount);
27
- for (let i = 0; i < boneCount; i++)
28
- mats[i] = new Mat4(pose.subarray(i * 16, i * 16 + 16));
29
- // The host copies back only the bones physics can write — the bones of
30
- // dynamic bodies. Kinematic bones must NOT round-trip: in the pipelined
31
- // protocol they would drag a stale (frame-old) pose over the live one.
32
- const dynamicBones = [];
33
- for (const rb of msg.rigidbodies) {
34
- if (rb.type === RigidbodyType.Dynamic && rb.mass > 0 && rb.boneIndex >= 0)
35
- dynamicBones.push(rb.boneIndex);
36
- }
37
- ctx.postMessage({ cmd: "ready", dynamicBones });
38
- return;
39
- }
40
- if (!physics)
41
- return;
42
- const incoming = new Float32Array(msg.bones);
43
- pose.set(incoming);
44
- const t0 = performance.now();
45
- if (msg.cmd === "step")
46
- physics.step(msg.dt, mats, inverseBind);
47
- else
48
- physics.reset(mats);
49
- const stepMs = performance.now() - t0;
50
- incoming.set(pose);
51
- ctx.postMessage({ cmd: "stepped", bones: msg.bones, stepMs }, [msg.bones]);
52
- };
@@ -1,18 +0,0 @@
1
- declare class PhysicsProfiler {
2
- enabled: boolean;
3
- private sums;
4
- private counts;
5
- begin(): number;
6
- end(label: string, t0: number): void;
7
- record(label: string, ms: number): void;
8
- snapshot(): Record<string, {
9
- avgMs: number;
10
- calls: number;
11
- totalMs: number;
12
- }>;
13
- reset(): void;
14
- }
15
- export declare const profiler: PhysicsProfiler;
16
- export type ProfileSnapshot = ReturnType<PhysicsProfiler["snapshot"]>;
17
- export {};
18
- //# sourceMappingURL=profile.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/physics/profile.ts"],"names":[],"mappings":"AAIA,cAAM,eAAe;IACnB,OAAO,UAAQ;IACf,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,MAAM,CAA8C;IAE5D,KAAK,IAAI,MAAM;IAIf,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAU7E,KAAK,IAAI,IAAI;CAId;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA;AAC7C,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA"}
@@ -1,44 +0,0 @@
1
- // Per-phase wall-clock accumulator. Off by default — when `enabled` is false,
2
- // begin()/end() short-circuit so production has zero overhead. Toggle via
3
- // `RezePhysics.setProfileEnabled(true)` and read with `getProfile()`.
4
- class PhysicsProfiler {
5
- constructor() {
6
- this.enabled = false;
7
- this.sums = Object.create(null);
8
- this.counts = Object.create(null);
9
- }
10
- begin() {
11
- return this.enabled ? performance.now() : 0;
12
- }
13
- end(label, t0) {
14
- if (!this.enabled)
15
- return;
16
- const dt = performance.now() - t0;
17
- this.sums[label] = (this.sums[label] ?? 0) + dt;
18
- this.counts[label] = (this.counts[label] ?? 0) + 1;
19
- }
20
- // Direct accumulator for durations not measured via begin()/end() — used by
21
- // GPU timestamp readback, which arrives async after the frame has ended.
22
- record(label, ms) {
23
- if (!this.enabled)
24
- return;
25
- this.sums[label] = (this.sums[label] ?? 0) + ms;
26
- this.counts[label] = (this.counts[label] ?? 0) + 1;
27
- }
28
- // Snapshot since last reset(). avgMs is per call; totalMs is the cumulative
29
- // window cost — divide by elapsed wall time to get a "% of frame budget".
30
- snapshot() {
31
- const out = {};
32
- for (const k in this.sums) {
33
- const total = this.sums[k];
34
- const calls = this.counts[k];
35
- out[k] = { avgMs: calls > 0 ? total / calls : 0, calls, totalMs: total };
36
- }
37
- return out;
38
- }
39
- reset() {
40
- this.sums = Object.create(null);
41
- this.counts = Object.create(null);
42
- }
43
- }
44
- export const profiler = new PhysicsProfiler();
@@ -1,28 +0,0 @@
1
- import type { Rigidbody, Joint } from "./types";
2
- import type { Mat4 } from "../math";
3
- export declare class WorkerPhysics {
4
- private readonly worker;
5
- private dynamicBones;
6
- private readonly boneCount;
7
- /** Transfer buffer when idle; null while a step is in flight. */
8
- private buf;
9
- /** Latest completed pose from the worker (copied out of the transfer buffer). */
10
- private readonly result;
11
- private hasResult;
12
- private pendingDt;
13
- private queuedReset;
14
- /** Worker-side cost of the last completed step — for engine stats. */
15
- stepMs: number;
16
- private constructor();
17
- static supported(): boolean;
18
- static create(rigidbodies: Rigidbody[], joints: Joint[], inverseBind: Float32Array): Promise<WorkerPhysics>;
19
- /** Same signature as RezePhysics.step — the engine cannot tell them apart.
20
- * (inverseBind was shipped to the worker at init; the param is unused.) */
21
- step(dt: number, boneWorldMatrices: Mat4[], _inverseBind: Float32Array): void;
22
- /** Reset rides the same pipeline: the next posted frame carries a reset
23
- * command instead of a step, and stale results stop applying immediately. */
24
- reset(boneWorldMatrices: Mat4[]): void;
25
- dispose(): void;
26
- private onStepped;
27
- }
28
- //# sourceMappingURL=worker-physics.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"worker-physics.d.ts","sourceRoot":"","sources":["../../src/physics/worker-physics.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAYnC,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,iEAAiE;IACjE,OAAO,CAAC,GAAG,CAAoB;IAC/B,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,WAAW,CAAQ;IAC3B,sEAAsE;IACtE,MAAM,SAAI;IAEV,OAAO;IAOP,MAAM,CAAC,SAAS,IAAI,OAAO;IAI3B,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IA6B3G;gFAC4E;IAC5E,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI;IAuB7E;kFAC8E;IAC9E,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IAOtC,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,SAAS;CAQlB"}