555 lines
15 KiB
Markdown
555 lines
15 KiB
Markdown
# WASM Web App Enhancements Plan
|
|
|
|
## Executive Summary
|
|
|
|
This document outlines the comprehensive plan to make yaze's WASM web build fully featured, robust, and user-friendly. The goal is to achieve feature parity with the native desktop application where technically feasible, while leveraging browser-specific capabilities where appropriate.
|
|
|
|
## Current Status
|
|
|
|
### Completed
|
|
- [x] Basic WASM build configuration
|
|
- [x] pthread support for Emscripten
|
|
- [x] Network abstraction layer (IHttpClient, IWebSocket)
|
|
- [x] Emscripten HTTP client using `emscripten_fetch()`
|
|
- [x] Emscripten WebSocket using browser API
|
|
- [x] **Phase 1**: File System Layer (WasmStorage, WasmFileDialog)
|
|
- [x] **Phase 2**: Error Handling Infrastructure (WasmErrorHandler)
|
|
- [x] **Phase 3**: Progressive Loading UI (WasmLoadingManager)
|
|
- [x] **Phase 4**: Offline Support (Service Workers, PWA manifest)
|
|
- [x] **Phase 5**: AI Service Integration (BrowserAIService, WasmSecureStorage)
|
|
- [x] **Phase 6**: Local Storage Persistence (WasmSettings, AutoSaveManager)
|
|
|
|
- [x] **Phase 7**: Web Workers for heavy processing (WasmWorkerPool)
|
|
- [x] **Phase 8**: Emulator Audio (WebAudio, WasmAudioBackend)
|
|
|
|
### In Progress
|
|
- [ ] WASM CI build verification
|
|
- [ ] Integration of loading manager with gfx::Arena
|
|
- [ ] Integration testing across all phases
|
|
|
|
---
|
|
|
|
## Phase 1: File System Layer
|
|
|
|
### Overview
|
|
WASM builds cannot access the local filesystem directly. We need a virtualized file system layer that uses browser storage APIs.
|
|
|
|
### Implementation
|
|
|
|
#### 1.1 IndexedDB Storage Backend
|
|
```cpp
|
|
// src/app/platform/wasm/wasm_storage.h
|
|
class WasmStorage {
|
|
public:
|
|
// Store ROM data
|
|
static absl::Status SaveRom(const std::string& name, const std::vector<uint8_t>& data);
|
|
static absl::StatusOr<std::vector<uint8_t>> LoadRom(const std::string& name);
|
|
static absl::Status DeleteRom(const std::string& name);
|
|
static std::vector<std::string> ListRoms();
|
|
|
|
// Store project files (JSON, palettes, patches)
|
|
static absl::Status SaveProject(const std::string& name, const std::string& json);
|
|
static absl::StatusOr<std::string> LoadProject(const std::string& name);
|
|
|
|
// Store user preferences
|
|
static absl::Status SavePreferences(const nlohmann::json& prefs);
|
|
static absl::StatusOr<nlohmann::json> LoadPreferences();
|
|
};
|
|
```
|
|
|
|
#### 1.2 File Upload Handler
|
|
```cpp
|
|
// JavaScript interop for file input
|
|
EM_JS(void, openFileDialog, (const char* accept, int callback_id), {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = UTF8ToString(accept);
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const data = new Uint8Array(reader.result);
|
|
Module._handleFileLoaded(callback_id, data);
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
};
|
|
input.click();
|
|
});
|
|
```
|
|
|
|
#### 1.3 File Download Handler
|
|
```cpp
|
|
// Download ROM or project file
|
|
EM_JS(void, downloadFile, (const char* filename, const uint8_t* data, size_t size), {
|
|
const blob = new Blob([HEAPU8.subarray(data, data + size)], {type: 'application/octet-stream'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = UTF8ToString(filename);
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/app/platform/wasm/wasm_storage.h`
|
|
- `src/app/platform/wasm/wasm_storage.cc`
|
|
- `src/app/platform/wasm/wasm_file_dialog.h`
|
|
- `src/app/platform/wasm/wasm_file_dialog.cc`
|
|
|
|
---
|
|
|
|
## Phase 2: Error Handling Infrastructure
|
|
|
|
### Overview
|
|
Browser-based applications need specialized error handling that integrates with the web UI and provides user-friendly feedback.
|
|
|
|
### Implementation
|
|
|
|
#### 2.1 Browser Error Handler
|
|
```cpp
|
|
// src/app/platform/wasm/wasm_error_handler.h
|
|
class WasmErrorHandler {
|
|
public:
|
|
// Display error in browser UI
|
|
static void ShowError(const std::string& title, const std::string& message);
|
|
static void ShowWarning(const std::string& title, const std::string& message);
|
|
static void ShowInfo(const std::string& title, const std::string& message);
|
|
|
|
// Toast notifications (non-blocking)
|
|
static void Toast(const std::string& message, ToastType type, int duration_ms = 3000);
|
|
|
|
// Progress indicators
|
|
static void ShowProgress(const std::string& task, float progress);
|
|
static void HideProgress();
|
|
|
|
// Confirmation dialogs
|
|
static void Confirm(const std::string& message, std::function<void(bool)> callback);
|
|
};
|
|
```
|
|
|
|
#### 2.2 JavaScript Integration
|
|
```javascript
|
|
// src/web/error_handler.js
|
|
window.showYazeError = function(title, message) {
|
|
// Create styled error modal
|
|
const modal = document.createElement('div');
|
|
modal.className = 'yaze-error-modal';
|
|
modal.innerHTML = `
|
|
<div class="yaze-error-content">
|
|
<h2>${escapeHtml(title)}</h2>
|
|
<p>${escapeHtml(message)}</p>
|
|
<button onclick="this.parentElement.parentElement.remove()">OK</button>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
};
|
|
|
|
window.showYazeToast = function(message, type, duration) {
|
|
const toast = document.createElement('div');
|
|
toast.className = `yaze-toast yaze-toast-${type}`;
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => toast.remove(), duration);
|
|
};
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/app/platform/wasm/wasm_error_handler.h`
|
|
- `src/app/platform/wasm/wasm_error_handler.cc`
|
|
- `src/web/error_handler.js`
|
|
- `src/web/error_handler.css`
|
|
|
|
---
|
|
|
|
## Phase 3: Progressive Loading UI
|
|
|
|
### Overview
|
|
ROM loading and graphics processing can take significant time. Users need visual feedback and the ability to cancel long operations.
|
|
|
|
### Implementation
|
|
|
|
#### 3.1 Loading Manager
|
|
```cpp
|
|
// src/app/platform/wasm/wasm_loading_manager.h
|
|
class WasmLoadingManager {
|
|
public:
|
|
// Start a loading operation with progress tracking
|
|
static LoadingHandle BeginLoading(const std::string& task_name);
|
|
|
|
// Update progress (0.0 to 1.0)
|
|
static void UpdateProgress(LoadingHandle handle, float progress);
|
|
static void UpdateMessage(LoadingHandle handle, const std::string& message);
|
|
|
|
// Check if user requested cancel
|
|
static bool IsCancelled(LoadingHandle handle);
|
|
|
|
// Complete the loading operation
|
|
static void EndLoading(LoadingHandle handle);
|
|
};
|
|
```
|
|
|
|
#### 3.2 Integration with gfx::Arena
|
|
```cpp
|
|
// Modified graphics loading to report progress
|
|
void Arena::LoadGraphicsWithProgress(Rom* rom) {
|
|
auto handle = WasmLoadingManager::BeginLoading("Loading Graphics");
|
|
|
|
for (int i = 0; i < kNumGraphicsSheets; i++) {
|
|
if (WasmLoadingManager::IsCancelled(handle)) {
|
|
WasmLoadingManager::EndLoading(handle);
|
|
return; // User cancelled
|
|
}
|
|
|
|
LoadGraphicsSheet(rom, i);
|
|
WasmLoadingManager::UpdateProgress(handle, static_cast<float>(i) / kNumGraphicsSheets);
|
|
WasmLoadingManager::UpdateMessage(handle, absl::StrFormat("Sheet %d/%d", i, kNumGraphicsSheets));
|
|
|
|
// Yield to browser event loop periodically
|
|
emscripten_sleep(0);
|
|
}
|
|
|
|
WasmLoadingManager::EndLoading(handle);
|
|
}
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/app/platform/wasm/wasm_loading_manager.h`
|
|
- `src/app/platform/wasm/wasm_loading_manager.cc`
|
|
- `src/web/loading_indicator.js`
|
|
- `src/web/loading_indicator.css`
|
|
|
|
---
|
|
|
|
## Phase 4: Offline Support (Service Workers)
|
|
|
|
### Overview
|
|
Cache the WASM binary and assets for offline use, enabling users to work without an internet connection.
|
|
|
|
### Implementation
|
|
|
|
#### 4.1 Service Worker
|
|
```javascript
|
|
// src/web/service-worker.js
|
|
const CACHE_NAME = 'yaze-cache-v1';
|
|
const ASSETS = [
|
|
'/yaze.wasm',
|
|
'/yaze.js',
|
|
'/yaze.data',
|
|
'/index.html',
|
|
'/style.css',
|
|
'/fonts/',
|
|
];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
event.respondWith(
|
|
caches.match(event.request).then((response) => {
|
|
return response || fetch(event.request);
|
|
})
|
|
);
|
|
});
|
|
```
|
|
|
|
#### 4.2 Registration
|
|
```javascript
|
|
// src/web/index.html
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/service-worker.js')
|
|
.then((registration) => {
|
|
console.log('Service Worker registered:', registration.scope);
|
|
});
|
|
}
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/web/service-worker.js`
|
|
- `src/web/manifest.json` (PWA manifest)
|
|
|
|
---
|
|
|
|
## Phase 5: AI Service Integration
|
|
|
|
### Overview
|
|
Integrate the network abstraction layer with AI services (Gemini, Ollama) for browser-based AI assistance.
|
|
|
|
### Implementation
|
|
|
|
#### 5.1 Browser AI Client
|
|
```cpp
|
|
// src/cli/service/ai/browser_ai_service.h
|
|
#ifdef __EMSCRIPTEN__
|
|
class BrowserAIService : public IAIService {
|
|
public:
|
|
explicit BrowserAIService(const AIConfig& config);
|
|
|
|
absl::StatusOr<std::string> GenerateResponse(const std::string& prompt) override;
|
|
absl::StatusOr<std::string> AnalyzeImage(const gfx::Bitmap& image,
|
|
const std::string& prompt) override;
|
|
|
|
private:
|
|
std::unique_ptr<IHttpClient> http_client_;
|
|
std::string api_key_; // User-provided, never stored in binary
|
|
std::string model_;
|
|
};
|
|
#endif
|
|
```
|
|
|
|
#### 5.2 API Key Management
|
|
```cpp
|
|
// Secure API key storage in browser
|
|
EM_JS(void, storeApiKey, (const char* service, const char* key), {
|
|
// Use sessionStorage for temporary storage (cleared on tab close)
|
|
// or encrypted localStorage for persistent storage
|
|
sessionStorage.setItem('yaze_' + UTF8ToString(service) + '_key', UTF8ToString(key));
|
|
});
|
|
|
|
EM_JS(char*, retrieveApiKey, (const char* service), {
|
|
const key = sessionStorage.getItem('yaze_' + UTF8ToString(service) + '_key');
|
|
if (!key) return null;
|
|
const len = lengthBytesUTF8(key) + 1;
|
|
const ptr = _malloc(len);
|
|
stringToUTF8(key, ptr, len);
|
|
return ptr;
|
|
});
|
|
```
|
|
|
|
### CORS Considerations
|
|
- Gemini API: Should work with browser fetch (Google APIs support CORS)
|
|
- Ollama: Requires `--cors` flag or proxy server for local instances
|
|
|
|
### Files to Create
|
|
- `src/cli/service/ai/browser_ai_service.h`
|
|
- `src/cli/service/ai/browser_ai_service.cc`
|
|
- `src/app/platform/wasm/wasm_browser_storage.h`
|
|
- `src/app/platform/wasm/wasm_browser_storage.cc`
|
|
|
|
---
|
|
|
|
## Phase 6: Local Storage Persistence
|
|
|
|
### Overview
|
|
Persist user settings, recent files, undo history, and workspace layouts in browser storage.
|
|
|
|
### Implementation
|
|
|
|
#### 6.1 Settings Persistence
|
|
```cpp
|
|
// src/app/platform/wasm/wasm_settings.h
|
|
class WasmSettings {
|
|
public:
|
|
// User preferences
|
|
static void SaveTheme(const std::string& theme);
|
|
static std::string LoadTheme();
|
|
|
|
// Recent files (stored as IndexedDB references)
|
|
static void AddRecentFile(const std::string& name);
|
|
static std::vector<std::string> GetRecentFiles();
|
|
|
|
// Workspace layouts
|
|
static void SaveWorkspace(const std::string& name, const std::string& layout_json);
|
|
static std::string LoadWorkspace(const std::string& name);
|
|
|
|
// Undo history (for crash recovery)
|
|
static void SaveUndoHistory(const std::string& editor, const std::vector<uint8_t>& history);
|
|
static std::vector<uint8_t> LoadUndoHistory(const std::string& editor);
|
|
};
|
|
```
|
|
|
|
#### 6.2 Auto-Save & Recovery
|
|
```cpp
|
|
// Periodic auto-save
|
|
class AutoSaveManager {
|
|
public:
|
|
void Start(int interval_seconds = 60);
|
|
void Stop();
|
|
|
|
// Called on page unload
|
|
void EmergencySave();
|
|
|
|
// Called on startup
|
|
bool HasRecoveryData();
|
|
void RecoverLastSession();
|
|
void ClearRecoveryData();
|
|
};
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/app/platform/wasm/wasm_settings.h`
|
|
- `src/app/platform/wasm/wasm_settings.cc`
|
|
- `src/app/platform/wasm/wasm_autosave.h`
|
|
- `src/app/platform/wasm/wasm_autosave.cc`
|
|
|
|
---
|
|
|
|
## Phase 7: Web Workers for Heavy Processing
|
|
|
|
### Overview
|
|
Offload CPU-intensive operations to Web Workers to prevent UI freezing.
|
|
|
|
### Implementation
|
|
|
|
#### 7.1 Background Processing Worker
|
|
```cpp
|
|
// Operations to run in Web Worker:
|
|
// - ROM decompression (LC-LZ2)
|
|
// - Graphics sheet decoding
|
|
// - Palette calculations
|
|
// - Asar assembly compilation
|
|
|
|
class WasmWorkerPool {
|
|
public:
|
|
using TaskCallback = std::function<void(const std::vector<uint8_t>&)>;
|
|
|
|
// Submit work to background thread
|
|
void SubmitTask(const std::string& type,
|
|
const std::vector<uint8_t>& input,
|
|
TaskCallback callback);
|
|
|
|
// Wait for all tasks
|
|
void WaitAll();
|
|
};
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/app/platform/wasm/wasm_worker_pool.h`
|
|
- `src/app/platform/wasm/wasm_worker_pool.cc`
|
|
- `src/web/worker.js`
|
|
|
|
---
|
|
|
|
## Phase 8: Emulator Integration
|
|
|
|
### Overview
|
|
The SNES emulator can run in WASM with WebAudio for sound output.
|
|
|
|
### Implementation
|
|
|
|
#### 8.1 WebAudio Backend
|
|
```cpp
|
|
// src/app/emu/platform/wasm/wasm_audio.h
|
|
class WasmAudioBackend : public IAudioBackend {
|
|
public:
|
|
void Initialize(int sample_rate, int buffer_size) override;
|
|
void QueueSamples(const int16_t* samples, size_t count) override;
|
|
void Shutdown() override;
|
|
};
|
|
```
|
|
|
|
#### 8.2 Canvas Rendering
|
|
```cpp
|
|
// Use EM_ASM for direct canvas manipulation if needed
|
|
// Or use existing ImGui/SDL2 rendering (already WASM compatible)
|
|
```
|
|
|
|
### Files to Create
|
|
- `src/app/emu/platform/wasm/wasm_audio.h`
|
|
- `src/app/emu/platform/wasm/wasm_audio.cc`
|
|
|
|
---
|
|
|
|
## Feature Availability Matrix
|
|
|
|
| Feature | Native | WASM | Notes |
|
|
|---------|--------|------|-------|
|
|
| ROM Loading | File dialog | File input + IndexedDB | Full support |
|
|
| ROM Saving | Direct write | Blob download | Full support |
|
|
| Overworld Editor | Full | Full | No limitations |
|
|
| Dungeon Editor | Full | Full | No limitations |
|
|
| Graphics Editor | Full | Full | No limitations |
|
|
| Palette Editor | Full | Full | No limitations |
|
|
| Emulator | Full | Full | WebAudio for sound |
|
|
| AI (Gemini) | HTTP | Browser Fetch | Requires API key |
|
|
| AI (Ollama) | HTTP | Requires proxy | CORS limitation |
|
|
| Asar Assembly | Full | In-memory | No file I/O |
|
|
| Collaboration | gRPC | WebSocket | Different protocol |
|
|
| Crash Recovery | File-based | IndexedDB | Equivalent |
|
|
| Offline Mode | N/A | Service Worker | WASM-only feature |
|
|
|
|
---
|
|
|
|
## CMake Configuration
|
|
|
|
### Recommended Preset Updates
|
|
```json
|
|
{
|
|
"name": "wasm-release",
|
|
"cacheVariables": {
|
|
"YAZE_BUILD_WASM_PLATFORM": "ON",
|
|
"YAZE_WASM_ENABLE_WORKERS": "ON",
|
|
"YAZE_WASM_ENABLE_OFFLINE": "ON",
|
|
"YAZE_WASM_ENABLE_AI": "ON"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Source Organization
|
|
```
|
|
src/app/platform/
|
|
wasm/
|
|
wasm_storage.h/.cc
|
|
wasm_file_dialog.h/.cc
|
|
wasm_error_handler.h/.cc
|
|
wasm_loading_manager.h/.cc
|
|
wasm_settings.h/.cc
|
|
wasm_autosave.h/.cc
|
|
wasm_worker_pool.h/.cc
|
|
wasm_browser_storage.h/.cc
|
|
src/web/
|
|
index.html
|
|
shell.html
|
|
style.css
|
|
error_handler.js
|
|
loading_indicator.js
|
|
service-worker.js
|
|
worker.js
|
|
manifest.json
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Priority
|
|
|
|
### High Priority (Required for MVP)
|
|
1. File System Layer (Phase 1)
|
|
2. Error Handling (Phase 2)
|
|
3. Progressive Loading (Phase 3)
|
|
|
|
### Medium Priority (Enhanced Experience)
|
|
4. Local Storage Persistence (Phase 6)
|
|
5. Offline Support (Phase 4)
|
|
|
|
### Lower Priority (Advanced Features)
|
|
6. AI Integration (Phase 5)
|
|
7. Web Workers (Phase 7)
|
|
8. Emulator Audio (Phase 8)
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
- [ ] User can load ROM from local file system
|
|
- [ ] User can save modified ROM to downloads
|
|
- [ ] Loading progress is visible with cancel option
|
|
- [ ] Errors display user-friendly messages
|
|
- [ ] Settings persist across sessions
|
|
- [ ] App works offline after first load
|
|
- [ ] AI assistance available via Gemini API
|
|
- [ ] No UI freezes during heavy operations
|
|
- [ ] Emulator runs with sound
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [Emscripten Documentation](https://emscripten.org/docs/)
|
|
- [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
|
- [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
|
|
- [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
|
- [WebAudio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
|