Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions demos/webgpu-widget-factory/build-final.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/bin/bash
set -euo pipefail

echo "[Build] GTK widget factory with ASYNCIFY support"

cd "$(dirname "$0")"

# Download fonts if needed
if [ ! -f "fonts/Roboto-Regular.ttf" ]; then
echo "[Build] Downloading fonts..."
bash fonts/download-roboto.sh
fi

# Verify fonts are present
echo "[Build] Verifying fonts..."
for font in Roboto-Regular.ttf Roboto-Bold.ttf Roboto-Italic.ttf Roboto-BoldItalic.ttf; do
if [ ! -f "fonts/$font" ]; then
echo "ERROR: Missing font file: fonts/$font"
echo "Run: bash fonts/download-roboto.sh"
exit 1
fi
done
echo "[Build] ✓ All fonts present"

# Check for GTK install directory
if [ -d "../../install" ]; then
GTK_INSTALL="../../install"
echo "[Build] Using GTK installation: $GTK_INSTALL"
elif [ -d "/opt/gtk-wasm" ]; then
GTK_INSTALL="/opt/gtk-wasm"
echo "[Build] Using system GTK installation: $GTK_INSTALL"
else
echo "[Build] WARNING: No GTK installation found"
echo "[Build] Building with system emscripten packages"
GTK_INSTALL=""
fi

# Build compiler flags
CFLAGS="-I/opt/wasm/include"
LDFLAGS="-L/opt/wasm/lib"

if [ -n "$GTK_INSTALL" ]; then
CFLAGS="$CFLAGS -I$GTK_INSTALL/include/cairo -I$GTK_INSTALL/include/pango-1.0 -I$GTK_INSTALL/include/glib-2.0 -I$GTK_INSTALL/lib/glib-2.0/include -I$GTK_INSTALL/include/fontconfig -I$GTK_INSTALL/include/freetype2 -I$GTK_INSTALL/include/harfbuzz -I$GTK_INSTALL/include/fribidi"
LDFLAGS="$LDFLAGS -L$GTK_INSTALL/lib"
fi

LIBS="-lcairo -lpangocairo-1.0 -lpango-1.0 -lfontconfig -lfreetype -lharfbuzz -lfribidi -lglib-2.0"

echo "[Build] Compiling with ASYNCIFY support..."

# Build with ASYNCIFY enabled
emcc main.c \
$CFLAGS \
$LDFLAGS \
$LIBS \
--preload-file fonts@/fonts \
--preload-file fonts/fonts.conf@/etc/fonts/fonts.conf \
-sASYNCIFY=1 \
-sASYNCIFY_STACK_SIZE=12KB \
-sASYNCIFY_IMPORTS='["emscripten_sleep"]' \
-sEXPORTED_FUNCTIONS='["_main","_start_demo","_init_webgpu","_render_widgets","_cleanup","_get_fps","_get_frame_time","_get_widget_count","_get_simd_speedup"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap","callMain"]' \
-sINVOKE_RUN=0 \
-sEXPORT_ES6=1 \
-sMODULARIZE=1 \
-sEXPORT_NAME=GtkWidgetFactory \
-sSINGLE_FILE=0 \
-sENVIRONMENT=web,webview,worker \
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=128MB \
-sMAXIMUM_MEMORY=1GB \
-sSTACK_SIZE=2MB \
-O3 -flto \
-o gtk-widget-factory-native.js

