diff --git a/.gitignore b/.gitignore index 79615c0..d9b1820 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ wxwidgets-clean/ /tests/apps/kicad/*.wasm /tests/apps/kicad/*.tar.gz !tests/apps/kicad/pcbnew.html +/tests/apps/gal-webgl/*.js +/tests/apps/gal-webgl/*.wasm /temp/ *.log *.tmp diff --git a/features/webgl/0003-webgl-gal-implementation-plan.md b/features/webgl/0003-webgl-gal-implementation-plan.md new file mode 100644 index 0000000..b31427d --- /dev/null +++ b/features/webgl/0003-webgl-gal-implementation-plan.md @@ -0,0 +1,181 @@ +# WebGL GAL Port - Master Plan + +## Goal +Port KiCad's GAL (Graphics Abstraction Layer) from OpenGL to WebGL to enable full KiCad functionality in the browser. Use the existing 28-scenario test suite to verify visual parity between native OpenGL and WebGL implementations. + +## Current State +- **GAL Test Harness**: 28 scenarios covering 100% of GAL API (~70 methods) +- **Native Test**: C++ binary using real OPENGL_GAL, outputs PNGs to `tests/gal-regression/baseline/` +- **KiCad WASM**: Already runs in browser but crashes on OpenGL compatibility issues +- **OpenGL GAL**: ~9,500 lines of C++/GLSL using mix of legacy GL and modern VBOs + +## Key Decisions +- **Approach**: Copy and modify existing OpenGL GAL code +- **Scope**: Full feature parity (all 28 scenarios) +- **Location**: Develop in `tests/gal-regression/` first, move to KiCad later + +## Architecture + +**Two-backend test architecture:** +``` +┌─────────────────────────────────────────────────────────────┐ +│ SAME 28 SCENARIO FILES │ +│ (scenarios/*.cpp - pure GAL API calls) │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ +┌─────────────────────────────┐ ┌─────────────────────────┐ +│ NATIVE TEST HARNESS │ │ WEBGL TEST HARNESS │ +│ (gal_native_test.cpp) │ │ (gal_webgl_test.cpp) │ +│ │ │ │ +│ Uses: OPENGL_GAL │ │ Uses: WEBGL_GAL │ +│ Runs: macOS native │ │ Runs: Browser/WASM │ +└─────────────────────────────┘ └─────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────┐ ┌─────────────────────────┐ +│ output/native/gal-*.png │ │ output/webgl/gal-*.png │ +└─────────────────────────────┘ └─────────────────────────┘ +``` + +## Master Test Script: `scripts/test-gal-regression.sh` + +**This is the only script we run.** Single command to build, test, and compare everything: + +```bash +#!/bin/bash +# Single script to build, run, and compare both backends + +# 1. BUILD BOTH +scripts/build-gal-native-test.sh +scripts/build-gal-webgl-test.sh + +# 2. RUN BOTH TESTS +./tests/gal-regression/native/build/gal_native_test --output tests/gal-regression/output/native/ +npx playwright test gal-webgl.spec.ts # outputs to tests/gal-regression/output/webgl/ + +# 3. COMPARE (two-level) +compare_screenshots output/native/ baseline/ # Catch native regressions +compare_screenshots output/webgl/ output/native/ # Verify WebGL matches native + +# 4. REPORT +# Exit 0 if all match, exit 1 if any differ +``` + +**Two-level comparison:** +1. **native vs baseline** → Catches if native code regressed +2. **webgl vs native** → Verifies WebGL implementation matches + +**Output structure:** +``` +tests/gal-regression/ +├── baseline/ # Committed reference screenshots +├── output/ +│ ├── native/ # Fresh native run +│ └── webgl/ # WebGL run via Playwright +``` + +## Phases + +### Phase 1: Master Test Script & WebGL Harness Infrastructure +Create the unified test script and WASM-based test harness. + +**Deliverables:** +- [x] `scripts/test-gal-regression.sh` - Master build/test/compare script +- [x] `tests/gal-regression/wasm/` directory structure +- [x] `tests/gal-regression/wasm/Makefile` - Emscripten build (use Makefile, not CMake) +- [x] `tests/gal-regression/wasm/gal_webgl_test.cpp` - WASM entry point +- [x] `tests/gal-regression/wasm/gal_webgl_test.html` - Test page with canvas +- [x] `scripts/build-gal-webgl-test.sh` - WASM build script (uses Makefile) +- [x] `tests/e2e/gal-webgl.spec.ts` - Playwright spec for screenshots + +**Build Approach:** +- Use a Makefile with direct `em++` calls (like `tests/apps/Makefile.wasm`) +- Avoid `emcmake cmake` which requires Python 3.10+ (system has 3.9.6) +- Follow the same pattern as `build-wasm-test.sh` + +**Verification:** Master script builds both, runs native successfully, WebGL loads empty page. + +### Phase 2: WEBGL_GAL Implementation +Copy and modify OpenGL GAL to create pure WebGL implementation. + +**Approach:** Start with `kicad/common/gal/opengl/opengl_gal.cpp`, then: +1. Replace legacy immediate mode (`glBegin/glEnd`) with VBO-based rendering +2. Replace GL matrix stack with glm matrices (already used internally) +3. Adapt shaders for WebGL 2.0 / GLSL ES 3.0 +4. Handle WebGL-specific limitations + +**Key Files:** +- [ ] `tests/gal-regression/wasm/webgl_gal.h` - Class declaration +- [ ] `tests/gal-regression/wasm/webgl_gal.cpp` - Main implementation +- [ ] `tests/gal-regression/wasm/webgl_shaders.cpp` - Shader sources +- [ ] Adapt vertex_manager, gpu_manager as needed + +**Verification:** Scenario 0 (basic-lines) renders, master script compares successfully. + +### Phase 3: Complete API Coverage +Implement all GAL methods to pass all 28 scenarios. + +**Method Groups (incremental):** +1. Basic drawing: DrawLine, DrawSegment, DrawCircle, DrawArc +2. Shapes: DrawRectangle, DrawPolygon, DrawPolyline +3. Advanced: DrawBezier, DrawBezierArc, DrawArcSegment, DrawSegmentChain +4. State: Colors, transforms, depth testing, render targets +5. Groups: BeginGroup, EndGroup, DrawGroup, ChangeGroupColor/Depth +6. Text: DrawGlyph, DrawGlyphs, BitmapText +7. Special: DrawGrid, DrawCursor, DrawBitmap + +**Verification:** Run master script after each group - all implemented scenarios match. + +### Phase 4: Integration with KiCad +Move WEBGL_GAL into KiCad source and enable for browser builds. + +**Deliverables:** +- [ ] Move `webgl_gal.*` to `kicad/common/gal/webgl/` +- [ ] CMake integration for Emscripten builds +- [ ] Runtime GAL selection based on platform +- [ ] KiCad WASM builds successfully with WEBGL_GAL + +**Verification:** KiCad loads in browser, opens PCB, renders correctly. + +## File Structure + +``` +tests/gal-regression/ +├── baseline/ # Committed reference (from native) +├── output/ +│ ├── native/ # Fresh native run +│ └── webgl/ # WebGL run via Playwright +├── native/ # Native C++ harness (existing) +│ ├── gal_native_test.cpp +│ └── ... +├── wasm/ # WASM harness (new) +│ ├── Makefile # Use Makefile, not CMake (avoids Python issues) +│ ├── gal_webgl_test.cpp +│ ├── gal_webgl_test.html +│ ├── webgl_gal.h +│ ├── webgl_gal.cpp +│ └── webgl_shaders.cpp +└── scenarios/ # Shared scenarios (existing) + +scripts/ +├── build-gal-native-test.sh # existing +├── build-gal-webgl-test.sh # new +└── test-gal-regression.sh # new - MASTER SCRIPT +``` + +## Critical Files to Modify/Create + +**New files:** +- `scripts/test-gal-regression.sh` - Master test script +- `scripts/build-gal-webgl-test.sh` - WASM build +- `tests/gal-regression/wasm/*` - All WASM harness files +- `tests/e2e/gal-webgl.spec.ts` - Playwright test + +**Reference files (copy from):** +- `kicad/common/gal/opengl/opengl_gal.cpp` (3098 lines) +- `kicad/common/gal/opengl/opengl_gal.h` (614 lines) +- `kicad/common/gal/opengl/shader.cpp` (298 lines) +- `kicad/common/gal/opengl/vertex_manager.cpp` (318 lines) +- `kicad/common/gal/opengl/gpu_manager.cpp` (340 lines) diff --git a/scripts/build-gal-webgl-test.sh b/scripts/build-gal-webgl-test.sh new file mode 100755 index 0000000..727ccc1 --- /dev/null +++ b/scripts/build-gal-webgl-test.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# +# Build script for the WebGL GAL test harness (WASM) +# +# This builds a WASM module that renders GAL test scenarios using WebGL, +# allowing comparison against native OpenGL rendering. +# +# Uses a Makefile with direct em++ calls (like build-wasm-test.sh) +# to avoid emcmake Python 3.10+ requirement. +# + +# Redirect all output to a log file (re-execs script with redirection) +source "$(dirname "$0")/common/logging.sh" + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common environment (sets up Python 3.10+ for Emscripten) +QUIET=1 source "$SCRIPT_DIR/common/env.sh" + +TEST_DIR="$PROJECT_ROOT/tests/gal-regression/wasm" +OUTPUT_DIR="$PROJECT_ROOT/tests/apps/gal-webgl" + +echo "Building GAL WebGL Test..." +echo " Test dir: $TEST_DIR" +echo " Output dir: $OUTPUT_DIR" +echo " EMSDK_PYTHON: ${EMSDK_PYTHON:-NOT SET}" +echo " clang: $(which clang)" + +# Verify em++ is available +if ! command -v em++ &> /dev/null; then + echo "ERROR: em++ not found. Please install via: brew install emscripten" + exit 1 +fi + +echo " Emscripten: $(em++ --version 2>&1 | head -1)" + +# Parse arguments +DEBUG_BUILD=0 +CLEAN_BUILD=0 + +for arg in "$@"; do + if [ "$arg" = "--debug" ]; then + DEBUG_BUILD=1 + elif [ "$arg" = "--clean" ]; then + CLEAN_BUILD=1 + fi +done + +# Build using Makefile +cd "$TEST_DIR" + +if [ "$CLEAN_BUILD" = "1" ]; then + echo "" + echo "Cleaning..." + make clean 2>/dev/null || true +fi + +echo "" +echo "Building..." +if [ "$DEBUG_BUILD" = "1" ]; then + make DEBUG=1 +else + make +fi + +echo "" +echo "Build successful!" +echo "" +echo "Files in $OUTPUT_DIR:" +ls -lh "$OUTPUT_DIR" +echo "" +echo "To test locally:" +echo " cd $PROJECT_ROOT/tests" +echo " npx serve apps" +echo " # Open http://localhost:3000/gal-webgl/gal_webgl_test.html" diff --git a/scripts/common/env.sh b/scripts/common/env.sh index 04aae92..268b94a 100755 --- a/scripts/common/env.sh +++ b/scripts/common/env.sh @@ -36,6 +36,49 @@ export WX_BUILD="$BUILD_ROOT/wxwidgets-universal" # Emscripten settings export EMSDK_QUIET=1 +# Homebrew Emscripten configuration +# The em++ script uses EMSDK_PYTHON (not PYTHON env var) for the Python interpreter +# and finds clang via PATH - Emscripten bundles its own LLVM with WebAssembly support +if [[ -d "/opt/homebrew/Cellar/emscripten" ]]; then + _EM_VERSION=$(ls /opt/homebrew/Cellar/emscripten/ | sort -V | tail -1) + _EM_LLVM_BIN="/opt/homebrew/Cellar/emscripten/$_EM_VERSION/libexec/llvm/bin" + if [[ -d "$_EM_LLVM_BIN" ]]; then + # Add bundled LLVM to PATH so Emscripten finds its clang (not /usr/bin/clang) + export PATH="$_EM_LLVM_BIN:$PATH" + fi +elif [[ -d "/usr/local/Cellar/emscripten" ]]; then + _EM_VERSION=$(ls /usr/local/Cellar/emscripten/ | sort -V | tail -1) + _EM_LLVM_BIN="/usr/local/Cellar/emscripten/$_EM_VERSION/libexec/llvm/bin" + if [[ -d "$_EM_LLVM_BIN" ]]; then + export PATH="$_EM_LLVM_BIN:$PATH" + fi +fi + +# Emscripten 4.0.22+ requires Python 3.10+ (uses match statement and type union syntax) +# The em++ shell script checks EMSDK_PYTHON first, then falls back to `which python3` +# Set EMSDK_PYTHON to Homebrew's Python to ensure correct version is used +if [[ -d "/opt/homebrew/opt/python@3.14/bin" ]]; then + export EMSDK_PYTHON="/opt/homebrew/opt/python@3.14/bin/python3.14" +elif [[ -d "/opt/homebrew/opt/python@3.13/bin" ]]; then + export EMSDK_PYTHON="/opt/homebrew/opt/python@3.13/bin/python3.13" +elif [[ -d "/opt/homebrew/opt/python@3.12/bin" ]]; then + export EMSDK_PYTHON="/opt/homebrew/opt/python@3.12/bin/python3.12" +elif [[ -d "/opt/homebrew/opt/python@3.11/bin" ]]; then + export EMSDK_PYTHON="/opt/homebrew/opt/python@3.11/bin/python3.11" +elif [[ -d "/opt/homebrew/opt/python@3.10/bin" ]]; then + export EMSDK_PYTHON="/opt/homebrew/opt/python@3.10/bin/python3.10" +elif [[ -d "/usr/local/opt/python@3.14/bin" ]]; then + export EMSDK_PYTHON="/usr/local/opt/python@3.14/bin/python3.14" +elif [[ -d "/usr/local/opt/python@3.13/bin" ]]; then + export EMSDK_PYTHON="/usr/local/opt/python@3.13/bin/python3.13" +elif [[ -d "/usr/local/opt/python@3.12/bin" ]]; then + export EMSDK_PYTHON="/usr/local/opt/python@3.12/bin/python3.12" +elif [[ -d "/usr/local/opt/python@3.11/bin" ]]; then + export EMSDK_PYTHON="/usr/local/opt/python@3.11/bin/python3.11" +elif [[ -d "/usr/local/opt/python@3.10/bin" ]]; then + export EMSDK_PYTHON="/usr/local/opt/python@3.10/bin/python3.10" +fi + # Common compiler flags export EMCC_CFLAGS="-fPIC -DEMSCRIPTEN" export EMCC_CXXFLAGS="-fPIC -DEMSCRIPTEN -std=c++17" diff --git a/scripts/test-gal-regression.sh b/scripts/test-gal-regression.sh new file mode 100755 index 0000000..fb4ea2a --- /dev/null +++ b/scripts/test-gal-regression.sh @@ -0,0 +1,367 @@ +#!/bin/bash + +# GAL Regression Test - Master Script +# ==================================== +# Single script to build, run, and compare native OpenGL and WebGL GAL implementations. +# +# Two-level comparison: +# 1. native vs baseline - Catches if native code regressed +# 2. webgl vs native - Verifies WebGL implementation matches native +# +# Usage: +# ./scripts/test-gal-regression.sh # Run all tests +# ./scripts/test-gal-regression.sh native # Run native only +# ./scripts/test-gal-regression.sh webgl # Run webgl only (requires native output) +# ./scripts/test-gal-regression.sh compare # Compare only (skip builds) +# ./scripts/test-gal-regression.sh -v # Verbose output + +# Redirect all output to a log file (re-execs script with redirection) +source "$(dirname "$0")/common/logging.sh" + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Directories +GAL_REGRESSION_DIR="$PROJECT_ROOT/tests/gal-regression" +BASELINE_DIR="$GAL_REGRESSION_DIR/baseline" +OUTPUT_DIR="$GAL_REGRESSION_DIR/output" +NATIVE_OUTPUT_DIR="$OUTPUT_DIR/native" +WEBGL_OUTPUT_DIR="$OUTPUT_DIR/webgl" +NATIVE_BUILD_DIR="$GAL_REGRESSION_DIR/native/build" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Parse arguments +VERBOSE="" +RUN_NATIVE=true +RUN_WEBGL=true +COMPARE_ONLY=false + +for arg in "$@"; do + case $arg in + native) + RUN_WEBGL=false + ;; + webgl) + RUN_NATIVE=false + ;; + compare) + COMPARE_ONLY=true + ;; + -v|--verbose) + VERBOSE="-v" + ;; + esac +done + +# ============================================================================ +# Helper Functions +# ============================================================================ + +log_header() { + echo "" + echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}" +} + +log_step() { + echo -e "${YELLOW}>>> $1${NC}" +} + +log_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +log_error() { + echo -e "${RED}✗ $1${NC}" +} + +# Compare two directories of screenshots +# Usage: compare_screenshots