Add granular coroutine yield phases for Tydra conversion

- 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 <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2026-01-07 04:13:51 +09:00
parent f6c9ad1d44
commit c6ace402dc
2 changed files with 105 additions and 34 deletions

View File

@@ -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<const uint8_t *>(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", error_);
result.set("error", "Failed to read USDZ assetInfo");
co_return result;
}
// Phase 4: Complete
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<int>(info.meshes_processed),
static_cast<int>(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", converter.GetError());
co_return result;
}
// Phase 6: Complete
reportAsyncPhaseStart("complete", 1.0f);
// Final yield to ensure UI updates

View File

@@ -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
});
}
}