echo ""
echo "✅ Build complete!"
echo ""
echo "Outputs:"
echo " - gtk-widget-factory-native.js (JS wrapper)"
echo " - gtk-widget-factory-native.wasm (WASM module with ASYNCIFY)"
echo " - gtk-widget-factory-native.data (1.4MB fonts)"
echo ""
echo "ASYNCIFY is enabled for async font loading!"
echo ""
echo "To test:"
echo " deno test --allow-read --allow-write --import-map=import_map.json test-wasm-init.ts"
75 changes: 46 additions & 29 deletions demos/webgpu-widget-factory/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,17 @@ int init_webgpu() {
printf("✅ Cairo surface created: %dx%d\n", CANVAS_WIDTH, CANVAS_HEIGHT);

// Initialize fontconfig with embedded fonts
printf("Initializing fontconfig...\n");
printf("[WASM] Initializing fontconfig...\n");

// Set fontconfig configuration file
setenv("FONTCONFIG_FILE", "/etc/fonts/fonts.conf", 1);

// Initialize fontconfig library first
if (!FcInit()) {
printf("❌ FcInit() failed\n");
printf("[WASM] ❌ FcInit() failed\n");
return -1;
}
printf("✅ FcInit() completed\n");
printf("[WASM] ✅ FcInit() completed\n");

// Get current config and add fonts to it
FcConfig *config = FcConfigGetCurrent();
Expand Down Expand Up @@ -156,38 +159,37 @@ int init_webgpu() {
FcBool set_result = FcConfigSetCurrent(config);
printf("FcConfigSetCurrent returned: %d\n", set_result);

// List available fonts for debugging
// Verify fonts were loaded
FcPattern *pat = FcPatternCreate();
FcObjectSet *os = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, NULL);
FcFontSet *fs = FcFontList(config, pat, os);

if (fs) {
printf("Found %d fonts:\n", fs->nfont);
for (int i = 0; i < fs->nfont && i < 10; i++) {
FcChar8 *family, *style, *file;
printf("[WASM] Found %d fonts:\n", fs->nfont);
for (int i = 0; i < fs->nfont; i++) {
FcChar8 *family, *style;
if (FcPatternGetString(fs->fonts[i], FC_FAMILY, 0, &family) == FcResultMatch &&
FcPatternGetString(fs->fonts[i], FC_STYLE, 0, &style) == FcResultMatch &&
FcPatternGetString(fs->fonts[i], FC_FILE, 0, &file) == FcResultMatch) {
printf(" - %s %s (%s)\n", family, style, file);
FcPatternGetString(fs->fonts[i], FC_STYLE, 0, &style) == FcResultMatch) {
printf("[WASM] %s %s\n", family, style);
}
}
FcFontSetDestroy(fs);
} else {
printf("❌ FcFontList returned NULL\n");
printf("[WASM] ❌ FcFontList returned NULL\n");
}
FcObjectSetDestroy(os);
FcPatternDestroy(pat);

if (fs && fs->nfont == 0) {
printf("❌ No fonts found - cannot initialize Pango\n");
printf("[WASM] ❌ No fonts found - fontconfig misconfigured\n");
return -1;
}

printf("✅ Fontconfig initialized with %d fonts\n", fs ? fs->nfont : 0);
printf("[WASM] ✅ Fontconfig initialized with embedded fonts\n");

if (fs->nfont < 4) {
printf("⚠️ Warning: Expected 4 fonts but only found %d\n", fs->nfont);
printf("⚠️ Proceeding with limited font support\n");
if (fs && fs->nfont < 4) {
printf("[WASM] ⚠️ Warning: Expected 4 fonts but only found %d\n", fs->nfont);
printf("[WASM] ⚠️ Proceeding with limited font support\n");
}

// Skip Pango - use Cairo toy font API directly
Expand Down Expand Up @@ -685,25 +687,40 @@ static bool fonts_available() {
// Called after filesystem is ready
EMSCRIPTEN_KEEPALIVE
void start_demo() {
printf("Starting demo after filesystem ready...\n");

// Check if fonts are available
if (!fonts_available()) {
printf("⚠️ Fonts not yet loaded, retrying...\n");
// Schedule retry in 50ms
EM_ASM({
setTimeout(function() {
Module._start_demo();
}, 50);
});
printf("[WASM] Widget factory demo starting...\n");
printf("[WASM] Waiting for fonts to load...\n");

// Poll for font file existence using ASYNCIFY
// This allows C code to "sleep" and yield to the JS event loop
int retries = 0;
const int MAX_RETRIES = 100; // 1 second timeout (100 * 10ms)

while (retries < MAX_RETRIES) {
FILE *f = fopen("/fonts/Roboto-Regular.ttf", "rb");
if (f) {
fclose(f);
printf("[WASM] ✅ Fonts available after %dms\n", retries * 10);
break;
}

// Sleep for 10ms and yield to event loop
// This allows Emscripten to continue loading files
emscripten_sleep(10);
retries++;
}

if (retries >= MAX_RETRIES) {
printf("[WASM] ❌ Timeout waiting for fonts\n");
printf("[WASM] Font loading failed - check build configuration\n");
return;
}

printf("✅ Fonts are available, initializing...\n");
printf("[WASM] ✅ Fonts are available, initializing...\n");

if (init_webgpu() == 0) {
printf("[WASM] Starting main loop...\n");
emscripten_set_main_loop(main_loop, 60, 1);
} else {
printf("Failed to initialize, exiting\n");
printf("[WASM] Failed to initialize, exiting\n");
}
}
55 changes: 39 additions & 16 deletions demos/webgpu-widget-factory/test-wasm-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ async function loadWASMModule(): Promise<WASMModule> {
}
};

// Track file loading completion
let filesLoaded = false;

// Initialize module with WASM binary and preloaded data
const module: WASMModule = await moduleFactory.default({
wasmBinary: wasmBytes.buffer,
Expand All @@ -67,28 +70,48 @@ async function loadWASMModule(): Promise<WASMModule> {
},
onRuntimeInitialized: () => {
console.log("✅ WASM runtime initialized");
},
// Monitor file loading dependencies
monitorRunDependencies: (left: number) => {
console.log(`[Deno] Run dependencies remaining: ${left}`);

if (left === 0) {
console.log("[Deno] All files loaded, starting demo...");
filesLoaded = true;

// Now safe to start demo - fonts are loaded
// Note: This will throw "unwind" which is expected for emscripten_set_main_loop
try {
module.ccall('start_demo', null, [], []);
} catch (e) {
if (e !== "unwind") {
console.error("[Deno] Error starting demo:", e);
throw e;
}
// "unwind" is expected - main loop started successfully
}
}
}
});

// Call main() after module is initialized
// Call main() to trigger file loading (with INVOKE_RUN=0, just loads files)
module.callMain();

// Wait a bit for filesystem to start loading
await new Promise(resolve => setTimeout(resolve, 100));
console.log("[Deno] Waiting for file loading to complete...");

// Call start_demo - it will retry if fonts aren't ready yet
// Note: This will throw "unwind" which is expected behavior for emscripten_set_main_loop
try {
module._start_demo();
} catch (e) {
if (e !== "unwind") {
throw e; // Re-throw if it's not the expected unwind exception
}
// "unwind" is expected - it means the main loop started successfully
// Wait for files to load and demo to start
let waitCount = 0;
while (!filesLoaded && waitCount < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
waitCount++;
}

if (!filesLoaded) {
throw new Error("Timeout waiting for files to load");
}

// Wait for initialization to complete (with retries)
await new Promise(resolve => setTimeout(resolve, 500));
// Wait a bit more for initialization to complete
await new Promise(resolve => setTimeout(resolve, 300));

return { module, canvas };
}
Expand All @@ -106,10 +129,10 @@ Deno.test({

console.log("\n🧪 Testing initialization...");

// start_demo() was already called by loadWASMModule()
// start_demo() was already called by monitorRunDependencies callback
// The "unwind" exception is expected - it's how emscripten_set_main_loop works
// Wait a bit for initialization to complete and render some frames
await new Promise(resolve => setTimeout(resolve, 300));
await new Promise(resolve => setTimeout(resolve, 200));

// Get metrics to verify initialization succeeded
const widgetCount = module._get_widget_count();
Expand Down
Loading