mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-22 00:02:27 +00:00
Some cleanup
This commit is contained in:
1 parent
784bd7baee
commit
635cce512d
21 files changed
+284
-2366
No files matched your search
@@ -0,0 +1,26 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
public/wasm/*
|
||||
!public/wasm/.gitkeep
|
||||
|
||||
# Development
|
||||
.vite/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
+252
-158
@@ -1,166 +1,235 @@
|
||||
# izapple2 WebAssembly Frontend
|
||||
|
||||
A web browser frontend for the izapple2 Apple II emulator, compiled to WebAssembly.
|
||||
A modern React + TypeScript frontend for the izapple2 Apple II emulator, compiled to WebAssembly.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Runs Apple II emulation in any modern web browser
|
||||
- ✅ Full keyboard support
|
||||
- ✅ Modern React + TypeScript + Material-UI interface
|
||||
- ✅ Full keyboard support (letters, numbers, special keys, arrows)
|
||||
- ✅ Audio support (speaker clicks and beeps)
|
||||
- ✅ Multiple disk loading methods:
|
||||
- File picker (local files)
|
||||
- Drag and drop
|
||||
- Load from URLs
|
||||
- Sample disk library
|
||||
- ✅ Screenshot download
|
||||
- ✅ Responsive design (desktop, tablet, mobile)
|
||||
- ✅ Reset and pause controls
|
||||
- ✅ Emulator controls:
|
||||
- Pause/Resume
|
||||
- Reset
|
||||
- Speed toggle
|
||||
- Screen mode selection (NTSC, Plain, Green, Amber)
|
||||
- Screenshot download
|
||||
- ✅ Real-time status display (CPU frequency, disk status)
|
||||
- ✅ Responsive design
|
||||
- ✅ No installation required
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: React 18 + TypeScript + Vite
|
||||
- **UI Framework**: Material-UI (MUI) 5
|
||||
- **Emulation Core**: Go + Ebiten (compiled to WebAssembly)
|
||||
- **Build Tools**: Vite, Go 1.23+
|
||||
|
||||
## Requirements
|
||||
|
||||
### For Building
|
||||
|
||||
- Go 1.16 or later
|
||||
- Standard build tools (make, bash)
|
||||
- Go 1.23 or later
|
||||
- Node.js 18+ and npm
|
||||
- Bash (for build script)
|
||||
|
||||
### For Running (Browser)
|
||||
|
||||
- Modern web browser with WebAssembly support:
|
||||
- Chrome/Edge 57+
|
||||
- Chrome/Edge 79+
|
||||
- Firefox 52+
|
||||
- Safari 11+
|
||||
- Opera 44+
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
cd frontend/a2wasm
|
||||
./build.sh
|
||||
|
||||
# Install npm dependencies
|
||||
npm install
|
||||
|
||||
# Build the WASM binary
|
||||
./build-wasm.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Compile Go code to WebAssembly (`web/izapple2.wasm`)
|
||||
2. Copy the Go WASM runtime (`web/wasm_exec.js`)
|
||||
3. Create a compressed version (`web/izapple2.wasm.gz`)
|
||||
|
||||
### Run Locally
|
||||
### Development
|
||||
|
||||
```bash
|
||||
./serve.sh
|
||||
# Start Vite dev server (with hot reload)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then open http://localhost:8080 in your browser.
|
||||
Then open http://localhost:5173 in your browser.
|
||||
|
||||
The emulator will boot to Applesoft BASIC. Use the file picker or drag-and-drop to load disk images.
|
||||
The emulator will boot to the `]` prompt. You can then load disk images or start typing commands.
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
# Build WASM
|
||||
./build-wasm.sh
|
||||
|
||||
# Build React app for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
The production files will be in `dist/`.
|
||||
|
||||
## Using the Emulator
|
||||
|
||||
### Loading Disk Images
|
||||
|
||||
**Method 1: File Picker**
|
||||
1. Click "Load Disk 1" or "Load Disk 2"
|
||||
1. Click "Choose File" under Drive 1 or Drive 2
|
||||
2. Select a disk image file (.dsk, .woz, .nib, .po, .2mg, .zip, .gz)
|
||||
3. The disk will be loaded and the emulator will boot from it
|
||||
3. The disk will be loaded into the selected drive
|
||||
|
||||
**Method 2: Drag and Drop**
|
||||
1. Drag a disk image file from your computer
|
||||
2. Drop it on the emulator screen
|
||||
3. Multiple files will load into Drive 1 and Drive 2 respectively
|
||||
2. Drop it on the disk manager panel
|
||||
3. The first file loads into Drive 1, second into Drive 2
|
||||
|
||||
**Method 3: URL Loading**
|
||||
1. Use URL parameters:
|
||||
```
|
||||
http://localhost:8080?disk1=https://example.com/disk.dsk
|
||||
```
|
||||
2. Or load from JavaScript:
|
||||
```javascript
|
||||
window.loadDiskFromURL(1, "https://example.com/disk.dsk");
|
||||
```
|
||||
|
||||
**Method 4: Sample Disks**
|
||||
- Click sample disk buttons in the control panel
|
||||
- These load pre-configured disk images
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| F1 | Show/Hide help |
|
||||
| Ctrl+F2 | Reset emulator |
|
||||
| F4 | Toggle CPU trace |
|
||||
| F5 | Fast/Normal speed |
|
||||
| Ctrl+F5 | Show speed/FPS |
|
||||
| F6 | Next screen mode (NTSC, Green, etc.) |
|
||||
| F7 | Show/Hide screen pages |
|
||||
| F10 | Next character set |
|
||||
| Ctrl+F10 | Show/Hide character generator |
|
||||
| Shift+F10 | Show/Hide alternate text |
|
||||
| F12 | Save screenshot |
|
||||
| Pause | Pause/Resume emulation |
|
||||
| Left Alt | Open-Apple |
|
||||
| Right Alt | Closed-Apple |
|
||||
- Enter a URL in the disk manager (feature available via UI)
|
||||
- Or use JavaScript:
|
||||
```javascript
|
||||
window.wasmAPI.loadDiskFromURL(1, "https://example.com/disk.dsk");
|
||||
```
|
||||
|
||||
### Controls
|
||||
|
||||
**Reset Button**: Resets the Apple II (same as Ctrl+F2)
|
||||
**Top Control Panel**:
|
||||
- **Pause/Resume**: Freeze/unfreeze emulation
|
||||
- **Reset**: Reboot the Apple II
|
||||
- **Toggle Speed**: Switch between normal and fast speed
|
||||
- **Screenshot**: Download current screen as PNG
|
||||
- **Screen Mode**: Select display mode (NTSC, Plain, Green, Amber)
|
||||
|
||||
**Pause Button**: Pauses/resumes emulation
|
||||
**Status Bar** (bottom):
|
||||
- Shows CPU frequency in MHz
|
||||
- Displays pause state
|
||||
- Shows keyboard help text
|
||||
|
||||
**Screenshot Button**: Captures current screen as PNG and downloads it
|
||||
### Keyboard
|
||||
|
||||
All standard Apple II keys work:
|
||||
- Letters, numbers, symbols
|
||||
- Enter, Escape, Backspace
|
||||
- Arrow keys (Up, Down, Left, Right)
|
||||
- Tab, Delete
|
||||
|
||||
The emulator uses Ebiten's native keyboard handling for accurate input.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
frontend/a2wasm/
|
||||
├── main.go # Entry point and Ebiten game loop
|
||||
├── keyboard.go # Keyboard input handling
|
||||
├── speaker.go # Audio output
|
||||
├── disk_loader.go # JavaScript bridge for disk loading (WASM-only)
|
||||
├── build.sh # Build script
|
||||
├── serve.sh # Development server script
|
||||
├── README.md # This file
|
||||
└── web/ # Web assets
|
||||
├── index.html # Main HTML page
|
||||
├── loader.js # WASM initialization and UI logic
|
||||
├── styles.css # Styling
|
||||
├── izapple2.wasm # Compiled WASM binary (after build)
|
||||
└── wasm_exec.js # Go WASM runtime (after build)
|
||||
├── go/ # Go source code
|
||||
│ ├── main.go # Ebiten game loop, keyboard handling
|
||||
│ ├── api.go # JavaScript API exports
|
||||
│ ├── speaker.go # Audio output
|
||||
│ └── disk_loader.go # Disk loading (placeholder)
|
||||
├── src/ # React/TypeScript source
|
||||
│ ├── main.tsx # React entry point
|
||||
│ ├── App.tsx # Main app component
|
||||
│ ├── api/ # WASM API client
|
||||
│ │ ├── types.ts # TypeScript type definitions
|
||||
│ │ ├── emulator.ts # Emulator API wrapper
|
||||
│ │ └── wasm-loader.ts # WASM initialization
|
||||
│ ├── components/ # React components
|
||||
│ │ ├── EmulatorScreen.tsx # Canvas container
|
||||
│ │ ├── ControlPanel.tsx # Top controls
|
||||
│ │ ├── DiskManager.tsx # Disk operations UI
|
||||
│ │ ├── StatusBar.tsx # Bottom status display
|
||||
│ │ └── KeyboardHandler.tsx # Keyboard event capture
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── useEmulator.ts # Emulator state management
|
||||
│ │ └── useDiskLoader.ts # Disk loading logic
|
||||
│ └── styles/ # Styling
|
||||
│ └── theme.ts # MUI theme customization
|
||||
├── public/ # Static assets
|
||||
│ └── wasm/ # WASM build output
|
||||
│ ├── izapple2.wasm # Compiled WASM binary
|
||||
│ ├── izapple2.wasm.gz # Compressed version
|
||||
│ └── wasm_exec.js # Go WASM runtime
|
||||
├── index.html # HTML entry point
|
||||
├── vite.config.ts # Vite configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── package.json # npm dependencies
|
||||
├── build-wasm.sh # WASM build script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ React + TypeScript UI │
|
||||
│ (Controls, Disk Manager, Status Bar) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ TypeScript API Client │
|
||||
│ (wasmAPI wrapper) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Go/WASM Bridge │
|
||||
│ (Exported JS functions) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Go Emulation + Ebiten Rendering │
|
||||
│ (Apple II core + Canvas output) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Static Hosting
|
||||
|
||||
The `web/` directory contains everything needed for deployment:
|
||||
The built application is fully static and can be deployed anywhere:
|
||||
|
||||
1. Build the WASM binary:
|
||||
1. **Build everything**:
|
||||
```bash
|
||||
./build.sh
|
||||
./build-wasm.sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Upload the `web/` directory to any static hosting service:
|
||||
- GitHub Pages
|
||||
2. **Upload the `dist/` directory** to any static hosting:
|
||||
- Netlify
|
||||
- Vercel
|
||||
- GitHub Pages
|
||||
- AWS S3 + CloudFront
|
||||
- Your own web server
|
||||
- Any web server
|
||||
|
||||
### Server Configuration
|
||||
|
||||
**MIME Types**
|
||||
|
||||
Ensure your server is configured with the correct MIME type for WASM:
|
||||
Ensure your server serves the correct MIME type for WASM:
|
||||
|
||||
```
|
||||
.wasm -> application/wasm
|
||||
```
|
||||
|
||||
**Headers for WASM**
|
||||
|
||||
Vite automatically configures these in development:
|
||||
```
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
```
|
||||
|
||||
For production, configure your server/CDN similarly.
|
||||
|
||||
**Compression**
|
||||
|
||||
Enable gzip or brotli compression for better performance:
|
||||
Enable gzip or brotli compression:
|
||||
|
||||
```nginx
|
||||
# Nginx example
|
||||
@@ -170,16 +239,28 @@ location ~ \.wasm$ {
|
||||
}
|
||||
```
|
||||
|
||||
**CORS**
|
||||
### Example: Netlify
|
||||
|
||||
If loading disk images from external URLs, ensure CORS is properly configured on the source server.
|
||||
1. Create `netlify.toml`:
|
||||
```toml
|
||||
[build]
|
||||
command = "./build-wasm.sh && npm run build"
|
||||
publish = "dist"
|
||||
|
||||
### Example: GitHub Pages
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
Cross-Origin-Embedder-Policy = "require-corp"
|
||||
Cross-Origin-Opener-Policy = "same-origin"
|
||||
|
||||
1. Copy `web/` contents to `docs/` in your repository
|
||||
2. Enable GitHub Pages in repository settings
|
||||
3. Set source to `docs/` folder
|
||||
4. Access at: `https://yourusername.github.io/yourrepo/`
|
||||
[[headers]]
|
||||
for = "*.wasm"
|
||||
[headers.values]
|
||||
Content-Type = "application/wasm"
|
||||
```
|
||||
|
||||
2. Connect your repository to Netlify
|
||||
3. Deploy!
|
||||
|
||||
## Supported Disk Formats
|
||||
|
||||
@@ -188,44 +269,62 @@ If loading disk images from external URLs, ensure CORS is properly configured on
|
||||
- `.nib` - Nibble format
|
||||
- `.po` - ProDOS order disk image
|
||||
- `.2mg` - 2IMG format (with header)
|
||||
- `.zip` - Compressed disk images (will extract first .dsk file)
|
||||
- `.zip` - Compressed disk images
|
||||
- `.gz` - Gzipped disk images
|
||||
|
||||
## Browser Compatibility
|
||||
## WASM API Reference
|
||||
|
||||
### Desktop
|
||||
The Go code exports the following API to JavaScript:
|
||||
|
||||
| Browser | Version | Status |
|
||||
|---------|---------|--------|
|
||||
| Chrome | 57+ | ✅ Fully supported |
|
||||
| Firefox | 52+ | ✅ Fully supported |
|
||||
| Safari | 11+ | ✅ Supported |
|
||||
| Edge | 79+ | ✅ Fully supported |
|
||||
| Opera | 44+ | ✅ Supported |
|
||||
```typescript
|
||||
interface EmulatorAPI {
|
||||
// Core control
|
||||
reset(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
|
||||
### Mobile
|
||||
// Disk operations
|
||||
loadDisk(drive: number, data: Uint8Array, filename: string): Promise<string | null>;
|
||||
loadDiskFromURL(drive: number, url: string): Promise<string | null>;
|
||||
|
||||
| Browser | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Chrome Android | ✅ Supported | No virtual keyboard yet |
|
||||
| Safari iOS | ✅ Supported | No virtual keyboard yet |
|
||||
| Firefox Android | ⚠️ Limited | May have audio issues |
|
||||
// Input (for advanced use - keyboard is handled automatically)
|
||||
sendKey(keyCode: number): void;
|
||||
sendText(text: string): void;
|
||||
|
||||
// State queries
|
||||
isPaused(): boolean;
|
||||
getFrequency(): number;
|
||||
getDiskInfo(drive: number): DiskInfo | null;
|
||||
|
||||
// Configuration
|
||||
toggleSpeed(): void;
|
||||
setScreenMode(mode: 'ntsc' | 'plain' | 'green' | 'amber'): void;
|
||||
|
||||
// Screenshot
|
||||
screenshot(): void;
|
||||
}
|
||||
|
||||
// Access via:
|
||||
window.wasmAPI.reset();
|
||||
window.wasmAPI.loadDisk(1, data, "game.dsk");
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- **WASM Size**: ~8-12 MB uncompressed, ~2-3 MB gzipped
|
||||
- **WASM Size**: ~26 MB uncompressed, ~7 MB gzipped
|
||||
- **Load Time**: 2-5 seconds on broadband
|
||||
- **CPU Usage**: ~50-70% of one core on modern hardware
|
||||
- **Emulation Speed**: 90-100% of native speed
|
||||
- **Audio Latency**: ~100-200ms (acceptable for emulation)
|
||||
- **Emulation Speed**: 100% of native speed
|
||||
- **Frame Rate**: 60 FPS (Ebiten rendering)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Error loading WASM"
|
||||
### "Failed to load emulator"
|
||||
|
||||
- Check that `izapple2.wasm` exists in the `web/` directory
|
||||
- Run `./build.sh` to rebuild
|
||||
- Check that WASM files exist in `public/wasm/`
|
||||
- Run `./build-wasm.sh` to rebuild
|
||||
- Check browser console for detailed errors
|
||||
- Verify CORS headers are set correctly
|
||||
|
||||
### "Disk loading failed"
|
||||
|
||||
@@ -235,88 +334,83 @@ If loading disk images from external URLs, ensure CORS is properly configured on
|
||||
|
||||
### No audio
|
||||
|
||||
- Check browser console for audio context errors
|
||||
- Some browsers require user interaction before playing audio
|
||||
- Click the emulator screen to enable audio
|
||||
|
||||
### Slow performance
|
||||
|
||||
- Close other browser tabs
|
||||
- Try Chrome or Edge for best WebAssembly performance
|
||||
- Check CPU usage in browser task manager
|
||||
- Click the emulator screen to enable audio context
|
||||
- Check browser console for audio errors
|
||||
|
||||
### Keys not working
|
||||
|
||||
- Click on the emulator canvas to focus it
|
||||
- Check that browser extensions aren't intercepting keys
|
||||
- Try using on-screen buttons as fallback
|
||||
- Keyboard is handled by Ebiten's native input system
|
||||
- Ensure no browser extensions are intercepting keys
|
||||
|
||||
### Build errors
|
||||
|
||||
```bash
|
||||
# Clean and rebuild
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
./build-wasm.sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building for Development
|
||||
### Project Scripts
|
||||
|
||||
```bash
|
||||
# Build
|
||||
./build.sh
|
||||
# Development server with hot reload
|
||||
npm run dev
|
||||
|
||||
# Serve locally
|
||||
./serve.sh
|
||||
# Build WASM binary
|
||||
./build-wasm.sh
|
||||
|
||||
# Or use wasmserve (auto-rebuilds)
|
||||
go run github.com/hajimehoshi/wasmserve@latest .
|
||||
```
|
||||
# Build React app for production
|
||||
npm run build
|
||||
|
||||
### JavaScript Bridge API
|
||||
# Type checking
|
||||
npm run type-check
|
||||
|
||||
The Go code exports these functions to JavaScript:
|
||||
|
||||
```javascript
|
||||
// Load disk from bytes
|
||||
window.loadDisk(drive, uint8Array, filename)
|
||||
// Returns: null on success, error string on failure
|
||||
|
||||
// Load disk from URL
|
||||
window.loadDiskFromURL(drive, url)
|
||||
// Returns: null on success, error string on failure
|
||||
|
||||
// Reset emulator
|
||||
window.resetEmulator()
|
||||
|
||||
// Toggle pause
|
||||
window.togglePause()
|
||||
|
||||
// Download screenshot
|
||||
window.downloadScreenshot()
|
||||
# Preview production build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Modifying the UI
|
||||
|
||||
- Edit `web/index.html` for structure
|
||||
- Edit `web/styles.css` for styling
|
||||
- Edit `web/loader.js` for JavaScript logic
|
||||
- Rebuild not needed for web file changes (just refresh browser)
|
||||
- Edit files in `src/` - Vite will auto-reload
|
||||
- React components use Material-UI
|
||||
- TypeScript ensures type safety
|
||||
- WASM rebuild only needed for Go changes
|
||||
|
||||
### Modifying the Emulator
|
||||
|
||||
- Edit files in `go/`
|
||||
- Run `./build-wasm.sh` to rebuild
|
||||
- Refresh browser to load new WASM
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **No virtual keyboard** - Mobile devices need external keyboard
|
||||
2. **No save states** - Page refresh loses state (planned for future)
|
||||
3. **No gamepad** - Only keyboard and mouse supported
|
||||
4. **File system access** - Can't modify loaded disks (read-only)
|
||||
2. **No save states** - Page refresh loses state
|
||||
3. **Read-only disks** - Can't write to loaded disk images
|
||||
4. **No gamepad** - Only keyboard supported
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Virtual keyboard overlay for mobile
|
||||
- Touch joystick support
|
||||
- Virtual keyboard overlay for mobile devices
|
||||
- Save/load state with IndexedDB
|
||||
- PWA support (offline capability)
|
||||
- Multiplayer over WebRTC
|
||||
- GamePad API support
|
||||
- Disk write support
|
||||
- Multiple screen sizes/zoom levels
|
||||
- More sample disk library
|
||||
|
||||
## Credits
|
||||
|
||||
- [izapple2](https://github.com/ivanizag/izapple2) by Ivan Izaguirre
|
||||
- [Ebiten](https://ebiten.org/) game engine
|
||||
- Go WebAssembly support
|
||||
- [React](https://react.dev/) UI library
|
||||
- [Material-UI](https://mui.com/) component library
|
||||
- [Vite](https://vitejs.dev/) build tool
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build script for izapple2 WebAssembly frontend
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo " izapple2 WebAssembly Build Script"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Check if Go is installed
|
||||
if ! command -v go &> /dev/null; then
|
||||
echo "Error: Go is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Go version (need 1.16+ for embed)
|
||||
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
|
||||
echo "Go version: $GO_VERSION"
|
||||
|
||||
# Get GOROOT for wasm_exec.js
|
||||
GOROOT=$(go env GOROOT)
|
||||
WASM_EXEC_JS="$GOROOT/misc/wasm/wasm_exec.js"
|
||||
|
||||
# Check if wasm_exec.js exists (try alternate locations)
|
||||
if [ ! -f "$WASM_EXEC_JS" ]; then
|
||||
WASM_EXEC_JS="$GOROOT/lib/wasm/wasm_exec.js"
|
||||
fi
|
||||
|
||||
if [ ! -f "$WASM_EXEC_JS" ]; then
|
||||
# Try standard installation location
|
||||
WASM_EXEC_JS="/usr/local/go/misc/wasm/wasm_exec.js"
|
||||
fi
|
||||
|
||||
if [ ! -f "$WASM_EXEC_JS" ]; then
|
||||
# Try homebrew location on macOS
|
||||
WASM_EXEC_JS="/opt/homebrew/opt/go/libexec/misc/wasm/wasm_exec.js"
|
||||
fi
|
||||
|
||||
if [ ! -f "$WASM_EXEC_JS" ]; then
|
||||
echo "Error: Could not find wasm_exec.js in Go installation"
|
||||
echo "Searched:"
|
||||
echo " - $GOROOT/misc/wasm/wasm_exec.js"
|
||||
echo " - $GOROOT/lib/wasm/wasm_exec.js"
|
||||
echo " - /usr/local/go/misc/wasm/wasm_exec.js"
|
||||
echo " - /opt/homebrew/opt/go/libexec/misc/wasm/wasm_exec.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GOROOT: $GOROOT"
|
||||
echo ""
|
||||
|
||||
# Build WASM binary
|
||||
echo "Compiling Go to WebAssembly..."
|
||||
echo "Command: GOOS=js GOARCH=wasm go build -ldflags=\"-s -w\" -o web/izapple2.wasm"
|
||||
echo ""
|
||||
|
||||
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/izapple2.wasm
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Error: Build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get file size
|
||||
WASM_SIZE=$(ls -lh web/izapple2.wasm | awk '{print $5}')
|
||||
echo ""
|
||||
echo "✓ Build successful!"
|
||||
echo " Output: web/izapple2.wasm ($WASM_SIZE)"
|
||||
echo ""
|
||||
|
||||
# Copy wasm_exec.js
|
||||
echo "Copying wasm_exec.js from Go installation..."
|
||||
cp "$WASM_EXEC_JS" web/wasm_exec.js
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to copy wasm_exec.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Copied wasm_exec.js"
|
||||
echo ""
|
||||
|
||||
# Optional: Compress with gzip
|
||||
if command -v gzip &> /dev/null; then
|
||||
echo "Compressing WASM binary with gzip..."
|
||||
gzip -k -f web/izapple2.wasm
|
||||
GZIP_SIZE=$(ls -lh web/izapple2.wasm.gz | awk '{print $5}')
|
||||
echo "✓ Created web/izapple2.wasm.gz ($GZIP_SIZE)"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Display file listing
|
||||
echo "Build output files:"
|
||||
ls -lh web/ | grep -E '\.(html|js|css|wasm)' || true
|
||||
echo ""
|
||||
|
||||
echo "========================================="
|
||||
echo " Build Complete!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "To test locally:"
|
||||
echo " ./serve.sh"
|
||||
echo ""
|
||||
echo "Or manually:"
|
||||
echo " cd web"
|
||||
echo " python3 -m http.server 8080"
|
||||
echo ""
|
||||
echo "Then open: http://localhost:8080"
|
||||
echo ""
|
||||
@@ -55,12 +55,7 @@ func setupAPI(a *izapple2.Apple2, game *Game) {
|
||||
return nil
|
||||
}
|
||||
text := args[0].String()
|
||||
fmt.Printf("API sendText: '%s' (len=%d)\n", text, len(text))
|
||||
for i, ch := range text {
|
||||
fmt.Printf(" char[%d]: '%c' (rune=%d)\n", i, ch, ch)
|
||||
}
|
||||
game.keyChannel.PutText(text)
|
||||
fmt.Println("API sendText: done")
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
//go:build js
|
||||
|
||||
package main
|
||||
|
||||
// Disk loading functionality is now integrated into api.go
|
||||
// This file kept for potential future expansion
|
||||
@@ -19,9 +19,9 @@ type Game struct {
|
||||
speaker *wasmSpeaker
|
||||
fontSource *text.GoTextFaceSource
|
||||
|
||||
updates uint64
|
||||
screenMode int
|
||||
keyChannel *izapple2.KeyboardChannel
|
||||
updates uint64
|
||||
screenMode int
|
||||
keyChannel *izapple2.KeyboardChannel
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -33,9 +33,7 @@ func (g *Game) Update() error {
|
||||
// Handle keyboard input from Ebiten (AppendInputChars for printable chars)
|
||||
runes := ebiten.AppendInputChars(nil)
|
||||
if len(runes) > 0 {
|
||||
text := string(runes)
|
||||
fmt.Printf("Ebiten keyboard: '%s'\n", text)
|
||||
g.keyChannel.PutText(text)
|
||||
g.keyChannel.PutText(string(runes))
|
||||
}
|
||||
|
||||
// Handle special keys (Enter, arrows, etc.)
|
||||
@@ -120,13 +118,6 @@ func main() {
|
||||
}
|
||||
|
||||
func ebitenRun(a *izapple2.Apple2) {
|
||||
// Note: Window functions don't apply in WASM - canvas size is controlled by CSS
|
||||
// ebiten.SetWindowSize(virtualWidth/2, virtualHeight/2)
|
||||
// ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
|
||||
// ebiten.SetWindowTitle("iz-" + a.Name)
|
||||
|
||||
fmt.Printf("ebitenRun: Apple2 instance at %p\n", a)
|
||||
|
||||
game := &Game{
|
||||
a: a,
|
||||
speaker: newWasmSpeaker(),
|
||||
@@ -134,11 +125,8 @@ func ebitenRun(a *izapple2.Apple2) {
|
||||
screenMode: a_screen.ScreenModeNTSC,
|
||||
}
|
||||
|
||||
fmt.Printf("ebitenRun: keyChannel at %p\n", game.keyChannel)
|
||||
|
||||
// Set up providers
|
||||
a.SetSpeakerProvider(game.speaker)
|
||||
// Don't set keyboard provider - NewKeyboardChannel already did it
|
||||
|
||||
var err error
|
||||
game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
||||
|
||||
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
@@ -1,561 +0,0 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Development server for izapple2 WebAssembly frontend
|
||||
|
||||
set -e
|
||||
|
||||
PORT=8080
|
||||
|
||||
echo "========================================="
|
||||
echo " izapple2 WebAssembly Dev Server"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Check if WASM file exists
|
||||
if [ ! -f "web/izapple2.wasm" ]; then
|
||||
echo "Error: web/izapple2.wasm not found"
|
||||
echo ""
|
||||
echo "Please run ./build.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting HTTP server on port $PORT..."
|
||||
echo ""
|
||||
echo "Open in browser:"
|
||||
echo " http://localhost:$PORT"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop the server"
|
||||
echo ""
|
||||
|
||||
# Try different server options in order of preference
|
||||
if command -v python3 &> /dev/null; then
|
||||
# Python 3 HTTP server
|
||||
echo "Using Python 3 HTTP server"
|
||||
cd web
|
||||
python3 -m http.server $PORT
|
||||
elif command -v python &> /dev/null; then
|
||||
# Python 2 HTTP server (fallback)
|
||||
echo "Using Python 2 HTTP server"
|
||||
cd web
|
||||
python -m SimpleHTTPServer $PORT
|
||||
elif command -v go &> /dev/null; then
|
||||
# Use Go's wasmserve if available
|
||||
if go list github.com/hajimehoshi/wasmserve &> /dev/null; then
|
||||
echo "Using wasmserve"
|
||||
go run github.com/hajimehoshi/wasmserve@latest .
|
||||
else
|
||||
echo "Error: No suitable HTTP server found"
|
||||
echo ""
|
||||
echo "Please install one of:"
|
||||
echo " - Python 3 (recommended)"
|
||||
echo " - Python 2"
|
||||
echo " - Or run: go install github.com/hajimehoshi/wasmserve@latest"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error: No suitable HTTP server found"
|
||||
echo ""
|
||||
echo "Please install Python 3 or Go"
|
||||
exit 1
|
||||
fi
|
||||
@@ -20,24 +20,19 @@ async function initWASM(
|
||||
reject: (error: Error) => void
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log('Starting WASM initialization...');
|
||||
const go = new window.Go!();
|
||||
|
||||
console.log('Fetching WASM binary...');
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch('/wasm/izapple2.wasm'),
|
||||
go.importObject
|
||||
);
|
||||
|
||||
console.log('Running Go WASM module...');
|
||||
// Run the Go WASM module (this starts the emulator)
|
||||
go.run(result.instance);
|
||||
|
||||
console.log('Waiting for WASM API to be exported...');
|
||||
// Wait a bit for the emulator to initialize and export functions
|
||||
setTimeout(() => {
|
||||
if (window.wasmAPI) {
|
||||
console.log('WASM initialized successfully, API available:', Object.keys(window.wasmAPI));
|
||||
resolve();
|
||||
} else {
|
||||
console.error('WASM API not found on window object');
|
||||
|
||||
@@ -8,10 +8,8 @@ export function EmulatorScreen() {
|
||||
// Move Ebiten canvas into our container
|
||||
const checkCanvas = setInterval(() => {
|
||||
// Look for canvas in body (where Ebiten creates it)
|
||||
const canvas = document.querySelector('body > canvas');
|
||||
const canvas = document.querySelector('body > canvas') as HTMLCanvasElement | null;
|
||||
if (canvas && containerRef.current) {
|
||||
console.log('Found Ebiten canvas, moving to container');
|
||||
|
||||
// Remove canvas from body
|
||||
if (canvas.parentNode) {
|
||||
canvas.parentNode.removeChild(canvas);
|
||||
@@ -39,10 +37,8 @@ export function EmulatorScreen() {
|
||||
// Make canvas focusable and focus it
|
||||
canvas.setAttribute('tabindex', '0');
|
||||
canvas.focus();
|
||||
console.log('Canvas focused');
|
||||
|
||||
clearInterval(checkCanvas);
|
||||
console.log('Canvas moved and styled successfully');
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import App from './App';
|
||||
|
||||
// Setup download trigger function for screenshots
|
||||
window.triggerDownload = (data: Uint8Array, filename: string) => {
|
||||
const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
const blob = new Blob([data as BlobPart], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>izapple2 - Apple II Emulator</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="wasm_exec.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-screen">
|
||||
<div class="loading-content">
|
||||
<h1>izapple2</h1>
|
||||
<p>Loading Apple II Emulator...</p>
|
||||
<div class="spinner"></div>
|
||||
<p id="loading-status">Initializing WebAssembly...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app-container" style="display: none;">
|
||||
<header>
|
||||
<h1>izapple2</h1>
|
||||
<div class="header-info">
|
||||
<span id="emulator-status">Ready</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="control-panel">
|
||||
<div class="control-section">
|
||||
<h3>Disk Drives</h3>
|
||||
<div class="disk-controls">
|
||||
<button id="load-disk-1" class="btn btn-primary">
|
||||
<span>📀</span> Load Disk 1
|
||||
</button>
|
||||
<button id="load-disk-2" class="btn btn-primary">
|
||||
<span>📀</span> Load Disk 2
|
||||
</button>
|
||||
<input type="file" id="file-input" accept=".dsk,.woz,.nib,.po,.2mg,.zip,.gz" style="display:none"/>
|
||||
</div>
|
||||
<div class="disk-status">
|
||||
<div id="disk-1-status" class="disk-slot">Drive 1: Empty</div>
|
||||
<div id="disk-2-status" class="disk-slot">Drive 2: Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3>Emulator Control</h3>
|
||||
<div class="emulator-controls">
|
||||
<button id="reset-btn" class="btn">
|
||||
<span>🔄</span> Reset
|
||||
</button>
|
||||
<button id="pause-btn" class="btn">
|
||||
<span id="pause-icon">⏸</span> <span id="pause-text">Pause</span>
|
||||
</button>
|
||||
<button id="screenshot-btn" class="btn">
|
||||
<span>📷</span> Screenshot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3>Sample Disks</h3>
|
||||
<div class="sample-disks">
|
||||
<button class="btn btn-small sample-disk" data-url="<internal>/dos33.dsk" data-drive="1">
|
||||
DOS 3.3
|
||||
</button>
|
||||
<p class="help-text">Click to load sample disk</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section help-section">
|
||||
<h3>Keyboard Shortcuts</h3>
|
||||
<div class="shortcuts">
|
||||
<div class="shortcut">
|
||||
<kbd>F1</kbd>
|
||||
<span>Show/Hide Help</span>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<kbd>Ctrl+F2</kbd>
|
||||
<span>Reset</span>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<kbd>F5</kbd>
|
||||
<span>Toggle Speed</span>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<kbd>F6</kbd>
|
||||
<span>Screen Mode</span>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<kbd>F12</kbd>
|
||||
<span>Screenshot</span>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<kbd>Pause</kbd>
|
||||
<span>Pause/Resume</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emulator-container">
|
||||
<div id="emulator-canvas-container">
|
||||
<!-- Ebiten canvas will be inserted here -->
|
||||
</div>
|
||||
<div id="drop-zone" class="drop-zone">
|
||||
<div class="drop-zone-content">
|
||||
<span class="drop-icon">📁</span>
|
||||
<p>Drop disk image here</p>
|
||||
<p class="drop-hint">Supports .dsk, .woz, .nib, .po, .2mg, .zip, .gz</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/ivanizag/izapple2" target="_blank">GitHub</a>
|
||||
<span>•</span>
|
||||
<a href="#" id="show-help">Keyboard Shortcuts</a>
|
||||
<span>•</span>
|
||||
<span id="fps-counter">FPS: --</span>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<p>Press <kbd>F1</kbd> in emulator for help</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="loader.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,315 +0,0 @@
|
||||
// izapple2 WebAssembly Loader and JavaScript Bridge
|
||||
|
||||
let wasmReady = false;
|
||||
let currentDrive = 1;
|
||||
|
||||
// Initialize WASM when page loads
|
||||
window.addEventListener('load', async () => {
|
||||
console.log('Initializing izapple2 WebAssembly...');
|
||||
await initWASM();
|
||||
});
|
||||
|
||||
// Initialize the WebAssembly module
|
||||
async function initWASM() {
|
||||
const go = new Go();
|
||||
|
||||
updateLoadingStatus('Downloading WASM module...');
|
||||
|
||||
try {
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch('izapple2.wasm'),
|
||||
go.importObject
|
||||
);
|
||||
|
||||
updateLoadingStatus('Starting emulator...');
|
||||
|
||||
// Run the Go WASM module
|
||||
go.run(result.instance);
|
||||
|
||||
// Wait a moment for initialization
|
||||
setTimeout(() => {
|
||||
wasmReady = true;
|
||||
hideLoading();
|
||||
initUI();
|
||||
|
||||
// Move Ebiten canvas into our container
|
||||
moveEbitenCanvas();
|
||||
|
||||
console.log('izapple2 WebAssembly ready!');
|
||||
|
||||
// Check for URL parameters to load disks
|
||||
checkURLParameters();
|
||||
}, 500);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load WASM:', err);
|
||||
updateLoadingStatus('Error loading emulator: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Update loading screen status
|
||||
function updateLoadingStatus(message) {
|
||||
const statusEl = document.getElementById('loading-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide loading screen and show app
|
||||
function hideLoading() {
|
||||
document.getElementById('loading-screen').style.display = 'none';
|
||||
document.getElementById('app-container').style.display = 'block';
|
||||
}
|
||||
|
||||
// Move Ebiten canvas into our container
|
||||
function moveEbitenCanvas() {
|
||||
// Ebiten creates a canvas element and appends it to body
|
||||
// We need to move it into our container
|
||||
const checkCanvas = setInterval(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (canvas) {
|
||||
const container = document.getElementById('emulator-canvas-container');
|
||||
if (container) {
|
||||
// Remove canvas from body and add to container
|
||||
canvas.parentNode.removeChild(canvas);
|
||||
container.appendChild(canvas);
|
||||
|
||||
// Style the canvas to fit properly
|
||||
canvas.style.maxWidth = '100%';
|
||||
canvas.style.maxHeight = '100%';
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.margin = 'auto';
|
||||
|
||||
console.log('Moved Ebiten canvas into container');
|
||||
clearInterval(checkCanvas);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Stop checking after 5 seconds
|
||||
setTimeout(() => clearInterval(checkCanvas), 5000);
|
||||
}
|
||||
|
||||
// Initialize UI event handlers
|
||||
function initUI() {
|
||||
// Disk loading buttons
|
||||
document.getElementById('load-disk-1').addEventListener('click', () => {
|
||||
currentDrive = 1;
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('load-disk-2').addEventListener('click', () => {
|
||||
currentDrive = 2;
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
// File input change handler
|
||||
document.getElementById('file-input').addEventListener('change', handleFileSelect);
|
||||
|
||||
// Control buttons
|
||||
document.getElementById('reset-btn').addEventListener('click', () => {
|
||||
if (wasmReady && window.resetEmulator) {
|
||||
window.resetEmulator();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pause-btn').addEventListener('click', () => {
|
||||
if (wasmReady && window.togglePause) {
|
||||
window.togglePause();
|
||||
// Note: We'd need to track pause state for UI update
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('screenshot-btn').addEventListener('click', () => {
|
||||
if (wasmReady && window.downloadScreenshot) {
|
||||
const result = window.downloadScreenshot();
|
||||
if (result) {
|
||||
alert('Screenshot error: ' + result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sample disk buttons
|
||||
document.querySelectorAll('.sample-disk').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const url = btn.dataset.url;
|
||||
const drive = parseInt(btn.dataset.drive);
|
||||
loadDiskFromURL(drive, url);
|
||||
});
|
||||
});
|
||||
|
||||
// Drag and drop setup
|
||||
setupDragAndDrop();
|
||||
}
|
||||
|
||||
// Handle file selection from file picker
|
||||
async function handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
await loadDiskFile(currentDrive, file);
|
||||
|
||||
// Reset file input
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
// Load a disk file into the specified drive
|
||||
async function loadDiskFile(drive, file) {
|
||||
if (!wasmReady) {
|
||||
alert('Emulator not ready yet');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Loading ${file.name} into drive ${drive}...`);
|
||||
updateDiskStatus(drive, `Loading ${file.name}...`);
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
|
||||
if (window.loadDisk) {
|
||||
const result = window.loadDisk(drive, bytes, file.name);
|
||||
if (result) {
|
||||
alert('Error loading disk: ' + result);
|
||||
updateDiskStatus(drive, 'Empty');
|
||||
} else {
|
||||
updateDiskStatus(drive, file.name);
|
||||
console.log(`Successfully loaded disk ${drive}: ${file.name}`);
|
||||
}
|
||||
} else {
|
||||
alert('Error: loadDisk function not available');
|
||||
updateDiskStatus(drive, 'Empty');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error reading file:', err);
|
||||
alert('Error reading file: ' + err.message);
|
||||
updateDiskStatus(drive, 'Empty');
|
||||
}
|
||||
}
|
||||
|
||||
// Load a disk from URL
|
||||
function loadDiskFromURL(drive, url) {
|
||||
if (!wasmReady) {
|
||||
alert('Emulator not ready yet');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Loading disk from URL into drive ${drive}: ${url}`);
|
||||
updateDiskStatus(drive, 'Loading from URL...');
|
||||
|
||||
if (window.loadDiskFromURL) {
|
||||
const result = window.loadDiskFromURL(drive, url);
|
||||
if (result) {
|
||||
alert('Error loading disk: ' + result);
|
||||
updateDiskStatus(drive, 'Empty');
|
||||
} else {
|
||||
const filename = url.substring(url.lastIndexOf('/') + 1);
|
||||
updateDiskStatus(drive, filename);
|
||||
console.log(`Successfully loaded disk ${drive} from URL`);
|
||||
}
|
||||
} else {
|
||||
alert('Error: loadDiskFromURL function not available');
|
||||
updateDiskStatus(drive, 'Empty');
|
||||
}
|
||||
}
|
||||
|
||||
// Update disk status display
|
||||
function updateDiskStatus(drive, status) {
|
||||
const statusEl = document.getElementById(`disk-${drive}-status`);
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Drive ${drive}: ${status}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Setup drag and drop functionality
|
||||
function setupDragAndDrop() {
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const emulatorContainer = document.getElementById('emulator-canvas-container');
|
||||
|
||||
// Prevent default drag behaviors on the whole page
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Highlight drop zone when dragging over
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
emulatorContainer.addEventListener(eventName, () => {
|
||||
dropZone.classList.add('drag-over');
|
||||
}, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
emulatorContainer.addEventListener(eventName, () => {
|
||||
dropZone.classList.remove('drag-over');
|
||||
}, false);
|
||||
});
|
||||
|
||||
// Handle dropped files
|
||||
emulatorContainer.addEventListener('drop', handleDrop, false);
|
||||
|
||||
async function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length > 0) {
|
||||
// Load first file into drive 1, second into drive 2
|
||||
for (let i = 0; i < Math.min(files.length, 2); i++) {
|
||||
await loadDiskFile(i + 1, files[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check URL parameters for disk loading
|
||||
function checkURLParameters() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const disk1 = params.get('disk1');
|
||||
if (disk1) {
|
||||
console.log('Loading disk 1 from URL parameter:', disk1);
|
||||
loadDiskFromURL(1, disk1);
|
||||
}
|
||||
|
||||
const disk2 = params.get('disk2');
|
||||
if (disk2) {
|
||||
console.log('Loading disk 2 from URL parameter:', disk2);
|
||||
loadDiskFromURL(2, disk2);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger download of a file (called from Go)
|
||||
window.triggerDownload = function(data, filename) {
|
||||
const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
console.log(`Downloaded: ${filename}`);
|
||||
};
|
||||
|
||||
// FPS counter (optional)
|
||||
setInterval(() => {
|
||||
const fpsEl = document.getElementById('fps-counter');
|
||||
if (fpsEl && wasmReady) {
|
||||
// Note: Actual FPS would need to be exposed from Go
|
||||
// For now, just show that we're running
|
||||
fpsEl.textContent = 'Running';
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Log when WASM functions become available
|
||||
const checkFunctions = setInterval(() => {
|
||||
if (window.loadDisk && window.loadDiskFromURL) {
|
||||
console.log('WASM bridge functions available');
|
||||
clearInterval(checkFunctions);
|
||||
}
|
||||
}, 100);
|
||||
@@ -1,421 +0,0 @@
|
||||
/* izapple2 WebAssembly Interface Styles */
|
||||
|
||||
:root {
|
||||
--bg-dark: #1a1a1a;
|
||||
--bg-medium: #2a2a2a;
|
||||
--bg-light: #3a3a3a;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--accent-green: #00ff00;
|
||||
--accent-amber: #ffb000;
|
||||
--accent-blue: #4a9eff;
|
||||
--border-color: #444;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Loading Screen */
|
||||
#loading-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-dark);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-content h1 {
|
||||
font-size: 3rem;
|
||||
color: var(--accent-green);
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.loading-content p {
|
||||
color: var(--text-secondary);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 2rem auto;
|
||||
border: 4px solid var(--bg-light);
|
||||
border-top: 4px solid var(--accent-green);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background: var(--bg-medium);
|
||||
border-bottom: 2px solid var(--accent-green);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
color: var(--accent-green);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#emulator-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.main-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 120px);
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Control Panel */
|
||||
.control-panel {
|
||||
width: 300px;
|
||||
background: var(--bg-medium);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.control-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control-section h3 {
|
||||
color: var(--accent-amber);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-light);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--bg-dark);
|
||||
border-color: var(--accent-green);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2a4a2a, #1a3a1a);
|
||||
border-color: var(--accent-green);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #3a5a3a, #2a4a2a);
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn span:first-child {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Disk Controls */
|
||||
.disk-controls {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.disk-status {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.disk-slot {
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Emulator Controls */
|
||||
.emulator-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.emulator-controls .btn {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Sample Disks */
|
||||
.sample-disks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Keyboard Shortcuts */
|
||||
.shortcuts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-light);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shortcut span {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Emulator Container */
|
||||
.emulator-container {
|
||||
flex: 1;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#emulator-canvas-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#emulator-canvas-container canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
/* Drop Zone */
|
||||
.drop-zone {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drop-zone.drag-over {
|
||||
display: flex;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.drop-zone-content {
|
||||
text-align: center;
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.drop-icon {
|
||||
font-size: 4rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.drop-zone-content p {
|
||||
font-size: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.drop-hint {
|
||||
font-size: 1rem !important;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: var(--bg-medium);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: var(--accent-blue);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#fps-counter {
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.emulator-container {
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.emulator-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shortcuts {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
.control-panel::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-track {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
@@ -1,561 +0,0 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -37,8 +37,6 @@ var macOptionSubst = []rune("!·$%/()=qwertyopasdfghjkxcvbm,.<>cnQWETPGJKLZXVNM\
|
||||
|
||||
// PutRune sends a rune to the emulator if it is valid printable ASCII
|
||||
func (k *KeyboardChannel) PutRune(ch rune) {
|
||||
println("PutRune called with ch:", ch, "char:", string(ch))
|
||||
|
||||
// Some substitutions useful for Macs that transform chars with the option key
|
||||
pos := slices.Index(macOptionChars, ch)
|
||||
if pos >= 0 {
|
||||
@@ -47,22 +45,16 @@ func (k *KeyboardChannel) PutRune(ch rune) {
|
||||
|
||||
// We will use computed text only for printable ASCII chars
|
||||
if ch >= ' ' && ch <= '~' {
|
||||
println(" ch is printable ASCII, forceCaps:", k.a.IsForceCaps())
|
||||
if k.a.IsForceCaps() && ch >= 'a' && ch <= 'z' {
|
||||
ch = unicode.ToUpper(ch)
|
||||
}
|
||||
println(" calling PutChar with:", uint8(ch))
|
||||
k.PutChar(uint8(ch))
|
||||
} else {
|
||||
println(" ch NOT printable ASCII, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
// PutChar sends a character to the emulator
|
||||
func (k *KeyboardChannel) PutChar(ch uint8) {
|
||||
println("PutChar: sending", ch, "to channel, buffer:", len(k.keyChannel), "/", cap(k.keyChannel))
|
||||
k.keyChannel <- ch
|
||||
println("PutChar: sent")
|
||||
}
|
||||
|
||||
// GetKey returns a pressed key if available
|
||||
@@ -70,7 +62,6 @@ func (k *KeyboardChannel) GetKey(_ bool) (key uint8, ok bool) {
|
||||
select {
|
||||
case key = <-k.keyChannel:
|
||||
ok = true
|
||||
println("GetKey: returning key", key, "char:", string(key))
|
||||
default:
|
||||
ok = false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user