Use coroutine async loading in progress-demo.js with debug logging

- 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 <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2026-01-07 03:40:00 +09:00
parent c66a71428e
commit f6c9ad1d44
3 changed files with 175 additions and 38 deletions

View File

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

View File

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

View File

@@ -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,