mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add EM_JS synchronous progress callbacks for Tydra conversion
- Add EM_JS functions in binding.cc for direct C++ to JS progress reporting - reportTydraProgress: mesh conversion progress (current/total, stage, name) - reportTydraStage: conversion stage changes - reportTydraComplete: conversion completion with counts - Update TinyUSDZLoader.js with callback options: - onTydraProgress, onTydraStage, onTydraComplete - setTydraProgressCallback/setTydraStageCallback/setTydraCompleteCallback methods - Integrate callbacks in progress-demo.js for real-time UI updates - Add design document for JS/WASM synchronous event update patterns This enables real-time progress updates during Tydra scene conversion without requiring ASYNCIFY, using Emscripten's EM_JS for synchronous JavaScript calls from C++. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,47 @@
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
// ============================================================================
|
||||
// EM_JS: Synchronous JavaScript callbacks for progress reporting
|
||||
// These functions are called from C++ during Tydra conversion to report
|
||||
// progress to JavaScript in real-time without ASYNCIFY.
|
||||
// ============================================================================
|
||||
|
||||
// Report mesh conversion progress
|
||||
// Called for each mesh during Tydra conversion
|
||||
EM_JS(void, reportTydraProgress, (int current, int total, const char* stage, const char* meshName, float progress), {
|
||||
if (typeof Module.onTydraProgress === 'function') {
|
||||
Module.onTydraProgress({
|
||||
meshCurrent: current,
|
||||
meshTotal: total,
|
||||
stage: UTF8ToString(stage),
|
||||
meshName: UTF8ToString(meshName),
|
||||
progress: progress
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Report conversion stage change
|
||||
EM_JS(void, reportTydraStage, (const char* stage, const char* message), {
|
||||
if (typeof Module.onTydraStage === 'function') {
|
||||
Module.onTydraStage({
|
||||
stage: UTF8ToString(stage),
|
||||
message: UTF8ToString(message)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Report conversion completion
|
||||
EM_JS(void, reportTydraComplete, (int meshCount, int materialCount, int textureCount), {
|
||||
if (typeof Module.onTydraComplete === 'function') {
|
||||
Module.onTydraComplete({
|
||||
meshCount: meshCount,
|
||||
materialCount: materialCount,
|
||||
textureCount: textureCount
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
namespace detail {
|
||||
|
||||
std::array<double, 9> toArray(const tinyusdz::value::matrix3d &m) {
|
||||
@@ -972,7 +1013,7 @@ class TinyUSDZLoaderNative {
|
||||
// RenderScene: Scene graph object which is suited for GL/Vulkan renderer
|
||||
tinyusdz::tydra::RenderSceneConverter converter;
|
||||
|
||||
// Set up detailed progress callback to update parsing_progress_
|
||||
// Set up detailed progress callback to update parsing_progress_ and call JS
|
||||
converter.SetDetailedProgressCallback(
|
||||
[](const tinyusdz::tydra::DetailedProgressInfo &info, void *userptr) -> bool {
|
||||
ParsingProgress *pp = static_cast<ParsingProgress *>(userptr);
|
||||
@@ -987,6 +1028,16 @@ class TinyUSDZLoaderNative {
|
||||
// Update progress: parsing is 0-80%, conversion is 80-100%
|
||||
pp->progress = 0.8f + (info.progress * 0.2f);
|
||||
}
|
||||
|
||||
// Call JavaScript synchronously via EM_JS
|
||||
reportTydraProgress(
|
||||
static_cast<int>(info.meshes_processed),
|
||||
static_cast<int>(info.meshes_total),
|
||||
info.GetStageName(), // Already returns const char*
|
||||
info.current_mesh_name.c_str(),
|
||||
info.progress
|
||||
);
|
||||
|
||||
return true; // Continue conversion
|
||||
},
|
||||
&parsing_progress_);
|
||||
@@ -1114,7 +1165,7 @@ class TinyUSDZLoaderNative {
|
||||
// RenderScene: Scene graph object which is suited for GL/Vulkan renderer
|
||||
tinyusdz::tydra::RenderSceneConverter converter;
|
||||
|
||||
// Set up detailed progress callback to update parsing_progress_
|
||||
// Set up detailed progress callback to update parsing_progress_ and call JS
|
||||
converter.SetDetailedProgressCallback(
|
||||
[](const tinyusdz::tydra::DetailedProgressInfo &info, void *userptr) -> bool {
|
||||
ParsingProgress *pp = static_cast<ParsingProgress *>(userptr);
|
||||
@@ -1129,6 +1180,16 @@ class TinyUSDZLoaderNative {
|
||||
// Update progress: parsing is 0-80%, conversion is 80-100%
|
||||
pp->progress = 0.8f + (info.progress * 0.2f);
|
||||
}
|
||||
|
||||
// Call JavaScript synchronously via EM_JS
|
||||
reportTydraProgress(
|
||||
static_cast<int>(info.meshes_processed),
|
||||
static_cast<int>(info.meshes_total),
|
||||
info.GetStageName(), // Already returns const char*
|
||||
info.current_mesh_name.c_str(),
|
||||
info.progress
|
||||
);
|
||||
|
||||
return true; // Continue conversion
|
||||
},
|
||||
&parsing_progress_);
|
||||
|
||||
285
web/docs/PROGRESS-UPDATE-DESIGN.md
Normal file
285
web/docs/PROGRESS-UPDATE-DESIGN.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# JS/WASM Synchronous Progress Update Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes design options for synchronous progress reporting from C++/WASM to JavaScript without using ASYNCIFY. The goal is to enable interactive progress updates during Tydra scene conversion.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current implementation uses `printf()` statements in C++ (render-data.cc) which output to the browser console synchronously. However, there's no mechanism to:
|
||||
1. Intercept these messages in JavaScript
|
||||
2. Parse progress information
|
||||
3. Update UI elements in real-time
|
||||
|
||||
## Key Insight: Emscripten printf() Behavior
|
||||
|
||||
Based on research ([Emscripten Module documentation](https://emscripten.org/docs/api_reference/module.html)):
|
||||
|
||||
- `printf()` in WASM outputs to console.log (line-buffered, needs `\n`)
|
||||
- Output goes through `Module['print']` (stdout) or `Module['printErr']` (stderr)
|
||||
- `Module['print']` can be overridden **before** module initialization
|
||||
- Emscripten provides low-level functions: `emscripten_out()`, `emscripten_err()`, `emscripten_console_log()`
|
||||
|
||||
## Design Options
|
||||
|
||||
### Option 1: Module.print Override (Recommended)
|
||||
|
||||
**Approach**: Override `Module['print']` to intercept and parse progress messages from C++.
|
||||
|
||||
**Pros**:
|
||||
- No C++ changes required (already using printf)
|
||||
- Works with existing progress logging
|
||||
- Zero overhead when not monitoring
|
||||
- Clean separation of concerns
|
||||
|
||||
**Cons**:
|
||||
- Requires parsing string output
|
||||
- Must define Module before WASM loads
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```javascript
|
||||
// In pre-module-init code (before WASM loads)
|
||||
const progressParser = {
|
||||
onProgress: null,
|
||||
totalMeshes: 0,
|
||||
currentMesh: 0
|
||||
};
|
||||
|
||||
// Pattern: [Tydra] Mesh 3/10: /root/mesh_003
|
||||
const MESH_PROGRESS_REGEX = /\[Tydra\] Mesh (\d+)\/(\d+)(?:\s*\([^)]+\))?:\s*(.+)/;
|
||||
const COUNT_REGEX = /\[Tydra\] Found (\d+) meshes/;
|
||||
const COMPLETE_REGEX = /\[Tydra\] Conversion complete/;
|
||||
|
||||
function createModuleWithProgressCapture(onProgress) {
|
||||
progressParser.onProgress = onProgress;
|
||||
|
||||
return {
|
||||
print: function(text) {
|
||||
console.log(text); // Keep console output
|
||||
|
||||
// Parse progress messages
|
||||
let match;
|
||||
if ((match = MESH_PROGRESS_REGEX.exec(text))) {
|
||||
const current = parseInt(match[1]);
|
||||
const total = parseInt(match[2]);
|
||||
const meshName = match[3];
|
||||
onProgress({
|
||||
type: 'mesh',
|
||||
current,
|
||||
total,
|
||||
name: meshName,
|
||||
percentage: (current / total) * 100
|
||||
});
|
||||
} else if ((match = COUNT_REGEX.exec(text))) {
|
||||
progressParser.totalMeshes = parseInt(match[1]);
|
||||
onProgress({
|
||||
type: 'count',
|
||||
total: progressParser.totalMeshes
|
||||
});
|
||||
} else if (COMPLETE_REGEX.test(text)) {
|
||||
onProgress({
|
||||
type: 'complete'
|
||||
});
|
||||
}
|
||||
},
|
||||
printErr: function(text) {
|
||||
console.error(text);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Usage in TinyUSDZLoader.js
|
||||
const Module = createModuleWithProgressCapture((progress) => {
|
||||
updateProgressBar(progress.percentage);
|
||||
updateMeshStatus(`${progress.current}/${progress.total}: ${progress.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
**Integration with existing code**:
|
||||
|
||||
```javascript
|
||||
// progress-demo.js modification
|
||||
async function loadUSDWithProgress(url) {
|
||||
// Create loader with progress capture
|
||||
loaderState.loader = new TinyUSDZLoader({
|
||||
moduleOverrides: {
|
||||
print: (text) => {
|
||||
console.log(text);
|
||||
parseTydraProgress(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
// ... rest of loading code
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 2: EM_ASM Direct JavaScript Call
|
||||
|
||||
**Approach**: Call JavaScript directly from C++ using `EM_ASM` macro.
|
||||
|
||||
**Pros**:
|
||||
- Direct, typed communication
|
||||
- No string parsing required
|
||||
- Can call any JavaScript function
|
||||
|
||||
**Cons**:
|
||||
- Requires C++ code changes
|
||||
- Adds emscripten-specific code to core library
|
||||
- Compile-time dependency on emscripten headers
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```cpp
|
||||
// In render-data.cc (or a wrapper layer)
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
|
||||
void ReportMeshProgressToJS(size_t current, size_t total, const char* meshName) {
|
||||
EM_ASM({
|
||||
if (typeof Module.onMeshProgress === 'function') {
|
||||
Module.onMeshProgress($0, $1, UTF8ToString($2));
|
||||
}
|
||||
}, current, total, meshName);
|
||||
}
|
||||
#else
|
||||
void ReportMeshProgressToJS(size_t, size_t, const char*) {}
|
||||
#endif
|
||||
|
||||
// Call from MeshVisitor:
|
||||
ReportMeshProgressToJS(meshes_processed, meshes_total, path.c_str());
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript side
|
||||
const Module = {
|
||||
onMeshProgress: function(current, total, name) {
|
||||
updateProgressUI(current, total, name);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 3: EM_JS Function Declaration
|
||||
|
||||
**Approach**: Declare JavaScript function inline in C++ header.
|
||||
|
||||
**Pros**:
|
||||
- Cleaner than EM_ASM
|
||||
- Type-safe function signature
|
||||
- Compiled once
|
||||
|
||||
**Cons**:
|
||||
- Same as Option 2 (requires C++ changes)
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```cpp
|
||||
// binding.cc or progress-bridge.h
|
||||
#include <emscripten/em_js.h>
|
||||
|
||||
EM_JS(void, reportMeshProgress, (int current, int total, const char* name), {
|
||||
if (Module.onMeshProgress) {
|
||||
Module.onMeshProgress(current, total, UTF8ToString(name));
|
||||
}
|
||||
});
|
||||
|
||||
// Use in render-data.cc via extern declaration or header include
|
||||
extern "C" void reportMeshProgress(int current, int total, const char* name);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 4: Shared Memory Buffer
|
||||
|
||||
**Approach**: Use a shared memory region that C++ writes to and JS polls.
|
||||
|
||||
**Pros**:
|
||||
- No function call overhead
|
||||
- Works with SharedArrayBuffer for workers
|
||||
- Can be polled at any rate
|
||||
|
||||
**Cons**:
|
||||
- Requires polling from JS side
|
||||
- More complex synchronization
|
||||
- SharedArrayBuffer requires specific headers
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```cpp
|
||||
// C++ side
|
||||
struct ProgressBuffer {
|
||||
uint32_t currentMesh;
|
||||
uint32_t totalMeshes;
|
||||
uint32_t flags; // bit 0: updated, bit 1: complete
|
||||
char meshName[256];
|
||||
};
|
||||
|
||||
// Exposed via embind
|
||||
ProgressBuffer* getProgressBuffer();
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JS side - poll during animation frame
|
||||
function pollProgress() {
|
||||
const buffer = Module.getProgressBuffer();
|
||||
if (buffer.flags & 1) { // updated flag
|
||||
updateUI(buffer.currentMesh, buffer.totalMeshes, buffer.meshName);
|
||||
buffer.flags &= ~1; // clear flag
|
||||
}
|
||||
if (!(buffer.flags & 2)) { // not complete
|
||||
requestAnimationFrame(pollProgress);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Option 1 (Module.print Override)** is recommended because:
|
||||
|
||||
1. **Zero C++ changes**: Current printf statements already work
|
||||
2. **Non-invasive**: Doesn't pollute core library with emscripten code
|
||||
3. **Flexible**: Can enable/disable progress capture per-load
|
||||
4. **Maintainable**: All JS-specific code stays in JS layer
|
||||
5. **Proven pattern**: Used by many emscripten projects
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Basic Module.print Override
|
||||
1. Modify TinyUSDZLoader.js to accept `moduleOverrides` option
|
||||
2. Create progress message parser utility
|
||||
3. Connect parser to progress UI in progress-demo.js
|
||||
|
||||
### Phase 2: Structured Progress Messages (Optional Enhancement)
|
||||
1. Standardize printf format in render-data.cc with JSON-parseable structure
|
||||
2. Example: `[TYDRA:PROGRESS] {"type":"mesh","current":3,"total":10,"name":"/root/mesh"}`
|
||||
3. Makes parsing more robust
|
||||
|
||||
### Phase 3: Progress Event System
|
||||
1. Create EventEmitter-style interface for progress events
|
||||
2. Support multiple listeners
|
||||
3. Add batch/debounce option for high-frequency updates
|
||||
|
||||
## Current printf Format Reference
|
||||
|
||||
From render-data.cc:
|
||||
```
|
||||
[Tydra] Counting primitives...
|
||||
[Tydra] Found 10 meshes (8 mesh, 1 cube, 1 sphere), 3 materials
|
||||
[Tydra] Mesh 1/10: /root/Mesh_001
|
||||
[Tydra] Mesh 2/10: /root/Mesh_002
|
||||
[Tydra] Mesh 3/10 (cube): /root/Cube_001
|
||||
[Tydra] Mesh 4/10 (sphere): /root/Sphere_001
|
||||
[Tydra] Conversion complete: 10 meshes, 3 materials, 5 textures
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Emscripten Module object documentation](https://emscripten.org/docs/api_reference/module.html)
|
||||
- [Emscripten console.h documentation](https://emscripten.org/docs/api_reference/console.h.html)
|
||||
- [GitHub Discussion: How does emscripten_console_xxx work?](https://github.com/emscripten-core/emscripten/discussions/21783)
|
||||
@@ -879,7 +879,33 @@ async function loadUSDWithProgress(source, isFile = false) {
|
||||
try {
|
||||
// Initialize loader if needed
|
||||
if (!loaderState.loader) {
|
||||
loaderState.loader = new TinyUSDZLoader();
|
||||
loaderState.loader = new TinyUSDZLoader(undefined, {
|
||||
// EM_JS synchronous progress callback - called directly from C++ during conversion
|
||||
onTydraProgress: (info) => {
|
||||
// info: {meshCurrent, meshTotal, stage, meshName, progress}
|
||||
const meshProgress = info.meshTotal > 0
|
||||
? `${info.meshCurrent}/${info.meshTotal}`
|
||||
: '';
|
||||
const meshName = info.meshName ? info.meshName.split('/').pop() : '';
|
||||
|
||||
updateProgressUI({
|
||||
stage: 'parsing',
|
||||
percentage: 30 + (info.progress * 50), // parsing takes 30-80%
|
||||
message: meshProgress
|
||||
? `Converting: ${meshProgress} ${meshName}`
|
||||
: `Converting: ${info.stage}`
|
||||
});
|
||||
},
|
||||
onTydraComplete: (info) => {
|
||||
// info: {meshCount, materialCount, textureCount}
|
||||
console.log(`[Tydra] Complete: ${info.meshCount} meshes, ${info.materialCount} materials, ${info.textureCount} textures`);
|
||||
updateProgressUI({
|
||||
stage: 'building',
|
||||
percentage: 80,
|
||||
message: `Building ${info.meshCount} meshes...`
|
||||
});
|
||||
}
|
||||
});
|
||||
await loaderState.loader.init();
|
||||
|
||||
// Setup TinyUSDZ for MaterialX texture decoding
|
||||
|
||||
@@ -134,6 +134,9 @@ class TinyUSDZLoader extends Loader {
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {number} options.maxMemoryLimitMB - Maximum memory limit in MB (default: 2048 for WASM32, 8192 for WASM64)
|
||||
* @param {boolean} options.useZstdCompressedWasm - Use compressed WASM (default: false)
|
||||
* @param {Function} options.onTydraProgress - Callback for Tydra conversion progress ({meshCurrent, meshTotal, stage, meshName, progress}) => void
|
||||
* @param {Function} options.onTydraStage - Callback for Tydra stage changes ({stage, message}) => void
|
||||
* @param {Function} options.onTydraComplete - Callback for Tydra conversion completion ({meshCount, materialCount, textureCount}) => void
|
||||
*/
|
||||
constructor(manager, options = {}) {
|
||||
super(manager);
|
||||
@@ -163,6 +166,12 @@ class TinyUSDZLoader extends Loader {
|
||||
// Memory limit in MB - defaults are set by the native module based on WASM architecture
|
||||
// (2GB for WASM32, 8GB for WASM64). If not specified, the native default will be used.
|
||||
this.maxMemoryLimitMB_ = options.maxMemoryLimitMB;
|
||||
|
||||
// EM_JS synchronous progress callbacks for Tydra conversion
|
||||
// These are called directly from C++ during conversion without ASYNCIFY
|
||||
this.onTydraProgress_ = options.onTydraProgress || null;
|
||||
this.onTydraStage_ = options.onTydraStage || null;
|
||||
this.onTydraComplete_ = options.onTydraComplete || null;
|
||||
}
|
||||
|
||||
// Decompress zstd compressed WASM
|
||||
@@ -274,6 +283,18 @@ class TinyUSDZLoader extends Loader {
|
||||
// return scriptDirectory + path;
|
||||
//}
|
||||
|
||||
// Set up EM_JS synchronous progress callbacks
|
||||
// These are called from C++ during Tydra conversion without ASYNCIFY
|
||||
if (this.onTydraProgress_) {
|
||||
initOptions.onTydraProgress = this.onTydraProgress_;
|
||||
}
|
||||
if (this.onTydraStage_) {
|
||||
initOptions.onTydraStage = this.onTydraStage_;
|
||||
}
|
||||
if (this.onTydraComplete_) {
|
||||
initOptions.onTydraComplete = this.onTydraComplete_;
|
||||
}
|
||||
|
||||
this.native_ = await initTinyUSDZNative(initOptions);
|
||||
if (!this.native_) {
|
||||
throw new Error('TinyUSDZLoader: Failed to initialize native module.');
|
||||
@@ -311,6 +332,41 @@ class TinyUSDZLoader extends Loader {
|
||||
this.progressCallback_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Tydra progress callback for mesh conversion updates
|
||||
* This is called synchronously from C++ via EM_JS during scene conversion
|
||||
* @param {Function} callback - ({meshCurrent, meshTotal, stage, meshName, progress}) => void
|
||||
*/
|
||||
setTydraProgressCallback(callback) {
|
||||
this.onTydraProgress_ = callback;
|
||||
// Update native module if already initialized
|
||||
if (this.native_) {
|
||||
this.native_.onTydraProgress = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Tydra stage callback for conversion stage changes
|
||||
* @param {Function} callback - ({stage, message}) => void
|
||||
*/
|
||||
setTydraStageCallback(callback) {
|
||||
this.onTydraStage_ = callback;
|
||||
if (this.native_) {
|
||||
this.native_.onTydraStage = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Tydra completion callback
|
||||
* @param {Function} callback - ({meshCount, materialCount, textureCount}) => void
|
||||
*/
|
||||
setTydraCompleteCallback(callback) {
|
||||
this.onTydraComplete_ = callback;
|
||||
if (this.native_) {
|
||||
this.native_.onTydraComplete = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current maximum memory limit in MB
|
||||
* @returns {number|undefined} Memory limit in megabytes, or undefined if using native default
|
||||
|
||||
Reference in New Issue
Block a user