@volter/blender-engine 0.1.61 → 0.1.62

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.
@@ -1,7 +1,9 @@
1
1
  /** A cooperative job advances only when its owner asks for its next unit.
2
2
  * Checkpoints park the producer; they do not renew a timer while work runs. */
3
3
  export type PullResult<T> = { load: 'continue'; token: string; phase: string } | { load: 'done'; value: T };
4
+ let nextJob = 0;
4
5
  export class PullJob<T> {
6
+ private readonly job = ++nextJob;
5
7
  private started = false;
6
8
  private finished = false;
7
9
  private sequence = 0;
@@ -17,7 +19,7 @@ export class PullJob<T> {
17
19
  this.started = true;
18
20
  void Promise.resolve().then(() => this.work(phase => new Promise<void>(resolve => {
19
21
  this.resume = resolve;
20
- this.expected = `load:${++this.sequence}`;
22
+ this.expected = `load:${this.job}:${++this.sequence}`;
21
23
  const waiter = this.waiter!; this.waiter = null;
22
24
  waiter.resolve({ load: 'continue', token: this.expected, phase });
23
25
  }))).then(value => {
@@ -159,7 +159,7 @@ export class BlenderRuntime {
159
159
  #terminated = false;
160
160
  #dirty = false;
161
161
  readonly #beforeUnload = (event: BeforeUnloadEvent): void => {
162
- if (!this.#dirty && this.#pending.size === 0) return;
162
+ if (!this.#dirty && this.#acceptedRequests === 0) return;
163
163
  event.preventDefault();
164
164
  event.returnValue = '';
165
165
  };
@@ -259,7 +259,7 @@ export class BlenderRuntime {
259
259
  );
260
260
  this.#project = project;
261
261
  this.#document ??= document ?? 'models/model.blend';
262
- this.#started ??= this.#load({
262
+ this.#started ??= this.#request({
263
263
  op: 'start',
264
264
  project,
265
265
  ...(document === undefined ? {} : { document }),
@@ -267,15 +267,6 @@ export class BlenderRuntime {
267
267
  return this.#started;
268
268
  }
269
269
 
270
- async #load(request: Extract<Request, { op: 'start' }>): Promise<RuntimeStart> {
271
- let result = await this.#request(request) as PullResult<RuntimeStart>;
272
- while (result?.load === 'continue') {
273
- this.#loadBoundary = result.phase;
274
- result = await this.#request({ op: 'load-next', token: result.token }) as PullResult<RuntimeStart>;
275
- }
276
- return result?.load === 'done' ? result.value : result as unknown as RuntimeStart;
277
- }
278
-
279
270
  #ready(): Promise<RuntimeStart> {
280
271
  if (this.#project === null)
281
272
  return Promise.reject(
@@ -566,9 +557,32 @@ export class BlenderRuntime {
566
557
  this.#lastCallWindow = { start: started, end: started + elapsed };
567
558
  }
568
559
 
560
+ #acceptedRequests = 0;
561
+ #requestTail: Promise<void> = Promise.resolve();
569
562
  #request(request: Request, shutdown = false): Promise<unknown> {
570
563
  if (this.#terminated || (this.#stopping && !shutdown))
571
564
  return Promise.reject(new Error('The Blender session is stopping or terminated'));
565
+ const began = performance.now();
566
+ this.#acceptedRequests++;
567
+ const answer = this.#requestTail.then(async () => {
568
+ this.#loadBoundary = request.op;
569
+ let result = await this.#wireRequest(request) as PullResult<unknown>;
570
+ while (result?.load === 'continue') {
571
+ this.#loadBoundary = result.phase;
572
+ result = await this.#wireRequest({ op: 'load-next', token: result.token }) as PullResult<unknown>;
573
+ }
574
+ return result?.load === 'done' ? result.value : result;
575
+ }).finally(() => {
576
+ this.#acceptedRequests--;
577
+ const totalMs = Math.round(performance.now() - began);
578
+ try { if (totalMs >= 1000) this.#options.log?.('log', `@@VOLTER-WORK op=${request.op} totalMs=${totalMs}`); } catch { /* diagnostics cannot poison the queue */ }
579
+ });
580
+ this.#requestTail = answer.then(() => {}, () => {});
581
+ return answer;
582
+ }
583
+
584
+ #wireRequest(request: Request): Promise<unknown> {
585
+ if (this.#terminated) return Promise.reject(new Error('The Blender session is terminated'));
572
586
  const id = ++this.#nextId;
573
587
  return new Promise((resolve, reject) => {
574
588
  this.#pending.set(id, { resolve, reject });
@@ -4421,8 +4421,11 @@ def rna_rig(object_name=None):
4421
4421
  subjects = [obj for obj in bpy.context.view_layer.objects
4422
4422
  if obj.type == "MESH" and obj.data is not None
4423
4423
  and obj.find_armature() is not None]
4424
- return {"frame": scene_frame, "named": object_name,
4425
- "rigs": [_rig_of(obj) for obj in subjects]}
4424
+ rigs = []
4425
+ for obj in subjects:
4426
+ _asked({"checkpoint": "rig-object"})
4427
+ rigs.append(_rig_of(obj))
4428
+ return {"frame": scene_frame, "named": object_name, "rigs": rigs}
4426
4429
 
4427
4430
 
4428
4431
  def _rig_of(obj):
@@ -4506,7 +4509,11 @@ def _rig_of(obj):
4506
4509
  truncated = 0
4507
4510
  unweighted = 0
4508
4511
  most = 0
4512
+ checkpoint_at = time.monotonic()
4509
4513
  for i, vertex in enumerate(mesh.vertices):
4514
+ if i % 1024 == 0 and time.monotonic() - checkpoint_at >= 0.05:
4515
+ _asked({"checkpoint": "rig-weights"})
4516
+ checkpoint_at = time.monotonic()
4510
4517
  pairs = []
4511
4518
  for element in vertex.groups:
4512
4519
  bone = group_to_bone.get(int(element.group))
package/browser/worker.ts CHANGED
@@ -573,17 +573,22 @@ async function listSessionFiles(files: BlenderFiles, root: string): Promise<File
573
573
  return out;
574
574
  }
575
575
 
576
+ function pullWork(label: string, work: () => Promise<unknown>): Promise<unknown> {
577
+ if (loadCheckpoint) throw new Error('The current Blender operation must finish first');
578
+ loadJob = new PullJob(async checkpoint => {
579
+ loadCheckpoint = checkpoint;
580
+ const began = performance.now();
581
+ try { return await work(); }
582
+ finally { loadCheckpoint = null; log('log', `@@VOLTER-WORK op=${label} totalMs=${Math.round(performance.now() - began)}`); }
583
+ });
584
+ return loadJob.step();
585
+ }
586
+
576
587
  async function handle(request: WorkerRequest): Promise<unknown> {
577
588
  switch (request.op) {
578
589
  case 'start': {
579
590
  if (loadJob) throw new Error('The Blender load has already started');
580
- loadJob = new PullJob(async checkpoint => {
581
- loadCheckpoint = checkpoint;
582
- const began = performance.now();
583
- try { return await start(request.project, request.document); }
584
- finally { loadCheckpoint = null; log('log', `@@VOLTER-LOAD totalMs=${Math.round(performance.now() - began)}`); }
585
- });
586
- return loadJob.step();
591
+ return pullWork('start', () => start(request.project, request.document));
587
592
  }
588
593
  case 'load-next':
589
594
  if (!loadJob) throw new Error('No Blender load to continue');
@@ -632,7 +637,7 @@ async function handle(request: WorkerRequest): Promise<unknown> {
632
637
  case 'present':
633
638
  // Straight through to `session.py`'s own `present` op — the worker adds
634
639
  // nothing, and a capture-less present answers `{ presented, revision }`.
635
- return ask({ op: 'present' });
640
+ return pullWork('present', () => ask({ op: 'present' }));
636
641
  case 'scene-info':
637
642
  return session.sceneInfo();
638
643
  case 'object-info':
@@ -698,10 +703,10 @@ async function handle(request: WorkerRequest): Promise<unknown> {
698
703
  // because `collection` crossed `runtime.ts` and `session.py` and never
699
704
  // this switch, with no error anywhere.
700
705
  case 'rig':
701
- return ask({
706
+ return pullWork('rig', () => ask({
702
707
  op: 'rig',
703
708
  ...(request.object === undefined ? {} : { object: request.object }),
704
- });
709
+ }));
705
710
  case 'action-clip':
706
711
  return ask({
707
712
  op: 'action-clip',
@@ -791,7 +796,7 @@ self.onmessage = (event: MessageEvent<WorkerRequest>) => {
791
796
  async function answerRequest(request: WorkerRequest): Promise<void> {
792
797
  try {
793
798
  const result = await handle(request);
794
- if (documentDirty) await saveDocument();
799
+ if (!loadCheckpoint && documentDirty) await saveDocument();
795
800
  await reportHistory();
796
801
  post({ id: request.id, result });
797
802
  } catch (error) {
@@ -811,7 +816,7 @@ async function answerRequest(request: WorkerRequest): Promise<void> {
811
816
  }
812
817
 
813
818
  async function reportHistory(): Promise<void> {
814
- if (!engine || !session) return;
819
+ if (loadCheckpoint || !engine || !session) return;
815
820
  const entries = await engine.request({ op: 'history-events' }) as import('./protocol').NativeHistoryEntry[];
816
821
  if (entries.length) post({ op: 'history', entries });
817
822
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/blender-engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "GPL-3.0-or-later",
5
- "version": "0.1.61",
5
+ "version": "0.1.62",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },