diff --git a/frontend/a2wasm/.gitignore b/frontend/a2wasm/.gitignore new file mode 100644 index 00000000..d2dc3050 --- /dev/null +++ b/frontend/a2wasm/.gitignore @@ -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 diff --git a/frontend/a2wasm/README.md b/frontend/a2wasm/README.md index a7e56035..7a7b0a49 100644 --- a/frontend/a2wasm/README.md +++ b/frontend/a2wasm/README.md @@ -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; + loadDiskFromURL(drive: number, url: string): Promise; -| 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 diff --git a/frontend/a2wasm/build.sh b/frontend/a2wasm/build.sh deleted file mode 100755 index 3396c1f8..00000000 --- a/frontend/a2wasm/build.sh +++ /dev/null @@ -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 "" diff --git a/frontend/a2wasm/go/api.go b/frontend/a2wasm/go/api.go index a7978b75..d7eb56d0 100644 --- a/frontend/a2wasm/go/api.go +++ b/frontend/a2wasm/go/api.go @@ -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 }) diff --git a/frontend/a2wasm/go/disk_loader.go b/frontend/a2wasm/go/disk_loader.go deleted file mode 100644 index 6850a9b9..00000000 --- a/frontend/a2wasm/go/disk_loader.go +++ /dev/null @@ -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 diff --git a/frontend/a2wasm/go/main.go b/frontend/a2wasm/go/main.go index ccfd1954..f7172c91 100644 --- a/frontend/a2wasm/go/main.go +++ b/frontend/a2wasm/go/main.go @@ -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)) diff --git a/frontend/a2wasm/public/wasm/.gitkeep b/frontend/a2wasm/public/wasm/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/frontend/a2wasm/public/wasm/izapple2.wasm b/frontend/a2wasm/public/wasm/izapple2.wasm deleted file mode 100755 index 2591870a..00000000 Binary files a/frontend/a2wasm/public/wasm/izapple2.wasm and /dev/null differ diff --git a/frontend/a2wasm/public/wasm/izapple2.wasm.gz b/frontend/a2wasm/public/wasm/izapple2.wasm.gz deleted file mode 100755 index 6bebf007..00000000 Binary files a/frontend/a2wasm/public/wasm/izapple2.wasm.gz and /dev/null differ diff --git a/frontend/a2wasm/public/wasm/wasm_exec.js b/frontend/a2wasm/public/wasm/wasm_exec.js deleted file mode 100644 index bc6f2102..00000000 --- a/frontend/a2wasm/public/wasm/wasm_exec.js +++ /dev/null @@ -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; - }; - } - } -})(); diff --git a/frontend/a2wasm/serve.sh b/frontend/a2wasm/serve.sh deleted file mode 100755 index dc0a5649..00000000 --- a/frontend/a2wasm/serve.sh +++ /dev/null @@ -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 diff --git a/frontend/a2wasm/src/api/wasm-loader.ts b/frontend/a2wasm/src/api/wasm-loader.ts index 0c34d13a..1f41276a 100644 --- a/frontend/a2wasm/src/api/wasm-loader.ts +++ b/frontend/a2wasm/src/api/wasm-loader.ts @@ -20,24 +20,19 @@ async function initWASM( reject: (error: Error) => void ): Promise { 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'); diff --git a/frontend/a2wasm/src/components/EmulatorScreen.tsx b/frontend/a2wasm/src/components/EmulatorScreen.tsx index 16861ff4..2a6f2dcd 100644 --- a/frontend/a2wasm/src/components/EmulatorScreen.tsx +++ b/frontend/a2wasm/src/components/EmulatorScreen.tsx @@ -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); diff --git a/frontend/a2wasm/src/main.tsx b/frontend/a2wasm/src/main.tsx index 051846c7..1dc53868 100644 --- a/frontend/a2wasm/src/main.tsx +++ b/frontend/a2wasm/src/main.tsx @@ -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; diff --git a/frontend/a2wasm/web/index.html b/frontend/a2wasm/web/index.html deleted file mode 100644 index b0790158..00000000 --- a/frontend/a2wasm/web/index.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - izapple2 - Apple II Emulator - - - - -
-
-

izapple2

-

Loading Apple II Emulator...

-
-

Initializing WebAssembly...

-
-
- - - - - - diff --git a/frontend/a2wasm/web/izapple2.wasm b/frontend/a2wasm/web/izapple2.wasm deleted file mode 100755 index fd77b54b..00000000 Binary files a/frontend/a2wasm/web/izapple2.wasm and /dev/null differ diff --git a/frontend/a2wasm/web/izapple2.wasm.gz b/frontend/a2wasm/web/izapple2.wasm.gz deleted file mode 100755 index 6ed70999..00000000 Binary files a/frontend/a2wasm/web/izapple2.wasm.gz and /dev/null differ diff --git a/frontend/a2wasm/web/loader.js b/frontend/a2wasm/web/loader.js deleted file mode 100644 index f9a77892..00000000 --- a/frontend/a2wasm/web/loader.js +++ /dev/null @@ -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); diff --git a/frontend/a2wasm/web/styles.css b/frontend/a2wasm/web/styles.css deleted file mode 100644 index 5951284d..00000000 --- a/frontend/a2wasm/web/styles.css +++ /dev/null @@ -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); -} diff --git a/frontend/a2wasm/web/wasm_exec.js b/frontend/a2wasm/web/wasm_exec.js deleted file mode 100644 index bc6f2102..00000000 --- a/frontend/a2wasm/web/wasm_exec.js +++ /dev/null @@ -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; - }; - } - } -})(); diff --git a/keyboard.go b/keyboard.go index 49747130..b09528a9 100644 --- a/keyboard.go +++ b/keyboard.go @@ -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 }