301 lines
9.4 KiB
Markdown
301 lines
9.4 KiB
Markdown
|
|
# 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:
|
||
|
|
1. Creates Web Workers for each pthread
|
||
|
|
2. Uses SharedArrayBuffer for shared memory between workers
|
||
|
|
3. Implements pthread synchronization primitives (mutex, condition variables)
|
||
|
|
4. Existing C++ thread code compiles without modification
|
||
|
|
|
||
|
|
## Build Configuration
|
||
|
|
|
||
|
|
### Minimal Flags
|
||
|
|
|
||
|
|
```bash
|
||
|
|
emcc main.cpp -o app.js \
|
||
|
|
-pthread \
|
||
|
|
-sPROXY_TO_PTHREAD=1
|
||
|
|
```
|
||
|
|
|
||
|
|
### Recommended Full Configuration
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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:
|
||
|
|
```bash
|
||
|
|
-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.
|
||
|
|
|
||
|
|
```http
|
||
|
|
Cross-Origin-Embedder-Policy: require-corp
|
||
|
|
Cross-Origin-Opener-Policy: same-origin
|
||
|
|
```
|
||
|
|
|
||
|
|
### Server Configuration Examples
|
||
|
|
|
||
|
|
**Nginx:**
|
||
|
|
```nginx
|
||
|
|
location / {
|
||
|
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||
|
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Apache (.htaccess):**
|
||
|
|
```apache
|
||
|
|
Header set Cross-Origin-Embedder-Policy "require-corp"
|
||
|
|
Header set Cross-Origin-Opener-Policy "same-origin"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Express.js:**
|
||
|
|
```javascript
|
||
|
|
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):**
|
||
|
|
```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:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
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
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
#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
|
||
|
|
|
||
|
|
1. **Add build flags:**
|
||
|
|
```bash
|
||
|
|
-pthread -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=4
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Configure server headers:**
|
||
|
|
```
|
||
|
|
Cross-Origin-Embedder-Policy: require-corp
|
||
|
|
Cross-Origin-Opener-Policy: same-origin
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Test in browser:**
|
||
|
|
```javascript
|
||
|
|
console.log('Threading:', crossOriginIsolated && typeof SharedArrayBuffer !== 'undefined');
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Done.** Existing C++ threading code works.
|
||
|
|
|
||
|
|
## References
|
||
|
|
|
||
|
|
- [Emscripten pthreads documentation](https://emscripten.org/docs/porting/pthreads.html)
|
||
|
|
- [MDN SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)
|
||
|
|
- [Chrome SharedArrayBuffer update](https://developer.chrome.com/blog/enabling-shared-array-buffer)
|
||
|
|
- [web.dev WebAssembly threads guide](https://web.dev/articles/webassembly-threads)
|