pcbjam/docs/PTHREADS.md
2025-12-04 08:33:45 +01:00

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:

  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

emcc main.cpp -o app.js \
    -pthread \
    -sPROXY_TO_PTHREAD=1
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

  1. Add build flags:
-pthread -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE=4
  1. Configure server headers:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
  1. Test in browser:
console.log('Threading:', crossOriginIsolated && typeof SharedArrayBuffer !== 'undefined');
  1. Done. Existing C++ threading code works.

References