mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
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:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user