Skip to content

Commit 63ff754

Browse files
Merge pull request #5 from ReactVision/develop
Release 1.0.1
2 parents dc12b9c + e659912 commit 63ff754

15 files changed

Lines changed: 1001 additions & 58 deletions

‎CHANGELOG.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## 1.0.1
4+
5+
Needs the virocore web build from the same change: the new renderer calls are
6+
absent from 1.0.0's binary, and each falls back to the old path when missing.
7+
8+
### Fixed
9+
10+
- **A runtime abort is reported.** `ViroWebRendererOptions.onAbort`, `renderer.addAbortListener()` and `renderer.abortedWith`. An abort — out of memory past the heap's ceiling, most often — is terminal, and it used to surface only as a rejection a model load caught, so nothing reached a host's error tracking. Model loads in flight settle as failed.
11+
- **Model files are removed from the virtual FS once loaded.** MEMFS holds every file in JS memory until unlinked, so each model stayed resident for the life of the page.
12+
- **The camera feed is drawn from the stream, not the tracker's buffer.** The stream is requested at `feedWidth`/`feedHeight` (default 1280x960) and uploaded to a source texture straight from the `<video>`; the tracker gets a downscaled copy at `captureWidth` along the stream's aspect. No readback, no per-frame texture. Older renderers keep the RGBA path.
13+
- **Relocalising frames keep their rotation.** tinyvio's `poseConfidence` now decides the renderer's state: Full or PositionNoScale is Normal, RotationOnly is Limited (rotation applied, position held), None holds the last pose. An engine without it is drawn only while Running.
14+
- **The pose is smoothed** with a One Euro filter before `arSetPose` (`poseSmoothing`, or `false` for the raw solve). tinyvio does not filter, and the raw pose shook content visibly while tracking was healthy.
15+
- **`onStatus` is debounced** out of Normal (15 frames) and back at once, and carries the frame's `confidence` and `reason`. `reason === NoGravity` that persists means no motion events are arriving.
16+
- **The default asset paths keep their trailing slash under Vite.** Vite rewrites `new URL("../slam/", import.meta.url)` at dev time to the directory without its `/`, so the tracker asked for `.../slamtinyvio-slam.wasm` and AR aborted ("both async and sync fetching of the wasm failed"). The renderer's `../wasm/` base had the same pattern. Both bases now end in `/`.
17+
- **An engine loaded through `loadSlam` finds its own `.wasm`.** The session pointed every engine at this package's `slam/` copy, including one a host loaded from its own script, which then fetched a file that was not there. With a custom `loadSlam` and no `slamBaseUrl`, the engine resolves the `.wasm` next to its script.
18+
- Playback reports untracked frames as Unavailable, so they hold the last tracked pose as the live path does.
19+
20+
### Added
21+
22+
- `PoseConfidence`, `TrackingReason`, `PoseFilter`, `ViroRendererAbortError`; `ViroSceneApi.createSourceTexture` and `updateTextureFromSource`.
23+
- `test/session.test.mjs` in `npm test`, and `npm run test:browser`: taps, the heap, the abort, source textures and dropout holding, in a real Chrome. Against 1.0.0's binary nine of its checks fail.
24+
325
## 1.0.0
426

527
First published release.

‎RELEASE_NOTES.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Release Notes
22

