9.4 KiB
Emscripten pthreads for KiCad WASM
Overview
Emscripten can compile C++ threading code to WebAssembly using Web Workers and SharedArrayBuffer. This allows KiCad's existing BS::thread_pool code to work unchanged in the browser.
How It Works
┌──────────────────────────────────────────────────────────────┐
│ Browser Environment │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ SharedArrayBuffer (Shared Memory) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Main │ │ Worker │ │ Worker │ ... │ │
│ │ │ Thread │ │ Thread │ │ Thread │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ BOARD* │ │ BOARD* │ │ BOARD* │ │ │
│ │ │ (same!) │ │ (same!) │ │ (same!) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ All threads share memory - C++ pointers work normally │
└──────────────────────────────────────────────────────────────┘
Emscripten:
- Creates Web Workers for each pthread
- Uses SharedArrayBuffer for shared memory between workers
- Implements pthread synchronization primitives (mutex, condition variables)
- Existing C++ thread code compiles without modification
Build Configuration
Minimal Flags
emcc main.cpp -o app.js \
-pthread \
-sPROXY_TO_PTHREAD=1
Recommended Full Configuration
emcc main.cpp -o app.js \
-pthread \
-sPROXY_TO_PTHREAD=1 \
-sPTHREAD_POOL_SIZE="Math.min(navigator.hardwareConcurrency, 8)" \
-sALLOW_BLOCKING_ON_MAIN_THREAD=0 \
-sOFFSCREENCANVAS_SUPPORT=1 \
-sOFFSCREEN_FRAMEBUFFER=1 \
-sMALLOC=mimalloc \
-sINITIAL_MEMORY=512MB \
-sSTACK_SIZE=5MB \
-sPTHREAD_POOL_SIZE_STRICT=0
Flag Explanations
| Flag | Purpose |
|---|---|
-pthread |
Enable pthread support (required) |
-sPROXY_TO_PTHREAD=1 |
Move main() to worker thread, keeps browser responsive |
-sPTHREAD_POOL_SIZE=N |
Pre-create N worker threads (avoids async creation delay) |
-sALLOW_BLOCKING_ON_MAIN_THREAD=0 |
Error if main thread blocks (catches bugs) |
-sOFFSCREENCANVAS_SUPPORT=1 |
WebGL rendering in worker threads |
-sOFFSCREEN_FRAMEBUFFER=1 |
Fallback if OffscreenCanvas unavailable |
-sMALLOC=mimalloc |
Better allocator for multithreaded code |
-sPTHREAD_POOL_SIZE_STRICT=0 |
Allow pool to grow beyond initial size |
Dynamic Thread Pool Size
Use JavaScript expression for dynamic sizing:
-sPTHREAD_POOL_SIZE="Math.min(navigator.hardwareConcurrency, 8)"
This creates threads based on available CPU cores (max 8).
Required HTTP Headers
CRITICAL: SharedArrayBuffer requires these headers or it won't be available.
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Server Configuration Examples
Nginx:
location / {
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
Apache (.htaccess):
Header set Cross-Origin-Embedder-Policy "require-corp"
Header set Cross-Origin-Opener-Policy "same-origin"
Express.js:
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }
]
}
]
}
CloudFlare Pages (_headers):
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Browser Support
| Browser | Version | Status |
|---|---|---|
| Chrome | 92+ | ✅ Full support |
| Firefox | 79+ | ✅ Full support |
| Safari | 16.4+ | ✅ Full support |
| Edge | 92+ | ✅ Full support (Chromium) |
All modern browsers support SharedArrayBuffer with proper headers.
Feature Detection
Check if threading is available at runtime:
function checkThreadingSupport() {
const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined';
const isCrossOriginIsolated = crossOriginIsolated;
console.log('SharedArrayBuffer available:', hasSharedArrayBuffer);
console.log('Cross-origin isolated:', isCrossOriginIsolated);
if (!hasSharedArrayBuffer) {
console.error('SharedArrayBuffer not available - check HTTPS');
}
if (!isCrossOriginIsolated) {
console.error('Not cross-origin isolated - check COOP/COEP headers');
}
return hasSharedArrayBuffer && isCrossOriginIsolated;
}
PROXY_TO_PTHREAD Explained
Without PROXY_TO_PTHREAD:
- main() runs on browser's main thread
- Any blocking call (mutex, sleep, join) freezes the browser
- User can't interact while computation runs
With PROXY_TO_PTHREAD:
- Emscripten creates a new main() that spawns a worker
- Your original main() runs entirely in that worker
- Browser main thread stays responsive
- Blocking calls work normally
Always use PROXY_TO_PTHREAD for applications with UI.
Memory Considerations
Each worker thread loads:
- Complete JavaScript glue code
- Complete WASM module
Memory usage scales with thread count:
- 2 threads: ~200MB typical
- 8 threads: ~400MB typical
- 20 threads: ~600MB+ typical
Keep thread pool size reasonable (4-8 threads recommended).
Debugging
Check if pthreads are working
#include <emscripten/threading.h>
#include <thread>
int main() {
printf("Main thread: %d\n", emscripten_is_main_runtime_thread());
printf("Hardware concurrency: %d\n", std::thread::hardware_concurrency());
std::thread t([]() {
printf("Worker thread: %d\n", emscripten_is_main_runtime_thread());
});
t.join();
return 0;
}
Common Issues
"SharedArrayBuffer is not defined"
- Missing COOP/COEP headers
- Not served over HTTPS (except localhost)
"pthread_create failed"
- Thread pool exhausted, increase PTHREAD_POOL_SIZE
- Or use PTHREAD_POOL_SIZE_STRICT=0 to allow growth
Browser hangs/freezes
- Missing PROXY_TO_PTHREAD flag
- Blocking on main thread
Memory errors
- Increase INITIAL_MEMORY
- Increase STACK_SIZE (each thread needs stack space)
Comparison with Alternatives
| Approach | KiCad Changes | Deployment | Performance |
|---|---|---|---|
| Emscripten pthreads | None | Needs headers | Best |
| Synchronous stub | None (stub only) | Works everywhere | Slowest |
| Manual Web Workers | Significant | Works everywhere | Good |
KiCad-Specific Notes
KiCad uses BS::thread_pool (Barak Shoshany's thread pool):
- Location:
kicad/thirdparty/thread-pool/bs_thread_pool.hpp - Uses standard C++ threads internally
- Works unchanged with Emscripten pthreads
Heavy threading usage in:
- DRC (Design Rule Check) -
pcbnew/drc/ - Zone filling -
pcbnew/zone_filler.cpp - 3D raytracing -
3d-viewer/3d_rendering/raytracing/ - Library loading - background tasks
With pthreads enabled, all these run in parallel as they do natively.
Hosting Compatibility
| Host | Headers Configurable | pthreads Work |
|---|---|---|
| GitHub Pages | ❌ No | ❌ No |
| Netlify Free | ❌ No | ❌ No |
| Netlify Pro | ✅ Yes | ✅ Yes |
| Vercel | ✅ Yes | ✅ Yes |
| CloudFlare Pages | ✅ Yes | ✅ Yes |
| AWS S3 + CloudFront | ✅ Yes | ✅ Yes |
| Your own server | ✅ Yes | ✅ Yes |
| localhost | ✅ N/A | ✅ Yes |
Quick Start
- Add build flags:
-pthread -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=4
- Configure server headers:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
- Test in browser:
console.log('Threading:', crossOriginIsolated && typeof SharedArrayBuffer !== 'undefined');
- Done. Existing C++ threading code works.