diff --git a/web/binding.cc b/web/binding.cc index 00980d91..479b6672 100644 --- a/web/binding.cc +++ b/web/binding.cc @@ -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 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(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(info.meshes_processed), + static_cast(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(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(info.meshes_processed), + static_cast(info.meshes_total), + info.GetStageName(), // Already returns const char* + info.current_mesh_name.c_str(), + info.progress + ); + return true; // Continue conversion }, &parsing_progress_); diff --git a/web/docs/PROGRESS-UPDATE-DESIGN.md b/web/docs/PROGRESS-UPDATE-DESIGN.md new file mode 100644 index 00000000..435cf9e4 --- /dev/null +++ b/web/docs/PROGRESS-UPDATE-DESIGN.md @@ -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 + +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 + +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) diff --git a/web/js/progress-demo.js b/web/js/progress-demo.js index a1a385b6..60ef5179 100644 --- a/web/js/progress-demo.js +++ b/web/js/progress-demo.js @@ -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 diff --git a/web/js/src/tinyusdz/TinyUSDZLoader.js b/web/js/src/tinyusdz/TinyUSDZLoader.js index 0a6f29d4..1c11b4c7 100644 --- a/web/js/src/tinyusdz/TinyUSDZLoader.js +++ b/web/js/src/tinyusdz/TinyUSDZLoader.js @@ -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