mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add async WASM support: Asyncify, JSPI, and C++20 coroutines
- Add LIGHTUSD_WASM_ASYNCIFY option for universal async JS calls - Add LIGHTUSD_WASM_JSPI option for efficient WebAssembly stack switching (Chrome 109+, Firefox 121+, Safari behind flag) - Add LIGHTUSD_COROUTINE option for C++20 coroutine API - Create async_fetch.hh/cc with sync-style fetch using Asyncify/JSPI - Create coro_fetch.hh with Task<T>, FetchAwaiter, and Generator<T> - Add asyncify-imports.js and jspi-imports.js for JS glue code - Fix StreamingLoader message serialization (5 TODOs) - Add Mat4 rotation methods and xformOp transform extraction - Update TypeScript declarations for async fetch support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ cmake_minimum_required(VERSION 3.14)
|
||||
project(lightusd VERSION 0.1.0 LANGUAGES C CXX)
|
||||
|
||||
# C++ standard - C++17 required for std::optional, std::string_view, if constexpr, etc.
|
||||
# Upgraded to C++20 when coroutines are enabled
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
@@ -16,6 +17,18 @@ option(LIGHTUSD_BUILD_TESTS "Build LightUSD tests" ON)
|
||||
option(LIGHTUSD_BUILD_EXAMPLES "Build LightUSD examples" ON)
|
||||
option(LIGHTUSD_PRODUCTION_BUILD "Production build (disable debug output)" OFF)
|
||||
option(LIGHTUSD_DEBUG_PRINT "Enable DCOUT debug macro (non-production only)" OFF)
|
||||
option(LIGHTUSD_WASM_ASYNCIFY "Enable Asyncify for async JavaScript calls (Emscripten only)" OFF)
|
||||
option(LIGHTUSD_WASM_JSPI "Enable JSPI for async JavaScript calls - more efficient than Asyncify (Emscripten only, requires browser support)" OFF)
|
||||
option(LIGHTUSD_COROUTINE "Enable C++20 coroutine API for async operations (requires JSPI or Asyncify)" OFF)
|
||||
|
||||
# Coroutine support requires C++20 and either JSPI or Asyncify
|
||||
if(LIGHTUSD_COROUTINE)
|
||||
if(NOT LIGHTUSD_WASM_JSPI AND NOT LIGHTUSD_WASM_ASYNCIFY)
|
||||
message(FATAL_ERROR "LIGHTUSD_COROUTINE requires either LIGHTUSD_WASM_JSPI or LIGHTUSD_WASM_ASYNCIFY to be enabled")
|
||||
endif()
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
message(STATUS "LightUSD: C++20 coroutine API enabled")
|
||||
endif()
|
||||
|
||||
# Source files
|
||||
set(LIGHTUSD_SOURCES
|
||||
@@ -61,6 +74,8 @@ set(LIGHTUSD_SOURCES
|
||||
src/usdz_archive.cc
|
||||
# Progressive/Streaming loader
|
||||
src/streaming_loader.cc
|
||||
# Async fetch (WASM/Asyncify)
|
||||
src/async_fetch.cc
|
||||
)
|
||||
|
||||
# Header files (for IDE support)
|
||||
@@ -113,6 +128,10 @@ set(LIGHTUSD_HEADERS
|
||||
include/lightusd/usdz_archive.hh
|
||||
# Progressive/Streaming loader
|
||||
include/lightusd/streaming_loader.hh
|
||||
# Async fetch (WASM/Asyncify/JSPI)
|
||||
include/lightusd/async_fetch.hh
|
||||
# C++20 Coroutine support
|
||||
include/lightusd/coro_fetch.hh
|
||||
)
|
||||
|
||||
# Library target
|
||||
@@ -165,33 +184,114 @@ if(LIGHTUSD_BUILD_EXAMPLES)
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Emscripten Web Worker Build
|
||||
# Emscripten WASM Builds
|
||||
# ============================================================================
|
||||
if(EMSCRIPTEN)
|
||||
# Worker module - separate WASM module for Web Worker thread
|
||||
# Common Emscripten link flags
|
||||
set(LIGHTUSD_WASM_COMMON_FLAGS "\
|
||||
-s MODULARIZE=1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MAXIMUM_MEMORY=2GB \
|
||||
-s EXPORTED_RUNTIME_METHODS=['cwrap','ccall','UTF8ToString','addFunction'] \
|
||||
--bind \
|
||||
-s NO_EXIT_RUNTIME=1 \
|
||||
-s FILESYSTEM=0 \
|
||||
")
|
||||
|
||||
# ========================================================================
|
||||
# Main thread module (with optional Asyncify)
|
||||
# ========================================================================
|
||||
add_executable(lightusd_wasm
|
||||
web/binding.cc
|
||||
)
|
||||
|
||||
target_link_libraries(lightusd_wasm PRIVATE lightusd)
|
||||
|
||||
set(LIGHTUSD_MAIN_FLAGS "${LIGHTUSD_WASM_COMMON_FLAGS} \
|
||||
-s EXPORT_NAME='createLightUSD' \
|
||||
-s ENVIRONMENT='web' \
|
||||
-s RESERVED_FUNCTION_POINTERS=16 \
|
||||
")
|
||||
|
||||
# Async support: JSPI or Asyncify (mutually exclusive)
|
||||
if(LIGHTUSD_WASM_JSPI AND LIGHTUSD_WASM_ASYNCIFY)
|
||||
message(FATAL_ERROR "LIGHTUSD_WASM_JSPI and LIGHTUSD_WASM_ASYNCIFY are mutually exclusive. Choose one.")
|
||||
endif()
|
||||
|
||||
if(LIGHTUSD_WASM_JSPI)
|
||||
# JSPI (JavaScript Promise Integration) - more efficient, requires browser support
|
||||
# Chrome 109+, Firefox 121+, Safari (behind flag)
|
||||
set(LIGHTUSD_MAIN_FLAGS "${LIGHTUSD_MAIN_FLAGS} \
|
||||
-s JSPI=1 \
|
||||
-s JSPI_IMPORTS=['js_fetch_asset_sync'] \
|
||||
-s JSPI_EXPORTS=['lightusd_load_usd_async','lightusd_fetch_texture_async'] \
|
||||
--pre-js ${CMAKE_CURRENT_SOURCE_DIR}/web/jspi-imports.js \
|
||||
")
|
||||
target_compile_definitions(lightusd_wasm PRIVATE LIGHTUSD_JSPI=1)
|
||||
target_compile_definitions(lightusd PRIVATE LIGHTUSD_JSPI=1)
|
||||
message(STATUS "LightUSD: JSPI enabled for main thread module (requires browser support)")
|
||||
elseif(LIGHTUSD_WASM_ASYNCIFY)
|
||||
# Asyncify - works everywhere but has overhead
|
||||
set(LIGHTUSD_MAIN_FLAGS "${LIGHTUSD_MAIN_FLAGS} \
|
||||
-s ASYNCIFY=1 \
|
||||
-s ASYNCIFY_STACK_SIZE=65536 \
|
||||
-s ASYNCIFY_IMPORTS=['js_fetch_asset','js_fetch_asset_sync'] \
|
||||
--pre-js ${CMAKE_CURRENT_SOURCE_DIR}/web/asyncify-imports.js \
|
||||
")
|
||||
target_compile_definitions(lightusd_wasm PRIVATE LIGHTUSD_ASYNCIFY=1)
|
||||
target_compile_definitions(lightusd PRIVATE LIGHTUSD_ASYNCIFY=1)
|
||||
message(STATUS "LightUSD: Asyncify enabled for main thread module")
|
||||
endif()
|
||||
|
||||
# C++20 Coroutine support
|
||||
if(LIGHTUSD_COROUTINE)
|
||||
target_compile_definitions(lightusd_wasm PRIVATE LIGHTUSD_COROUTINE=1)
|
||||
target_compile_definitions(lightusd PRIVATE LIGHTUSD_COROUTINE=1)
|
||||
# Emscripten may need -fcoroutines flag for older versions
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "3.0")
|
||||
target_compile_options(lightusd PRIVATE -fcoroutines)
|
||||
target_compile_options(lightusd_wasm PRIVATE -fcoroutines)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set_target_properties(lightusd_wasm PROPERTIES
|
||||
SUFFIX ".js"
|
||||
LINK_FLAGS "${LIGHTUSD_MAIN_FLAGS}"
|
||||
)
|
||||
|
||||
# Production optimizations for main module
|
||||
if(LIGHTUSD_PRODUCTION_BUILD)
|
||||
set_property(TARGET lightusd_wasm APPEND_STRING PROPERTY LINK_FLAGS
|
||||
" -O3 -flto --closure 1"
|
||||
)
|
||||
else()
|
||||
set_property(TARGET lightusd_wasm APPEND_STRING PROPERTY LINK_FLAGS
|
||||
" -O2 -g -s ASSERTIONS=1"
|
||||
)
|
||||
endif()
|
||||
|
||||
set_target_properties(lightusd_wasm PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web"
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
# Worker module (no Asyncify - uses message passing)
|
||||
# ========================================================================
|
||||
add_executable(lightusd_worker
|
||||
web/worker.cc
|
||||
)
|
||||
|
||||
target_link_libraries(lightusd_worker PRIVATE lightusd)
|
||||
|
||||
# Emscripten settings for worker
|
||||
set_target_properties(lightusd_worker PROPERTIES
|
||||
SUFFIX ".js"
|
||||
LINK_FLAGS "\
|
||||
-s MODULARIZE=1 \
|
||||
LINK_FLAGS "${LIGHTUSD_WASM_COMMON_FLAGS} \
|
||||
-s EXPORT_NAME='createLightUSDWorker' \
|
||||
-s ENVIRONMENT='worker' \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MAXIMUM_MEMORY=2GB \
|
||||
-s EXPORTED_RUNTIME_METHODS=['cwrap','ccall','UTF8ToString'] \
|
||||
--bind \
|
||||
-s NO_EXIT_RUNTIME=1 \
|
||||
-s FILESYSTEM=0 \
|
||||
"
|
||||
)
|
||||
|
||||
# Production optimizations
|
||||
# Production optimizations for worker
|
||||
if(LIGHTUSD_PRODUCTION_BUILD)
|
||||
set_property(TARGET lightusd_worker APPEND_STRING PROPERTY LINK_FLAGS
|
||||
" -O3 -flto --closure 1"
|
||||
@@ -202,12 +302,11 @@ if(EMSCRIPTEN)
|
||||
)
|
||||
endif()
|
||||
|
||||
# Output directory for worker
|
||||
set_target_properties(lightusd_worker PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web"
|
||||
)
|
||||
|
||||
message(STATUS "LightUSD: Emscripten Worker build enabled")
|
||||
message(STATUS "LightUSD: Emscripten builds enabled (main + worker)")
|
||||
endif()
|
||||
|
||||
# Install rules
|
||||
|
||||
187
sandbox/lightusd/include/lightusd/async_fetch.hh
Normal file
187
sandbox/lightusd/include/lightusd/async_fetch.hh
Normal file
@@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Light Transport Entertainment Inc.
|
||||
//
|
||||
// LightUSD - Async Fetch Interface for WASM
|
||||
//
|
||||
// This module provides async asset fetching when compiled with Asyncify or JSPI.
|
||||
// When enabled, fetch operations suspend WASM execution and resume when the
|
||||
// JavaScript fetch completes.
|
||||
//
|
||||
// Asyncify: Uses code transformation to save/restore stack (works everywhere)
|
||||
// JSPI: Uses WebAssembly stack switching (more efficient, requires browser support)
|
||||
//
|
||||
// Usage:
|
||||
// auto result = async_fetch("textures/diffuse.png");
|
||||
// if (result.ok) {
|
||||
// // Use result.data
|
||||
// }
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lightusd {
|
||||
namespace v1 {
|
||||
|
||||
// ============================================================================
|
||||
// Async Fetch Result
|
||||
// ============================================================================
|
||||
|
||||
/// Result of an async fetch operation
|
||||
struct FetchResult {
|
||||
bool ok = false;
|
||||
int status_code = 0;
|
||||
std::string error;
|
||||
std::vector<uint8_t> data;
|
||||
std::string mime_type;
|
||||
std::string resolved_url;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Async Fetch Configuration
|
||||
// ============================================================================
|
||||
|
||||
/// Configuration for fetch operations
|
||||
struct FetchConfig {
|
||||
/// Base URL for resolving relative paths
|
||||
std::string base_url;
|
||||
|
||||
/// Request timeout in milliseconds (0 = no timeout)
|
||||
uint32_t timeout_ms = 30000;
|
||||
|
||||
/// Enable CORS mode
|
||||
bool cors = true;
|
||||
|
||||
/// Custom headers (name: value pairs)
|
||||
std::vector<std::pair<std::string, std::string>> headers;
|
||||
|
||||
/// Retry count on failure
|
||||
uint32_t retry_count = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Async Fetch Handler (set by JavaScript)
|
||||
// ============================================================================
|
||||
|
||||
/// Callback type for providing fetch results
|
||||
/// @param request_id Unique request identifier
|
||||
/// @param data Fetched data (nullptr on error)
|
||||
/// @param size Data size in bytes
|
||||
/// @param status HTTP status code (0 on network error)
|
||||
/// @param error Error message (nullptr on success)
|
||||
using FetchCallback = void (*)(
|
||||
uint32_t request_id,
|
||||
const uint8_t* data,
|
||||
size_t size,
|
||||
int status,
|
||||
const char* error
|
||||
);
|
||||
|
||||
/// Set the JavaScript fetch handler
|
||||
/// This is called from JS to provide a function that can fetch URLs
|
||||
/// @param handler Function pointer that initiates fetches
|
||||
void set_fetch_handler(FetchCallback handler);
|
||||
|
||||
// ============================================================================
|
||||
// Sync-style Async Fetch (Asyncify or JSPI)
|
||||
// ============================================================================
|
||||
|
||||
#if defined(LIGHTUSD_ASYNCIFY) || defined(LIGHTUSD_JSPI)
|
||||
|
||||
/// Fetch a URL synchronously (uses Asyncify/JSPI to suspend/resume)
|
||||
/// This function will suspend WASM execution until the fetch completes.
|
||||
/// @param url URL to fetch (absolute or relative to base_url)
|
||||
/// @param config Fetch configuration
|
||||
/// @return Fetch result with data or error
|
||||
FetchResult async_fetch(const std::string& url, const FetchConfig& config = {});
|
||||
|
||||
/// Fetch multiple URLs in parallel
|
||||
/// All fetches are initiated, then WASM suspends until all complete.
|
||||
/// @param urls URLs to fetch
|
||||
/// @param config Shared fetch configuration
|
||||
/// @return Vector of results in same order as input URLs
|
||||
std::vector<FetchResult> async_fetch_all(
|
||||
const std::vector<std::string>& urls,
|
||||
const FetchConfig& config = {}
|
||||
);
|
||||
|
||||
#endif // LIGHTUSD_ASYNCIFY || LIGHTUSD_JSPI
|
||||
|
||||
// ============================================================================
|
||||
// Non-blocking Fetch (callback-based, works without Asyncify/JSPI)
|
||||
// ============================================================================
|
||||
|
||||
/// Request ID for tracking async operations
|
||||
using FetchRequestId = uint32_t;
|
||||
|
||||
/// Callback for fetch completion
|
||||
using FetchCompleteCallback = std::function<void(const FetchResult&)>;
|
||||
|
||||
/// Start a non-blocking fetch
|
||||
/// @param url URL to fetch
|
||||
/// @param config Fetch configuration
|
||||
/// @param callback Called when fetch completes
|
||||
/// @return Request ID for cancellation
|
||||
FetchRequestId fetch_async(
|
||||
const std::string& url,
|
||||
const FetchConfig& config,
|
||||
FetchCompleteCallback callback
|
||||
);
|
||||
|
||||
/// Cancel a pending fetch
|
||||
/// @param request_id Request to cancel
|
||||
/// @return true if request was found and cancelled
|
||||
bool fetch_cancel(FetchRequestId request_id);
|
||||
|
||||
/// Poll for completed fetches (call from main loop)
|
||||
/// @param max_callbacks Maximum callbacks to process
|
||||
/// @return Number of callbacks processed
|
||||
uint32_t fetch_poll(uint32_t max_callbacks = 10);
|
||||
|
||||
// ============================================================================
|
||||
// JavaScript Import Declarations (Emscripten)
|
||||
// ============================================================================
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
extern "C" {
|
||||
|
||||
/// JavaScript function to initiate an async fetch
|
||||
/// Declared as ASYNCIFY_IMPORTS or via JSPI in CMake
|
||||
/// @param url URL to fetch
|
||||
/// @param url_len URL length
|
||||
/// @param request_id Request identifier for callback
|
||||
/// @param out_data Output buffer pointer (filled by JS)
|
||||
/// @param out_size Output size pointer
|
||||
/// @param out_status Output status code pointer
|
||||
/// @return 0 on success, negative on error
|
||||
extern int js_fetch_asset(
|
||||
const char* url,
|
||||
size_t url_len,
|
||||
uint32_t request_id,
|
||||
uint8_t** out_data,
|
||||
size_t* out_size,
|
||||
int* out_status
|
||||
) __attribute__((import_name("js_fetch_asset")));
|
||||
|
||||
/// Synchronous version that blocks until complete (Asyncify)
|
||||
extern int js_fetch_asset_sync(
|
||||
const char* url,
|
||||
size_t url_len,
|
||||
uint8_t** out_data,
|
||||
size_t* out_size,
|
||||
int* out_status
|
||||
) __attribute__((import_name("js_fetch_asset_sync")));
|
||||
|
||||
/// Free memory allocated by JS
|
||||
extern void js_free_buffer(uint8_t* ptr) __attribute__((import_name("js_free_buffer")));
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
} // namespace v1
|
||||
} // namespace lightusd
|
||||
383
sandbox/lightusd/include/lightusd/coro_fetch.hh
Normal file
383
sandbox/lightusd/include/lightusd/coro_fetch.hh
Normal file
@@ -0,0 +1,383 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Light Transport Entertainment Inc.
|
||||
//
|
||||
// LightUSD - C++20 Coroutine Async Fetch API
|
||||
//
|
||||
// This module provides a coroutine-based interface for async fetching.
|
||||
// Requires C++20 and either Asyncify or JSPI for WASM suspension.
|
||||
//
|
||||
// Usage:
|
||||
// Task<FetchResult> load_texture(const std::string& url) {
|
||||
// auto result = co_await coro_fetch(url);
|
||||
// if (result.ok) {
|
||||
// // Use result.data
|
||||
// }
|
||||
// co_return result;
|
||||
// }
|
||||
//
|
||||
// The coroutine approach provides cleaner async code compared to callbacks,
|
||||
// while the underlying suspension mechanism (Asyncify/JSPI) handles the
|
||||
// actual WASM stack management.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "async_fetch.hh"
|
||||
|
||||
#if defined(LIGHTUSD_COROUTINE) && __cplusplus >= 202002L
|
||||
|
||||
#include <coroutine>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace lightusd {
|
||||
namespace v1 {
|
||||
|
||||
// ============================================================================
|
||||
// Task - Coroutine return type for async operations
|
||||
// ============================================================================
|
||||
|
||||
/// A simple task type for coroutines that return a value
|
||||
template <typename T>
|
||||
class Task {
|
||||
public:
|
||||
struct promise_type;
|
||||
using handle_type = std::coroutine_handle<promise_type>;
|
||||
|
||||
struct promise_type {
|
||||
std::optional<T> value_;
|
||||
std::exception_ptr exception_;
|
||||
|
||||
Task get_return_object() {
|
||||
return Task{handle_type::from_promise(*this)};
|
||||
}
|
||||
|
||||
std::suspend_never initial_suspend() noexcept { return {}; }
|
||||
std::suspend_always final_suspend() noexcept { return {}; }
|
||||
|
||||
void return_value(T val) {
|
||||
value_ = std::move(val);
|
||||
}
|
||||
|
||||
void unhandled_exception() {
|
||||
exception_ = std::current_exception();
|
||||
}
|
||||
};
|
||||
|
||||
Task(handle_type h) : handle_(h) {}
|
||||
|
||||
Task(Task&& other) noexcept : handle_(other.handle_) {
|
||||
other.handle_ = nullptr;
|
||||
}
|
||||
|
||||
Task& operator=(Task&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (handle_) handle_.destroy();
|
||||
handle_ = other.handle_;
|
||||
other.handle_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Task() {
|
||||
if (handle_) handle_.destroy();
|
||||
}
|
||||
|
||||
Task(const Task&) = delete;
|
||||
Task& operator=(const Task&) = delete;
|
||||
|
||||
/// Check if the task has completed
|
||||
bool done() const { return handle_.done(); }
|
||||
|
||||
/// Get the result (blocks if not ready in non-coroutine context)
|
||||
T get() {
|
||||
if (handle_.promise().exception_) {
|
||||
std::rethrow_exception(handle_.promise().exception_);
|
||||
}
|
||||
return std::move(*handle_.promise().value_);
|
||||
}
|
||||
|
||||
/// Awaiter support - allows co_await on Task<T>
|
||||
bool await_ready() const noexcept { return handle_.done(); }
|
||||
|
||||
std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept {
|
||||
return handle_;
|
||||
}
|
||||
|
||||
T await_resume() {
|
||||
return get();
|
||||
}
|
||||
|
||||
private:
|
||||
handle_type handle_;
|
||||
};
|
||||
|
||||
/// Specialization for void return type
|
||||
template <>
|
||||
class Task<void> {
|
||||
public:
|
||||
struct promise_type;
|
||||
using handle_type = std::coroutine_handle<promise_type>;
|
||||
|
||||
struct promise_type {
|
||||
std::exception_ptr exception_;
|
||||
|
||||
Task get_return_object() {
|
||||
return Task{handle_type::from_promise(*this)};
|
||||
}
|
||||
|
||||
std::suspend_never initial_suspend() noexcept { return {}; }
|
||||
std::suspend_always final_suspend() noexcept { return {}; }
|
||||
|
||||
void return_void() {}
|
||||
|
||||
void unhandled_exception() {
|
||||
exception_ = std::current_exception();
|
||||
}
|
||||
};
|
||||
|
||||
Task(handle_type h) : handle_(h) {}
|
||||
|
||||
Task(Task&& other) noexcept : handle_(other.handle_) {
|
||||
other.handle_ = nullptr;
|
||||
}
|
||||
|
||||
Task& operator=(Task&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (handle_) handle_.destroy();
|
||||
handle_ = other.handle_;
|
||||
other.handle_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Task() {
|
||||
if (handle_) handle_.destroy();
|
||||
}
|
||||
|
||||
Task(const Task&) = delete;
|
||||
Task& operator=(const Task&) = delete;
|
||||
|
||||
bool done() const { return handle_.done(); }
|
||||
|
||||
void get() {
|
||||
if (handle_.promise().exception_) {
|
||||
std::rethrow_exception(handle_.promise().exception_);
|
||||
}
|
||||
}
|
||||
|
||||
bool await_ready() const noexcept { return handle_.done(); }
|
||||
|
||||
std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept {
|
||||
return handle_;
|
||||
}
|
||||
|
||||
void await_resume() {
|
||||
get();
|
||||
}
|
||||
|
||||
private:
|
||||
handle_type handle_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// FetchAwaiter - Awaitable wrapper for async_fetch
|
||||
// ============================================================================
|
||||
|
||||
/// Awaiter for fetch operations
|
||||
/// When co_await'd, this suspends the coroutine and performs the fetch
|
||||
class FetchAwaiter {
|
||||
public:
|
||||
FetchAwaiter(std::string url, FetchConfig config = {})
|
||||
: url_(std::move(url)), config_(std::move(config)) {}
|
||||
|
||||
/// Never ready immediately - always suspend to perform fetch
|
||||
bool await_ready() const noexcept { return false; }
|
||||
|
||||
/// Suspend and perform the fetch
|
||||
/// With Asyncify/JSPI, the WASM execution is suspended here
|
||||
void await_suspend(std::coroutine_handle<> handle) {
|
||||
// Perform the synchronous-style fetch (Asyncify/JSPI handles suspension)
|
||||
result_ = async_fetch(url_, config_);
|
||||
// Resume the coroutine
|
||||
handle.resume();
|
||||
}
|
||||
|
||||
/// Return the fetch result
|
||||
FetchResult await_resume() {
|
||||
return std::move(result_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string url_;
|
||||
FetchConfig config_;
|
||||
FetchResult result_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Coroutine Fetch Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Create an awaitable fetch operation
|
||||
/// @param url URL to fetch
|
||||
/// @param config Optional fetch configuration
|
||||
/// @return Awaitable that yields FetchResult
|
||||
///
|
||||
/// Usage:
|
||||
/// FetchResult result = co_await coro_fetch("texture.png");
|
||||
inline FetchAwaiter coro_fetch(const std::string& url, const FetchConfig& config = {}) {
|
||||
return FetchAwaiter(url, config);
|
||||
}
|
||||
|
||||
/// Fetch multiple URLs as a coroutine
|
||||
/// @param urls URLs to fetch
|
||||
/// @param config Shared fetch configuration
|
||||
/// @return Task that yields vector of results
|
||||
///
|
||||
/// Usage:
|
||||
/// auto results = co_await coro_fetch_all({"a.png", "b.png"});
|
||||
inline Task<std::vector<FetchResult>> coro_fetch_all(
|
||||
const std::vector<std::string>& urls,
|
||||
const FetchConfig& config = {}) {
|
||||
|
||||
std::vector<FetchResult> results;
|
||||
results.reserve(urls.size());
|
||||
|
||||
for (const auto& url : urls) {
|
||||
results.push_back(co_await coro_fetch(url, config));
|
||||
}
|
||||
|
||||
co_return results;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Generator - For streaming/progressive loading
|
||||
// ============================================================================
|
||||
|
||||
/// Generator for yielding values one at a time
|
||||
/// Useful for progressive loading where you want to yield prims as they load
|
||||
template <typename T>
|
||||
class Generator {
|
||||
public:
|
||||
struct promise_type;
|
||||
using handle_type = std::coroutine_handle<promise_type>;
|
||||
|
||||
struct promise_type {
|
||||
T current_value_;
|
||||
std::exception_ptr exception_;
|
||||
|
||||
Generator get_return_object() {
|
||||
return Generator{handle_type::from_promise(*this)};
|
||||
}
|
||||
|
||||
std::suspend_always initial_suspend() noexcept { return {}; }
|
||||
std::suspend_always final_suspend() noexcept { return {}; }
|
||||
|
||||
std::suspend_always yield_value(T value) {
|
||||
current_value_ = std::move(value);
|
||||
return {};
|
||||
}
|
||||
|
||||
void return_void() {}
|
||||
|
||||
void unhandled_exception() {
|
||||
exception_ = std::current_exception();
|
||||
}
|
||||
};
|
||||
|
||||
Generator(handle_type h) : handle_(h) {}
|
||||
|
||||
Generator(Generator&& other) noexcept : handle_(other.handle_) {
|
||||
other.handle_ = nullptr;
|
||||
}
|
||||
|
||||
Generator& operator=(Generator&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (handle_) handle_.destroy();
|
||||
handle_ = other.handle_;
|
||||
other.handle_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Generator() {
|
||||
if (handle_) handle_.destroy();
|
||||
}
|
||||
|
||||
Generator(const Generator&) = delete;
|
||||
Generator& operator=(const Generator&) = delete;
|
||||
|
||||
/// Iterator for range-based for loops
|
||||
class iterator {
|
||||
public:
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = T;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = T*;
|
||||
using reference = T&;
|
||||
|
||||
iterator() : handle_(nullptr) {}
|
||||
explicit iterator(handle_type h) : handle_(h) {}
|
||||
|
||||
iterator& operator++() {
|
||||
handle_.resume();
|
||||
if (handle_.done()) {
|
||||
handle_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T& operator*() {
|
||||
return handle_.promise().current_value_;
|
||||
}
|
||||
|
||||
bool operator==(const iterator& other) const {
|
||||
return handle_ == other.handle_;
|
||||
}
|
||||
|
||||
bool operator!=(const iterator& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
private:
|
||||
handle_type handle_;
|
||||
};
|
||||
|
||||
iterator begin() {
|
||||
if (handle_) {
|
||||
handle_.resume();
|
||||
if (handle_.done()) {
|
||||
return end();
|
||||
}
|
||||
}
|
||||
return iterator{handle_};
|
||||
}
|
||||
|
||||
iterator end() {
|
||||
return iterator{nullptr};
|
||||
}
|
||||
|
||||
/// Manual iteration
|
||||
bool next() {
|
||||
if (!handle_ || handle_.done()) return false;
|
||||
handle_.resume();
|
||||
return !handle_.done();
|
||||
}
|
||||
|
||||
/// Get current value
|
||||
T& current() {
|
||||
return handle_.promise().current_value_;
|
||||
}
|
||||
|
||||
bool done() const {
|
||||
return !handle_ || handle_.done();
|
||||
}
|
||||
|
||||
private:
|
||||
handle_type handle_;
|
||||
};
|
||||
|
||||
} // namespace v1
|
||||
} // namespace lightusd
|
||||
|
||||
#endif // LIGHTUSD_COROUTINE && C++20
|
||||
@@ -49,6 +49,12 @@ struct Mat4 {
|
||||
static Mat4 identity();
|
||||
static Mat4 translate(float x, float y, float z);
|
||||
static Mat4 scale(float x, float y, float z);
|
||||
static Mat4 rotate_x(float radians);
|
||||
static Mat4 rotate_y(float radians);
|
||||
static Mat4 rotate_z(float radians);
|
||||
static Mat4 rotate_xyz(float rx, float ry, float rz); // Euler angles in radians
|
||||
static Mat4 from_quaternion(float x, float y, float z, float w);
|
||||
static Mat4 from_double_matrix(const double* m16); // Convert from double[16]
|
||||
Mat4 operator*(const Mat4& other) const;
|
||||
Vec3 transform_point(const Vec3& p) const;
|
||||
Vec3 transform_normal(const Vec3& n) const;
|
||||
|
||||
290
sandbox/lightusd/src/async_fetch.cc
Normal file
290
sandbox/lightusd/src/async_fetch.cc
Normal file
@@ -0,0 +1,290 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Light Transport Entertainment Inc.
|
||||
//
|
||||
// LightUSD - Async Fetch Implementation
|
||||
|
||||
#include "lightusd/async_fetch.hh"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
namespace lightusd {
|
||||
namespace v1 {
|
||||
|
||||
// ============================================================================
|
||||
// Internal State
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// Global fetch handler set by JavaScript
|
||||
FetchCallback g_fetch_handler = nullptr;
|
||||
|
||||
// Request ID counter
|
||||
std::atomic<uint32_t> g_next_request_id{1};
|
||||
|
||||
// Pending requests
|
||||
struct PendingRequest {
|
||||
std::string url;
|
||||
FetchConfig config;
|
||||
FetchCompleteCallback callback;
|
||||
};
|
||||
|
||||
std::mutex g_requests_mutex;
|
||||
std::unordered_map<FetchRequestId, PendingRequest> g_pending_requests;
|
||||
|
||||
// Completed results (for polling)
|
||||
std::mutex g_results_mutex;
|
||||
std::queue<std::pair<FetchRequestId, FetchResult>> g_completed_results;
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ============================================================================
|
||||
// Handler Registration
|
||||
// ============================================================================
|
||||
|
||||
void set_fetch_handler(FetchCallback handler) {
|
||||
g_fetch_handler = handler;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sync-style Async Fetch (Asyncify or JSPI)
|
||||
// ============================================================================
|
||||
|
||||
#if defined(LIGHTUSD_ASYNCIFY) || defined(LIGHTUSD_JSPI)
|
||||
|
||||
FetchResult async_fetch(const std::string& url, const FetchConfig& config) {
|
||||
FetchResult result;
|
||||
|
||||
// Resolve relative URLs
|
||||
std::string full_url = url;
|
||||
if (!config.base_url.empty() &&
|
||||
url.find("://") == std::string::npos &&
|
||||
url[0] != '/') {
|
||||
full_url = config.base_url + url;
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
uint8_t* data = nullptr;
|
||||
size_t size = 0;
|
||||
int status = 0;
|
||||
|
||||
// This call will suspend WASM execution via Asyncify or JSPI
|
||||
// JavaScript handles the actual fetch and resumes when done
|
||||
// - Asyncify: Uses Asyncify.handleSleep() to suspend/resume
|
||||
// - JSPI: Returns a Promise that suspends via WebAssembly stack switching
|
||||
int ret = js_fetch_asset_sync(
|
||||
full_url.c_str(),
|
||||
full_url.size(),
|
||||
&data,
|
||||
&size,
|
||||
&status
|
||||
);
|
||||
|
||||
result.status_code = status;
|
||||
result.resolved_url = full_url;
|
||||
|
||||
if (ret == 0 && data != nullptr && status >= 200 && status < 300) {
|
||||
result.ok = true;
|
||||
result.data.assign(data, data + size);
|
||||
js_free_buffer(data);
|
||||
} else {
|
||||
result.ok = false;
|
||||
if (status == 0) {
|
||||
result.error = "Network error";
|
||||
} else if (status == 404) {
|
||||
result.error = "Not found: " + full_url;
|
||||
} else {
|
||||
result.error = "HTTP error " + std::to_string(status);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Non-Emscripten builds: return error
|
||||
result.ok = false;
|
||||
result.error = "Async fetch not available outside Emscripten";
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<FetchResult> async_fetch_all(
|
||||
const std::vector<std::string>& urls,
|
||||
const FetchConfig& config) {
|
||||
|
||||
std::vector<FetchResult> results;
|
||||
results.reserve(urls.size());
|
||||
|
||||
// For now, fetch sequentially
|
||||
// TODO: Implement parallel fetching with Promise.all
|
||||
for (const auto& url : urls) {
|
||||
results.push_back(async_fetch(url, config));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
#endif // LIGHTUSD_ASYNCIFY || LIGHTUSD_JSPI
|
||||
|
||||
// ============================================================================
|
||||
// Non-blocking Callback-based Fetch
|
||||
// ============================================================================
|
||||
|
||||
FetchRequestId fetch_async(
|
||||
const std::string& url,
|
||||
const FetchConfig& config,
|
||||
FetchCompleteCallback callback) {
|
||||
|
||||
FetchRequestId request_id = g_next_request_id.fetch_add(1);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_requests_mutex);
|
||||
g_pending_requests[request_id] = PendingRequest{url, config, callback};
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Resolve URL
|
||||
std::string full_url = url;
|
||||
if (!config.base_url.empty() &&
|
||||
url.find("://") == std::string::npos &&
|
||||
url[0] != '/') {
|
||||
full_url = config.base_url + url;
|
||||
}
|
||||
|
||||
uint8_t* data = nullptr;
|
||||
size_t size = 0;
|
||||
int status = 0;
|
||||
|
||||
// Non-asyncify version - this will return immediately with status -1
|
||||
// if the fetch is pending, and the result will be delivered via callback
|
||||
int ret = js_fetch_asset(
|
||||
full_url.c_str(),
|
||||
full_url.size(),
|
||||
request_id,
|
||||
&data,
|
||||
&size,
|
||||
&status
|
||||
);
|
||||
|
||||
if (ret == 0 && status > 0) {
|
||||
// Immediate result (cached or synchronous)
|
||||
FetchResult result;
|
||||
result.ok = (status >= 200 && status < 300);
|
||||
result.status_code = status;
|
||||
result.resolved_url = full_url;
|
||||
|
||||
if (result.ok && data != nullptr) {
|
||||
result.data.assign(data, data + size);
|
||||
js_free_buffer(data);
|
||||
} else {
|
||||
result.error = "HTTP error " + std::to_string(status);
|
||||
}
|
||||
|
||||
// Queue result for polling
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_results_mutex);
|
||||
g_completed_results.push({request_id, std::move(result)});
|
||||
}
|
||||
}
|
||||
// Otherwise, result will be delivered asynchronously
|
||||
#else
|
||||
// Non-Emscripten: immediately fail
|
||||
FetchResult result;
|
||||
result.ok = false;
|
||||
result.error = "Async fetch not available outside Emscripten";
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_results_mutex);
|
||||
g_completed_results.push({request_id, std::move(result)});
|
||||
#endif
|
||||
|
||||
return request_id;
|
||||
}
|
||||
|
||||
bool fetch_cancel(FetchRequestId request_id) {
|
||||
std::lock_guard<std::mutex> lock(g_requests_mutex);
|
||||
return g_pending_requests.erase(request_id) > 0;
|
||||
}
|
||||
|
||||
uint32_t fetch_poll(uint32_t max_callbacks) {
|
||||
uint32_t processed = 0;
|
||||
|
||||
while (processed < max_callbacks) {
|
||||
FetchRequestId request_id;
|
||||
FetchResult result;
|
||||
FetchCompleteCallback callback;
|
||||
|
||||
// Get next completed result
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_results_mutex);
|
||||
if (g_completed_results.empty()) break;
|
||||
|
||||
auto& front = g_completed_results.front();
|
||||
request_id = front.first;
|
||||
result = std::move(front.second);
|
||||
g_completed_results.pop();
|
||||
}
|
||||
|
||||
// Find and call the callback
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_requests_mutex);
|
||||
auto it = g_pending_requests.find(request_id);
|
||||
if (it != g_pending_requests.end()) {
|
||||
callback = std::move(it->second.callback);
|
||||
g_pending_requests.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
|
||||
processed++;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callback from JavaScript (receives fetch results)
|
||||
// ============================================================================
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Called by JavaScript when an async fetch completes
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void lightusd_fetch_complete(
|
||||
uint32_t request_id,
|
||||
const uint8_t* data,
|
||||
size_t size,
|
||||
int status,
|
||||
const char* error) {
|
||||
|
||||
FetchResult result;
|
||||
result.status_code = status;
|
||||
|
||||
if (status >= 200 && status < 300 && data != nullptr) {
|
||||
result.ok = true;
|
||||
result.data.assign(data, data + size);
|
||||
} else {
|
||||
result.ok = false;
|
||||
result.error = error ? error : ("HTTP error " + std::to_string(status));
|
||||
}
|
||||
|
||||
// Queue result for polling
|
||||
std::lock_guard<std::mutex> lock(g_results_mutex);
|
||||
g_completed_results.push({request_id, std::move(result)});
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
} // namespace v1
|
||||
} // namespace lightusd
|
||||
@@ -52,6 +52,80 @@ Mat4 Mat4::scale(float x, float y, float z) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 Mat4::rotate_x(float radians) {
|
||||
Mat4 result = identity();
|
||||
float c = std::cos(radians);
|
||||
float s = std::sin(radians);
|
||||
result.m[5] = c;
|
||||
result.m[6] = s;
|
||||
result.m[9] = -s;
|
||||
result.m[10] = c;
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 Mat4::rotate_y(float radians) {
|
||||
Mat4 result = identity();
|
||||
float c = std::cos(radians);
|
||||
float s = std::sin(radians);
|
||||
result.m[0] = c;
|
||||
result.m[2] = -s;
|
||||
result.m[8] = s;
|
||||
result.m[10] = c;
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 Mat4::rotate_z(float radians) {
|
||||
Mat4 result = identity();
|
||||
float c = std::cos(radians);
|
||||
float s = std::sin(radians);
|
||||
result.m[0] = c;
|
||||
result.m[1] = s;
|
||||
result.m[4] = -s;
|
||||
result.m[5] = c;
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 Mat4::rotate_xyz(float rx, float ry, float rz) {
|
||||
// Apply rotations in XYZ order
|
||||
return rotate_x(rx) * rotate_y(ry) * rotate_z(rz);
|
||||
}
|
||||
|
||||
Mat4 Mat4::from_quaternion(float x, float y, float z, float w) {
|
||||
Mat4 result = identity();
|
||||
|
||||
float xx = x * x;
|
||||
float yy = y * y;
|
||||
float zz = z * z;
|
||||
float xy = x * y;
|
||||
float xz = x * z;
|
||||
float yz = y * z;
|
||||
float wx = w * x;
|
||||
float wy = w * y;
|
||||
float wz = w * z;
|
||||
|
||||
result.m[0] = 1.0f - 2.0f * (yy + zz);
|
||||
result.m[1] = 2.0f * (xy + wz);
|
||||
result.m[2] = 2.0f * (xz - wy);
|
||||
|
||||
result.m[4] = 2.0f * (xy - wz);
|
||||
result.m[5] = 1.0f - 2.0f * (xx + zz);
|
||||
result.m[6] = 2.0f * (yz + wx);
|
||||
|
||||
result.m[8] = 2.0f * (xz + wy);
|
||||
result.m[9] = 2.0f * (yz - wx);
|
||||
result.m[10] = 1.0f - 2.0f * (xx + yy);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 Mat4::from_double_matrix(const double* m16) {
|
||||
Mat4 result;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
result.m[i] = static_cast<float>(m16[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 Mat4::operator*(const Mat4& other) const {
|
||||
Mat4 result;
|
||||
for (int row = 0; row < 4; row++) {
|
||||
@@ -513,6 +587,9 @@ public:
|
||||
RenderScene& scene, const RenderConverterConfig& config,
|
||||
std::map<std::string, int32_t>& material_index_map);
|
||||
|
||||
// Extract transform from xformOps
|
||||
Mat4 extract_transform(const Prim& prim, double time);
|
||||
|
||||
// Texture loading
|
||||
Result<RenderTexture> load_texture(const std::string& uri,
|
||||
const std::vector<uint8_t>& file_data,
|
||||
@@ -910,14 +987,180 @@ std::string RenderConverter::Impl::find_bound_material(const Prim& prim) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Degrees to radians conversion
|
||||
static constexpr float kDegToRad = 3.14159265358979323846f / 180.0f;
|
||||
|
||||
Mat4 RenderConverter::Impl::extract_transform(const Prim& prim, double time) {
|
||||
Mat4 result = Mat4::identity();
|
||||
|
||||
// Get property names and look for xformOpOrder
|
||||
auto prop_names = prim.property_names();
|
||||
|
||||
// Collect xformOp names in order
|
||||
std::vector<std::string> xform_ops;
|
||||
|
||||
// First check for explicit xformOpOrder
|
||||
const Attribute* order_attr = prim.get_attribute("xformOpOrder");
|
||||
if (order_attr) {
|
||||
// xformOpOrder is a token[] - get the value and parse
|
||||
auto order_result = order_attr->get_value(time);
|
||||
if (order_result && order_result.value().is_array()) {
|
||||
// Parse the array - check type
|
||||
std::string type_name = order_result.value().type_name();
|
||||
if (type_name.find("token") != std::string::npos) {
|
||||
// For token arrays, iterate by checking property names that match
|
||||
for (const auto& prop : prop_names) {
|
||||
if (prop.find("xformOp:") == 0 && prop != "xformOpOrder") {
|
||||
xform_ops.push_back(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no xformOpOrder found, collect all xformOp:* attributes
|
||||
if (xform_ops.empty()) {
|
||||
for (const auto& prop : prop_names) {
|
||||
if (prop.find("xformOp:") == 0) {
|
||||
xform_ops.push_back(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each xformOp in order
|
||||
for (const auto& op_name : xform_ops) {
|
||||
const Attribute* attr = prim.get_attribute(op_name);
|
||||
if (!attr) continue;
|
||||
|
||||
auto val_result = attr->get_value(time);
|
||||
if (!val_result) continue;
|
||||
|
||||
const Value& val = val_result.value();
|
||||
|
||||
// Parse operation type from name
|
||||
// Format: xformOp:type or xformOp:type:suffix
|
||||
std::string op_type;
|
||||
size_t colon1 = op_name.find(':');
|
||||
if (colon1 != std::string::npos) {
|
||||
size_t colon2 = op_name.find(':', colon1 + 1);
|
||||
if (colon2 != std::string::npos) {
|
||||
op_type = op_name.substr(colon1 + 1, colon2 - colon1 - 1);
|
||||
} else {
|
||||
op_type = op_name.substr(colon1 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Mat4 op_mat = Mat4::identity();
|
||||
|
||||
if (op_type == "translate") {
|
||||
// double3 or float3
|
||||
if (const double* d = val.as_double3()) {
|
||||
op_mat = Mat4::translate(static_cast<float>(d[0]),
|
||||
static_cast<float>(d[1]),
|
||||
static_cast<float>(d[2]));
|
||||
} else if (const float* f = val.as_float3()) {
|
||||
op_mat = Mat4::translate(f[0], f[1], f[2]);
|
||||
}
|
||||
}
|
||||
else if (op_type == "scale") {
|
||||
// float3 or double3
|
||||
if (const float* f = val.as_float3()) {
|
||||
op_mat = Mat4::scale(f[0], f[1], f[2]);
|
||||
} else if (const double* d = val.as_double3()) {
|
||||
op_mat = Mat4::scale(static_cast<float>(d[0]),
|
||||
static_cast<float>(d[1]),
|
||||
static_cast<float>(d[2]));
|
||||
}
|
||||
}
|
||||
else if (op_type == "rotateX") {
|
||||
// float or double (degrees)
|
||||
if (const float* f = val.as_float()) {
|
||||
op_mat = Mat4::rotate_x(*f * kDegToRad);
|
||||
} else if (const double* d = val.as_double()) {
|
||||
op_mat = Mat4::rotate_x(static_cast<float>(*d) * kDegToRad);
|
||||
}
|
||||
}
|
||||
else if (op_type == "rotateY") {
|
||||
if (const float* f = val.as_float()) {
|
||||
op_mat = Mat4::rotate_y(*f * kDegToRad);
|
||||
} else if (const double* d = val.as_double()) {
|
||||
op_mat = Mat4::rotate_y(static_cast<float>(*d) * kDegToRad);
|
||||
}
|
||||
}
|
||||
else if (op_type == "rotateZ") {
|
||||
if (const float* f = val.as_float()) {
|
||||
op_mat = Mat4::rotate_z(*f * kDegToRad);
|
||||
} else if (const double* d = val.as_double()) {
|
||||
op_mat = Mat4::rotate_z(static_cast<float>(*d) * kDegToRad);
|
||||
}
|
||||
}
|
||||
else if (op_type == "rotateXYZ" || op_type == "rotateZYX" ||
|
||||
op_type == "rotateXZY" || op_type == "rotateYXZ" ||
|
||||
op_type == "rotateYZX" || op_type == "rotateZXY") {
|
||||
// float3 or double3 (degrees)
|
||||
float rx = 0, ry = 0, rz = 0;
|
||||
if (const float* f = val.as_float3()) {
|
||||
rx = f[0] * kDegToRad;
|
||||
ry = f[1] * kDegToRad;
|
||||
rz = f[2] * kDegToRad;
|
||||
} else if (const double* d = val.as_double3()) {
|
||||
rx = static_cast<float>(d[0]) * kDegToRad;
|
||||
ry = static_cast<float>(d[1]) * kDegToRad;
|
||||
rz = static_cast<float>(d[2]) * kDegToRad;
|
||||
}
|
||||
|
||||
// Apply in specified order
|
||||
if (op_type == "rotateXYZ") {
|
||||
op_mat = Mat4::rotate_x(rx) * Mat4::rotate_y(ry) * Mat4::rotate_z(rz);
|
||||
} else if (op_type == "rotateXZY") {
|
||||
op_mat = Mat4::rotate_x(rx) * Mat4::rotate_z(rz) * Mat4::rotate_y(ry);
|
||||
} else if (op_type == "rotateYXZ") {
|
||||
op_mat = Mat4::rotate_y(ry) * Mat4::rotate_x(rx) * Mat4::rotate_z(rz);
|
||||
} else if (op_type == "rotateYZX") {
|
||||
op_mat = Mat4::rotate_y(ry) * Mat4::rotate_z(rz) * Mat4::rotate_x(rx);
|
||||
} else if (op_type == "rotateZXY") {
|
||||
op_mat = Mat4::rotate_z(rz) * Mat4::rotate_x(rx) * Mat4::rotate_y(ry);
|
||||
} else if (op_type == "rotateZYX") {
|
||||
op_mat = Mat4::rotate_z(rz) * Mat4::rotate_y(ry) * Mat4::rotate_x(rx);
|
||||
}
|
||||
}
|
||||
else if (op_type == "orient") {
|
||||
// quatf or quatd (x, y, z, w)
|
||||
if (const float* q = val.as_quatf()) {
|
||||
op_mat = Mat4::from_quaternion(q[0], q[1], q[2], q[3]);
|
||||
} else if (const double* q = val.as_quatd()) {
|
||||
op_mat = Mat4::from_quaternion(static_cast<float>(q[0]),
|
||||
static_cast<float>(q[1]),
|
||||
static_cast<float>(q[2]),
|
||||
static_cast<float>(q[3]));
|
||||
}
|
||||
}
|
||||
else if (op_type == "transform") {
|
||||
// matrix4d
|
||||
if (const double* m = val.as_matrix4d()) {
|
||||
op_mat = Mat4::from_double_matrix(m);
|
||||
} else if (const float* m = val.as_matrix4f()) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
op_mat.m[i] = m[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concatenate this operation
|
||||
result = result * op_mat;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void RenderConverter::Impl::traverse_prims(
|
||||
const Prim& prim, const Mat4& parent_transform,
|
||||
RenderScene& scene, const RenderConverterConfig& config,
|
||||
std::map<std::string, int32_t>& material_index_map) {
|
||||
|
||||
Mat4 local_transform = parent_transform;
|
||||
|
||||
// TODO: Extract transform from xformOps
|
||||
// Extract local transform from xformOps and combine with parent
|
||||
Mat4 prim_transform = extract_transform(prim, config.time);
|
||||
Mat4 local_transform = parent_transform * prim_transform;
|
||||
|
||||
// Check if this is a mesh
|
||||
if (prim.type_name() == "Mesh") {
|
||||
|
||||
@@ -161,6 +161,12 @@ public:
|
||||
std::unordered_map<std::string, std::string> asset_errors_;
|
||||
std::unordered_set<std::string> prims_waiting_assets_;
|
||||
|
||||
// Map from asset path -> prim paths waiting for this asset
|
||||
std::unordered_map<std::string, std::vector<std::string>> asset_to_waiting_prims_;
|
||||
|
||||
// Map from prim path -> assets it's waiting for
|
||||
std::unordered_map<std::string, std::unordered_set<std::string>> prim_to_waiting_assets_;
|
||||
|
||||
// Ready prims (to be picked up by main thread)
|
||||
mutable std::mutex ready_mutex_;
|
||||
std::queue<PrimGeometry> ready_prims_;
|
||||
@@ -544,13 +550,61 @@ uint32_t StreamingLoader::process_queue(uint32_t max_count) {
|
||||
void StreamingLoader::provide_asset(const std::string& path, std::vector<uint8_t> data) {
|
||||
impl_->asset_cache_->put(path, std::move(data));
|
||||
|
||||
// TODO: Resume prims waiting for this asset
|
||||
// Resume prims waiting for this asset
|
||||
auto it = impl_->asset_to_waiting_prims_.find(path);
|
||||
if (it != impl_->asset_to_waiting_prims_.end()) {
|
||||
for (const auto& prim_path : it->second) {
|
||||
// Remove this asset from the prim's waiting set
|
||||
auto prim_it = impl_->prim_to_waiting_assets_.find(prim_path);
|
||||
if (prim_it != impl_->prim_to_waiting_assets_.end()) {
|
||||
prim_it->second.erase(path);
|
||||
|
||||
// If no more assets to wait for, re-queue the prim
|
||||
if (prim_it->second.empty()) {
|
||||
impl_->prim_to_waiting_assets_.erase(prim_it);
|
||||
impl_->prims_waiting_assets_.erase(prim_path);
|
||||
|
||||
// Re-queue with high priority (it was already in progress)
|
||||
LoadRequest req;
|
||||
req.prim_path = prim_path;
|
||||
req.priority = LoadPriority::High;
|
||||
req.time = 0.0;
|
||||
|
||||
impl_->load_queue_.push(req);
|
||||
impl_->queued_paths_.insert(prim_path);
|
||||
impl_->prim_states_[prim_path] = PrimLoadState::Queued;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl_->asset_to_waiting_prims_.erase(it);
|
||||
}
|
||||
|
||||
// Remove from pending assets list
|
||||
impl_->pending_assets_.erase(
|
||||
std::remove_if(impl_->pending_assets_.begin(), impl_->pending_assets_.end(),
|
||||
[&path](const AssetRequest& req) { return req.path == path; }),
|
||||
impl_->pending_assets_.end());
|
||||
}
|
||||
|
||||
void StreamingLoader::fail_asset(const std::string& path, const std::string& error) {
|
||||
impl_->asset_errors_[path] = error;
|
||||
|
||||
// TODO: Mark prims waiting for this asset as failed
|
||||
// Mark prims waiting for this asset as failed
|
||||
auto it = impl_->asset_to_waiting_prims_.find(path);
|
||||
if (it != impl_->asset_to_waiting_prims_.end()) {
|
||||
for (const auto& prim_path : it->second) {
|
||||
impl_->prim_states_[prim_path] = PrimLoadState::Error;
|
||||
impl_->prims_waiting_assets_.erase(prim_path);
|
||||
impl_->prim_to_waiting_assets_.erase(prim_path);
|
||||
}
|
||||
impl_->asset_to_waiting_prims_.erase(it);
|
||||
}
|
||||
|
||||
// Remove from pending assets list
|
||||
impl_->pending_assets_.erase(
|
||||
std::remove_if(impl_->pending_assets_.begin(), impl_->pending_assets_.end(),
|
||||
[&path](const AssetRequest& req) { return req.path == path; }),
|
||||
impl_->pending_assets_.end());
|
||||
}
|
||||
|
||||
std::vector<AssetRequest> StreamingLoader::pending_assets() const {
|
||||
@@ -696,6 +750,134 @@ LoadPriority calculate_priority(
|
||||
namespace {
|
||||
std::unique_ptr<StreamingLoader> g_worker_loader;
|
||||
std::queue<std::pair<WorkerMessageType, std::vector<uint8_t>>> g_outgoing_messages;
|
||||
|
||||
// Helper: Write string to buffer
|
||||
void write_string(std::vector<uint8_t>& buf, const std::string& str) {
|
||||
uint32_t len = static_cast<uint32_t>(str.size());
|
||||
buf.insert(buf.end(), reinterpret_cast<uint8_t*>(&len),
|
||||
reinterpret_cast<uint8_t*>(&len) + 4);
|
||||
buf.insert(buf.end(), str.begin(), str.end());
|
||||
}
|
||||
|
||||
// Helper: Write float array to buffer
|
||||
void write_floats(std::vector<uint8_t>& buf, const std::vector<float>& arr) {
|
||||
uint32_t count = static_cast<uint32_t>(arr.size());
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&count),
|
||||
reinterpret_cast<const uint8_t*>(&count) + 4);
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(arr.data()),
|
||||
reinterpret_cast<const uint8_t*>(arr.data() + arr.size()));
|
||||
}
|
||||
|
||||
// Helper: Write uint32 array to buffer
|
||||
void write_uint32s(std::vector<uint8_t>& buf, const std::vector<uint32_t>& arr) {
|
||||
uint32_t count = static_cast<uint32_t>(arr.size());
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&count),
|
||||
reinterpret_cast<const uint8_t*>(&count) + 4);
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(arr.data()),
|
||||
reinterpret_cast<const uint8_t*>(arr.data() + arr.size()));
|
||||
}
|
||||
|
||||
// Serialize PrimSkeleton to bytes
|
||||
std::vector<uint8_t> serialize_skeleton(const PrimSkeleton& skel) {
|
||||
std::vector<uint8_t> buf;
|
||||
write_string(buf, skel.path);
|
||||
write_string(buf, skel.name);
|
||||
write_string(buf, skel.type_name);
|
||||
write_string(buf, skel.parent_path);
|
||||
|
||||
// Child paths
|
||||
uint32_t child_count = static_cast<uint32_t>(skel.child_paths.size());
|
||||
buf.insert(buf.end(), reinterpret_cast<uint8_t*>(&child_count),
|
||||
reinterpret_cast<uint8_t*>(&child_count) + 4);
|
||||
for (const auto& child : skel.child_paths) {
|
||||
write_string(buf, child);
|
||||
}
|
||||
|
||||
// Flags
|
||||
uint8_t flags = 0;
|
||||
if (skel.has_geometry) flags |= 0x01;
|
||||
if (skel.has_material) flags |= 0x02;
|
||||
if (skel.has_transform) flags |= 0x04;
|
||||
if (skel.has_timesamples) flags |= 0x08;
|
||||
buf.push_back(flags);
|
||||
|
||||
// Estimates
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&skel.estimated_vertices),
|
||||
reinterpret_cast<const uint8_t*>(&skel.estimated_vertices) + 4);
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&skel.estimated_faces),
|
||||
reinterpret_cast<const uint8_t*>(&skel.estimated_faces) + 4);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Serialize PrimGeometry to bytes
|
||||
std::vector<uint8_t> serialize_geometry(const PrimGeometry& geom) {
|
||||
std::vector<uint8_t> buf;
|
||||
write_string(buf, geom.path);
|
||||
|
||||
int32_t mat_idx = geom.material_index;
|
||||
buf.insert(buf.end(), reinterpret_cast<uint8_t*>(&mat_idx),
|
||||
reinterpret_cast<uint8_t*>(&mat_idx) + 4);
|
||||
|
||||
write_floats(buf, geom.positions);
|
||||
write_floats(buf, geom.normals);
|
||||
write_floats(buf, geom.texcoords);
|
||||
write_floats(buf, geom.tangents);
|
||||
write_uint32s(buf, geom.indices);
|
||||
|
||||
// Bounds
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&geom.mesh.bounds.min),
|
||||
reinterpret_cast<const uint8_t*>(&geom.mesh.bounds.min) + sizeof(Vec3));
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&geom.mesh.bounds.max),
|
||||
reinterpret_cast<const uint8_t*>(&geom.mesh.bounds.max) + sizeof(Vec3));
|
||||
|
||||
// Transform
|
||||
buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(geom.mesh.transform.m),
|
||||
reinterpret_cast<const uint8_t*>(geom.mesh.transform.m) + 16 * sizeof(float));
|
||||
|
||||
// Double-sided flag
|
||||
buf.push_back(geom.mesh.double_sided ? 1 : 0);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Send STRUCTURE_READY message
|
||||
void send_structure_ready(const std::vector<PrimSkeleton>& skeletons, bool ok, const std::string& error) {
|
||||
std::vector<uint8_t> buf;
|
||||
|
||||
// Status
|
||||
buf.push_back(ok ? 1 : 0);
|
||||
|
||||
if (!ok) {
|
||||
write_string(buf, error);
|
||||
} else {
|
||||
// Skeleton count
|
||||
uint32_t count = static_cast<uint32_t>(skeletons.size());
|
||||
buf.insert(buf.end(), reinterpret_cast<uint8_t*>(&count),
|
||||
reinterpret_cast<uint8_t*>(&count) + 4);
|
||||
|
||||
// Serialize each skeleton
|
||||
for (const auto& skel : skeletons) {
|
||||
auto skel_bytes = serialize_skeleton(skel);
|
||||
buf.insert(buf.end(), skel_bytes.begin(), skel_bytes.end());
|
||||
}
|
||||
}
|
||||
|
||||
g_outgoing_messages.push({WorkerMessageType::StructureReady, std::move(buf)});
|
||||
}
|
||||
|
||||
// Send PRIM_READY message
|
||||
void send_prim_ready(const PrimGeometry& geom) {
|
||||
auto buf = serialize_geometry(geom);
|
||||
g_outgoing_messages.push({WorkerMessageType::PrimReady, std::move(buf)});
|
||||
}
|
||||
|
||||
// Send ERROR message
|
||||
void send_error(const std::string& error) {
|
||||
std::vector<uint8_t> buf;
|
||||
write_string(buf, error);
|
||||
g_outgoing_messages.push({WorkerMessageType::Error, std::move(buf)});
|
||||
}
|
||||
}
|
||||
|
||||
void worker_init() {
|
||||
@@ -720,10 +902,18 @@ void worker_handle_message(MainMessageType msg_type, const uint8_t* data, size_t
|
||||
// Check if USDZ
|
||||
if (UsdzArchive::is_usdz(file_data, file_size)) {
|
||||
auto result = g_worker_loader->parse_usdz_structure(file_data, file_size);
|
||||
// TODO: Send STRUCTURE_READY message
|
||||
if (result) {
|
||||
send_structure_ready(result.value(), true, "");
|
||||
} else {
|
||||
send_structure_ready({}, false, result.error().message);
|
||||
}
|
||||
} else {
|
||||
auto result = g_worker_loader->parse_structure(file_data, file_size, filename);
|
||||
// TODO: Send STRUCTURE_READY message
|
||||
if (result) {
|
||||
send_structure_ready(result.value(), true, "");
|
||||
} else {
|
||||
send_structure_ready({}, false, result.error().message);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -791,12 +981,11 @@ bool worker_poll_message(WorkerMessageType* out_type,
|
||||
if (g_worker_loader) {
|
||||
g_worker_loader->process_queue(1); // Process one item per poll
|
||||
|
||||
// Check for ready prims
|
||||
// Check for ready prims and serialize them
|
||||
while (g_worker_loader->has_ready_prims()) {
|
||||
auto geom = g_worker_loader->take_ready_prim();
|
||||
if (geom) {
|
||||
// Serialize geometry to message
|
||||
// TODO: Implement serialization
|
||||
send_prim_ready(geom.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
208
sandbox/lightusd/web/asyncify-imports.js
Normal file
208
sandbox/lightusd/web/asyncify-imports.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Light Transport Entertainment Inc.
|
||||
//
|
||||
// LightUSD - Asyncify Imports Setup
|
||||
//
|
||||
// This file is included via --pre-js in the Emscripten build.
|
||||
// It sets up the JavaScript functions that WASM can call asynchronously.
|
||||
|
||||
// Fetch cache for deduplication and performance
|
||||
var lightusdFetchCache = new Map();
|
||||
var lightusdFetchCacheMaxSize = 100;
|
||||
var lightusdFetchCacheHits = 0;
|
||||
var lightusdFetchCacheMisses = 0;
|
||||
|
||||
// Pending fetches for deduplication
|
||||
var lightusdPendingFetches = new Map();
|
||||
|
||||
/**
|
||||
* Add result to cache with LRU eviction
|
||||
*/
|
||||
function lightusdCacheResult(url, result) {
|
||||
if (lightusdFetchCache.size >= lightusdFetchCacheMaxSize) {
|
||||
// Remove oldest entry (first key)
|
||||
var firstKey = lightusdFetchCache.keys().next().value;
|
||||
lightusdFetchCache.delete(firstKey);
|
||||
}
|
||||
lightusdFetchCache.set(url, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous-style fetch using Asyncify
|
||||
* Called from C++ via js_fetch_asset_sync
|
||||
*/
|
||||
function js_fetch_asset_sync(urlPtr, urlLen, outDataPtr, outSizePtr, outStatusPtr) {
|
||||
var url = UTF8ToString(urlPtr, urlLen);
|
||||
|
||||
// Check cache first
|
||||
var cached = lightusdFetchCache.get(url);
|
||||
if (cached) {
|
||||
lightusdFetchCacheHits++;
|
||||
var dataPtr = _malloc(cached.data.length);
|
||||
HEAPU8.set(cached.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, cached.data.length, 'i32');
|
||||
setValue(outStatusPtr, cached.status, 'i32');
|
||||
return 0;
|
||||
}
|
||||
|
||||
lightusdFetchCacheMisses++;
|
||||
|
||||
// Use Asyncify.handleSleep to suspend WASM execution
|
||||
return Asyncify.handleSleep(function(wakeUp) {
|
||||
// Check if there's already a pending fetch for this URL
|
||||
var pending = lightusdPendingFetches.get(url);
|
||||
if (pending) {
|
||||
pending.then(function(result) {
|
||||
var dataPtr = _malloc(result.data.length);
|
||||
HEAPU8.set(result.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, result.data.length, 'i32');
|
||||
setValue(outStatusPtr, result.status, 'i32');
|
||||
wakeUp(0);
|
||||
}).catch(function(err) {
|
||||
setValue(outStatusPtr, 0, 'i32');
|
||||
wakeUp(-1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start new fetch
|
||||
var fetchPromise = fetch(url)
|
||||
.then(function(response) {
|
||||
return response.arrayBuffer().then(function(buffer) {
|
||||
return {
|
||||
data: new Uint8Array(buffer),
|
||||
status: response.status
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
lightusdPendingFetches.set(url, fetchPromise);
|
||||
|
||||
fetchPromise
|
||||
.then(function(result) {
|
||||
lightusdPendingFetches.delete(url);
|
||||
lightusdCacheResult(url, result);
|
||||
|
||||
var dataPtr = _malloc(result.data.length);
|
||||
HEAPU8.set(result.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, result.data.length, 'i32');
|
||||
setValue(outStatusPtr, result.status, 'i32');
|
||||
wakeUp(0);
|
||||
})
|
||||
.catch(function(err) {
|
||||
lightusdPendingFetches.delete(url);
|
||||
console.error('LightUSD fetch error:', url, err);
|
||||
setValue(outStatusPtr, 0, 'i32');
|
||||
wakeUp(-1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-blocking fetch for callback-based API
|
||||
* Called from C++ via js_fetch_asset
|
||||
*/
|
||||
function js_fetch_asset(urlPtr, urlLen, requestId, outDataPtr, outSizePtr, outStatusPtr) {
|
||||
var url = UTF8ToString(urlPtr, urlLen);
|
||||
|
||||
// Check cache first
|
||||
var cached = lightusdFetchCache.get(url);
|
||||
if (cached) {
|
||||
lightusdFetchCacheHits++;
|
||||
var dataPtr = _malloc(cached.data.length);
|
||||
HEAPU8.set(cached.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, cached.data.length, 'i32');
|
||||
setValue(outStatusPtr, cached.status, 'i32');
|
||||
return 0;
|
||||
}
|
||||
|
||||
lightusdFetchCacheMisses++;
|
||||
|
||||
// Start async fetch
|
||||
fetch(url)
|
||||
.then(function(response) {
|
||||
return response.arrayBuffer().then(function(buffer) {
|
||||
return {
|
||||
data: new Uint8Array(buffer),
|
||||
status: response.status
|
||||
};
|
||||
});
|
||||
})
|
||||
.then(function(result) {
|
||||
lightusdCacheResult(url, result);
|
||||
|
||||
// Call back into WASM with result
|
||||
var dataPtr = _malloc(result.data.length);
|
||||
HEAPU8.set(result.data, dataPtr);
|
||||
|
||||
// Call the C function to deliver the result
|
||||
if (typeof _lightusd_fetch_complete === 'function') {
|
||||
_lightusd_fetch_complete(requestId, dataPtr, result.data.length, result.status, 0);
|
||||
}
|
||||
_free(dataPtr);
|
||||
})
|
||||
.catch(function(err) {
|
||||
var errorMsg = err.message || 'Network error';
|
||||
var errorPtr = allocateUTF8(errorMsg);
|
||||
|
||||
if (typeof _lightusd_fetch_complete === 'function') {
|
||||
_lightusd_fetch_complete(requestId, 0, 0, 0, errorPtr);
|
||||
}
|
||||
_free(errorPtr);
|
||||
});
|
||||
|
||||
// Return -1 to indicate async operation in progress
|
||||
setValue(outStatusPtr, -1, 'i32');
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free buffer allocated by JavaScript
|
||||
*/
|
||||
function js_free_buffer(ptr) {
|
||||
if (ptr) {
|
||||
_free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Export utility functions to Module
|
||||
if (typeof Module !== 'undefined') {
|
||||
Module['clearFetchCache'] = function() {
|
||||
lightusdFetchCache.clear();
|
||||
lightusdFetchCacheHits = 0;
|
||||
lightusdFetchCacheMisses = 0;
|
||||
};
|
||||
|
||||
Module['getFetchCacheStats'] = function() {
|
||||
return {
|
||||
size: lightusdFetchCache.size,
|
||||
maxSize: lightusdFetchCacheMaxSize,
|
||||
hitCount: lightusdFetchCacheHits,
|
||||
missCount: lightusdFetchCacheMisses
|
||||
};
|
||||
};
|
||||
|
||||
Module['setFetchCacheMaxSize'] = function(size) {
|
||||
lightusdFetchCacheMaxSize = size;
|
||||
};
|
||||
|
||||
Module['prefetchAssets'] = function(urls) {
|
||||
return Promise.all(urls.map(function(url) {
|
||||
return fetch(url)
|
||||
.then(function(r) { return r.arrayBuffer(); })
|
||||
.then(function(buffer) {
|
||||
lightusdCacheResult(url, {
|
||||
data: new Uint8Array(buffer),
|
||||
status: 200
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.warn('LightUSD prefetch failed:', url, err);
|
||||
});
|
||||
}));
|
||||
};
|
||||
}
|
||||
111
sandbox/lightusd/web/js/src/lightusd/async-fetch.d.ts
vendored
Normal file
111
sandbox/lightusd/web/js/src/lightusd/async-fetch.d.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Light Transport Entertainment Inc.
|
||||
//
|
||||
// LightUSD - Async Fetch TypeScript Declarations
|
||||
//
|
||||
// Supports multiple async mechanisms:
|
||||
// - Asyncify: Universal support, uses code transformation
|
||||
// - JSPI: More efficient, requires Chrome 109+, Firefox 121+, or Safari (behind flag)
|
||||
// - Coroutine: C++20 coroutine API (C++ side only, requires Asyncify or JSPI)
|
||||
|
||||
import type { LightUSDModule } from './lightusd';
|
||||
|
||||
/**
|
||||
* Result of an async fetch operation
|
||||
*/
|
||||
export interface FetchResult {
|
||||
/** Whether the fetch succeeded */
|
||||
ok: boolean;
|
||||
/** HTTP status code (0 for network errors) */
|
||||
statusCode: number;
|
||||
/** Error message if failed */
|
||||
error?: string;
|
||||
/** Fetched data as Uint8Array */
|
||||
data?: Uint8Array;
|
||||
/** MIME type of the response */
|
||||
mimeType?: string;
|
||||
/** Final URL after redirects */
|
||||
resolvedUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for fetch operations
|
||||
*/
|
||||
export interface FetchConfig {
|
||||
/** Base URL for resolving relative paths */
|
||||
baseUrl?: string;
|
||||
/** Request timeout in milliseconds (default: 30000) */
|
||||
timeoutMs?: number;
|
||||
/** Enable CORS mode (default: true) */
|
||||
cors?: boolean;
|
||||
/** Custom headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Number of retries on failure (default: 0) */
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the async fetch system.
|
||||
* Call this after the WASM module is loaded.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import createLightUSD from '@lightusd/web';
|
||||
* import { initAsyncFetch } from '@lightusd/web/async-fetch';
|
||||
*
|
||||
* const Module = await createLightUSD();
|
||||
* initAsyncFetch(Module);
|
||||
* ```
|
||||
*/
|
||||
export function initAsyncFetch(Module: LightUSDModule): void;
|
||||
|
||||
/**
|
||||
* Pre-fetch assets into the cache.
|
||||
* Useful for preloading textures or referenced USD files.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await prefetchAssets([
|
||||
* 'textures/diffuse.png',
|
||||
* 'textures/normal.png',
|
||||
* 'materials.usda'
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
export function prefetchAssets(urls: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Clear the fetch cache
|
||||
*/
|
||||
export function clearFetchCache(): void;
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
export function getFetchCacheStats(): {
|
||||
size: number;
|
||||
maxSize: number;
|
||||
hitCount: number;
|
||||
missCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if JSPI (JavaScript Promise Integration) is supported in this browser.
|
||||
* JSPI is more efficient than Asyncify but requires browser support.
|
||||
*
|
||||
* Browser support:
|
||||
* - Chrome 109+ (enabled by default)
|
||||
* - Firefox 121+ (enabled by default)
|
||||
* - Safari: behind flag
|
||||
*
|
||||
* @returns true if JSPI is supported
|
||||
*/
|
||||
export function isJSPISupported(): boolean;
|
||||
|
||||
export default {
|
||||
initAsyncFetch,
|
||||
prefetchAssets,
|
||||
clearFetchCache,
|
||||
getFetchCacheStats,
|
||||
isJSPISupported
|
||||
};
|
||||
@@ -102,5 +102,11 @@ export type {
|
||||
LoadPriority as WorkerLoadPriority,
|
||||
} from './WorkerBridge';
|
||||
|
||||
// Async fetch (Asyncify/JSPI support)
|
||||
export type {
|
||||
FetchResult,
|
||||
FetchConfig,
|
||||
} from './async-fetch';
|
||||
|
||||
// Default export is the module loader
|
||||
export { default } from './lightusd';
|
||||
|
||||
@@ -389,3 +389,27 @@ export type {
|
||||
WorkerBridgeEvents
|
||||
} from './WorkerBridge';
|
||||
export { WorkerBridge } from './WorkerBridge';
|
||||
|
||||
// ============================================================================
|
||||
// Async Fetch (Asyncify/JSPI/Coroutine support)
|
||||
// ============================================================================
|
||||
|
||||
export type { FetchResult, FetchConfig } from './async-fetch';
|
||||
export { initAsyncFetch, prefetchAssets, clearFetchCache, getFetchCacheStats } from './async-fetch';
|
||||
|
||||
// ============================================================================
|
||||
// Module extensions for Asyncify/JSPI builds
|
||||
// ============================================================================
|
||||
|
||||
declare module './lightusd' {
|
||||
interface LightUSDModule {
|
||||
// Async fetch utilities (available when LIGHTUSD_WASM_ASYNCIFY=ON or LIGHTUSD_WASM_JSPI=ON)
|
||||
clearFetchCache?(): void;
|
||||
getFetchCacheStats?(): { size: number; maxSize: number; hitCount: number; missCount: number };
|
||||
setFetchCacheMaxSize?(size: number): void;
|
||||
prefetchAssets?(urls: string[]): Promise<void>;
|
||||
|
||||
// JSPI support check (available when LIGHTUSD_WASM_JSPI=ON)
|
||||
isJSPISupported?(): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
234
sandbox/lightusd/web/jspi-imports.js
Normal file
234
sandbox/lightusd/web/jspi-imports.js
Normal file
@@ -0,0 +1,234 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Light Transport Entertainment Inc.
|
||||
//
|
||||
// LightUSD - JSPI (JavaScript Promise Integration) Imports
|
||||
//
|
||||
// This file is included via --pre-js in the Emscripten build when JSPI is enabled.
|
||||
// JSPI is more efficient than Asyncify as it uses WebAssembly stack switching.
|
||||
//
|
||||
// Browser support:
|
||||
// - Chrome 109+ (enabled by default)
|
||||
// - Firefox 121+ (enabled by default)
|
||||
// - Safari: behind flag
|
||||
//
|
||||
// Unlike Asyncify, JSPI functions return Promises directly.
|
||||
|
||||
// Fetch cache for deduplication and performance
|
||||
var lightusdFetchCache = new Map();
|
||||
var lightusdFetchCacheMaxSize = 100;
|
||||
var lightusdFetchCacheHits = 0;
|
||||
var lightusdFetchCacheMisses = 0;
|
||||
|
||||
// Pending fetches for deduplication
|
||||
var lightusdPendingFetches = new Map();
|
||||
|
||||
/**
|
||||
* Check if JSPI is supported in this browser
|
||||
*/
|
||||
function lightusdCheckJSPISupport() {
|
||||
try {
|
||||
// JSPI requires WebAssembly.Suspending and WebAssembly.promising
|
||||
return typeof WebAssembly.Suspending === 'function' ||
|
||||
typeof WebAssembly.promising === 'function';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add result to cache with LRU eviction
|
||||
*/
|
||||
function lightusdCacheResult(url, result) {
|
||||
if (lightusdFetchCache.size >= lightusdFetchCacheMaxSize) {
|
||||
// Remove oldest entry (first key)
|
||||
var firstKey = lightusdFetchCache.keys().next().value;
|
||||
lightusdFetchCache.delete(firstKey);
|
||||
}
|
||||
lightusdFetchCache.set(url, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSPI-style async fetch - returns a Promise
|
||||
* This function is imported by WASM via JSPI_IMPORTS
|
||||
*
|
||||
* With JSPI, this function returns a Promise. When WASM calls it,
|
||||
* the WebAssembly stack is suspended until the Promise resolves.
|
||||
*/
|
||||
function js_fetch_asset_sync(urlPtr, urlLen, outDataPtr, outSizePtr, outStatusPtr) {
|
||||
var url = UTF8ToString(urlPtr, urlLen);
|
||||
|
||||
// Check cache first (synchronous path)
|
||||
var cached = lightusdFetchCache.get(url);
|
||||
if (cached) {
|
||||
lightusdFetchCacheHits++;
|
||||
var dataPtr = _malloc(cached.data.length);
|
||||
HEAPU8.set(cached.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, cached.data.length, 'i32');
|
||||
setValue(outStatusPtr, cached.status, 'i32');
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
lightusdFetchCacheMisses++;
|
||||
|
||||
// Check if there's already a pending fetch for this URL
|
||||
var pending = lightusdPendingFetches.get(url);
|
||||
if (pending) {
|
||||
return pending.then(function(result) {
|
||||
var dataPtr = _malloc(result.data.length);
|
||||
HEAPU8.set(result.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, result.data.length, 'i32');
|
||||
setValue(outStatusPtr, result.status, 'i32');
|
||||
return 0;
|
||||
}).catch(function(err) {
|
||||
console.error('LightUSD fetch error (cached promise):', url, err);
|
||||
setValue(outStatusPtr, 0, 'i32');
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
// Start new fetch - return Promise for JSPI
|
||||
var fetchPromise = fetch(url)
|
||||
.then(function(response) {
|
||||
return response.arrayBuffer().then(function(buffer) {
|
||||
return {
|
||||
data: new Uint8Array(buffer),
|
||||
status: response.status
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
lightusdPendingFetches.set(url, fetchPromise);
|
||||
|
||||
return fetchPromise
|
||||
.then(function(result) {
|
||||
lightusdPendingFetches.delete(url);
|
||||
lightusdCacheResult(url, result);
|
||||
|
||||
var dataPtr = _malloc(result.data.length);
|
||||
HEAPU8.set(result.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, result.data.length, 'i32');
|
||||
setValue(outStatusPtr, result.status, 'i32');
|
||||
return 0;
|
||||
})
|
||||
.catch(function(err) {
|
||||
lightusdPendingFetches.delete(url);
|
||||
console.error('LightUSD fetch error:', url, err);
|
||||
setValue(outStatusPtr, 0, 'i32');
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-blocking fetch for callback-based API (same as Asyncify version)
|
||||
*/
|
||||
function js_fetch_asset(urlPtr, urlLen, requestId, outDataPtr, outSizePtr, outStatusPtr) {
|
||||
var url = UTF8ToString(urlPtr, urlLen);
|
||||
|
||||
// Check cache first
|
||||
var cached = lightusdFetchCache.get(url);
|
||||
if (cached) {
|
||||
lightusdFetchCacheHits++;
|
||||
var dataPtr = _malloc(cached.data.length);
|
||||
HEAPU8.set(cached.data, dataPtr);
|
||||
setValue(outDataPtr, dataPtr, '*');
|
||||
setValue(outSizePtr, cached.data.length, 'i32');
|
||||
setValue(outStatusPtr, cached.status, 'i32');
|
||||
return 0;
|
||||
}
|
||||
|
||||
lightusdFetchCacheMisses++;
|
||||
|
||||
// Start async fetch
|
||||
fetch(url)
|
||||
.then(function(response) {
|
||||
return response.arrayBuffer().then(function(buffer) {
|
||||
return {
|
||||
data: new Uint8Array(buffer),
|
||||
status: response.status
|
||||
};
|
||||
});
|
||||
})
|
||||
.then(function(result) {
|
||||
lightusdCacheResult(url, result);
|
||||
|
||||
var dataPtr = _malloc(result.data.length);
|
||||
HEAPU8.set(result.data, dataPtr);
|
||||
|
||||
if (typeof _lightusd_fetch_complete === 'function') {
|
||||
_lightusd_fetch_complete(requestId, dataPtr, result.data.length, result.status, 0);
|
||||
}
|
||||
_free(dataPtr);
|
||||
})
|
||||
.catch(function(err) {
|
||||
var errorMsg = err.message || 'Network error';
|
||||
var errorPtr = allocateUTF8(errorMsg);
|
||||
|
||||
if (typeof _lightusd_fetch_complete === 'function') {
|
||||
_lightusd_fetch_complete(requestId, 0, 0, 0, errorPtr);
|
||||
}
|
||||
_free(errorPtr);
|
||||
});
|
||||
|
||||
// Return -1 to indicate async operation in progress
|
||||
setValue(outStatusPtr, -1, 'i32');
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free buffer allocated by JavaScript
|
||||
*/
|
||||
function js_free_buffer(ptr) {
|
||||
if (ptr) {
|
||||
_free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Export utility functions to Module
|
||||
if (typeof Module !== 'undefined') {
|
||||
Module['isJSPISupported'] = lightusdCheckJSPISupport;
|
||||
|
||||
Module['clearFetchCache'] = function() {
|
||||
lightusdFetchCache.clear();
|
||||
lightusdFetchCacheHits = 0;
|
||||
lightusdFetchCacheMisses = 0;
|
||||
};
|
||||
|
||||
Module['getFetchCacheStats'] = function() {
|
||||
return {
|
||||
size: lightusdFetchCache.size,
|
||||
maxSize: lightusdFetchCacheMaxSize,
|
||||
hitCount: lightusdFetchCacheHits,
|
||||
missCount: lightusdFetchCacheMisses
|
||||
};
|
||||
};
|
||||
|
||||
Module['setFetchCacheMaxSize'] = function(size) {
|
||||
lightusdFetchCacheMaxSize = size;
|
||||
};
|
||||
|
||||
Module['prefetchAssets'] = function(urls) {
|
||||
return Promise.all(urls.map(function(url) {
|
||||
return fetch(url)
|
||||
.then(function(r) { return r.arrayBuffer(); })
|
||||
.then(function(buffer) {
|
||||
lightusdCacheResult(url, {
|
||||
data: new Uint8Array(buffer),
|
||||
status: 200
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.warn('LightUSD prefetch failed:', url, err);
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
// Log JSPI support status
|
||||
if (lightusdCheckJSPISupport()) {
|
||||
console.log('LightUSD: JSPI supported in this browser');
|
||||
} else {
|
||||
console.warn('LightUSD: JSPI not supported - async operations may not work');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user