3+
## 1.0.1
4+
5+
Fixes found running the web player against real scenes. The compiled renderer in this release is virocore 3.0.2; the new calls fall back to the old path on an older binary, but the heap, tap and dropout fixes live in the binary and need this one.
6+
7+
### Fixed
8+
9+
- **Large models no longer kill the renderer.** The heap was fixed at 256 MB, and a 30 to 40 MB generated GLB crossed it once decoded; `abort("OOM")` is permanent, so every later call failed and the canvas stayed black. The heap now grows to 2 GB, and glTF loads no longer copy the model.
10+
- **An abort is reported.** `onAbort` in the options, `renderer.addAbortListener()` and `renderer.abortedWith`. Model loads in flight settle as failed.
11+
- **Model files leave the virtual FS once loaded,** instead of staying in JS memory for the life of the page.
12+
- **Taps land where they are drawn.** They were mirrored about the horizontal axis.
13+
- **Web AR holds its pose through a dropout** instead of snapping to a default orientation. tinyvio's `poseConfidence` decides the state: RotationOnly applies rotation and holds position, None holds the last pose.
14+
- **The pose is smoothed** with a One Euro filter (`poseSmoothing`, or `false` for the raw solve), and `onStatus` is debounced out of Normal and carries `confidence` and `reason`.
15+
- **The camera feed draws from the stream** at `feedWidth`/`feedHeight` (default 1280x960) through a source texture, with no per-frame readback; the tracker gets a downscaled copy.
16+
- **Web AR starts under Vite.** The tracker's wasm path lost its trailing slash in dev, and an engine loaded through `loadSlam` was pointed at the package's copy instead of its own.
17+
18+
### Added
19+
20+
- `PoseConfidence`, `TrackingReason`, `PoseFilter`, `ViroRendererAbortError`; `ViroSceneApi.createSourceTexture` and `updateTextureFromSource`.
21+
322
## 1.0.0
423

524
First published release. The Viro renderer — the same C++ engine ViroReact runs natively on iOS, Android, Apple Vision Pro and Meta Quest — compiled to WebAssembly and drawing through WebGL2.

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@reactvision/viro-web-renderer",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "WebAssembly/WebGL2 build of the Viro renderer (virocore) for the web platform",
55
"license": "MIT",
66
"author": "ReactVision",
@@ -48,7 +48,8 @@
4848
"scripts": {
4949
"copy-wasm": "bash scripts/copy-wasm.sh",
5050
"build": "tsc",
51-
"test": "node test/hitTest.test.mjs && node test/package.test.mjs",
51+
"test": "node test/hitTest.test.mjs && node test/session.test.mjs && node test/package.test.mjs",
52+
"test:browser": "node test/browser/run.mjs",
5253
"clean": "rm -rf dist *.tgz",
5354
"prepare": "npm run build",
5455
"prepublishOnly": "npm run copy-wasm && npm run copy-slam && npm run build && VIRO_REQUIRE_WASM=1 npm test",

‎src/ViroWebRenderer.ts‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type ViroNodeEventHandlers,
88
type ViroAnimationHandlers,
99
} from "./sceneApi.js";
10+
import { ViroRendererAbortError } from "./types.js";
1011
import type { ViroWebModule, ViroWebRendererOptions } from "./types.js";
1112

