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:
Syoyo Fujita
2025-12-20 06:51:10 +09:00
parent 21da2d7e45
commit 01df70a7ce
12 changed files with 2004 additions and 24 deletions

View File

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

View 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

View 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

View File

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

View 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

View File

@@ -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") {

View File

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

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

View 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
};

View File

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

View File

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

View 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');
}
}