This guide explains how to build and deploy PHPBoy for the browser using PHP-WASM.
PHPBoy uses php-wasm to run the entire PHP-based emulator in the browser via WebAssembly. This eliminates the need for a backend server and allows the emulator to run entirely client-side.
The WebAssembly build consists of several components:
- PHP Source Code: The core emulator logic written in PHP 8.5
- WASM I/O Adapters: PHP classes that bridge between the emulator and JavaScript
- JavaScript Bridge: Manages the PHP-WASM runtime and handles browser interactions
- Web UI: HTML/CSS/JavaScript interface for loading ROMs and controlling the emulator
Three key interfaces are implemented for WASM compatibility:
- WasmFramebuffer: Buffers pixel data for Canvas rendering
- WasmAudioSink: Buffers audio samples for Web Audio API
- WasmInput: Receives keyboard/touch input from JavaScript
Before building for WASM, ensure you have:
- PHP 8.4+ with Composer (for development)
- Docker (recommended for consistent builds)
- Node.js and npm (for serving the build)
- Python 3 (alternative for serving)
First, install PHP dependencies via Composer:
make installBuild the WASM distribution:
make build-wasmThis command:
- Creates a
dist/directory - Copies all web files (HTML, CSS, JavaScript)
- Copies PHP source code to
dist/php/src/ - Copies Composer dependencies to
dist/php/vendor/
Serve the build locally for testing:
make serve-wasmThis starts an HTTP server on http://localhost:8080.
Alternatively, using npm:
npm install
npm run serveOr using Python directly:
cd dist
python3 -m http.server 8080- Open
http://localhost:8080in your browser - Click "Choose ROM File" and select a .gb or .gbc ROM
- The emulator should load and start running
dist/
├── index.html # Main HTML page
├── css/
│ └── styles.css # Styling
├── js/
│ └── phpboy.js # JavaScript bridge
├── phpboy-wasm.php # PHP entry point
└── php/
├── src/ # PHP source code
│ ├── Emulator.php
│ ├── Cpu/
│ ├── Ppu/
│ ├── Apu/
│ └── Frontend/
│ └── Wasm/ # WASM adapters
├── vendor/ # Composer dependencies
└── composer.json
PHPBoy uses the php-wasm library to run PHP in the browser:
import { PhpWeb } from 'php-wasm/PhpWeb.mjs';
const php = new PhpWeb();
await php.binary; // Wait for PHP runtime to loadROMs are loaded into PHP's virtual filesystem:
const romData = new Uint8Array(arrayBuffer);
await php.writeFile('/rom.gb', romData);JavaScript drives the emulation loop:
// Execute one frame
const result = await php.run(`<?php
$emulator->step();
$pixels = $framebuffer->getPixelsRGBA();
$audio = $audioSink->getSamplesFlat();
echo json_encode(['pixels' => $pixels, 'audio' => $audio]);
`);
// Render to canvas
const data = JSON.parse(result.body);
renderFrame(data.pixels);
queueAudio(data.audio);Keyboard events are passed to PHP:
document.addEventListener('keydown', async (e) => {
await php.run(`<?php
$input->setButtonState(${buttonCode}, true);
`);
});- Target: 60 FPS (59.7 Hz for Game Boy accuracy)
- Actual performance depends on:
- Browser (Chrome/Firefox/Safari)
- Device CPU speed
- PHP-WASM overhead
- Use
requestAnimationFramefor smooth rendering - Buffer audio samples to prevent underruns
- Minimize PHP-JS bridge calls by batching operations
- Use typed arrays for pixel/audio data transfer
The dist/ directory is fully static and can be deployed to:
- GitHub Pages
- Netlify
- Vercel
- AWS S3 + CloudFront
- Any static file host
PHP-WASM loads WebAssembly files that require proper CORS headers:
Access-Control-Allow-Origin: *
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Most static hosts handle this automatically, but verify if you encounter loading issues.
Problem: "Failed to fetch" error when loading PHP runtime
Solution: Ensure you're serving from an HTTP server, not file://. Use make serve-wasm or similar.
Problem: "ROM file not found" error
Solution: Ensure the ROM is being written to /rom.gb in the virtual filesystem:
await php.writeFile('/rom.gb', romData);Problem: Emulator runs slowly, below 60 FPS
Solutions:
- Test in different browsers (Chrome typically fastest)
- Reduce emulation speed multiplier
- Disable audio temporarily
- Check browser console for errors
Problem: No audio or crackling/stuttering
Note: Audio implementation is basic and may require additional buffering. Full Web Audio API integration is complex and beyond initial implementation.
Tested browsers:
- ✅ Chrome 90+ (best performance)
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Edge 90+
WebAssembly and ES Modules are required.
Current WASM implementation has these limitations:
- Audio: Basic implementation, may have quality issues
- Save Files: Not persisted between sessions (TODO: localStorage)
- Performance: Slower than native PHP CLI
- File I/O: No direct filesystem access (uses virtual FS)
Potential improvements:
- Implement persistent save files with localStorage
- Add save state functionality
- Improve audio buffering with AudioWorklet
- Add mobile touch controls
- Optimize PHP-JS bridge for better performance
- Add WebGL rendering for better scaling
- Implement multiplayer via WebRTC