1213
const MODEL_EXT: Record<ViroModelFormat, string> = {
@@ -79,6 +80,8 @@ export class ViroWebRenderer {
7980
private readonly eventHandlers = new Map<ViroHandle, ViroNodeEventHandlers>();
8081
private readonly modelLoadResolvers = new Map<ViroHandle, (success: boolean) => void>();
8182
private readonly animationHandlers = new Map<ViroHandle, ViroAnimationHandlers>();
83+
private readonly abortListeners = new Set<(error: ViroRendererAbortError) => void>();
84+
private abortError: ViroRendererAbortError | null = null;
8285

8386
private constructor(
8487
private readonly module: ViroWebModule,
@@ -163,6 +166,7 @@ export class ViroWebRenderer {
163166
}
164167
const path = `${dir}/viro_model_${nodeHandle}.${MODEL_EXT[format]}`;
165168
this.module.FS.writeFile(path, bytes);
169+
const written = [path, ...resources.map((res) => `${dir}/${res.name}`)];
166170
return new Promise<boolean>((resolve) => {
167171
// One resolver per node, and a second load replaces the first. Settle the
168172
// one being replaced rather than dropping it: the native callback carries
@@ -171,7 +175,25 @@ export class ViroWebRenderer {
171175
this.modelLoadResolvers.get(nodeHandle)?.(false);
172176
this.modelLoadResolvers.set(nodeHandle, resolve);
173177
this.module.viroLoadModel(nodeHandle, path, format);
174-
});
178+
}).finally(() => this.unlinkAll(written));
179+
}
180+
181+
/**
182+
* The loaders read the virtual FS synchronously and are done with it by the
183+
* time the load settles. MEMFS keeps every file in JS memory until unlinked,
184+
* so without this each model stayed resident for the life of the page, once
185+
* per load.
186+
*/
187+
private unlinkAll(paths: string[]): void {
188+
const unlink = this.module.FS.unlink;
189+
if (!unlink) return;
190+
for (const p of paths) {
191+
try {
192+
unlink.call(this.module.FS, p);
193+
} catch {
194+
// Already gone, or a second load into the node replaced it.
195+
}
196+
}
175197
}
176198

177199
/**
@@ -183,7 +205,12 @@ export class ViroWebRenderer {
183205
const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
184206
const path = `/viro_env_${lightingEnvCounter++}.hdr`;
185207
this.module.FS.writeFile(path, bytes);
186-
const handle = this._scene.loadRadianceHDRTexture(path);
208+
let handle: ViroHandle;
209+
try {
210+
handle = this._scene.loadRadianceHDRTexture(path);
211+
} finally {
212+
this.unlinkAll([path]);
213+
}
187214
if (handle) this._scene.setLightingEnvironment(handle);
188215
return handle;
189216
}
@@ -237,14 +264,19 @@ export class ViroWebRenderer {
237264
canvas.width = width;
238265
canvas.height = height;
239266

267+
// The runtime can abort before there is a renderer to tell (during init);
268+
// create() then rejects on its own and there is no one else to notify.
269+
let renderer: ViroWebRenderer | undefined;
240270
const module = await loadViroWebModule(canvas, {
241271
locateFile: options.locateFile,
242272
baseUrl: options.assetBaseUrl,
243273
importGlue: options.importGlue,
274+
onAbort: (what) => renderer?.handleAbort(what),
244275
});
245276
module.initViroScene(selector, width, height);
246277

247-
const renderer = new ViroWebRenderer(module, canvas);
278+
renderer = new ViroWebRenderer(module, canvas);
279+
if (options.onAbort) renderer.addAbortListener(options.onAbort);
248280
// Once, at startup: what a bug report needs pasted back and cannot find
249281
// anywhere else, since the binary reaches an app through two hand copies.
250282
const buildId = renderer.scene.getBuildId();
@@ -326,6 +358,49 @@ export class ViroWebRenderer {
326358
this.disposed = true;
327359
}
328360

361+
/**
362+
* Set once the WASM runtime has aborted (see ViroWebRendererOptions.onAbort).
363+
* A renderer in that state cannot recover; a new one has to be created.
364+
*/
365+
get abortedWith(): ViroRendererAbortError | null {
366+
return this.abortError;
367+
}
368+
369+
/**
370+
* Be told when the runtime aborts. Called at most once per renderer, and
371+
* straight away when it already has. Returns an unsubscribe.
372+
*/
373+
addAbortListener(listener: (error: ViroRendererAbortError) => void): () => void {
374+
if (this.abortError) {
375+
listener(this.abortError);
376+
return () => {};
377+
}
378+
this.abortListeners.add(listener);
379+
return () => {
380+
this.abortListeners.delete(listener);
381+
};
382+
}
383+
384+
private handleAbort(what: unknown): void {
385+
if (this.abortError) return;
386+
// Emscripten passes "OOM" for a heap that could not grow and nothing at all
387+
// for most other aborts, a failed allocation past MAXIMUM_MEMORY included.
388+
const reason = (what instanceof Error ? what.message : String(what ?? "")) || "unknown";
389+
this.abortError = new ViroRendererAbortError(reason);
390+
// Nothing in flight can finish: the loader that would call back is gone.
391+
for (const resolve of this.modelLoadResolvers.values()) resolve(false);
392+
this.modelLoadResolvers.clear();
393+
const listeners = [...this.abortListeners];
394+
this.abortListeners.clear();
395+
for (const listener of listeners) {
396+
try {
397+
listener(this.abortError);
398+
} catch (err) {
399+
console.error("[Viro web] onAbort listener threw:", err);
400+
}
401+
}
402+
}
403+
329404
private assertLive(): void {
330405
if (this.disposed) {
331406
throw new Error("ViroWebRenderer: instance has been disposed");

0 commit comments

Comments
 (0)