@volter/blender-engine 0.1.49 → 0.1.50

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.
@@ -198,6 +198,21 @@ export async function startEmscriptenBlenderEngine(
198
198
  if (text.startsWith('@@VOLTER-READY ')) readyLine = text.slice('@@VOLTER-READY '.length);
199
199
  options.log(sessionLevel(text), text);
200
200
  };
201
+ // THE HEAP ITSELF, not a view of it. Blender runs on a pthread, so when its heap grows there
202
+ // this worker's `module.HEAPU8` keeps the length it had until the glue next refreshes it -- and
203
+ // no export the door calls does. MEASURED 2026-09-29 on the Stoneguard scene from
204
+ // game-benchmarks main: a mesh's columns at 1.14 GB were read through a 433 MB view, and the
205
+ // pull failed "outside the export arena". The glue builds its memory with `new
206
+ // WebAssembly.Memory` and exports no handle to it, so that constructor is observed while the
207
+ // factory runs; every read below takes a fresh view of `memory.buffer`.
208
+ const Memory = WebAssembly.Memory;
209
+ let memory: WebAssembly.Memory | null = null;
210
+ (WebAssembly as { Memory: unknown }).Memory = function (descriptor: WebAssembly.MemoryDescriptor) {
211
+ const made = new Memory(descriptor);
212
+ memory ??= made;
213
+ return made;
214
+ } as unknown as typeof WebAssembly.Memory;
215
+ const heap = (): Uint8Array => (memory ? new Uint8Array(memory.buffer) : module.HEAPU8);
201
216
  const module = await factory({
202
217
  arguments: ['--background', '--factory-startup', '--python', SESSION_SCRIPT],
203
218
  locateFile: (file: string) => artifactUrl(file),
@@ -253,6 +268,8 @@ export async function startEmscriptenBlenderEngine(
253
268
  );
254
269
  },
255
270
  ],
271
+ }).finally(() => {
272
+ (WebAssembly as { Memory: unknown }).Memory = Memory;
256
273
  });
257
274
  // `main()` runs on a pthread (`-sPROXY_TO_PTHREAD`), so the factory resolves
258
275
  // long before the session exists. The ready line is what says it does.
@@ -329,13 +346,15 @@ export async function startEmscriptenBlenderEngine(
329
346
  // A VIEW, NOT A COPY. The arena is wasm linear memory and under
330
347
  // `-sPROXY_TO_PTHREAD` that memory is SHARED, so `session-frame.mts`
331
348
  // slices every column it reads out of here; this call itself is free.
332
- readArena: async () =>
333
- module.HEAPU8.subarray(
334
- module._blender_web_export_buffer(),
335
- module._blender_web_export_buffer() + module._blender_web_export_buffer_size(),
336
- ),
349
+ // A DIRECT ARENA (address 0) IS THE WHOLE HEAP, at its length now: the size the door reports
350
+ // is `emscripten_get_heap_size()`, which is Blender's pthread's own view of the heap and is as
351
+ // stale as this worker's was (measured: 431 MB while a column lay at 2.0 GB).
352
+ readArena: async () => {
353
+ const base = module._blender_web_export_buffer();
354
+ return base === 0 ? heap() : heap().subarray(base, base + module._blender_web_export_buffer_size());
355
+ },
337
356
  bootMs,
338
- memoryBytes: () => module.HEAPU8.length,
357
+ memoryBytes: () => heap().length,
339
358
  releasedPayloadBytes,
340
359
  };
341
360
  }
@@ -162,6 +162,8 @@ def _mark(label):
162
162
  MEMORY_MARKS.append([label] + [round(reading.get(key, 0) / 1048576)
163
163
  for key in ("heap", "mallocInUse", "mallocFree", "guarded", "guardedPeak")])
164
164
  del MEMORY_MARKS[:-64]
165
+ # Also said, so an engine that dies of memory names in its last output the stage it reached.
166
+ _say("@@VOLTER-MEMORY %s heap=%d inUse=%d free=%d" % tuple(MEMORY_MARKS[-1][:4]))
165
167
  _real_stderr = sys.stderr
166
168
 
167
169
 
@@ -1611,7 +1613,7 @@ class Session:
1611
1613
  images = [name for name, image in frame["images"].items() if image.get("deferred")]
1612
1614
  if not meshes and not images:
1613
1615
  return False
1614
- ask({"hold": frame})
1616
+ _asked({"hold": frame})
1615
1617
  options = {"buffer_path": EXPORT_BUFFER_PATH} if EXPORT_BUFFER_PATH else {}
1616
1618
  for kind, keys, door in (("mesh", meshes, _blender_web.export_mesh),
1617
1619
  ("image", images, _blender_web.export_image)):
@@ -1626,7 +1628,7 @@ class Session:
1626
1628
  if piece[kind] is None:
1627
1629
  del frame["images"][key]
1628
1630
  continue
1629
- ask({kind: key, "piece": piece[kind]})
1631
+ _asked({kind: key, "piece": piece[kind]})
1630
1632
  return True
1631
1633
 
1632
1634
  def _send_encoded_image(self, key, deferred):
@@ -1666,7 +1668,7 @@ class Session:
1666
1668
  with open(path, "wb") as fh:
1667
1669
  fh.write(data)
1668
1670
  try:
1669
- ask({"image": key, "piece": {
1671
+ _asked({"image": key, "piece": {
1670
1672
  "revision": deferred["revision"],
1671
1673
  "mime": mime,
1672
1674
  "colorspace": "sRGB" if image.colorspace_settings.name == "sRGB" else "data",
@@ -2014,6 +2016,17 @@ def _pause(since):
2014
2016
  time.sleep(0.0002 if time.monotonic() - since < _BRISK_SECONDS else 0.002)
2015
2017
 
2016
2018
 
2019
+ def _asked(payload):
2020
+ """`ask`, failing as the tab failed: a piece the worker could not take comes back as
2021
+ `{"error": ...}`, and a pull that read past it presented a frame missing that piece, refused
2022
+ later under another name ("deferred and never pulled")."""
2023
+ answer = ask(payload)
2024
+ if isinstance(answer, dict) and answer.get("error"):
2025
+ raise RuntimeError("The tab refused %s: %s" % (
2026
+ next((key for key in ("mesh", "image", "hold") if key in payload), "an ask"), answer["error"]))
2027
+ return answer
2028
+
2029
+
2017
2030
  def ask(payload):
2018
2031
  """Block until the tab answers. The worker's own thread is free while this
2019
2032
  waits; only the Blender pthread is held, which is where the operator is.
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.49",
5
+ "version": "0.1.50",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },