From c6ace402dc62c3afbe41b2ab8be73d16241a1ee7 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Wed, 7 Jan 2026 04:13:51 +0900 Subject: [PATCH] Add granular coroutine yield phases for Tydra conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove debug console.log statements from coroutine helpers - Split Tydra conversion into multiple phases with yields: - detecting: Format detection - parsing: USD parsing - setup: Converter environment setup - assets: Asset resolution setup - meshes: Tydra mesh conversion - complete: Done - Each phase yields to event loop, allowing browser repaints - Update progress-demo.js phase mapping with descriptive messages - The Tydra ConvertToRenderScene call is still blocking, but yields occur before and after it 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- web/binding.cc | 108 ++++++++++++++++++++++++++++++++-------- web/js/progress-demo.js | 31 ++++++------ 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/web/binding.cc b/web/binding.cc index 3c53ae9f..d0025027 100644 --- a/web/binding.cc +++ b/web/binding.cc @@ -125,19 +125,12 @@ 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(() => { - console.log('[Coroutine] Resumed from yield (rAF)'); - resolve(); - }); + requestAnimationFrame(() => resolve()); } else { // Fallback for non-browser environments (Node.js) - setTimeout(() => { - console.log('[Coroutine] Resumed from yield (setTimeout)'); - resolve(); - }, 0); + setTimeout(resolve, 0); } })); }); @@ -160,12 +153,9 @@ 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: phaseStr, + phase: UTF8ToString(phase), progress: progress }); } @@ -1332,22 +1322,100 @@ class TinyUSDZLoaderNative { // Yield after parsing to allow UI update co_await yieldToEventLoop(); - // Phase 3: Converting to RenderScene (Tydra) - reportAsyncPhaseStart("converting", 0.3f); + // Phase 3: Setup conversion environment + reportAsyncPhaseStart("setup", 0.3f); - // Yield again before heavy conversion + tinyusdz::tydra::RenderSceneConverterEnv env(stage); + env.scene_config.load_texture_assets = loadTextureInNative_; + env.material_config.preserve_texel_bitdepth = true; + env.mesh_config.lowmem = true; + env.mesh_config.enable_bone_reduction = enable_bone_reduction_; + env.mesh_config.target_bone_count = target_bone_count_; + + // Yield after setup co_await yieldToEventLoop(); - bool convert_ok = stageToRenderScene(stage, is_usdz, binary); + // Phase 4: Setup asset resolution + reportAsyncPhaseStart("assets", 0.4f); - if (!convert_ok) { + if (is_usdz) { + bool asset_on_memory = false; + if (!tinyusdz::ReadUSDZAssetInfoFromMemory( + reinterpret_cast(binary.c_str()), binary.size(), + asset_on_memory, &usdz_asset_, &warn_, &error_)) { + emscripten::val result = emscripten::val::object(); + result.set("success", false); + result.set("error", "Failed to read USDZ assetInfo"); + co_return result; + } + + tinyusdz::AssetResolutionResolver arr; + if (!tinyusdz::SetupUSDZAssetResolution(arr, &usdz_asset_)) { + emscripten::val result = emscripten::val::object(); + result.set("success", false); + result.set("error", "Failed to setup AssetResolution for USDZ"); + co_return result; + } + env.asset_resolver = arr; + } else { + tinyusdz::AssetResolutionResolver arr; + if (!SetupEMAssetResolution(arr, &em_resolver_)) { + emscripten::val result = emscripten::val::object(); + result.set("success", false); + result.set("error", "Failed to setup asset resolution"); + co_return result; + } + env.asset_resolver = arr; + } + + // Yield after asset resolution setup + co_await yieldToEventLoop(); + + // Phase 5: Converting meshes (Tydra) + reportAsyncPhaseStart("meshes", 0.5f); + + tinyusdz::tydra::RenderSceneConverter converter; + + // Set up progress callback that reports to JS + converter.SetDetailedProgressCallback( + [](const tinyusdz::tydra::DetailedProgressInfo &info, void *userptr) -> bool { + // Report progress to JS synchronously + reportTydraProgress( + static_cast(info.meshes_processed), + static_cast(info.meshes_total), + info.GetStageName(), + info.current_mesh_name.c_str(), + info.progress + ); + return true; + }, + nullptr); + + if (stage.metas().startTimeCode.authored()) { + env.timecode = stage.metas().startTimeCode.get_value(); + } + + // Yield before heavy conversion + co_await yieldToEventLoop(); + + loaded_ = converter.ConvertToRenderScene(env, &render_scene_); + + // Yield after conversion + co_await yieldToEventLoop(); + + if (!converter.GetWarning().empty()) { + if (!warn_.empty()) warn_ += "\n"; + warn_ += converter.GetWarning(); + } + + if (!loaded_) { emscripten::val result = emscripten::val::object(); result.set("success", false); - result.set("error", error_); + result.set("error", converter.GetError()); co_return result; } - // Phase 4: Complete + // Phase 6: Complete reportAsyncPhaseStart("complete", 1.0f); // Final yield to ensure UI updates diff --git a/web/js/progress-demo.js b/web/js/progress-demo.js index 75ec8e2d..36fadc89 100644 --- a/web/js/progress-demo.js +++ b/web/js/progress-demo.js @@ -1102,19 +1102,20 @@ async function loadUSDWithProgress(source, isFile = false) { source.name, { 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 } + 'detecting': { stage: 'parsing', pct: 30, msg: 'Detecting format...' }, + 'parsing': { stage: 'parsing', pct: 35, msg: 'Parsing USD...' }, + 'setup': { stage: 'parsing', pct: 45, msg: 'Setting up converter...' }, + 'assets': { stage: 'parsing', pct: 50, msg: 'Resolving assets...' }, + 'meshes': { stage: 'parsing', pct: 55, msg: 'Converting meshes...' }, + 'complete': { stage: 'building', pct: 80, msg: 'Building scene...' } }; - const mapped = phaseMap[info.phase] || { stage: 'parsing', pct: 30 + info.progress * 50 }; + const mapped = phaseMap[info.phase] || { stage: 'parsing', pct: 30 + info.progress * 50, msg: info.phase }; updateProgressUI({ stage: mapped.stage, percentage: mapped.pct, - message: `${info.phase}...` + message: mapped.msg }); } } @@ -1181,18 +1182,20 @@ async function loadUSDWithProgress(source, isFile = false) { source, { 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 } + 'detecting': { stage: 'parsing', pct: 30, msg: 'Detecting format...' }, + 'parsing': { stage: 'parsing', pct: 35, msg: 'Parsing USD...' }, + 'setup': { stage: 'parsing', pct: 45, msg: 'Setting up converter...' }, + 'assets': { stage: 'parsing', pct: 50, msg: 'Resolving assets...' }, + 'meshes': { stage: 'parsing', pct: 55, msg: 'Converting meshes...' }, + 'complete': { stage: 'building', pct: 80, msg: 'Building scene...' } }; - const mapped = phaseMap[info.phase] || { stage: 'parsing', pct: 30 + info.progress * 50 }; + const mapped = phaseMap[info.phase] || { stage: 'parsing', pct: 30 + info.progress * 50, msg: info.phase }; updateProgressUI({ stage: mapped.stage, percentage: mapped.pct, - message: `${info.phase}...` + message: mapped.msg }); } }