From f6c9ad1d4423d76374c26a66e5854b9af4a6243c Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Wed, 7 Jan 2026 03:40:00 +0900 Subject: [PATCH] Use coroutine async loading in progress-demo.js with debug logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add console.log debug prints at yield points in binding.cc: - Log when yielding to event loop - Log when resuming from yield (rAF or setTimeout) - Log phase name and progress percentage - Add hasAsyncSupport() method to TinyUSDZLoader.js: - Checks if loadFromBinaryAsync is available - Returns true if WASM was built with TINYUSDZ_WASM_COROUTINE=ON - Update progress-demo.js to use coroutine async when available: - Check hasAsyncSupport() before loading - Use parseAsync() for file and URL loading when available - Fall back to standard Promise-based loading if not - Map coroutine phases to progress UI stages - Manual fetch with progress for URL loading in async mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- web/binding.cc | 16 ++- web/js/progress-demo.js | 173 ++++++++++++++++++++------ web/js/src/tinyusdz/TinyUSDZLoader.js | 24 ++++ 3 files changed, 175 insertions(+), 38 deletions(-) diff --git a/web/binding.cc b/web/binding.cc index 80c22996..3c53ae9f 100644 --- a/web/binding.cc +++ b/web/binding.cc @@ -125,12 +125,19 @@ EM_JS(void, reportTydraComplete, (int meshCount, int materialCount, int textureC EM_JS(emscripten::EM_VAL, yieldToEventLoop_impl, (), { // Return a Promise that resolves on next animation frame // This gives the browser a chance to repaint + console.log('[Coroutine] Yielding to event loop...'); return Emval.toHandle(new Promise(resolve => { if (typeof requestAnimationFrame === 'function') { - requestAnimationFrame(() => resolve()); + requestAnimationFrame(() => { + console.log('[Coroutine] Resumed from yield (rAF)'); + resolve(); + }); } else { // Fallback for non-browser environments (Node.js) - setTimeout(resolve, 0); + setTimeout(() => { + console.log('[Coroutine] Resumed from yield (setTimeout)'); + resolve(); + }, 0); } })); }); @@ -153,9 +160,12 @@ inline emscripten::val yieldWithDelay(int delayMs) { // Report that async operation is starting (for JS progress UI) EM_JS(void, reportAsyncPhaseStart, (const char* phase, float progress), { + const phaseStr = UTF8ToString(phase); + const progressPct = (progress * 100).toFixed(0); + console.log(`[Coroutine] Phase: ${phaseStr} (${progressPct}%)`); if (typeof Module.onAsyncPhaseStart === 'function') { Module.onAsyncPhaseStart({ - phase: UTF8ToString(phase), + phase: phaseStr, progress: progress }); } diff --git a/web/js/progress-demo.js b/web/js/progress-demo.js index 19fe8bfa..75ec8e2d 100644 --- a/web/js/progress-demo.js +++ b/web/js/progress-demo.js @@ -1081,52 +1081,155 @@ async function loadUSDWithProgress(source, isFile = false) { let usd; + // Check if coroutine-based async loading is available + const hasCoroutineAsync = loaderState.loader.hasAsyncSupport && loaderState.loader.hasAsyncSupport(); + if (hasCoroutineAsync) { + console.log('[Progress Demo] Using C++20 coroutine async loading'); + } else { + console.log('[Progress Demo] Using standard Promise-based loading'); + } + if (isFile) { // Load from File object updateProgressUI({ stage: 'downloading', percentage: 0, message: 'Reading file...' }); const arrayBuffer = await source.arrayBuffer(); - updateProgressUI({ stage: 'parsing', percentage: 30, message: 'Parsing USD...' }); - usd = await new Promise((resolve, reject) => { - loaderState.loader.parse( + if (hasCoroutineAsync) { + // Use coroutine-based async loading - yields to browser between phases + updateProgressUI({ stage: 'parsing', percentage: 30, message: 'Parsing USD (coroutine async)...' }); + usd = await loaderState.loader.parseAsync( new Uint8Array(arrayBuffer), source.name, - resolve, - reject - ); - }); - } else { - // Load from URL with progress - usd = await new Promise((resolve, reject) => { - loaderState.loader.load( - source, - resolve, - (event) => { - if (event.stage === 'downloading') { - const pct = event.total > 0 ? Math.round((event.loaded / event.total) * 100) : 0; + { + onPhaseStart: (info) => { + console.log(`[Progress Demo] Coroutine phase: ${info.phase} (${(info.progress * 100).toFixed(0)}%)`); + // Map coroutine phases to our progress stages + const phaseMap = { + 'detecting': { stage: 'parsing', pct: 30 }, + 'parsing': { stage: 'parsing', pct: 35 }, + 'converting': { stage: 'parsing', pct: 50 }, + 'complete': { stage: 'building', pct: 80 } + }; + const mapped = phaseMap[info.phase] || { stage: 'parsing', pct: 30 + info.progress * 50 }; updateProgressUI({ - stage: 'downloading', - percentage: pct * 0.3, - message: event.message || `Downloading... ${pct}%` - }); - } else if (event.stage === 'parsing') { - // Show mesh progress if available from detailed callback - let message = 'Parsing USD...'; - if (event.meshesTotal && event.meshesTotal > 0) { - message = `Converting meshes (${event.meshesProcessed || 0}/${event.meshesTotal})...`; - } else if (event.tydraStage) { - message = `Converting: ${event.tydraStage}`; - } - updateProgressUI({ - stage: 'parsing', - percentage: 30 + event.percentage * 0.2, - message: message + stage: mapped.stage, + percentage: mapped.pct, + message: `${info.phase}...` }); } - }, - reject + } ); - }); + } else { + // Fallback to standard Promise-based loading + updateProgressUI({ stage: 'parsing', percentage: 30, message: 'Parsing USD...' }); + usd = await new Promise((resolve, reject) => { + loaderState.loader.parse( + new Uint8Array(arrayBuffer), + source.name, + resolve, + reject + ); + }); + } + } else { + // Load from URL + if (hasCoroutineAsync) { + // Use coroutine-based async loading for URL + // First fetch the file manually for download progress, then use parseAsync + updateProgressUI({ stage: 'downloading', percentage: 0, message: 'Downloading...' }); + + const response = await fetch(source); + if (!response.ok) { + throw new Error(`Failed to fetch ${source}: ${response.status}`); + } + + const contentLength = response.headers.get('content-length'); + const total = contentLength ? parseInt(contentLength, 10) : 0; + + let loaded = 0; + const reader = response.body.getReader(); + const chunks = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + loaded += value.length; + + const pct = total > 0 ? Math.round((loaded / total) * 100) : 0; + updateProgressUI({ + stage: 'downloading', + percentage: pct * 0.3, + message: `Downloading... ${pct}%` + }); + } + + // Combine chunks into single Uint8Array + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); + const binary = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + binary.set(chunk, offset); + offset += chunk.length; + } + + // Use coroutine-based async parsing + updateProgressUI({ stage: 'parsing', percentage: 30, message: 'Parsing USD (coroutine async)...' }); + usd = await loaderState.loader.parseAsync( + binary, + source, + { + onPhaseStart: (info) => { + console.log(`[Progress Demo] Coroutine phase: ${info.phase} (${(info.progress * 100).toFixed(0)}%)`); + const phaseMap = { + 'detecting': { stage: 'parsing', pct: 30 }, + 'parsing': { stage: 'parsing', pct: 35 }, + 'converting': { stage: 'parsing', pct: 50 }, + 'complete': { stage: 'building', pct: 80 } + }; + const mapped = phaseMap[info.phase] || { stage: 'parsing', pct: 30 + info.progress * 50 }; + updateProgressUI({ + stage: mapped.stage, + percentage: mapped.pct, + message: `${info.phase}...` + }); + } + } + ); + } else { + // Fallback: Load from URL with standard progress + usd = await new Promise((resolve, reject) => { + loaderState.loader.load( + source, + resolve, + (event) => { + if (event.stage === 'downloading') { + const pct = event.total > 0 ? Math.round((event.loaded / event.total) * 100) : 0; + updateProgressUI({ + stage: 'downloading', + percentage: pct * 0.3, + message: event.message || `Downloading... ${pct}%` + }); + } else if (event.stage === 'parsing') { + // Show mesh progress if available from detailed callback + let message = 'Parsing USD...'; + if (event.meshesTotal && event.meshesTotal > 0) { + message = `Converting meshes (${event.meshesProcessed || 0}/${event.meshesTotal})...`; + } else if (event.tydraStage) { + message = `Converting: ${event.tydraStage}`; + } + updateProgressUI({ + stage: 'parsing', + percentage: 30 + event.percentage * 0.2, + message: message + }); + } + }, + reject + ); + }); + } } loaderState.nativeLoader = usd; diff --git a/web/js/src/tinyusdz/TinyUSDZLoader.js b/web/js/src/tinyusdz/TinyUSDZLoader.js index 0734346a..208abb33 100644 --- a/web/js/src/tinyusdz/TinyUSDZLoader.js +++ b/web/js/src/tinyusdz/TinyUSDZLoader.js @@ -702,6 +702,30 @@ class TinyUSDZLoader extends Loader { }); } + /** + * Check if C++20 coroutine-based async loading is available. + * Returns true if the WASM module was compiled with TINYUSDZ_WASM_COROUTINE=ON. + * + * @returns {boolean} True if async support is available + */ + hasAsyncSupport() { + if (!this.native_) { + console.warn('[TinyUSDZLoader] hasAsyncSupport called before init()'); + return false; + } + + // Check if a temporary instance has loadFromBinaryAsync + try { + const usd = new this.native_.TinyUSDZLoaderNative(); + const hasMethod = typeof usd.loadFromBinaryAsync === 'function'; + console.log(`[TinyUSDZLoader] Coroutine async support: ${hasMethod ? 'available' : 'not available'}`); + return hasMethod; + } catch (e) { + console.warn('[TinyUSDZLoader] Error checking async support:', e); + return false; + } + } + /** * Parse USD binary data using C++20 coroutine-based async loading. * This method yields to the JavaScript event loop between processing phases,