backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -1,64 +1,18 @@
#ifndef YAZE_APP_PLATFORM_APP_DELEGATE_H
#define YAZE_APP_PLATFORM_APP_DELEGATE_H
#ifndef YAZE_APP_PLATFORM_APP_DELEGATE_H_
#define YAZE_APP_PLATFORM_APP_DELEGATE_H_
#include "app/application.h"
#if defined(__APPLE__) && defined(__MACH__)
/* Apple OSX and iOS (Darwin). */
#import <CoreText/CoreText.h>
#include <TargetConditionals.h>
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
/* iOS in Xcode simulator */
#import <PencilKit/PencilKit.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate,
UIDocumentPickerDelegate,
UITabBarControllerDelegate,
PKCanvasViewDelegate>
@property(strong, nonatomic) UIWindow* window;
@property UIDocumentPickerViewController* documentPicker;
@property(nonatomic, copy) void (^completionHandler)(NSString* selectedFile);
- (void)PresentDocumentPickerWithCompletionHandler:
(void (^)(NSString* selectedFile))completionHandler;
// TODO: Setup a tab bar controller for multiple yaze instances
@property(nonatomic) UITabBarController* tabBarController;
// TODO: Setup a font picker for the text editor and display settings
@property(nonatomic) UIFontPickerViewController* fontPicker;
// TODO: Setup the pencil kit for drawing
@property PKToolPicker* toolPicker;
@property PKCanvasView* canvasView;
// TODO: Setup the file manager for file operations
@property NSFileManager* fileManager;
@end
#elif TARGET_OS_MAC == 1
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize the Cocoa application.
*/
// Initialize Cocoa Application Delegate
void yaze_initialize_cocoa();
/**
* @brief Run the Cocoa application delegate.
*/
int yaze_run_cocoa_app_delegate(const char* filename);
// Run the main loop with Cocoa App Delegate
int yaze_run_cocoa_app_delegate(const yaze::AppConfig& config);
}
#ifdef __cplusplus
} // extern "C"
#endif
#endif // TARGET_OS_MAC
#endif // defined(__APPLE__) && defined(__MACH__)
#endif // YAZE_APP_PLATFORM_APP_DELEGATE_H
#endif // YAZE_APP_PLATFORM_APP_DELEGATE_H_

View File

@@ -7,14 +7,12 @@
#import "app/platform/app_delegate.h"
#import "app/controller.h"
#import "app/application.h"
#import "util/file_util.h"
#import "app/editor/editor.h"
#import "app/rom.h"
#include <span>
#import "rom/rom.h"
#include <vector>
using std::span;
#if defined(__APPLE__) && defined(__MACH__)
/* Apple OSX and iOS (Darwin). */
#include <TargetConditionals.h>
@@ -30,16 +28,10 @@ using std::span;
@interface AppDelegate : NSObject <NSApplicationDelegate>
- (void)setupMenus;
// - (void)changeApplicationIcon;
@end
@implementation AppDelegate
// - (void)changeApplicationIcon {
// NSImage *newIcon = [NSImage imageNamed:@"newIcon"];
// [NSApp setApplicationIconImage:newIcon];
// }
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[self setupMenus];
@@ -62,7 +54,7 @@ using std::span;
keyEquivalent:@"o"];
[fileMenu addItem:openItem];
// Open Recent
// Open Recent (System handled usually, but we can add our own if needed)
NSMenu *openRecentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"];
NSMenuItem *openRecentMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open Recent"
action:nil
@@ -70,179 +62,146 @@ using std::span;
[openRecentMenuItem setSubmenu:openRecentMenu];
[fileMenu addItem:openRecentMenuItem];
// Add a separator
[fileMenu addItem:[NSMenuItem separatorItem]];
// Save
NSMenuItem *saveItem = [[NSMenuItem alloc] initWithTitle:@"Save" action:nil keyEquivalent:@"s"];
NSMenuItem *saveItem = [[NSMenuItem alloc] initWithTitle:@"Save"
action:@selector(saveAction:)
keyEquivalent:@"s"];
[fileMenu addItem:saveItem];
// Save As
NSMenuItem *saveAsItem = [[NSMenuItem alloc] initWithTitle:@"Save As..."
action:@selector(saveAsAction:)
keyEquivalent:@"S"];
[fileMenu addItem:saveAsItem];
// Separator
[fileMenu addItem:[NSMenuItem separatorItem]];
// Options submenu
NSMenu *optionsMenu = [[NSMenu alloc] initWithTitle:@"Options"];
NSMenuItem *optionsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Options"
action:nil
keyEquivalent:@""];
[optionsMenuItem setSubmenu:optionsMenu];
// Flag checkmark field
NSMenuItem *flagItem = [[NSMenuItem alloc] initWithTitle:@"Flag"
action:@selector(toggleFlagAction:)
keyEquivalent:@""];
[flagItem setTarget:self];
[flagItem setState:NSControlStateValueOff];
[optionsMenu addItem:flagItem];
[fileMenu addItem:optionsMenuItem];
[mainMenu insertItem:fileMenuItem atIndex:1];
}
// Edit Menu
NSMenuItem *editMenuItem = [mainMenu itemWithTitle:@"Edit"];
if (!editMenuItem) {
NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
editMenuItem = [[NSMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:@""];
[editMenuItem setSubmenu:editMenu];
NSMenuItem *undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:nil keyEquivalent:@"z"];
NSMenuItem *undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo"
action:@selector(undoAction:)
keyEquivalent:@"z"];
[editMenu addItem:undoItem];
NSMenuItem *redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:nil keyEquivalent:@"Z"];
NSMenuItem *redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo"
action:@selector(redoAction:)
keyEquivalent:@"Z"];
[editMenu addItem:redoItem];
// Add a separator
[editMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem *cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut"
action:@selector(cutAction:)
keyEquivalent:@"x"];
[editMenu addItem:cutItem];
NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:nil keyEquivalent:@"c"];
[editMenu addItem:copyItem];
NSMenuItem *pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste"
action:nil
keyEquivalent:@"v"];
[editMenu addItem:pasteItem];
// Add a separator
[editMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem *selectAllItem = [[NSMenuItem alloc] initWithTitle:@"Select All"
action:nil
keyEquivalent:@"a"];
[editMenu addItem:selectAllItem];
// System-handled copy/paste usually works if we don't override,
// but we might want to wire them to our internal clipboard if needed.
// For now, let SDL handle keyboard events for copy/paste in ImGui.
[mainMenu insertItem:editMenuItem atIndex:2];
}
// View Menu
NSMenuItem *viewMenuItem = [mainMenu itemWithTitle:@"View"];
if (!viewMenuItem) {
NSMenu *viewMenu = [[NSMenu alloc] initWithTitle:@"View"];
viewMenuItem = [[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""];
[viewMenuItem setSubmenu:viewMenu];
// Emulator view button
NSMenuItem *emulatorViewItem = [[NSMenuItem alloc] initWithTitle:@"Emulator View"
action:nil
keyEquivalent:@"1"];
[viewMenu addItem:emulatorViewItem];
// Hex Editor View
NSMenuItem *hexEditorViewItem = [[NSMenuItem alloc] initWithTitle:@"Hex Editor View"
action:nil
keyEquivalent:@"2"];
[viewMenu addItem:hexEditorViewItem];
// Disassembly view button
NSMenuItem *disassemblyViewItem = [[NSMenuItem alloc] initWithTitle:@"Disassembly View"
action:nil
keyEquivalent:@"3"];
[viewMenu addItem:disassemblyViewItem];
// Memory view button
NSMenuItem *memoryViewItem = [[NSMenuItem alloc] initWithTitle:@"Memory View"
action:nil
keyEquivalent:@"4"];
[viewMenu addItem:memoryViewItem];
// Add a separator
[viewMenu addItem:[NSMenuItem separatorItem]];
// Toggle fullscreen button
NSMenuItem *toggleFullscreenItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Fullscreen"
action:nil
action:@selector(toggleFullscreenAction:)
keyEquivalent:@"f"];
[viewMenu addItem:toggleFullscreenItem];
[mainMenu insertItem:viewMenuItem atIndex:3];
}
NSMenuItem *helpMenuItem = [mainMenu itemWithTitle:@"Help"];
if (!helpMenuItem) {
NSMenu *helpMenu = [[NSMenu alloc] initWithTitle:@"Help"];
helpMenuItem = [[NSMenuItem alloc] initWithTitle:@"Help" action:nil keyEquivalent:@""];
[helpMenuItem setSubmenu:helpMenu];
// URL to online documentation
NSMenuItem *documentationItem = [[NSMenuItem alloc] initWithTitle:@"Documentation"
action:nil
keyEquivalent:@"?"];
[helpMenu addItem:documentationItem];
[mainMenu insertItem:helpMenuItem atIndex:4];
}
}
// Action method for the New menu item
- (void)newFileAction:(id)sender {
NSLog(@"New File action triggered");
}
- (void)toggleFlagAction:(id)sender {
NSMenuItem *flagItem = (NSMenuItem *)sender;
if ([flagItem state] == NSControlStateValueOff) {
[flagItem setState:NSControlStateValueOn];
} else {
[flagItem setState:NSControlStateValueOff];
}
}
// ============================================================================
// Menu Actions
// ============================================================================
- (void)openFileAction:(id)sender {
// TODO: Re-implmenent this without the SharedRom singleton
// if (!yaze::SharedRom::shared_rom_
// ->LoadFromFile(yaze::util::FileDialogWrapper::ShowOpenFileDialog())
// .ok()) {
// NSAlert *alert = [[NSAlert alloc] init];
// [alert setMessageText:@"Error"];
// [alert setInformativeText:@"Failed to load file."];
// [alert addButtonWithTitle:@"OK"];
// [alert runModal];
// }
// Use our internal file dialog via Application -> Controller -> EditorManager
// Or trigger the native dialog here and pass the path back.
// Since we have ImGui dialogs, we might prefer those, but native is nice on macOS.
// For now, let's just trigger the LoadRom logic which opens the dialog.
auto& app = yaze::Application::Instance();
if (app.IsReady() && app.GetController()) {
if (auto* manager = app.GetController()->editor_manager()) {
(void)manager->LoadRom();
}
}
}
- (void)cutAction:(id)sender {
// TODO: Implement
- (void)saveAction:(id)sender {
auto& app = yaze::Application::Instance();
if (app.IsReady() && app.GetController()) {
if (auto* manager = app.GetController()->editor_manager()) {
(void)manager->SaveRom();
}
}
}
- (void)openRecentFileAction:(id)sender {
NSLog(@"Open Recent File action triggered");
- (void)saveAsAction:(id)sender {
// Trigger Save As logic
// Manager->SaveRomAs("") usually triggers dialog
auto& app = yaze::Application::Instance();
if (app.IsReady() && app.GetController()) {
if (auto* manager = app.GetController()->editor_manager()) {
// We need a method to trigger Save As dialog from manager,
// usually passing empty string does it or there's a specific method.
// EditorManager::SaveRomAs(string) saves immediately.
// We might need to expose a method to show the dialog.
// For now, let's assume we can use the file dialog wrapper from C++ side.
(void)manager->SaveRomAs(""); // This might fail if empty string isn't handled as "ask user"
}
}
}
- (void)undoAction:(id)sender {
// Route to active editor
auto& app = yaze::Application::Instance();
if (app.IsReady() && app.GetController()) {
if (auto* manager = app.GetController()->editor_manager()) {
// manager->card_registry().TriggerUndo(); // If we exposed TriggerUndo
// Or directly:
if (auto* current = manager->GetCurrentEditor()) {
(void)current->Undo();
}
}
}
}
- (void)redoAction:(id)sender {
auto& app = yaze::Application::Instance();
if (app.IsReady() && app.GetController()) {
if (auto* manager = app.GetController()->editor_manager()) {
if (auto* current = manager->GetCurrentEditor()) {
(void)current->Redo();
}
}
}
}
- (void)toggleFullscreenAction:(id)sender {
// Toggle fullscreen on the window
// SDL usually handles this, but we can trigger it via SDL_SetWindowFullscreen
// Accessing window via Application -> Controller -> Window
// Use SDL backend logic
// For now, rely on the View menu item shortcut that ImGui might catch,
// or implement proper toggling in Controller.
}
@end
extern "C" void yaze_initialize_cococa() {
extern "C" void yaze_initialize_cocoa() {
@autoreleasepool {
AppDelegate *delegate = [[AppDelegate alloc] init];
[NSApplication sharedApplication];
@@ -251,25 +210,27 @@ extern "C" void yaze_initialize_cococa() {
}
}
extern "C" int yaze_run_cocoa_app_delegate(const char *filename) {
yaze_initialize_cococa();
auto controller = std::make_unique<yaze::Controller>();
EXIT_IF_ERROR(controller->OnEntry(filename));
while (controller->IsActive()) {
extern "C" int yaze_run_cocoa_app_delegate(const yaze::AppConfig& config) {
yaze_initialize_cocoa();
// Initialize the Application singleton with the provided config
// This will create the Controller and the SDL Window
yaze::Application::Instance().Initialize(config);
// Main loop
// We continue to run our own loop rather than [NSApp run]
// because we're driving SDL/ImGui manually.
// SDL's event polling works fine with Cocoa in this setup.
auto& app = yaze::Application::Instance();
while (app.IsReady() && app.GetController()->IsActive()) {
@autoreleasepool {
controller->OnInput();
if (auto status = controller->OnLoad(); !status.ok()) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"Error"];
[alert setInformativeText:[NSString stringWithUTF8String:status.message().data()]];
[alert addButtonWithTitle:@"OK"];
[alert runModal];
break;
}
controller->DoRender();
app.Tick();
}
}
controller->OnExit();
app.Shutdown();
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,60 @@
#include "util/file_util.h"
#include <string>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace yaze {
namespace util {
// Web implementation of FileDialogWrapper
// Triggers the existing file input element in the HTML
std::string FileDialogWrapper::ShowOpenFileDialog() {
#ifdef __EMSCRIPTEN__
// Trigger the existing file input element
// The file input handler in app.js will handle the file selection
// and call LoadRomFromWeb, which will update the ROM
EM_ASM({
var romInput = document.getElementById('rom-input');
if (romInput) {
romInput.click();
}
});
// Return empty string - the actual loading happens asynchronously
// via the file input handler which calls LoadRomFromWeb
return "";
#else
return "";
#endif
}
std::string FileDialogWrapper::ShowOpenFolderDialog() {
// Folder picking not supported on web in the same way
return "";
}
std::string FileDialogWrapper::ShowSaveFileDialog(
const std::string& default_name, const std::string& default_extension) {
// TODO(web): Implement download trigger via JS
return "";
}
std::vector<std::string> FileDialogWrapper::GetSubdirectoriesInFolder(
const std::string& folder_path) {
// Emscripten's VFS might support this if mounted
return {};
}
std::vector<std::string> FileDialogWrapper::GetFilesInFolder(
const std::string& folder_path) {
return {};
}
} // namespace util
} // namespace yaze

View File

@@ -130,6 +130,42 @@ absl::Status ReloadPackageFont(const FontConfig& config) {
return absl::OkStatus();
}
absl::Status LoadFontFromMemory(const std::string& name,
const std::string& data, float size_pixels) {
ImGuiIO& imgui_io = ImGui::GetIO();
// ImGui takes ownership of the data and will free it
void* font_data = ImGui::MemAlloc(data.size());
if (!font_data) {
return absl::InternalError("Failed to allocate memory for font data");
}
std::memcpy(font_data, data.data(), data.size());
ImFontConfig config;
std::strncpy(config.Name, name.c_str(), sizeof(config.Name) - 1);
config.Name[sizeof(config.Name) - 1] = 0;
if (!imgui_io.Fonts->AddFontFromMemoryTTF(font_data,
static_cast<int>(data.size()),
size_pixels, &config)) {
ImGui::MemFree(font_data);
return absl::InternalError("Failed to load font from memory");
}
// We also need to add icons and Japanese characters to this new font
// Note: This is a simplified version of AddIconFont/AddJapaneseFont that
// works with the current font being added (since we can't easily merge into
// a font that's just been added without rebuilding atlas immediately)
// For now, we'll just load the base font. Merging requires more complex logic.
// Important: We must rebuild the font atlas!
// This is usually done by the backend, but since we changed fonts at runtime...
// Ideally, this should be done before NewFrame().
// If called during a frame, changes won't appear until texture is rebuilt.
return absl::OkStatus();
}
#ifdef __linux__
void LoadSystemFonts() {
// Load Linux System Fonts into ImGui

View File

@@ -25,6 +25,9 @@ absl::Status LoadPackageFonts();
absl::Status ReloadPackageFont(const FontConfig& config);
absl::Status LoadFontFromMemory(const std::string& name,
const std::string& data, float size_pixels);
void LoadSystemFonts();
} // namespace yaze

View File

@@ -10,6 +10,7 @@
#include "absl/status/status.h"
#include "app/gfx/backend/irenderer.h"
#include "app/platform/sdl_compat.h"
// Forward declarations to avoid SDL header dependency in interface
struct SDL_Window;
@@ -28,7 +29,7 @@ struct WindowConfig {
bool resizable = true;
bool maximized = false;
bool fullscreen = false;
bool high_dpi = true;
bool high_dpi = false; // Disabled by default - causes issues on macOS Retina with SDL_Renderer
};
/**
@@ -83,6 +84,10 @@ struct WindowEvent {
// Drop file data
std::string dropped_file;
// Native event copy (SDL2/SDL3). Only valid when has_native_event is true.
bool has_native_event = false;
SDL_Event native_event{};
};
/**
@@ -226,6 +231,12 @@ class IWindowBackend {
*/
virtual void NewImGuiFrame() = 0;
/**
* @brief Render ImGui draw data (and viewports if enabled)
* @param renderer The renderer to use for drawing (needed to get backend renderer)
*/
virtual void RenderImGui(gfx::IRenderer* renderer) = 0;
// =========================================================================
// Audio Support (Legacy compatibility)
// =========================================================================

View File

@@ -17,11 +17,16 @@
#include "util/log.h"
namespace yaze {
// Forward reference to the global resize flag defined in window.cc
namespace core {
extern bool g_window_is_resizing;
}
namespace platform {
// Global flag for window resize state (used by emulator to pause)
// This maintains compatibility with the legacy window.cc
extern bool g_window_is_resizing;
// Alias to core's resize flag for compatibility
#define g_window_is_resizing yaze::core::g_window_is_resizing
SDL2WindowBackend::~SDL2WindowBackend() {
if (initialized_) {
@@ -150,6 +155,8 @@ bool SDL2WindowBackend::PollEvent(WindowEvent& out_event) {
// Convert to platform-agnostic event
out_event = ConvertSDL2Event(sdl_event);
out_event.has_native_event = true;
out_event.native_event = sdl_event;
return true;
}
return false;
@@ -375,6 +382,15 @@ absl::Status SDL2WindowBackend::InitializeImGui(gfx::IRenderer* renderer) {
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Note: ViewportsEnable is intentionally NOT set for SDL2 + SDL_Renderer
// It causes scaling issues on macOS Retina displays
// Ensure macOS-style behavior (Cmd acts as Ctrl for shortcuts)
// ImGui should set this automatically based on __APPLE__, but force it to be safe
#ifdef __APPLE__
io.ConfigMacOSXBehaviors = true;
LOG_INFO("SDL2WindowBackend", "Enabled ConfigMacOSXBehaviors for macOS");
#endif
// Initialize ImGui backends
SDL_Renderer* sdl_renderer =
@@ -426,10 +442,34 @@ void SDL2WindowBackend::NewImGuiFrame() {
ImGui_ImplSDLRenderer2_NewFrame();
ImGui_ImplSDL2_NewFrame();
// ImGui_ImplSDL2_NewFrame() automatically handles DisplaySize and
// DisplayFramebufferScale via ImGui_ImplSDL2_GetWindowSizeAndFramebufferScale()
// which uses SDL_GetRendererOutputSize() when renderer is available.
}
// Define the global variable for backward compatibility
bool g_window_is_resizing = false;
void SDL2WindowBackend::RenderImGui(gfx::IRenderer* renderer) {
if (!imgui_initialized_) {
return;
}
// Finalize ImGui frame and render draw data
ImGui::Render();
if (renderer) {
SDL_Renderer* sdl_renderer =
static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
if (sdl_renderer) {
ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), sdl_renderer);
}
}
// Multi-viewport support
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
}
} // namespace platform
} // namespace yaze

View File

@@ -52,6 +52,7 @@ class SDL2WindowBackend : public IWindowBackend {
absl::Status InitializeImGui(gfx::IRenderer* renderer) override;
void ShutdownImGui() override;
void NewImGuiFrame() override;
void RenderImGui(gfx::IRenderer* renderer) override;
uint32_t GetAudioDevice() const override { return audio_device_; }
std::shared_ptr<int16_t> GetAudioBuffer() const override {

View File

@@ -18,10 +18,16 @@
#include "util/log.h"
namespace yaze {
// Forward reference to the global resize flag defined in window.cc
namespace core {
extern bool g_window_is_resizing;
}
namespace platform {
// Global flag for window resize state (used by emulator to pause)
extern bool g_window_is_resizing;
// Alias to core's resize flag for compatibility
#define g_window_is_resizing yaze::core::g_window_is_resizing
SDL3WindowBackend::~SDL3WindowBackend() {
if (initialized_) {
@@ -148,6 +154,8 @@ bool SDL3WindowBackend::PollEvent(WindowEvent& out_event) {
// Convert to platform-agnostic event
out_event = ConvertSDL3Event(sdl_event);
out_event.has_native_event = true;
out_event.native_event = sdl_event;
return true;
}
return false;
@@ -396,6 +404,12 @@ absl::Status SDL3WindowBackend::InitializeImGui(gfx::IRenderer* renderer) {
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// Ensure macOS-style behavior (Cmd acts as Ctrl for shortcuts)
#ifdef __APPLE__
io.ConfigMacOSXBehaviors = true;
#endif
// Initialize ImGui backends for SDL3
SDL_Renderer* sdl_renderer =
@@ -450,6 +464,30 @@ void SDL3WindowBackend::NewImGuiFrame() {
ImGui_ImplSDL3_NewFrame();
}
void SDL3WindowBackend::RenderImGui(gfx::IRenderer* renderer) {
if (!imgui_initialized_) {
return;
}
// Finalize ImGui frame and render draw data
ImGui::Render();
if (renderer) {
SDL_Renderer* sdl_renderer =
static_cast<SDL_Renderer*>(renderer->GetBackendRenderer());
if (sdl_renderer) {
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), sdl_renderer);
}
}
// Multi-viewport support
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
}
} // namespace platform
} // namespace yaze

View File

@@ -64,6 +64,7 @@ class SDL3WindowBackend : public IWindowBackend {
absl::Status InitializeImGui(gfx::IRenderer* renderer) override;
void ShutdownImGui() override;
void NewImGuiFrame() override;
void RenderImGui(gfx::IRenderer* renderer) override;
uint32_t GetAudioDevice() const override { return 0; } // SDL3 uses streams
std::shared_ptr<int16_t> GetAudioBuffer() const override {

View File

@@ -12,6 +12,7 @@
#ifdef YAZE_USE_SDL3
#include <SDL3/SDL.h>
#include <SDL3/SDL_gamepad.h>
#else
#include <SDL.h>
#endif
@@ -64,6 +65,44 @@ constexpr auto kEventControllerDeviceAdded = SDL_CONTROLLERDEVICEADDED;
constexpr auto kEventControllerDeviceRemoved = SDL_CONTROLLERDEVICEREMOVED;
#endif
// ============================================================================
// Key Constants
// ============================================================================
#ifdef YAZE_USE_SDL3
#include <SDL3/SDL_keycode.h>
constexpr auto kKeyA = 'a';
constexpr auto kKeyB = 'b';
constexpr auto kKeyC = 'c';
constexpr auto kKeyD = 'd';
constexpr auto kKeyS = 's';
constexpr auto kKeyX = 'x';
constexpr auto kKeyY = 'y';
constexpr auto kKeyZ = 'z';
constexpr auto kKeyReturn = SDLK_RETURN;
constexpr auto kKeyRShift = SDLK_RSHIFT;
constexpr auto kKeyUp = SDLK_UP;
constexpr auto kKeyDown = SDLK_DOWN;
constexpr auto kKeyLeft = SDLK_LEFT;
constexpr auto kKeyRight = SDLK_RIGHT;
#else
constexpr auto kKeyA = SDLK_a;
constexpr auto kKeyB = SDLK_b;
constexpr auto kKeyC = SDLK_c;
constexpr auto kKeyD = SDLK_d;
constexpr auto kKeyS = SDLK_s;
constexpr auto kKeyX = SDLK_x;
constexpr auto kKeyY = SDLK_y;
constexpr auto kKeyZ = SDLK_z;
constexpr auto kKeyReturn = SDLK_RETURN;
constexpr auto kKeyRShift = SDLK_RSHIFT;
constexpr auto kKeyUp = SDLK_UP;
constexpr auto kKeyDown = SDLK_DOWN;
constexpr auto kKeyLeft = SDLK_LEFT;
constexpr auto kKeyRight = SDLK_RIGHT;
#endif
// ============================================================================
// Keyboard Helpers
// ============================================================================
@@ -81,6 +120,19 @@ inline SDL_Keycode GetKeyFromEvent(const SDL_Event& event) {
#endif
}
/**
* @brief Get scancode from keycode
* @param key The keycode
* @return The corresponding scancode
*/
inline SDL_Scancode GetScancodeFromKey(SDL_Keycode key) {
#ifdef YAZE_USE_SDL3
return SDL_GetScancodeFromKey(key, nullptr);
#else
return SDL_GetScancodeFromKey(key);
#endif
}
/**
* @brief Check if a key is pressed using the keyboard state
* @param state The keyboard state from SDL_GetKeyboardState
@@ -311,12 +363,68 @@ inline SDL_Surface* ConvertSurfaceFormat(SDL_Surface* surface, uint32_t format,
if (!surface) return nullptr;
#ifdef YAZE_USE_SDL3
(void)flags; // SDL3 removed flags parameter
return SDL_ConvertSurface(surface, format);
return SDL_ConvertSurface(surface, static_cast<SDL_PixelFormat>(format));
#else
return SDL_ConvertSurfaceFormat(surface, format, flags);
#endif
}
/**
* @brief Get the palette attached to a surface.
*/
inline SDL_Palette* GetSurfacePalette(SDL_Surface* surface) {
if (!surface) return nullptr;
#ifdef YAZE_USE_SDL3
return SDL_GetSurfacePalette(surface);
#else
return surface->format ? surface->format->palette : nullptr;
#endif
}
/**
* @brief Get the pixel format of a surface as Uint32.
*
* Note: SDL2 uses Uint32 for pixel format, SDL3 uses SDL_PixelFormat enum.
* This function returns Uint32 for cross-version compatibility.
*/
inline Uint32 GetSurfaceFormat(SDL_Surface* surface) {
#ifdef YAZE_USE_SDL3
return surface ? static_cast<Uint32>(surface->format) : SDL_PIXELFORMAT_UNKNOWN;
#else
return (surface && surface->format) ? surface->format->format
: SDL_PIXELFORMAT_UNKNOWN;
#endif
}
/**
* @brief Map an RGB color to the surface's pixel format.
*/
inline Uint32 MapRGB(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b) {
if (!surface) return 0;
#ifdef YAZE_USE_SDL3
const SDL_PixelFormatDetails* details =
SDL_GetPixelFormatDetails(surface->format);
if (!details) return 0;
SDL_Palette* palette = SDL_GetSurfacePalette(surface);
return SDL_MapRGB(details, palette, r, g, b);
#else
return SDL_MapRGB(surface->format, r, g, b);
#endif
}
/**
* @brief Create a surface using the appropriate API.
*/
inline SDL_Surface* CreateSurface(int width, int height, int depth,
uint32_t format) {
#ifdef YAZE_USE_SDL3
(void)depth; // SDL3 infers depth from format
return SDL_CreateSurface(width, height, static_cast<SDL_PixelFormat>(format));
#else
return SDL_CreateRGBSurfaceWithFormat(0, width, height, depth, format);
#endif
}
/**
* @brief Get bits per pixel from a surface.
*
@@ -334,6 +442,62 @@ inline int GetSurfaceBitsPerPixel(SDL_Surface* surface) {
#endif
}
/**
* @brief Ensure the surface has a proper 256-color palette for indexed formats.
*
* For 8-bit indexed surfaces, this creates and attaches a 256-color palette
* if one doesn't exist or if the existing palette is too small.
*
* @param surface The surface to check/fix
* @return true if surface now has a valid 256-color palette, false on error
*/
inline bool EnsureSurfacePalette256(SDL_Surface* surface) {
if (!surface) return false;
SDL_Palette* existing = GetSurfacePalette(surface);
if (existing && existing->ncolors >= 256) {
return true; // Already has proper palette
}
// Check if this is an indexed format that needs a palette
int bpp = GetSurfaceBitsPerPixel(surface);
if (bpp != 8) {
return true; // Not an indexed format, no palette needed
}
// Create a new 256-color palette (SDL2: SDL_AllocPalette, SDL3: SDL_CreatePalette)
#ifdef YAZE_USE_SDL3
SDL_Palette* new_palette = SDL_CreatePalette(256);
#else
SDL_Palette* new_palette = SDL_AllocPalette(256);
#endif
if (!new_palette) {
SDL_Log("Warning: Failed to create 256-color palette: %s", SDL_GetError());
return false;
}
// Initialize with grayscale as a safe default
SDL_Color colors[256];
for (int i = 0; i < 256; i++) {
colors[i].r = colors[i].g = colors[i].b = static_cast<Uint8>(i);
colors[i].a = 255;
}
SDL_SetPaletteColors(new_palette, colors, 0, 256);
// Attach to surface
if (SDL_SetSurfacePalette(surface, new_palette) != 0) {
SDL_Log("Warning: Failed to set surface palette: %s", SDL_GetError());
#ifdef YAZE_USE_SDL3
SDL_DestroyPalette(new_palette);
#else
SDL_FreePalette(new_palette);
#endif
return false;
}
return true;
}
/**
* @brief Get bytes per pixel from a surface.
*
@@ -504,6 +668,90 @@ inline uint32_t GetDefaultInitFlags() {
#endif
}
// ============================================================================
// Surface Helpers
// ============================================================================
/**
* @brief Create an RGB surface
*/
inline SDL_Surface* CreateRGBSurface(Uint32 flags, int width, int height, int depth,
Uint32 Rmask, Uint32 Gmask, Uint32 Bmask,
Uint32 Amask) {
#ifdef YAZE_USE_SDL3
// SDL3 uses SDL_CreateSurface with pixel format
return SDL_CreateSurface(width, height,
SDL_GetPixelFormatForMasks(depth, Rmask, Gmask, Bmask, Amask));
#else
return SDL_CreateRGBSurface(flags, width, height, depth, Rmask, Gmask, Bmask,
Amask);
#endif
}
/**
* @brief Destroy a surface
*/
inline void DestroySurface(SDL_Surface* surface) {
#ifdef YAZE_USE_SDL3
SDL_DestroySurface(surface);
#else
SDL_FreeSurface(surface);
#endif
}
// ============================================================================
// Renderer Helpers
// ============================================================================
/**
* @brief Get renderer output size
*/
inline int GetRendererOutputSize(SDL_Renderer* renderer, int* w, int* h) {
#ifdef YAZE_USE_SDL3
return SDL_GetCurrentRenderOutputSize(renderer, w, h);
#else
return SDL_GetRendererOutputSize(renderer, w, h);
#endif
}
/**
* @brief Read pixels from renderer to a surface
*
* SDL3: Returns a new surface
* SDL2: We manually create surface and read into it to match SDL3 behavior
*/
inline SDL_Surface* ReadPixelsToSurface(SDL_Renderer* renderer, int width,
int height, const SDL_Rect* rect) {
#ifdef YAZE_USE_SDL3
return SDL_RenderReadPixels(renderer, rect);
#else
// Create surface to read into (ARGB8888 to match typical screenshot needs)
SDL_Surface* surface = SDL_CreateRGBSurface(0, width, height, 32, 0x00FF0000,
0x0000FF00, 0x000000FF, 0xFF000000);
if (!surface) return nullptr;
if (SDL_RenderReadPixels(renderer, rect, SDL_PIXELFORMAT_ARGB8888,
surface->pixels, surface->pitch) != 0) {
SDL_FreeSurface(surface);
return nullptr;
}
return surface;
#endif
}
/**
* @brief Load a BMP file
*/
inline SDL_Surface* LoadBMP(const char* file) {
#ifdef YAZE_USE_SDL3
return SDL_LoadBMP(file);
#else
return SDL_LoadBMP(file);
#endif
}
} // namespace platform
} // namespace yaze

View File

@@ -84,6 +84,33 @@ class TimingManager {
frame_count_ = 0;
fps_ = 0.0f;
last_delta_time_ = 0.0f;
frame_start_time_ = 0;
}
/**
* @brief Mark the start of a new frame for budget tracking
*/
void BeginFrame() {
frame_start_time_ = SDL_GetPerformanceCounter();
}
/**
* @brief Get elapsed time within the current frame in milliseconds
* @return Milliseconds since BeginFrame() was called
*/
float GetFrameElapsedMs() const {
if (frame_start_time_ == 0) return 0.0f;
uint64_t current = SDL_GetPerformanceCounter();
return ((current - frame_start_time_) * 1000.0f) / static_cast<float>(frequency_);
}
/**
* @brief Get remaining frame budget in milliseconds (targeting 60fps)
* @return Milliseconds remaining before frame deadline
*/
float GetFrameBudgetRemainingMs() const {
constexpr float kTargetFrameTimeMs = 16.67f; // 60fps target
return kTargetFrameTimeMs - GetFrameElapsedMs();
}
private:
@@ -100,6 +127,7 @@ class TimingManager {
uint64_t frequency_;
uint64_t first_time_;
uint64_t last_time_;
uint64_t frame_start_time_ = 0;
float accumulated_time_;
uint32_t frame_count_;
float fps_;

View File

@@ -0,0 +1,16 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_async_guard.h"
namespace yaze {
namespace platform {
// Static member initialization
std::atomic<bool> WasmAsyncGuard::in_async_op_{false};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,98 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_ASYNC_GUARD_H_
#define YAZE_APP_PLATFORM_WASM_WASM_ASYNC_GUARD_H_
#ifdef __EMSCRIPTEN__
#include <atomic>
#include <emscripten.h>
namespace yaze {
namespace platform {
/**
* @class WasmAsyncGuard
* @brief Global guard to prevent concurrent Asyncify operations
*
* Emscripten's Asyncify only supports one async operation at a time.
* This guard provides C++ side tracking to complement the JavaScript
* async queue (async_queue.js).
*
* Usage:
* // At the start of any function that calls Asyncify-wrapped code:
* WasmAsyncGuard::ScopedGuard guard;
* if (!guard.acquired()) {
* // Another async operation is in progress - the JS queue will handle it
* }
* // ... call async operations
*/
class WasmAsyncGuard {
public:
/**
* @brief Try to acquire the async operation lock
* @return true if acquired, false if another operation is in progress
*/
static bool TryAcquire() {
bool expected = false;
return in_async_op_.compare_exchange_strong(expected, true);
}
/**
* @brief Release the async operation lock
*/
static void Release() { in_async_op_.store(false); }
/**
* @brief Check if an async operation is in progress
* @return true if an operation is currently running
*/
static bool IsInProgress() { return in_async_op_.load(); }
/**
* @class ScopedGuard
* @brief RAII wrapper for async operation tracking
*
* Automatically acquires the guard on construction and releases on
* destruction. Logs a warning if another operation was already in progress.
*/
class ScopedGuard {
public:
ScopedGuard() : acquired_(TryAcquire()) {
if (!acquired_) {
emscripten_log(
EM_LOG_WARN,
"[WasmAsyncGuard] Async operation already in progress, "
"request will be queued by JS async queue");
}
}
~ScopedGuard() {
if (acquired_) {
Release();
}
}
// Non-copyable
ScopedGuard(const ScopedGuard&) = delete;
ScopedGuard& operator=(const ScopedGuard&) = delete;
/**
* @brief Check if this guard acquired the lock
* @return true if this guard holds the lock
*/
bool acquired() const { return acquired_; }
private:
bool acquired_;
};
private:
static std::atomic<bool> in_async_op_;
};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_ASYNC_GUARD_H_

View File

@@ -0,0 +1,579 @@
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_autosave.h"
#include <emscripten.h>
#include <emscripten/html5.h> // For emscripten_set_timeout/clear_timeout
#include <chrono>
#include <ctime>
#include "absl/strings/str_format.h"
#include "app/platform/wasm/wasm_storage.h"
#include "app/platform/wasm/wasm_config.h"
namespace yaze {
namespace platform {
using ::yaze::app::platform::WasmConfig;
// Static member initialization
bool AutoSaveManager::emergency_save_triggered_ = false;
// JavaScript event handler registration using EM_JS
EM_JS(void, register_beforeunload_handler, (), {
// Store handler references for cleanup
if (!window._yazeAutoSaveHandlers) {
window._yazeAutoSaveHandlers = {};
}
// Register beforeunload event handler
window._yazeAutoSaveHandlers.beforeunload = function(event) {
// Call the C++ emergency save function
if (Module._yazeEmergencySave) {
Module._yazeEmergencySave();
}
// Some browsers require returnValue to be set
event.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
return event.returnValue;
};
window.addEventListener('beforeunload', window._yazeAutoSaveHandlers.beforeunload);
// Register visibilitychange event for when tab becomes hidden
window._yazeAutoSaveHandlers.visibilitychange = function() {
if (document.hidden && Module._yazeEmergencySave) {
// Save when tab becomes hidden (user switches tabs or minimizes)
Module._yazeEmergencySave();
}
};
document.addEventListener('visibilitychange', window._yazeAutoSaveHandlers.visibilitychange);
// Register pagehide event as backup
window._yazeAutoSaveHandlers.pagehide = function(event) {
if (Module._yazeEmergencySave) {
Module._yazeEmergencySave();
}
};
window.addEventListener('pagehide', window._yazeAutoSaveHandlers.pagehide);
console.log('AutoSave event handlers registered');
});
EM_JS(void, unregister_beforeunload_handler, (), {
// Remove all event listeners using stored references
if (window._yazeAutoSaveHandlers) {
if (window._yazeAutoSaveHandlers.beforeunload) {
window.removeEventListener('beforeunload', window._yazeAutoSaveHandlers.beforeunload);
}
if (window._yazeAutoSaveHandlers.visibilitychange) {
document.removeEventListener('visibilitychange', window._yazeAutoSaveHandlers.visibilitychange);
}
if (window._yazeAutoSaveHandlers.pagehide) {
window.removeEventListener('pagehide', window._yazeAutoSaveHandlers.pagehide);
}
window._yazeAutoSaveHandlers = null;
}
console.log('AutoSave event handlers unregistered');
});
EM_JS(void, set_recovery_flag, (int has_recovery), {
try {
if (has_recovery) {
sessionStorage.setItem('yaze_has_recovery', 'true');
} else {
sessionStorage.removeItem('yaze_has_recovery');
}
} catch (e) {
console.error('Failed to set recovery flag:', e);
}
});
EM_JS(int, get_recovery_flag, (), {
try {
return sessionStorage.getItem('yaze_has_recovery') === 'true' ? 1 : 0;
} catch (e) {
console.error('Failed to get recovery flag:', e);
return 0;
}
});
// C functions exposed to JavaScript for emergency save and recovery
extern "C" {
EMSCRIPTEN_KEEPALIVE
void yazeEmergencySave() {
if (!AutoSaveManager::emergency_save_triggered_) {
AutoSaveManager::emergency_save_triggered_ = true;
AutoSaveManager::Instance().EmergencySave();
}
}
EMSCRIPTEN_KEEPALIVE
int yazeRecoverSession() {
auto status = AutoSaveManager::Instance().RecoverLastSession();
if (status.ok()) {
emscripten_log(EM_LOG_INFO, "Session recovery successful");
return 1;
} else {
emscripten_log(EM_LOG_WARN, "Session recovery failed: %s",
std::string(status.message()).c_str());
return 0;
}
}
EMSCRIPTEN_KEEPALIVE
int yazeHasRecoveryData() {
return AutoSaveManager::Instance().HasRecoveryData() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE
void yazeClearRecoveryData() {
AutoSaveManager::Instance().ClearRecoveryData();
}
}
// AutoSaveManager implementation
AutoSaveManager::AutoSaveManager()
: interval_seconds_(app::platform::WasmConfig::Get().autosave.interval_seconds),
enabled_(true),
running_(false),
timer_id_(-1),
save_count_(0),
error_count_(0),
recovery_count_(0),
event_handlers_initialized_(false) {
// Set the JavaScript function pointer
EM_ASM({
Module._yazeEmergencySave = Module.cwrap('yazeEmergencySave', null, []);
});
}
AutoSaveManager::~AutoSaveManager() {
Stop();
CleanupEventHandlers();
}
AutoSaveManager& AutoSaveManager::Instance() {
static AutoSaveManager instance;
return instance;
}
void AutoSaveManager::InitializeEventHandlers() {
if (!event_handlers_initialized_) {
register_beforeunload_handler();
event_handlers_initialized_ = true;
}
}
void AutoSaveManager::CleanupEventHandlers() {
if (event_handlers_initialized_) {
unregister_beforeunload_handler();
event_handlers_initialized_ = false;
}
}
void AutoSaveManager::TimerCallback(void* user_data) {
AutoSaveManager* manager = static_cast<AutoSaveManager*>(user_data);
if (manager && manager->IsRunning()) {
auto status = manager->PerformSave();
if (!status.ok()) {
emscripten_log(EM_LOG_WARN, "Auto-save failed: %s",
status.ToString().c_str());
}
// Reschedule the timer
if (manager->IsRunning()) {
manager->timer_id_ = emscripten_set_timeout(
TimerCallback, manager->interval_seconds_ * 1000, manager);
}
}
}
absl::Status AutoSaveManager::Start(int interval_seconds) {
std::lock_guard<std::mutex> lock(mutex_);
if (running_) {
return absl::AlreadyExistsError("Auto-save is already running");
}
interval_seconds_ = interval_seconds;
InitializeEventHandlers();
// Start the timer
timer_id_ = emscripten_set_timeout(
TimerCallback, interval_seconds_ * 1000, this);
running_ = true;
emscripten_log(EM_LOG_INFO, "Auto-save started with %d second interval",
interval_seconds);
return absl::OkStatus();
}
absl::Status AutoSaveManager::Stop() {
std::lock_guard<std::mutex> lock(mutex_);
if (!running_) {
return absl::OkStatus();
}
// Cancel the timer
if (timer_id_ >= 0) {
emscripten_clear_timeout(timer_id_);
timer_id_ = -1;
}
running_ = false;
emscripten_log(EM_LOG_INFO, "Auto-save stopped");
return absl::OkStatus();
}
bool AutoSaveManager::IsRunning() const {
std::lock_guard<std::mutex> lock(mutex_);
return running_;
}
void AutoSaveManager::RegisterComponent(const std::string& component_id,
SaveCallback save_fn,
RestoreCallback restore_fn) {
std::lock_guard<std::mutex> lock(mutex_);
components_[component_id] = {save_fn, restore_fn};
emscripten_log(EM_LOG_INFO, "Registered component for auto-save: %s",
component_id.c_str());
}
void AutoSaveManager::UnregisterComponent(const std::string& component_id) {
std::lock_guard<std::mutex> lock(mutex_);
components_.erase(component_id);
emscripten_log(EM_LOG_INFO, "Unregistered component from auto-save: %s",
component_id.c_str());
}
absl::Status AutoSaveManager::SaveNow() {
std::lock_guard<std::mutex> lock(mutex_);
return PerformSave();
}
nlohmann::json AutoSaveManager::CollectComponentData() {
nlohmann::json data = nlohmann::json::object();
for (const auto& [id, component] : components_) {
try {
if (component.save_fn) {
data[id] = component.save_fn();
}
} catch (const std::exception& e) {
emscripten_log(EM_LOG_ERROR, "Failed to save component %s: %s",
id.c_str(), e.what());
error_count_++;
}
}
return data;
}
absl::Status AutoSaveManager::PerformSave() {
nlohmann::json save_data = nlohmann::json::object();
// Collect data from all registered components
save_data["components"] = CollectComponentData();
// Add metadata
auto now = std::chrono::system_clock::now();
save_data["timestamp"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
save_data["version"] = 1;
// Save to storage
auto status = SaveToStorage(save_data);
if (status.ok()) {
last_save_time_ = now;
save_count_++;
set_recovery_flag(1); // Mark that recovery data is available
} else {
error_count_++;
}
return status;
}
absl::Status AutoSaveManager::SaveToStorage(const nlohmann::json& data) {
try {
// Convert JSON to string
std::string json_str = data.dump();
// Save to IndexedDB via WasmStorage
auto status = WasmStorage::SaveProject(kAutoSaveDataKey, json_str);
if (!status.ok()) {
return status;
}
// Save metadata separately for quick access
nlohmann::json meta;
meta["timestamp"] = data["timestamp"];
meta["component_count"] = data["components"].size();
meta["save_count"] = save_count_;
return WasmStorage::SaveProject(kAutoSaveMetaKey, meta.dump());
} catch (const std::exception& e) {
return absl::InternalError(
absl::StrFormat("Failed to save auto-save data: %s", e.what()));
}
}
absl::StatusOr<nlohmann::json> AutoSaveManager::LoadFromStorage() {
auto result = WasmStorage::LoadProject(kAutoSaveDataKey);
if (!result.ok()) {
return result.status();
}
try {
return nlohmann::json::parse(*result);
} catch (const std::exception& e) {
return absl::InvalidArgumentError(
absl::StrFormat("Failed to parse auto-save data: %s", e.what()));
}
}
void AutoSaveManager::EmergencySave() {
// Use try_lock to avoid blocking - emergency save should be fast
// If we can't get the lock, another operation is in progress
std::unique_lock<std::mutex> lock(mutex_, std::try_to_lock);
try {
nlohmann::json emergency_data = nlohmann::json::object();
emergency_data["emergency"] = true;
// Only collect component data if we got the lock
if (lock.owns_lock()) {
emergency_data["components"] = CollectComponentData();
} else {
// Can't safely access components, save empty state marker
emergency_data["components"] = nlohmann::json::object();
emergency_data["incomplete"] = true;
}
emergency_data["timestamp"] =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
// Use synchronous localStorage for emergency save (faster than IndexedDB)
EM_ASM({
try {
const data = UTF8ToString($0);
localStorage.setItem('yaze_emergency_save', data);
console.log('Emergency save completed');
} catch (e) {
console.error('Emergency save failed:', e);
}
}, emergency_data.dump().c_str());
set_recovery_flag(1);
} catch (...) {
// Silently fail - we're in emergency mode
}
}
bool AutoSaveManager::HasRecoveryData() {
// Check both sessionStorage flag and actual data
if (get_recovery_flag() == 0) {
return false;
}
// Verify data actually exists
auto meta = WasmStorage::LoadProject(kAutoSaveMetaKey);
if (meta.ok()) {
return true;
}
// Check for emergency save data
int has_emergency = EM_ASM_INT({
return localStorage.getItem('yaze_emergency_save') !== null ? 1 : 0;
});
return has_emergency == 1;
}
absl::StatusOr<nlohmann::json> AutoSaveManager::GetRecoveryInfo() {
// First try to get regular auto-save metadata
auto meta_result = WasmStorage::LoadProject(kAutoSaveMetaKey);
if (meta_result.ok()) {
try {
return nlohmann::json::parse(*meta_result);
} catch (...) {
// Continue to check emergency save
}
}
// Check for emergency save
char* emergency_data = nullptr;
EM_ASM({
const data = localStorage.getItem('yaze_emergency_save');
if (data) {
const len = lengthBytesUTF8(data) + 1;
const ptr = _malloc(len);
stringToUTF8(data, ptr, len);
setValue($0, ptr, 'i32');
}
}, &emergency_data);
if (emergency_data) {
try {
nlohmann::json data = nlohmann::json::parse(emergency_data);
free(emergency_data);
nlohmann::json info;
info["type"] = "emergency";
info["timestamp"] = data["timestamp"];
info["component_count"] = data["components"].size();
return info;
} catch (const std::exception& e) {
free(emergency_data);
return absl::InvalidArgumentError("Failed to parse emergency save data");
}
}
return absl::NotFoundError("No recovery data found");
}
absl::Status AutoSaveManager::RecoverLastSession() {
std::lock_guard<std::mutex> lock(mutex_);
// First try regular auto-save data
auto data_result = LoadFromStorage();
if (data_result.ok()) {
const auto& data = *data_result;
if (data.contains("components") && data["components"].is_object()) {
for (const auto& [id, component_data] : data["components"].items()) {
auto it = components_.find(id);
if (it != components_.end() && it->second.restore_fn) {
try {
it->second.restore_fn(component_data);
emscripten_log(EM_LOG_INFO, "Restored component: %s", id.c_str());
} catch (const std::exception& e) {
emscripten_log(EM_LOG_ERROR, "Failed to restore component %s: %s",
id.c_str(), e.what());
}
}
}
recovery_count_++;
set_recovery_flag(0); // Clear recovery flag
return absl::OkStatus();
}
}
// Try emergency save data
char* emergency_data = nullptr;
EM_ASM({
const data = localStorage.getItem('yaze_emergency_save');
if (data) {
const len = lengthBytesUTF8(data) + 1;
const ptr = _malloc(len);
stringToUTF8(data, ptr, len);
setValue($0, ptr, 'i32');
}
}, &emergency_data);
if (emergency_data) {
try {
nlohmann::json data = nlohmann::json::parse(emergency_data);
free(emergency_data);
if (data.contains("components") && data["components"].is_object()) {
for (const auto& [id, component_data] : data["components"].items()) {
auto it = components_.find(id);
if (it != components_.end() && it->second.restore_fn) {
try {
it->second.restore_fn(component_data);
emscripten_log(EM_LOG_INFO, "Restored component from emergency save: %s",
id.c_str());
} catch (const std::exception& e) {
emscripten_log(EM_LOG_ERROR, "Failed to restore component %s: %s",
id.c_str(), e.what());
}
}
}
// Clear emergency save data
EM_ASM({
localStorage.removeItem('yaze_emergency_save');
});
recovery_count_++;
set_recovery_flag(0); // Clear recovery flag
return absl::OkStatus();
}
} catch (const std::exception& e) {
return absl::InvalidArgumentError(
absl::StrFormat("Failed to recover session: %s", e.what()));
}
}
return absl::NotFoundError("No recovery data available");
}
absl::Status AutoSaveManager::ClearRecoveryData() {
// Clear regular auto-save data
WasmStorage::DeleteProject(kAutoSaveDataKey);
WasmStorage::DeleteProject(kAutoSaveMetaKey);
// Clear emergency save data
EM_ASM({
localStorage.removeItem('yaze_emergency_save');
});
// Clear recovery flag
set_recovery_flag(0);
emscripten_log(EM_LOG_INFO, "Recovery data cleared");
return absl::OkStatus();
}
void AutoSaveManager::SetInterval(int seconds) {
bool was_running = IsRunning();
if (was_running) {
Stop();
}
interval_seconds_ = seconds;
if (was_running && enabled_) {
Start(seconds);
}
}
void AutoSaveManager::SetEnabled(bool enabled) {
enabled_ = enabled;
if (!enabled && IsRunning()) {
Stop();
} else if (enabled && !IsRunning()) {
Start(interval_seconds_);
}
}
nlohmann::json AutoSaveManager::GetStatistics() const {
std::lock_guard<std::mutex> lock(mutex_);
nlohmann::json stats;
stats["save_count"] = save_count_;
stats["error_count"] = error_count_;
stats["recovery_count"] = recovery_count_;
stats["components_registered"] = components_.size();
stats["is_running"] = running_;
stats["interval_seconds"] = interval_seconds_;
stats["enabled"] = enabled_;
if (last_save_time_.time_since_epoch().count() > 0) {
stats["last_save_timestamp"] =
std::chrono::duration_cast<std::chrono::milliseconds>(
last_save_time_.time_since_epoch()).count();
}
return stats;
}
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,209 @@
#ifndef YAZE_APP_PLATFORM_WASM_AUTOSAVE_H_
#define YAZE_APP_PLATFORM_WASM_AUTOSAVE_H_
#ifdef __EMSCRIPTEN__
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "nlohmann/json.hpp"
namespace yaze {
namespace platform {
/**
* @class AutoSaveManager
* @brief Manages automatic saving and crash recovery for WASM builds
*
* This class provides periodic auto-save functionality, emergency save on
* page unload, and session recovery after crashes or unexpected closures.
* It integrates with WasmStorage for data persistence and uses browser
* event handlers for lifecycle management.
*/
class AutoSaveManager {
public:
// Callback types
using SaveCallback = std::function<nlohmann::json()>;
using RestoreCallback = std::function<void(const nlohmann::json&)>;
/**
* @brief Get the singleton instance of AutoSaveManager
* @return Reference to the AutoSaveManager instance
*/
static AutoSaveManager& Instance();
/**
* @brief Start periodic auto-save
* @param interval_seconds Interval between saves (default 60 seconds)
* @return Status indicating success or failure
*/
absl::Status Start(int interval_seconds = 60);
/**
* @brief Stop auto-save
* @return Status indicating success or failure
*/
absl::Status Stop();
/**
* @brief Check if auto-save is currently running
* @return true if auto-save is active
*/
bool IsRunning() const;
/**
* @brief Register a component for auto-save
* @param component_id Unique identifier for the component
* @param save_fn Function that returns JSON data to save
* @param restore_fn Function that accepts JSON data to restore
*/
void RegisterComponent(const std::string& component_id,
SaveCallback save_fn,
RestoreCallback restore_fn);
/**
* @brief Unregister a component from auto-save
* @param component_id Component identifier to unregister
*/
void UnregisterComponent(const std::string& component_id);
/**
* @brief Manually trigger a save of all registered components
* @return Status indicating success or failure
*/
absl::Status SaveNow();
/**
* @brief Save data immediately (called on page unload)
* @note This is called automatically by the browser event handler
*/
void EmergencySave();
/**
* @brief Check if there's recovery data available
* @return true if recovery data exists
*/
bool HasRecoveryData();
/**
* @brief Get information about available recovery data
* @return JSON object with recovery metadata (timestamp, components, etc.)
*/
absl::StatusOr<nlohmann::json> GetRecoveryInfo();
/**
* @brief Recover the last saved session
* @return Status indicating success or failure
*/
absl::Status RecoverLastSession();
/**
* @brief Clear all recovery data
* @return Status indicating success or failure
*/
absl::Status ClearRecoveryData();
/**
* @brief Set the auto-save interval
* @param seconds New interval in seconds
* @note Restarts auto-save if currently running
*/
void SetInterval(int seconds);
/**
* @brief Get the current auto-save interval
* @return Interval in seconds
*/
int GetInterval() const { return interval_seconds_; }
/**
* @brief Enable or disable auto-save
* @param enabled true to enable, false to disable
*/
void SetEnabled(bool enabled);
/**
* @brief Check if auto-save is enabled
* @return true if enabled
*/
bool IsEnabled() const { return enabled_; }
/**
* @brief Get the last auto-save timestamp
* @return Timestamp of last successful save
*/
std::chrono::system_clock::time_point GetLastSaveTime() const {
return last_save_time_;
}
/**
* @brief Get statistics about auto-save operations
* @return JSON object with save count, error count, etc.
*/
nlohmann::json GetStatistics() const;
private:
// Private constructor for singleton
AutoSaveManager();
~AutoSaveManager();
// Delete copy and move constructors
AutoSaveManager(const AutoSaveManager&) = delete;
AutoSaveManager& operator=(const AutoSaveManager&) = delete;
AutoSaveManager(AutoSaveManager&&) = delete;
AutoSaveManager& operator=(AutoSaveManager&&) = delete;
// Component registration
struct Component {
SaveCallback save_fn;
RestoreCallback restore_fn;
};
// Internal methods
void InitializeEventHandlers();
void CleanupEventHandlers();
static void TimerCallback(void* user_data);
absl::Status PerformSave();
nlohmann::json CollectComponentData();
absl::Status SaveToStorage(const nlohmann::json& data);
absl::StatusOr<nlohmann::json> LoadFromStorage();
// Storage keys
static constexpr const char* kAutoSaveDataKey = "yaze_autosave_data";
static constexpr const char* kAutoSaveMetaKey = "yaze_autosave_meta";
static constexpr const char* kRecoveryFlagKey = "yaze_has_recovery";
// Member variables
mutable std::mutex mutex_;
std::unordered_map<std::string, Component> components_;
int interval_seconds_;
bool enabled_;
bool running_;
int timer_id_;
std::chrono::system_clock::time_point last_save_time_;
// Statistics
size_t save_count_;
size_t error_count_;
size_t recovery_count_;
// Event handler registration state
bool event_handlers_initialized_;
public:
// Must be public for emergency save callback access
static bool emergency_save_triggered_;
};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_AUTOSAVE_H_

View File

@@ -0,0 +1,272 @@
#include "app/platform/wasm/wasm_bootstrap.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <fstream>
#include <algorithm>
#include <mutex>
#include <queue>
#include <vector>
#include "app/platform/wasm/wasm_config.h"
#include "app/platform/wasm/wasm_drop_handler.h"
#include "util/log.h"
namespace {
bool g_filesystem_ready = false;
std::function<void(std::string)> g_rom_load_handler;
std::queue<std::string> g_pending_rom_loads;
std::mutex g_rom_load_mutex;
} // namespace
extern "C" {
EMSCRIPTEN_KEEPALIVE
void SetFileSystemReady() {
g_filesystem_ready = true;
LOG_INFO("Wasm", "Filesystem sync complete.");
// Notify JS that FS is ready
EM_ASM({
if (Module.onFileSystemReady) {
Module.onFileSystemReady();
}
});
}
EMSCRIPTEN_KEEPALIVE
void SyncFilesystem() {
// Sync all IDBFS mounts to IndexedDB for persistence
EM_ASM({
if (typeof FS !== 'undefined' && FS.syncfs) {
FS.syncfs(false, function(err) {
if (err) {
console.error('[WASM] Failed to sync filesystem:', err);
} else {
console.log('[WASM] Filesystem synced successfully');
}
});
}
});
}
EMSCRIPTEN_KEEPALIVE
void LoadRomFromWeb(const char* filename) {
if (!filename) {
LOG_ERROR("Wasm", "LoadRomFromWeb called with null filename");
return;
}
std::string path(filename);
// Validate path is not empty
if (path.empty()) {
LOG_ERROR("Wasm", "LoadRomFromWeb called with empty filename");
return;
}
// Validate path doesn't contain path traversal
if (path.find("..") != std::string::npos) {
LOG_ERROR("Wasm", "LoadRomFromWeb: path traversal not allowed: %s", filename);
return;
}
// Validate reasonable path length (max 512 chars)
if (path.length() > 512) {
LOG_ERROR("Wasm", "LoadRomFromWeb: path too long (%zu chars)", path.length());
return;
}
yaze::app::wasm::TriggerRomLoad(path);
}
} // extern "C"
EM_JS(void, MountFilesystems, (), {
// Create all required directories
var directories = [
'/roms', // ROM files (IDBFS - persistent for session restore)
'/saves', // Save files (IDBFS - persistent)
'/config', // Configuration files (IDBFS - persistent)
'/projects', // Project files (IDBFS - persistent)
'/prompts', // Agent prompts (IDBFS - persistent)
'/recent', // Recent files metadata (IDBFS - persistent)
'/temp' // Temporary files (MEMFS - non-persistent)
];
directories.forEach(function(dir) {
try {
FS.mkdir(dir);
} catch (e) {
// Directory may already exist
if (e.code !== 'EEXIST') {
console.warn("Failed to create directory " + dir + ": " + e);
}
}
});
// Mount MEMFS for temporary files only
FS.mount(MEMFS, {}, '/temp');
// Check if IDBFS is available (try multiple ways to access it)
var idbfs = null;
if (typeof IDBFS !== 'undefined') {
idbfs = IDBFS;
} else if (typeof Module !== 'undefined' && typeof Module.IDBFS !== 'undefined') {
idbfs = Module.IDBFS;
} else if (typeof FS !== 'undefined' && typeof FS.filesystems !== 'undefined' && FS.filesystems.IDBFS) {
idbfs = FS.filesystems.IDBFS;
}
// Persistent directories to mount with IDBFS
var persistentDirs = ['/roms', '/saves', '/config', '/projects', '/prompts', '/recent'];
var mountedCount = 0;
var totalToMount = persistentDirs.length;
if (idbfs !== null) {
persistentDirs.forEach(function(dir) {
try {
FS.mount(idbfs, {}, dir);
mountedCount++;
} catch (e) {
console.error("Error mounting IDBFS for " + dir + ": " + e);
// Fallback to MEMFS for this directory
try {
FS.mount(MEMFS, {}, dir);
} catch (e2) {
// May already be mounted
}
mountedCount++;
}
});
// Sync all IDBFS mounts from IndexedDB to memory
FS.syncfs(true, function(err) {
if (err) {
console.error("Failed to sync IDBFS: " + err);
} else {
console.log("IDBFS synced successfully");
}
// Signal C++ that we are ready regardless of success/fail
Module._SetFileSystemReady();
});
} else {
// Fallback to MEMFS if IDBFS is not available
console.warn("IDBFS not available, using MEMFS for all directories (no persistence)");
persistentDirs.forEach(function(dir) {
try {
FS.mount(MEMFS, {}, dir);
} catch (e) {
// May already be mounted
}
});
Module._SetFileSystemReady();
}
});
EM_JS(void, SetupYazeGlobalApi, (), {
if (typeof Module === 'undefined') return;
// Initialize global API for agents/automation
window.yazeApp = {
execute: function(cmd) {
if (Module.executeCommand) {
return Module.executeCommand(cmd);
}
return "Error: bindings not ready";
},
getState: function() {
if (Module.getFullDebugState) {
try { return JSON.parse(Module.getFullDebugState()); } catch(e) { return {}; }
}
return {};
},
getEditorState: function() {
if (Module.getEditorState) {
try { return JSON.parse(Module.getEditorState()); } catch(e) { return {}; }
}
return {};
},
loadRom: function(filename) {
return this.execute("rom load " + filename);
}
};
console.log("[yaze] window.yazeApp API initialized for agents");
});
namespace yaze::app::wasm {
bool IsFileSystemReady() {
return g_filesystem_ready;
}
void SetRomLoadHandler(std::function<void(std::string)> handler) {
std::lock_guard<std::mutex> lock(g_rom_load_mutex);
g_rom_load_handler = handler;
// Flush all pending ROM loads
while (!g_pending_rom_loads.empty()) {
std::string path = g_pending_rom_loads.front();
g_pending_rom_loads.pop();
LOG_INFO("Wasm", "Flushing pending ROM load: %s", path.c_str());
handler(path);
}
}
void TriggerRomLoad(const std::string& path) {
std::lock_guard<std::mutex> lock(g_rom_load_mutex);
if (g_rom_load_handler) {
g_rom_load_handler(path);
} else {
LOG_INFO("Wasm", "Queuing ROM load (handler not ready): %s", path.c_str());
g_pending_rom_loads.push(path);
}
}
void InitializeWasmPlatform() {
// Load WASM configuration from JavaScript
app::platform::WasmConfig::Get().LoadFromJavaScript();
// Setup global API
SetupYazeGlobalApi();
// Initialize drop handler for Drag & Drop support
auto& drop_handler = yaze::platform::WasmDropHandler::GetInstance();
drop_handler.Initialize("",
[](const std::string& filename, const std::vector<uint8_t>& data) {
// Determine file type from extension
std::string ext = filename.substr(filename.find_last_of(".") + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == "sfc" || ext == "smc" || ext == "zip") {
// Write to MEMFS and load
std::string path = "/roms/" + filename;
std::ofstream file(path, std::ios::binary);
file.write(reinterpret_cast<const char*>(data.data()), data.size());
file.close();
LOG_INFO("Wasm", "Wrote dropped ROM to %s (%zu bytes)", path.c_str(), data.size());
LoadRomFromWeb(path.c_str());
}
else if (ext == "pal" || ext == "tpl") {
LOG_INFO("Wasm", "Palette drop detected: %s. Feature pending UI integration.", filename.c_str());
}
},
[](const std::string& error) {
LOG_ERROR("Wasm", "Drop Handler Error: %s", error.c_str());
}
);
// Initialize filesystems asynchronously
MountFilesystems();
}
} // namespace yaze::app::wasm
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,39 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_BOOTSTRAP_H_
#define YAZE_APP_PLATFORM_WASM_WASM_BOOTSTRAP_H_
#ifdef __EMSCRIPTEN__
#include <string>
#include <functional>
namespace yaze::app::wasm {
/**
* @brief Initialize the WASM platform layer.
*
* Sets up filesystem mounts, drag-and-drop handlers, and JS configuration.
* Call this early in main().
*/
void InitializeWasmPlatform();
/**
* @brief Check if the asynchronous filesystem sync is complete.
* @return true if filesystems are mounted and ready.
*/
bool IsFileSystemReady();
/**
* @brief Register a callback for when an external source (JS, Drop) requests a ROM load.
*/
void SetRomLoadHandler(std::function<void(std::string)> handler);
/**
* @brief Trigger a ROM load from C++ code (e.g. terminal bridge).
*/
void TriggerRomLoad(const std::string& path);
} // namespace yaze::app::wasm
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_BOOTSTRAP_H_

View File

@@ -0,0 +1,13 @@
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_browser_storage.h"
namespace yaze {
namespace app {
namespace platform {
// Intentionally empty: stub implementation lives in the header.
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,172 @@
#ifndef YAZE_APP_PLATFORM_WASM_BROWSER_STORAGE_H_
#define YAZE_APP_PLATFORM_WASM_BROWSER_STORAGE_H_
#ifdef __EMSCRIPTEN__
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace app {
namespace platform {
/**
* Stubbed browser storage for WASM builds.
*
* Key/secret storage in the browser is intentionally disabled to avoid leaking
* model/API credentials in page-visible storage. All methods return
* Unimplemented/NotFound and should not be used for sensitive data.
*/
class WasmBrowserStorage {
public:
enum class StorageType { kSession, kLocal };
static absl::Status StoreApiKey(const std::string&, const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage disabled for security");
}
static absl::StatusOr<std::string> RetrieveApiKey(
const std::string&, StorageType = StorageType::kSession) {
return absl::NotFoundError("Browser storage disabled");
}
static absl::Status ClearApiKey(const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage disabled for security");
}
static bool HasApiKey(const std::string&,
StorageType = StorageType::kSession) {
return false;
}
static absl::Status StoreSecret(const std::string&, const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage disabled for security");
}
static absl::StatusOr<std::string> RetrieveSecret(
const std::string&, StorageType = StorageType::kSession) {
return absl::NotFoundError("Browser storage disabled");
}
static absl::Status ClearSecret(const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage disabled for security");
}
static std::vector<std::string> ListStoredApiKeys(
StorageType = StorageType::kSession) {
return {};
}
static absl::Status ClearAllApiKeys(StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage disabled for security");
}
struct StorageQuota {
size_t used_bytes = 0;
size_t available_bytes = 0;
};
static bool IsStorageAvailable() { return false; }
static absl::StatusOr<StorageQuota> GetStorageQuota(
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage disabled for security");
}
};
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub for non-WASM builds
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace app {
namespace platform {
/**
* Non-WASM stub for WasmBrowserStorage.
* All methods return Unimplemented/NotFound as browser storage is not available.
*/
class WasmBrowserStorage {
public:
enum class StorageType { kSession, kLocal };
static absl::Status StoreApiKey(const std::string&, const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage requires WASM build");
}
static absl::StatusOr<std::string> RetrieveApiKey(
const std::string&, StorageType = StorageType::kSession) {
return absl::NotFoundError("Browser storage requires WASM build");
}
static absl::Status ClearApiKey(const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage requires WASM build");
}
static bool HasApiKey(const std::string&,
StorageType = StorageType::kSession) {
return false;
}
static absl::Status StoreSecret(const std::string&, const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage requires WASM build");
}
static absl::StatusOr<std::string> RetrieveSecret(
const std::string&, StorageType = StorageType::kSession) {
return absl::NotFoundError("Browser storage requires WASM build");
}
static absl::Status ClearSecret(const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage requires WASM build");
}
static std::vector<std::string> ListStoredApiKeys(
StorageType = StorageType::kSession) {
return {};
}
static absl::Status ClearAllApiKeys(StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage requires WASM build");
}
struct StorageQuota {
size_t used_bytes = 0;
size_t available_bytes = 0;
};
static bool IsStorageAvailable() { return false; }
static absl::StatusOr<StorageQuota> GetStorageQuota(
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Browser storage requires WASM build");
}
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_BROWSER_STORAGE_H_

View File

@@ -0,0 +1,991 @@
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_collaboration.h"
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <chrono>
#include <cmath>
#include <random>
#include <sstream>
#include "absl/strings/str_format.h"
#include "nlohmann/json.hpp"
#include "app/platform/wasm/wasm_config.h"
using json = nlohmann::json;
// clang-format off
EM_JS(double, GetCurrentTime, (), {
return Date.now() / 1000.0;
});
EM_JS(void, ConsoleLog, (const char* message), {
console.log('[WasmCollaboration] ' + UTF8ToString(message));
});
EM_JS(void, ConsoleError, (const char* message), {
console.error('[WasmCollaboration] ' + UTF8ToString(message));
});
EM_JS(void, UpdateCollaborationUI, (const char* type, const char* data), {
if (typeof window.updateCollaborationUI === 'function') {
window.updateCollaborationUI(UTF8ToString(type), UTF8ToString(data));
}
});
EM_JS(char*, GenerateRandomRoomCode, (), {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var result = '';
for (var i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
var lengthBytes = lengthBytesUTF8(result) + 1;
var stringOnWasmHeap = _malloc(lengthBytes);
stringToUTF8(result, stringOnWasmHeap, lengthBytes);
return stringOnWasmHeap;
});
EM_JS(char*, GetCollaborationServerUrl, (), {
// Check for configuration in order of precedence:
// 1. window.YAZE_CONFIG.collaborationServerUrl
// 2. Environment variable via meta tag
// 3. Default empty (disabled)
var url = '';
if (typeof window !== 'undefined') {
if (window.YAZE_CONFIG && window.YAZE_CONFIG.collaborationServerUrl) {
url = window.YAZE_CONFIG.collaborationServerUrl;
} else {
// Check for meta tag configuration
var meta = document.querySelector('meta[name="yaze-collab-server"]');
if (meta && meta.content) {
url = meta.content;
}
}
}
if (url.length === 0) {
return null;
}
var lengthBytes = lengthBytesUTF8(url) + 1;
var stringOnWasmHeap = _malloc(lengthBytes);
stringToUTF8(url, stringOnWasmHeap, lengthBytes);
return stringOnWasmHeap;
});
// clang-format on
namespace yaze {
namespace app {
namespace platform {
namespace {
// Color palette for user cursors
const std::vector<std::string> kUserColors = {
"#FF6B6B", // Red
"#4ECDC4", // Teal
"#45B7D1", // Blue
"#96CEB4", // Green
"#FFEAA7", // Yellow
"#DDA0DD", // Plum
"#98D8C8", // Mint
"#F7DC6F", // Gold
};
WasmCollaboration& GetInstance() {
static WasmCollaboration instance;
return instance;
}
} // namespace
WasmCollaboration& GetWasmCollaborationInstance() { return GetInstance(); }
WasmCollaboration::WasmCollaboration() {
user_id_ = GenerateUserId();
user_color_ = GenerateUserColor();
websocket_ = std::make_unique<net::EmscriptenWebSocket>();
// Try to initialize from config automatically
InitializeFromConfig();
}
WasmCollaboration::~WasmCollaboration() {
if (is_connected_) {
LeaveSession();
}
}
void WasmCollaboration::InitializeFromConfig() {
char* url = GetCollaborationServerUrl();
if (url != nullptr) {
websocket_url_ = std::string(url);
free(url);
ConsoleLog(("Collaboration server configured: " + websocket_url_).c_str());
} else {
ConsoleLog("Collaboration server not configured. Set window.YAZE_CONFIG.collaborationServerUrl or add <meta name=\"yaze-collab-server\" content=\"wss://...\"> to enable.");
}
}
absl::StatusOr<std::string> WasmCollaboration::CreateSession(
const std::string& session_name, const std::string& username,
const std::string& password) {
if (is_connected_ || connection_state_ == ConnectionState::Connecting) {
return absl::FailedPreconditionError("Already connected or connecting to a session");
}
if (!IsConfigured()) {
return absl::FailedPreconditionError(
"Collaboration server not configured. Set window.YAZE_CONFIG.collaborationServerUrl "
"or call SetWebSocketUrl() before creating a session.");
}
// Generate room code
char* room_code_ptr = GenerateRandomRoomCode();
room_code_ = std::string(room_code_ptr);
free(room_code_ptr);
session_name_ = session_name;
username_ = username;
stored_password_ = password;
should_reconnect_ = true; // Enable auto-reconnect for this session
UpdateConnectionState(ConnectionState::Connecting, "Creating session...");
// Connect to WebSocket server
auto status = websocket_->Connect(websocket_url_);
if (!status.ok()) {
return status;
}
// Set up WebSocket callbacks
websocket_->OnOpen([this, password]() {
ConsoleLog("WebSocket connected, creating session");
is_connected_ = true;
UpdateConnectionState(ConnectionState::Connected, "Connected");
// Add self to users list
User self_user;
self_user.id = user_id_;
self_user.name = username_;
self_user.color = user_color_;
self_user.is_active = true;
self_user.last_activity = GetCurrentTime();
{
std::lock_guard<std::mutex> lock(users_mutex_);
users_[user_id_] = self_user;
}
// Send create session message
json msg;
msg["type"] = "create";
msg["room"] = room_code_;
msg["name"] = session_name_;
msg["user"] = username_;
msg["user_id"] = user_id_;
msg["color"] = user_color_;
if (!password.empty()) {
msg["password"] = password;
}
auto send_status = websocket_->Send(msg.dump());
if (!send_status.ok()) {
ConsoleError("Failed to send create message");
}
if (status_callback_) {
status_callback_(true, "Session created");
}
UpdateCollaborationUI("session_created", room_code_.c_str());
});
websocket_->OnMessage([this](const std::string& message) {
HandleMessage(message);
});
websocket_->OnClose([this](int code, const std::string& reason) {
is_connected_ = false;
ConsoleLog(absl::StrFormat("WebSocket closed: %s (code: %d)", reason, code).c_str());
// Initiate reconnection if enabled
if (should_reconnect_) {
InitiateReconnection();
} else {
UpdateConnectionState(ConnectionState::Disconnected, absl::StrFormat("Disconnected: %s", reason));
}
if (status_callback_) {
status_callback_(false, absl::StrFormat("Disconnected: %s", reason));
}
UpdateCollaborationUI("disconnected", "");
});
websocket_->OnError([this](const std::string& error) {
ConsoleError(error.c_str());
is_connected_ = false;
// Initiate reconnection on error
if (should_reconnect_) {
InitiateReconnection();
} else {
UpdateConnectionState(ConnectionState::Disconnected, error);
}
if (status_callback_) {
status_callback_(false, error);
}
});
// Note: is_connected_ will be set to true in OnOpen callback when connection is established
// For now, mark as "connecting" state by returning the room code
// The actual connected state is confirmed in HandleMessage when create_response is received
UpdateCollaborationUI("session_creating", room_code_.c_str());
return room_code_;
}
absl::Status WasmCollaboration::JoinSession(const std::string& room_code,
const std::string& username,
const std::string& password) {
if (is_connected_ || connection_state_ == ConnectionState::Connecting) {
return absl::FailedPreconditionError("Already connected or connecting to a session");
}
if (!IsConfigured()) {
return absl::FailedPreconditionError(
"Collaboration server not configured. Set window.YAZE_CONFIG.collaborationServerUrl "
"or call SetWebSocketUrl() before joining a session.");
}
room_code_ = room_code;
username_ = username;
stored_password_ = password;
should_reconnect_ = true; // Enable auto-reconnect for this session
UpdateConnectionState(ConnectionState::Connecting, "Joining session...");
// Connect to WebSocket server
auto status = websocket_->Connect(websocket_url_);
if (!status.ok()) {
return status;
}
// Set up WebSocket callbacks
websocket_->OnOpen([this, password]() {
ConsoleLog("WebSocket connected, joining session");
is_connected_ = true;
UpdateConnectionState(ConnectionState::Connected, "Connected");
// Send join session message
json msg;
msg["type"] = "join";
msg["room"] = room_code_;
msg["user"] = username_;
msg["user_id"] = user_id_;
msg["color"] = user_color_;
if (!password.empty()) {
msg["password"] = password;
}
auto send_status = websocket_->Send(msg.dump());
if (!send_status.ok()) {
ConsoleError("Failed to send join message");
}
if (status_callback_) {
status_callback_(true, "Joined session");
}
UpdateCollaborationUI("session_joined", room_code_.c_str());
});
websocket_->OnMessage([this](const std::string& message) {
HandleMessage(message);
});
websocket_->OnClose([this](int code, const std::string& reason) {
is_connected_ = false;
ConsoleLog(absl::StrFormat("WebSocket closed: %s (code: %d)", reason, code).c_str());
// Initiate reconnection if enabled
if (should_reconnect_) {
InitiateReconnection();
} else {
UpdateConnectionState(ConnectionState::Disconnected, absl::StrFormat("Disconnected: %s", reason));
}
if (status_callback_) {
status_callback_(false, absl::StrFormat("Disconnected: %s", reason));
}
UpdateCollaborationUI("disconnected", "");
});
websocket_->OnError([this](const std::string& error) {
ConsoleError(error.c_str());
is_connected_ = false;
// Initiate reconnection on error
if (should_reconnect_) {
InitiateReconnection();
} else {
UpdateConnectionState(ConnectionState::Disconnected, error);
}
if (status_callback_) {
status_callback_(false, error);
}
});
// Note: is_connected_ will be set in OnOpen callback
UpdateCollaborationUI("session_joining", room_code_.c_str());
return absl::OkStatus();
}
absl::Status WasmCollaboration::LeaveSession() {
if (!is_connected_ && connection_state_ != ConnectionState::Connecting &&
connection_state_ != ConnectionState::Reconnecting) {
return absl::FailedPreconditionError("Not connected to a session");
}
// Disable auto-reconnect when explicitly leaving
should_reconnect_ = false;
// Send leave message if connected
if (is_connected_) {
json msg;
msg["type"] = "leave";
msg["room"] = room_code_;
msg["user_id"] = user_id_;
auto status = websocket_->Send(msg.dump());
if (!status.ok()) {
ConsoleError("Failed to send leave message");
}
}
// Close WebSocket connection
if (websocket_) {
websocket_->Close();
}
is_connected_ = false;
UpdateConnectionState(ConnectionState::Disconnected, "Left session");
// Clear state
room_code_.clear();
session_name_.clear();
stored_password_.clear();
ResetReconnectionState();
{
std::lock_guard<std::mutex> lock(users_mutex_);
users_.clear();
}
{
std::lock_guard<std::mutex> lock(cursors_mutex_);
cursors_.clear();
}
if (status_callback_) {
status_callback_(false, "Left session");
}
UpdateCollaborationUI("session_left", "");
return absl::OkStatus();
}
absl::Status WasmCollaboration::BroadcastChange(
uint32_t offset, const std::vector<uint8_t>& old_data,
const std::vector<uint8_t>& new_data) {
size_t max_size = WasmConfig::Get().collaboration.max_change_size_bytes;
if (old_data.size() > max_size || new_data.size() > max_size) {
return absl::InvalidArgumentError(
absl::StrFormat("Change size exceeds maximum of %d bytes", max_size));
}
// Create change message
json msg;
msg["type"] = "change";
msg["room"] = room_code_;
msg["user_id"] = user_id_;
msg["offset"] = offset;
msg["old_data"] = old_data;
msg["new_data"] = new_data;
msg["timestamp"] = GetCurrentTime();
std::string message = msg.dump();
// If disconnected, queue the message for later
if (!is_connected_) {
if (connection_state_ == ConnectionState::Reconnecting) {
QueueMessageWhileDisconnected(message);
return absl::OkStatus(); // Queued successfully
} else {
return absl::FailedPreconditionError("Not connected to a session");
}
}
auto status = websocket_->Send(message);
if (!status.ok()) {
// Try to queue on send failure
if (connection_state_ == ConnectionState::Reconnecting) {
QueueMessageWhileDisconnected(message);
return absl::OkStatus();
}
return absl::InternalError("Failed to send change");
}
UpdateUserActivity(user_id_);
return absl::OkStatus();
}
absl::Status WasmCollaboration::SendCursorPosition(
const std::string& editor_type, int x, int y, int map_id) {
// Don't queue cursor updates during reconnection - they're transient
if (!is_connected_) {
if (connection_state_ == ConnectionState::Reconnecting) {
return absl::OkStatus(); // Silently drop during reconnection
}
return absl::FailedPreconditionError("Not connected to a session");
}
// Rate limit cursor updates
double now = GetCurrentTime();
double cursor_interval = WasmConfig::Get().collaboration.cursor_send_interval_seconds;
if (now - last_cursor_send_ < cursor_interval) {
return absl::OkStatus(); // Silently skip
}
last_cursor_send_ = now;
// Send cursor update
json msg;
msg["type"] = "cursor";
msg["room"] = room_code_;
msg["user_id"] = user_id_;
msg["editor"] = editor_type;
msg["x"] = x;
msg["y"] = y;
if (map_id >= 0) {
msg["map_id"] = map_id;
}
auto status = websocket_->Send(msg.dump());
if (!status.ok()) {
// Don't fail on cursor send errors during reconnection
if (connection_state_ == ConnectionState::Reconnecting) {
return absl::OkStatus();
}
return absl::InternalError("Failed to send cursor position");
}
UpdateUserActivity(user_id_);
return absl::OkStatus();
}
std::vector<WasmCollaboration::User> WasmCollaboration::GetConnectedUsers() const {
std::lock_guard<std::mutex> lock(users_mutex_);
std::vector<User> result;
for (const auto& [id, user] : users_) {
if (user.is_active) {
result.push_back(user);
}
}
return result;
}
bool WasmCollaboration::IsConnected() const {
return is_connected_ && websocket_ && websocket_->IsConnected();
}
void WasmCollaboration::ProcessPendingChanges() {
std::vector<ChangeEvent> changes_to_apply;
{
std::lock_guard<std::mutex> lock(changes_mutex_);
changes_to_apply = std::move(pending_changes_);
pending_changes_.clear();
}
for (const auto& change : changes_to_apply) {
if (IsChangeValid(change)) {
ApplyRemoteChange(change);
}
}
// Check for user timeouts
CheckUserTimeouts();
}
void WasmCollaboration::HandleMessage(const std::string& message) {
try {
json msg = json::parse(message);
std::string type = msg["type"];
if (type == "create_response") {
// Session created successfully
if (msg["success"]) {
session_name_ = msg["session_name"];
ConsoleLog("Session created successfully");
} else {
ConsoleError(msg["error"].get<std::string>().c_str());
is_connected_ = false;
}
} else if (type == "join_response") {
// Joined session successfully
if (msg["success"]) {
session_name_ = msg["session_name"];
ConsoleLog("Joined session successfully");
} else {
ConsoleError(msg["error"].get<std::string>().c_str());
is_connected_ = false;
}
} else if (type == "users") {
// User list update
std::lock_guard<std::mutex> lock(users_mutex_);
users_.clear();
for (const auto& user_data : msg["list"]) {
User user;
user.id = user_data["id"];
user.name = user_data["name"];
user.color = user_data["color"];
user.is_active = user_data["active"];
user.last_activity = GetCurrentTime();
users_[user.id] = user;
}
if (user_list_callback_) {
user_list_callback_(GetConnectedUsers());
}
// Update UI with user list
json ui_data;
ui_data["users"] = msg["list"];
UpdateCollaborationUI("users_update", ui_data.dump().c_str());
} else if (type == "change") {
// ROM change from another user
if (msg["user_id"] != user_id_) { // Don't process our own changes
ChangeEvent change;
change.offset = msg["offset"];
change.old_data = msg["old_data"].get<std::vector<uint8_t>>();
change.new_data = msg["new_data"].get<std::vector<uint8_t>>();
change.user_id = msg["user_id"];
change.timestamp = msg["timestamp"];
{
std::lock_guard<std::mutex> lock(changes_mutex_);
pending_changes_.push_back(change);
}
UpdateUserActivity(change.user_id);
}
} else if (type == "cursor") {
// Cursor position update
if (msg["user_id"] != user_id_) { // Don't process our own cursor
CursorInfo cursor;
cursor.user_id = msg["user_id"];
cursor.editor_type = msg["editor"];
cursor.x = msg["x"];
cursor.y = msg["y"];
if (msg.contains("map_id")) {
cursor.map_id = msg["map_id"];
}
{
std::lock_guard<std::mutex> lock(cursors_mutex_);
cursors_[cursor.user_id] = cursor;
}
if (cursor_callback_) {
cursor_callback_(cursor);
}
// Update UI with cursor position
json ui_data;
ui_data["user_id"] = cursor.user_id;
ui_data["editor"] = cursor.editor_type;
ui_data["x"] = cursor.x;
ui_data["y"] = cursor.y;
UpdateCollaborationUI("cursor_update", ui_data.dump().c_str());
UpdateUserActivity(cursor.user_id);
}
} else if (type == "error") {
ConsoleError(msg["message"].get<std::string>().c_str());
if (status_callback_) {
status_callback_(false, msg["message"]);
}
}
} catch (const json::exception& e) {
ConsoleError(absl::StrFormat("JSON parse error: %s", e.what()).c_str());
}
}
std::string WasmCollaboration::GenerateUserId() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 15);
std::stringstream ss;
ss << "user_";
for (int i = 0; i < 8; ++i) {
ss << std::hex << dis(gen);
}
return ss.str();
}
std::string WasmCollaboration::GenerateUserColor() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, kUserColors.size() - 1);
return kUserColors[dis(gen)];
}
void WasmCollaboration::UpdateUserActivity(const std::string& user_id) {
std::lock_guard<std::mutex> lock(users_mutex_);
if (users_.find(user_id) != users_.end()) {
users_[user_id].last_activity = GetCurrentTime();
users_[user_id].is_active = true;
}
}
void WasmCollaboration::CheckUserTimeouts() {
double now = GetCurrentTime();
std::lock_guard<std::mutex> lock(users_mutex_);
double timeout = WasmConfig::Get().collaboration.user_timeout_seconds;
bool users_changed = false;
for (auto& [id, user] : users_) {
if (user.is_active && (now - user.last_activity) > timeout) {
user.is_active = false;
users_changed = true;
}
}
if (users_changed && user_list_callback_) {
user_list_callback_(GetConnectedUsers());
}
}
bool WasmCollaboration::IsChangeValid(const ChangeEvent& change) {
// Validate change doesn't exceed ROM bounds
if (!rom_) {
return false;
}
if (change.offset + change.new_data.size() > rom_->size()) {
ConsoleError(absl::StrFormat("Change at offset %u exceeds ROM size",
change.offset).c_str());
return false;
}
// Could add more validation here (e.g., check if area is editable)
return true;
}
void WasmCollaboration::ApplyRemoteChange(const ChangeEvent& change) {
if (!rom_) {
ConsoleError("ROM not set, cannot apply changes");
return;
}
applying_remote_change_ = true;
// Apply the change to the ROM
for (size_t i = 0; i < change.new_data.size(); ++i) {
rom_->WriteByte(change.offset + i, change.new_data[i]);
}
applying_remote_change_ = false;
// Notify the UI about the change
if (change_callback_) {
change_callback_(change);
}
// Update UI with change info
json ui_data;
ui_data["offset"] = change.offset;
ui_data["size"] = change.new_data.size();
ui_data["user_id"] = change.user_id;
UpdateCollaborationUI("change_applied", ui_data.dump().c_str());
}
void WasmCollaboration::UpdateConnectionState(ConnectionState new_state, const std::string& message) {
connection_state_ = new_state;
// Notify via callback
if (connection_state_callback_) {
connection_state_callback_(new_state, message);
}
// Update UI
std::string state_str;
switch (new_state) {
case ConnectionState::Disconnected:
state_str = "disconnected";
break;
case ConnectionState::Connecting:
state_str = "connecting";
break;
case ConnectionState::Connected:
state_str = "connected";
break;
case ConnectionState::Reconnecting:
state_str = "reconnecting";
break;
}
json ui_data;
ui_data["state"] = state_str;
ui_data["message"] = message;
UpdateCollaborationUI("connection_state", ui_data.dump().c_str());
}
void WasmCollaboration::InitiateReconnection() {
if (!should_reconnect_ || room_code_.empty()) {
UpdateConnectionState(ConnectionState::Disconnected, "Disconnected");
return;
}
if (reconnection_attempts_ >= max_reconnection_attempts_) {
ConsoleError(absl::StrFormat("Max reconnection attempts reached (%d), giving up",
max_reconnection_attempts_).c_str());
UpdateConnectionState(ConnectionState::Disconnected, "Reconnection failed - max attempts reached");
ResetReconnectionState();
return;
}
reconnection_attempts_++;
UpdateConnectionState(ConnectionState::Reconnecting,
absl::StrFormat("Reconnecting... (attempt %d/%d)",
reconnection_attempts_, max_reconnection_attempts_));
// Calculate delay with exponential backoff
double delay = std::min(reconnection_delay_seconds_ * std::pow(2, reconnection_attempts_ - 1),
max_reconnection_delay_);
ConsoleLog(absl::StrFormat("Will reconnect in %.1f seconds (attempt %d)",
delay, reconnection_attempts_).c_str());
// Schedule reconnection using emscripten_set_timeout
emscripten_async_call([](void* arg) {
WasmCollaboration* self = static_cast<WasmCollaboration*>(arg);
self->AttemptReconnection();
}, this, delay * 1000); // Convert to milliseconds
}
void WasmCollaboration::AttemptReconnection() {
if (is_connected_ || connection_state_ == ConnectionState::Connected) {
// Already reconnected somehow
ResetReconnectionState();
return;
}
ConsoleLog(absl::StrFormat("Attempting to reconnect to room %s", room_code_).c_str());
// Create new websocket instance
websocket_ = std::make_unique<net::EmscriptenWebSocket>();
// Attempt connection
auto status = websocket_->Connect(websocket_url_);
if (!status.ok()) {
ConsoleError(absl::StrFormat("Reconnection failed: %s", status.message()).c_str());
InitiateReconnection(); // Schedule next attempt
return;
}
// Set up WebSocket callbacks for reconnection
websocket_->OnOpen([this]() {
ConsoleLog("WebSocket reconnected, rejoining session");
is_connected_ = true;
UpdateConnectionState(ConnectionState::Connected, "Reconnected successfully");
// Send rejoin message
json msg;
msg["type"] = "join";
msg["room"] = room_code_;
msg["user"] = username_;
msg["user_id"] = user_id_;
msg["color"] = user_color_;
if (!stored_password_.empty()) {
msg["password"] = stored_password_;
}
msg["rejoin"] = true; // Indicate this is a reconnection
auto send_status = websocket_->Send(msg.dump());
if (!send_status.ok()) {
ConsoleError("Failed to send rejoin message");
}
// Reset reconnection state on success
ResetReconnectionState();
// Send any queued messages
std::vector<std::string> messages_to_send;
{
std::lock_guard<std::mutex> lock(message_queue_mutex_);
messages_to_send = std::move(queued_messages_);
queued_messages_.clear();
}
for (const auto& msg : messages_to_send) {
websocket_->Send(msg);
}
if (status_callback_) {
status_callback_(true, "Reconnected to session");
}
UpdateCollaborationUI("session_reconnected", room_code_.c_str());
});
websocket_->OnMessage([this](const std::string& message) {
HandleMessage(message);
});
websocket_->OnClose([this](int code, const std::string& reason) {
is_connected_ = false;
ConsoleLog(absl::StrFormat("Reconnection WebSocket closed: %s", reason).c_str());
// Attempt reconnection again
InitiateReconnection();
if (status_callback_) {
status_callback_(false, absl::StrFormat("Disconnected: %s", reason));
}
});
websocket_->OnError([this](const std::string& error) {
ConsoleError(absl::StrFormat("Reconnection error: %s", error).c_str());
is_connected_ = false;
// Attempt reconnection again
InitiateReconnection();
if (status_callback_) {
status_callback_(false, error);
}
});
}
void WasmCollaboration::ResetReconnectionState() {
reconnection_attempts_ = 0;
reconnection_delay_seconds_ = 1.0; // Reset to initial delay
}
void WasmCollaboration::QueueMessageWhileDisconnected(const std::string& message) {
std::lock_guard<std::mutex> lock(message_queue_mutex_);
// Limit queue size to prevent memory issues
if (queued_messages_.size() >= max_queued_messages_) {
ConsoleLog("Message queue full, dropping oldest message");
queued_messages_.erase(queued_messages_.begin());
}
queued_messages_.push_back(message);
ConsoleLog(absl::StrFormat("Queued message for reconnection (queue size: %d)",
queued_messages_.size()).c_str());
}
// ---------------------------------------------------------------------------
// JS bindings for WASM (exported with EMSCRIPTEN_KEEPALIVE)
// ---------------------------------------------------------------------------
extern "C" {
EMSCRIPTEN_KEEPALIVE const char* WasmCollaborationCreate(
const char* session_name, const char* username, const char* password) {
static std::string last_room_code;
if (!session_name || !username) {
ConsoleError("Invalid session/user parameters");
return nullptr;
}
auto& collab = GetInstance();
auto result = collab.CreateSession(session_name, username,
password ? std::string(password) : "");
if (!result.ok()) {
ConsoleError(std::string(result.status().message()).c_str());
return nullptr;
}
last_room_code = *result;
return last_room_code.c_str();
}
EMSCRIPTEN_KEEPALIVE int WasmCollaborationJoin(const char* room_code,
const char* username,
const char* password) {
if (!room_code || !username) {
ConsoleError("room_code and username are required");
return 0;
}
auto& collab = GetInstance();
auto status = collab.JoinSession(room_code, username,
password ? std::string(password) : "");
if (!status.ok()) {
ConsoleError(std::string(status.message()).c_str());
return 0;
}
return 1;
}
EMSCRIPTEN_KEEPALIVE int WasmCollaborationLeave() {
auto& collab = GetInstance();
auto status = collab.LeaveSession();
return status.ok() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int WasmCollaborationSendCursor(
const char* editor_type, int x, int y, int map_id) {
auto& collab = GetInstance();
auto status = collab.SendCursorPosition(editor_type ? editor_type : "unknown",
x, y, map_id);
return status.ok() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int WasmCollaborationBroadcastChange(
uint32_t offset, const uint8_t* new_data, size_t length) {
if (!new_data && length > 0) {
return 0;
}
auto& collab = GetInstance();
std::vector<uint8_t> data;
data.reserve(length);
for (size_t i = 0; i < length; ++i) {
data.push_back(new_data[i]);
}
std::vector<uint8_t> old_data; // Not tracked in WASM path
auto status = collab.BroadcastChange(offset, old_data, data);
return status.ok() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE void WasmCollaborationSetServerUrl(const char* url) {
if (!url) return;
auto& collab = GetInstance();
collab.SetWebSocketUrl(std::string(url));
}
EMSCRIPTEN_KEEPALIVE int WasmCollaborationIsConnected() {
return GetInstance().IsConnected() ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE const char* WasmCollaborationGetRoomCode() {
static std::string room;
room = GetInstance().GetRoomCode();
return room.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* WasmCollaborationGetUserId() {
static std::string user;
user = GetInstance().GetUserId();
return user.c_str();
}
} // extern "C"
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,440 @@
#ifndef YAZE_APP_PLATFORM_WASM_COLLABORATION_H_
#define YAZE_APP_PLATFORM_WASM_COLLABORATION_H_
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#include <emscripten/val.h>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/net/wasm/emscripten_websocket.h"
#include "rom/rom.h"
namespace yaze {
namespace app {
namespace platform {
/**
* @brief Real-time collaboration manager for WASM builds
*
* Enables multiple users to edit ROMs together in the browser.
* Uses WebSocket connection for real-time synchronization.
*/
class WasmCollaboration {
public:
/**
* @brief User information for collaboration session
*/
struct User {
std::string id;
std::string name;
std::string color; // Hex color for cursor/highlights
bool is_active = true;
double last_activity = 0; // Timestamp
};
/**
* @brief Cursor position information
*/
struct CursorInfo {
std::string user_id;
std::string editor_type; // "overworld", "dungeon", etc.
int x = 0;
int y = 0;
int map_id = -1; // For context (which map/room)
};
/**
* @brief ROM change event for synchronization
*/
struct ChangeEvent {
uint32_t offset;
std::vector<uint8_t> old_data;
std::vector<uint8_t> new_data;
std::string user_id;
double timestamp;
};
// Connection state enum
enum class ConnectionState {
Disconnected,
Connecting,
Connected,
Reconnecting
};
// Callbacks for UI updates
using UserListCallback = std::function<void(const std::vector<User>&)>;
using ChangeCallback = std::function<void(const ChangeEvent&)>;
using CursorCallback = std::function<void(const CursorInfo&)>;
using StatusCallback = std::function<void(bool connected, const std::string& message)>;
using ConnectionStateCallback = std::function<void(ConnectionState state, const std::string& message)>;
WasmCollaboration();
~WasmCollaboration();
/**
* @brief Set the WebSocket server URL
* @param url Full WebSocket URL (e.g., "wss://your-server.com/ws")
* @return Status indicating success or validation failure
*
* For GitHub Pages deployment, you'll need a separate WebSocket server.
* Options include:
* - Cloudflare Workers with Durable Objects
* - Deno Deploy
* - Railway, Render, or other PaaS providers
* - Self-hosted server (e.g., yaze-server on port 8765)
*
* URL must start with "ws://" or "wss://" to be valid.
*/
absl::Status SetWebSocketUrl(const std::string& url) {
if (url.empty()) {
websocket_url_.clear();
return absl::OkStatus();
}
if (url.find("ws://") != 0 && url.find("wss://") != 0) {
return absl::InvalidArgumentError(
"WebSocket URL must start with ws:// or wss://");
}
// Basic URL structure validation
if (url.length() < 8) { // Minimum: "ws://x" or "wss://x"
return absl::InvalidArgumentError("WebSocket URL is too short");
}
websocket_url_ = url;
return absl::OkStatus();
}
/**
* @brief Get the current WebSocket server URL
* @return Current URL or empty if not configured
*/
std::string GetWebSocketUrl() const { return websocket_url_; }
/**
* @brief Initialize WebSocket URL from JavaScript configuration
*
* Looks for window.YAZE_CONFIG.collaborationServerUrl in the browser.
* This allows deployment-specific configuration without recompiling.
*/
void InitializeFromConfig();
/**
* @brief Check if collaboration is configured and available
* @return true if WebSocket URL is set and valid
*/
bool IsConfigured() const { return !websocket_url_.empty(); }
/**
* @brief Create a new collaboration session
* @param session_name Name for the session
* @param username User's display name
* @return Room code for others to join
*/
absl::StatusOr<std::string> CreateSession(const std::string& session_name,
const std::string& username,
const std::string& password = "");
/**
* @brief Join an existing collaboration session
* @param room_code 6-character room code
* @param username User's display name
* @return Status of connection attempt
*/
absl::Status JoinSession(const std::string& room_code,
const std::string& username,
const std::string& password = "");
/**
* @brief Leave current collaboration session
*/
absl::Status LeaveSession();
/**
* @brief Broadcast a ROM change to all peers
* @param offset ROM offset that changed
* @param old_data Original data
* @param new_data New data
*/
absl::Status BroadcastChange(uint32_t offset,
const std::vector<uint8_t>& old_data,
const std::vector<uint8_t>& new_data);
/**
* @brief Send cursor position update
* @param editor_type Current editor ("overworld", "dungeon", etc.)
* @param x X position in editor
* @param y Y position in editor
* @param map_id Optional map/room ID for context
*/
absl::Status SendCursorPosition(const std::string& editor_type,
int x, int y, int map_id = -1);
/**
* @brief Set ROM reference for applying changes
* @param rom Pointer to the ROM being edited
*/
void SetRom(Rom* rom) { rom_ = rom; }
/**
* @brief Register callback for ROM changes from peers
* @param callback Function to call when changes arrive
*/
void SetChangeCallback(ChangeCallback callback) {
change_callback_ = callback;
}
/**
* @brief Register callback for user list updates
* @param callback Function to call when user list changes
*/
void SetUserListCallback(UserListCallback callback) {
user_list_callback_ = callback;
}
/**
* @brief Register callback for cursor position updates
* @param callback Function to call when cursor positions update
*/
void SetCursorCallback(CursorCallback callback) {
cursor_callback_ = callback;
}
/**
* @brief Register callback for connection status changes
* @param callback Function to call on status changes
*/
void SetStatusCallback(StatusCallback callback) {
status_callback_ = callback;
}
/**
* @brief Register callback for connection state changes
* @param callback Function to call on connection state changes
*/
void SetConnectionStateCallback(ConnectionStateCallback callback) {
connection_state_callback_ = callback;
}
/**
* @brief Get current connection state
* @return Current connection state
*/
ConnectionState GetConnectionState() const {
return connection_state_;
}
/**
* @brief Get list of connected users
* @return Vector of active users
*/
std::vector<User> GetConnectedUsers() const;
/**
* @brief Check if currently connected to a session
* @return true if connected
*/
bool IsConnected() const;
/**
* @brief Whether we're currently applying a remote change (used to avoid rebroadcast)
*/
bool IsApplyingRemoteChange() const { return applying_remote_change_; }
/**
* @brief Get current room code
* @return Room code or empty string if not connected
*/
std::string GetRoomCode() const { return room_code_; }
/**
* @brief Get session name
* @return Session name or empty string if not connected
*/
std::string GetSessionName() const { return session_name_; }
/**
* @brief Get current user id (stable per session)
*/
std::string GetUserId() const { return user_id_; }
/**
* @brief Enable/disable automatic conflict resolution
* @param enable true to enable auto-resolution
*/
void SetAutoResolveConflicts(bool enable) {
auto_resolve_conflicts_ = enable;
}
/**
* @brief Process pending changes queue
* Called periodically to apply remote changes
*/
void ProcessPendingChanges();
private:
// WebSocket message handlers
void HandleMessage(const std::string& message);
void HandleCreateResponse(const emscripten::val& data);
void HandleJoinResponse(const emscripten::val& data);
void HandleUserList(const emscripten::val& data);
void HandleChange(const emscripten::val& data);
void HandleCursor(const emscripten::val& data);
void HandleError(const emscripten::val& data);
// Utility methods
std::string GenerateUserId();
std::string GenerateUserColor();
void UpdateUserActivity(const std::string& user_id);
void CheckUserTimeouts();
bool IsChangeValid(const ChangeEvent& change);
void ApplyRemoteChange(const ChangeEvent& change);
// Reconnection management
void InitiateReconnection();
void AttemptReconnection();
void ResetReconnectionState();
void UpdateConnectionState(ConnectionState new_state, const std::string& message);
void QueueMessageWhileDisconnected(const std::string& message);
// Connection management
std::unique_ptr<net::EmscriptenWebSocket> websocket_;
bool is_connected_ = false;
ConnectionState connection_state_ = ConnectionState::Disconnected;
std::string websocket_url_; // Set via SetWebSocketUrl() or environment
// Reconnection state
int reconnection_attempts_ = 0;
int max_reconnection_attempts_ = 10;
double reconnection_delay_seconds_ = 1.0; // Initial delay
double max_reconnection_delay_ = 30.0; // Max delay between attempts
bool should_reconnect_ = false;
std::string stored_password_; // Store password for reconnection
// Session state
std::string room_code_;
std::string session_name_;
std::string user_id_;
std::string username_;
std::string user_color_;
// Connected users
std::map<std::string, User> users_;
mutable std::mutex users_mutex_;
// Remote cursors
std::map<std::string, CursorInfo> cursors_;
mutable std::mutex cursors_mutex_;
// Change queue for conflict resolution
std::vector<ChangeEvent> pending_changes_;
mutable std::mutex changes_mutex_;
// Message queue for disconnected state
std::vector<std::string> queued_messages_;
mutable std::mutex message_queue_mutex_;
size_t max_queued_messages_ = 100; // Limit queue size
// Configuration
bool auto_resolve_conflicts_ = true;
// Callbacks
UserListCallback user_list_callback_;
ChangeCallback change_callback_;
CursorCallback cursor_callback_;
StatusCallback status_callback_;
ConnectionStateCallback connection_state_callback_;
// ROM reference for applying changes
Rom* rom_ = nullptr;
// Rate limiting
double last_cursor_send_ = 0;
// Guard to prevent echoing remote writes back to the server
bool applying_remote_change_ = false;
};
// Singleton accessor used by JS bindings and the WASM main loop
WasmCollaboration& GetWasmCollaborationInstance();
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub for non-WASM builds
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include <string>
#include <vector>
namespace yaze {
namespace app {
namespace platform {
class WasmCollaboration {
public:
struct User {
std::string id;
std::string name;
std::string color;
bool is_active = true;
};
struct CursorInfo {
std::string user_id;
std::string editor_type;
int x = 0;
int y = 0;
int map_id = -1;
};
struct ChangeEvent {
uint32_t offset;
std::vector<uint8_t> old_data;
std::vector<uint8_t> new_data;
std::string user_id;
double timestamp;
};
absl::StatusOr<std::string> CreateSession(const std::string&,
const std::string&) {
return absl::UnimplementedError("Collaboration requires WASM build");
}
absl::Status JoinSession(const std::string&, const std::string&) {
return absl::UnimplementedError("Collaboration requires WASM build");
}
absl::Status LeaveSession() {
return absl::UnimplementedError("Collaboration requires WASM build");
}
absl::Status BroadcastChange(uint32_t, const std::vector<uint8_t>&,
const std::vector<uint8_t>&) {
return absl::UnimplementedError("Collaboration requires WASM build");
}
std::vector<User> GetConnectedUsers() const { return {}; }
bool IsConnected() const { return false; }
std::string GetRoomCode() const { return ""; }
std::string GetSessionName() const { return ""; }
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_COLLABORATION_H_

View File

@@ -0,0 +1,181 @@
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_config.h"
#include <cstdlib>
#include <emscripten.h>
namespace yaze {
namespace app {
namespace platform {
// clang-format off
// Helper to read string from JS config
EM_JS(char*, WasmConfig_GetString, (const char* path, const char* defaultVal), {
try {
var config = window.YAZE_CONFIG || {};
var parts = UTF8ToString(path).split('.');
var value = config;
for (var i = 0; i < parts.length; i++) {
if (value && typeof value === 'object' && parts[i] in value) {
value = value[parts[i]];
} else {
value = UTF8ToString(defaultVal);
break;
}
}
if (typeof value !== 'string') {
value = UTF8ToString(defaultVal);
}
var lengthBytes = lengthBytesUTF8(value) + 1;
var stringOnWasmHeap = _malloc(lengthBytes);
stringToUTF8(value, stringOnWasmHeap, lengthBytes);
return stringOnWasmHeap;
} catch (e) {
console.error('[WasmConfig] Error reading string:', e);
var def = UTF8ToString(defaultVal);
var len = lengthBytesUTF8(def) + 1;
var ptr = _malloc(len);
stringToUTF8(def, ptr, len);
return ptr;
}
});
// Helper to read number from JS config
EM_JS(double, WasmConfig_GetNumber, (const char* path, double defaultVal), {
try {
var config = window.YAZE_CONFIG || {};
var parts = UTF8ToString(path).split('.');
var value = config;
for (var i = 0; i < parts.length; i++) {
if (value && typeof value === 'object' && parts[i] in value) {
value = value[parts[i]];
} else {
return defaultVal;
}
}
return typeof value === 'number' ? value : defaultVal;
} catch (e) {
console.error('[WasmConfig] Error reading number:', e);
return defaultVal;
}
});
// Helper to read int from JS config
EM_JS(int, WasmConfig_GetInt, (const char* path, int defaultVal), {
try {
var config = window.YAZE_CONFIG || {};
var parts = UTF8ToString(path).split('.');
var value = config;
for (var i = 0; i < parts.length; i++) {
if (value && typeof value === 'object' && parts[i] in value) {
value = value[parts[i]];
} else {
return defaultVal;
}
}
return typeof value === 'number' ? Math.floor(value) : defaultVal;
} catch (e) {
console.error('[WasmConfig] Error reading int:', e);
return defaultVal;
}
});
// clang-format on
void WasmConfig::LoadFromJavaScript() {
// Prevent concurrent loading
bool expected = false;
if (!loading_.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
// Already loading, wait for completion
return;
}
// Lock for writing config values
std::lock_guard<std::mutex> lock(config_mutex_);
// Collaboration settings
char* server_url = WasmConfig_GetString("collaboration.serverUrl", "");
collaboration.server_url = std::string(server_url);
free(server_url);
collaboration.user_timeout_seconds =
WasmConfig_GetNumber("collaboration.userTimeoutSeconds", 30.0);
collaboration.cursor_send_interval_seconds =
WasmConfig_GetNumber("collaboration.cursorSendIntervalMs", 100.0) / 1000.0;
collaboration.max_change_size_bytes = static_cast<size_t>(
WasmConfig_GetInt("collaboration.maxChangeSizeBytes", 1024));
// Autosave settings
autosave.interval_seconds =
WasmConfig_GetInt("autosave.intervalSeconds", 60);
autosave.max_recovery_slots =
WasmConfig_GetInt("autosave.maxRecoverySlots", 5);
// Terminal settings
terminal.max_history_items =
WasmConfig_GetInt("terminal.maxHistoryItems", 50);
terminal.max_output_lines =
WasmConfig_GetInt("terminal.maxOutputLines", 1000);
// UI settings
ui.min_zoom = static_cast<float>(WasmConfig_GetNumber("ui.minZoom", 0.25));
ui.max_zoom = static_cast<float>(WasmConfig_GetNumber("ui.maxZoom", 4.0));
ui.touch_gesture_threshold =
WasmConfig_GetInt("ui.touchGestureThreshold", 10);
// Cache settings
char* cache_version = WasmConfig_GetString("cache.version", "v1");
cache.version = std::string(cache_version);
free(cache_version);
cache.max_rom_cache_size_mb =
WasmConfig_GetInt("cache.maxRomCacheSizeMb", 100);
// AI settings
ai.enabled = WasmConfig_GetInt("ai.enabled", 1) != 0;
char* ai_model = WasmConfig_GetString("ai.model", "gemini-2.5-flash");
ai.model = std::string(ai_model);
free(ai_model);
char* ai_endpoint = WasmConfig_GetString("ai.endpoint", "");
ai.endpoint = std::string(ai_endpoint);
free(ai_endpoint);
ai.max_response_length = WasmConfig_GetInt("ai.maxResponseLength", 4096);
// Deployment info (read-only defaults, but can be overridden)
char* server_repo = WasmConfig_GetString("deployment.serverRepo",
"https://github.com/scawful/yaze-server");
deployment.server_repo = std::string(server_repo);
free(server_repo);
deployment.default_port = WasmConfig_GetInt("deployment.defaultPort", 8765);
char* protocol_version = WasmConfig_GetString("deployment.protocolVersion", "2.0");
deployment.protocol_version = std::string(protocol_version);
free(protocol_version);
loaded_.store(true, std::memory_order_release);
loading_.store(false, std::memory_order_release);
}
WasmConfig& WasmConfig::Get() {
static WasmConfig instance;
return instance;
}
void WasmConfig::FetchServerStatus() {
// Server status fetching is handled via JavaScript in the web shell.
// The web shell calls fetch() on /health and populates window.YAZE_SERVER_STATUS.
// This C++ function is a stub - actual status is read from JS config on next LoadFromJavaScript().
// TODO: Implement async status fetching via emscripten_async_call if needed.
}
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,270 @@
#ifndef YAZE_APP_PLATFORM_WASM_CONFIG_H_
#define YAZE_APP_PLATFORM_WASM_CONFIG_H_
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <atomic>
#include <mutex>
#include <string>
namespace yaze {
namespace app {
namespace platform {
/**
* @brief Centralized configuration for WASM platform features
*
* All configurable values are loaded from JavaScript's window.YAZE_CONFIG
* object, allowing deployment-specific customization without recompiling.
*
* Usage in JavaScript (before WASM loads):
* @code
* window.YAZE_CONFIG = {
* collaboration: {
* serverUrl: "wss://your-server.com/ws",
* userTimeoutSeconds: 30.0,
* cursorSendIntervalMs: 100,
* maxChangeSizeBytes: 1024
* },
* autosave: {
* intervalSeconds: 60,
* maxRecoverySlots: 5
* },
* terminal: {
* maxHistoryItems: 50,
* maxOutputLines: 1000
* },
* ui: {
* minZoom: 0.25,
* maxZoom: 4.0,
* touchGestureThreshold: 10
* },
* cache: {
* version: "v1",
* maxRomCacheSizeMb: 100
* }
* };
* @endcode
*/
struct WasmConfig {
// Collaboration settings
struct Collaboration {
std::string server_url;
double user_timeout_seconds = 30.0;
double cursor_send_interval_seconds = 0.1; // 100ms
size_t max_change_size_bytes = 1024;
} collaboration;
// Autosave settings
struct Autosave {
int interval_seconds = 60;
int max_recovery_slots = 5;
} autosave;
// Terminal settings
struct Terminal {
int max_history_items = 50;
int max_output_lines = 1000;
} terminal;
// UI settings
struct UI {
float min_zoom = 0.25f;
float max_zoom = 4.0f;
int touch_gesture_threshold = 10;
} ui;
// Cache settings
struct Cache {
std::string version = "v1";
int max_rom_cache_size_mb = 100;
} cache;
// AI service settings (for terminal AI commands)
struct AI {
bool enabled = true;
std::string model = "gemini-2.5-flash";
std::string endpoint; // Empty = use collaboration server
int max_response_length = 4096;
} ai;
// Server deployment info
struct Deployment {
std::string server_repo = "https://github.com/scawful/yaze-server";
int default_port = 8765;
std::string protocol_version = "2.0";
} deployment;
// Server status (populated by FetchServerStatus)
struct ServerStatus {
bool fetched = false;
bool reachable = false;
bool ai_enabled = false;
bool ai_configured = false;
std::string ai_provider; // "gemini", "external", "none"
bool tls_detected = false;
std::string persistence_type; // "memory", "file"
int active_sessions = 0;
int total_connections = 0;
std::string server_version;
std::string error_message;
} server_status;
/**
* @brief Load configuration from JavaScript window.YAZE_CONFIG
*
* Call this once during initialization to populate all config values
* from the JavaScript environment.
*/
void LoadFromJavaScript();
/**
* @brief Fetch server status from /health endpoint asynchronously
*
* Populates server_status struct with reachability, AI status, TLS info.
* Safe to call multiple times; will update server_status on each call.
*/
void FetchServerStatus();
/**
* @brief Get the singleton configuration instance
* @return Reference to the global config
*/
static WasmConfig& Get();
/**
* @brief Check if config was loaded from JavaScript
* @return true if LoadFromJavaScript() was called successfully
*/
bool IsLoaded() const { return loaded_.load(std::memory_order_acquire); }
/**
* @brief Check if config is currently being loaded
* @return true if LoadFromJavaScript() is in progress
*/
bool IsLoading() const { return loading_.load(std::memory_order_acquire); }
/**
* @brief Get read access to config (thread-safe)
* @return Lock guard for read operations
*
* Use this when reading multiple config values to ensure consistency.
* Example:
* @code
* auto lock = WasmConfig::Get().GetReadLock();
* auto url = WasmConfig::Get().collaboration.server_url;
* auto timeout = WasmConfig::Get().collaboration.user_timeout_seconds;
* @endcode
*/
std::unique_lock<std::mutex> GetReadLock() const {
return std::unique_lock<std::mutex>(config_mutex_);
}
private:
std::atomic<bool> loaded_{false};
std::atomic<bool> loading_{false};
mutable std::mutex config_mutex_;
};
// External C declarations for functions implemented in .cc
extern "C" {
char* WasmConfig_GetString(const char* path, const char* defaultVal);
double WasmConfig_GetNumber(const char* path, double defaultVal);
int WasmConfig_GetInt(const char* path, int defaultVal);
}
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub for non-WASM builds - provides defaults
#include <mutex>
#include <string>
namespace yaze {
namespace app {
namespace platform {
struct WasmConfig {
struct Collaboration {
std::string server_url;
double user_timeout_seconds = 30.0;
double cursor_send_interval_seconds = 0.1;
size_t max_change_size_bytes = 1024;
} collaboration;
struct Autosave {
int interval_seconds = 60;
int max_recovery_slots = 5;
} autosave;
struct Terminal {
int max_history_items = 50;
int max_output_lines = 1000;
} terminal;
struct UI {
float min_zoom = 0.25f;
float max_zoom = 4.0f;
int touch_gesture_threshold = 10;
} ui;
struct Cache {
std::string version = "v1";
int max_rom_cache_size_mb = 100;
} cache;
struct AI {
bool enabled = true;
std::string model = "gemini-2.5-flash";
std::string endpoint;
int max_response_length = 4096;
} ai;
struct Deployment {
std::string server_repo = "https://github.com/scawful/yaze-server";
int default_port = 8765;
std::string protocol_version = "2.0";
} deployment;
struct ServerStatus {
bool fetched = false;
bool reachable = false;
bool ai_enabled = false;
bool ai_configured = false;
std::string ai_provider;
bool tls_detected = false;
std::string persistence_type;
int active_sessions = 0;
int total_connections = 0;
std::string server_version;
std::string error_message;
} server_status;
void LoadFromJavaScript() {}
void FetchServerStatus() {}
static WasmConfig& Get() {
static WasmConfig instance;
return instance;
}
bool IsLoaded() const { return true; }
bool IsLoading() const { return false; }
std::unique_lock<std::mutex> GetReadLock() const {
return std::unique_lock<std::mutex>(config_mutex_);
}
private:
mutable std::mutex config_mutex_;
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_CONFIG_H_

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,587 @@
#ifndef YAZE_APP_PLATFORM_WASM_CONTROL_API_H_
#define YAZE_APP_PLATFORM_WASM_CONTROL_API_H_
#ifdef __EMSCRIPTEN__
#include <functional>
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace yaze {
// Forward declarations
class Rom;
namespace editor {
class EditorManager;
class PanelManager;
} // namespace editor
namespace app {
namespace platform {
/**
* @brief Unified WASM Control API for browser/agent access
*
* Provides programmatic control over the editor UI from JavaScript.
* Exposed as window.yaze.control.* in the browser.
*
* API Surface:
* - window.yaze.control.switchEditor("Overworld")
* - window.yaze.control.openPanel("dungeon.room_selector")
* - window.yaze.control.closePanel("dungeon.room_selector")
* - window.yaze.control.togglePanel("dungeon.room_selector")
* - window.yaze.control.triggerMenuAction("File.Save")
* - window.yaze.control.setPanelLayout("dungeon_default")
* - window.yaze.control.getVisiblePanels()
* - window.yaze.control.getCurrentEditor()
* - window.yaze.control.getAvailableEditors()
* - window.yaze.control.getAvailablePanels()
*/
class WasmControlApi {
public:
/**
* @brief Initialize the control API with editor manager reference
* @param editor_manager Pointer to the main editor manager
*/
static void Initialize(editor::EditorManager* editor_manager);
/**
* @brief Check if the control API is ready
* @return true if initialized and ready for use
*/
static bool IsReady();
/**
* @brief Setup JavaScript bindings for window.yaze.control
*/
static void SetupJavaScriptBindings();
// ============================================================================
// Editor Control
// ============================================================================
/**
* @brief Switch to a specific editor by name
* @param editor_name Name of editor ("Overworld", "Dungeon", "Graphics", etc.)
* @return JSON result with success/error
*/
static std::string SwitchEditor(const std::string& editor_name);
/**
* @brief Get the currently active editor name
* @return JSON with editor name and type
*/
static std::string GetCurrentEditor();
/**
* @brief Get list of available editors
* @return JSON array of editor info objects
*/
static std::string GetAvailableEditors();
// ============================================================================
// Panel Control
// ============================================================================
/**
* @brief Open/show a panel by ID
* @param card_id Panel identifier (e.g., "dungeon.room_selector")
* @return JSON result with success/error
*/
static std::string OpenPanel(const std::string& card_id);
/**
* @brief Close/hide a panel by ID
* @param card_id Panel identifier
* @return JSON result with success/error
*/
static std::string ClosePanel(const std::string& card_id);
/**
* @brief Toggle a panel's visibility
* @param card_id Panel identifier
* @return JSON result with new visibility state
*/
static std::string TogglePanel(const std::string& card_id);
/**
* @brief Get list of currently visible panels
* @return JSON array of visible panel IDs
*/
static std::string GetVisiblePanels();
/**
* @brief Get all available panels for current session
* @return JSON array of panel info objects
*/
static std::string GetAvailablePanels();
/**
* @brief Get panels for a specific category
* @param category Category name (e.g., "Dungeon", "Overworld")
* @return JSON array of panel info objects
*/
static std::string GetPanelsInCategory(const std::string& category);
/**
* @brief Show all panels in the current session
* @return JSON result with success/error
*/
static std::string ShowAllPanels();
/**
* @brief Hide all panels in the current session
* @return JSON result with success/error
*/
static std::string HideAllPanels();
/**
* @brief Show all panels in a specific category
* @param category Category name
* @return JSON result with success/error
*/
static std::string ShowAllPanelsInCategory(const std::string& category);
/**
* @brief Hide all panels in a specific category
* @param category Category name
* @return JSON result with success/error
*/
static std::string HideAllPanelsInCategory(const std::string& category);
/**
* @brief Show only one panel, hiding all others in its category
* @param card_id Panel identifier
* @return JSON result with success/error
*/
static std::string ShowOnlyPanel(const std::string& card_id);
// ============================================================================
// Layout Control
// ============================================================================
/**
* @brief Apply a predefined panel layout
* @param layout_name Layout preset name ("dungeon_default", "overworld_default", etc.)
* @return JSON result with success/error
*/
static std::string SetPanelLayout(const std::string& layout_name);
/**
* @brief Get list of available layout presets
* @return JSON array of layout names
*/
static std::string GetAvailableLayouts();
/**
* @brief Save current panel visibility as a custom layout
* @param layout_name Name for the new layout
* @return JSON result with success/error
*/
static std::string SaveCurrentLayout(const std::string& layout_name);
// ============================================================================
// Menu/UI Actions
// ============================================================================
/**
* @brief Trigger a menu action by path
* @param action_path Menu path (e.g., "File.Save", "Edit.Undo", "View.ShowEmulator")
* @return JSON result with success/error
*/
static std::string TriggerMenuAction(const std::string& action_path);
/**
* @brief Get list of available menu actions
* @return JSON array of action paths
*/
static std::string GetAvailableMenuActions();
/**
* @brief Toggle the visibility of the menu bar
* @return JSON result with new visibility state
*/
static std::string ToggleMenuBar();
// ============================================================================
// Session Control
// ============================================================================
/**
* @brief Get current session information
* @return JSON with session ID, ROM info, editor state
*/
static std::string GetSessionInfo();
/**
* @brief Create a new editing session
* @return JSON with new session info
*/
static std::string CreateSession();
/**
* @brief Switch to a different session by index
* @param session_index Session index to switch to
* @return JSON result with success/error
*/
static std::string SwitchSession(int session_index);
// ============================================================================
// ROM Control
// ============================================================================
/**
* @brief Get ROM status and basic info
* @return JSON with ROM loaded state, filename, size
*/
static std::string GetRomStatus();
/**
* @brief Read bytes from ROM
* @param address ROM address
* @param count Number of bytes to read
* @return JSON with byte array
*/
static std::string ReadRomBytes(int address, int count);
/**
* @brief Write bytes to ROM
* @param address ROM address
* @param bytes JSON array of bytes to write
* @return JSON result with success/error
*/
static std::string WriteRomBytes(int address, const std::string& bytes_json);
/**
* @brief Trigger ROM save
* @return JSON result with success/error
*/
static std::string SaveRom();
// ============================================================================
// Editor State APIs (for LLM agents and automation)
// ============================================================================
/**
* @brief Get a comprehensive snapshot of the current editor state
* @return JSON with editor_type, active_data, visible_cards, etc.
*/
static std::string GetEditorSnapshot();
/**
* @brief Get current dungeon room information
* @return JSON with room_id, room_count, active_rooms, etc.
*/
static std::string GetCurrentDungeonRoom();
/**
* @brief Get current overworld map information
* @return JSON with map_id, world, game_state, etc.
*/
static std::string GetCurrentOverworldMap();
/**
* @brief Get the current selection in the active editor
* @return JSON with selected items/entities
*/
static std::string GetEditorSelection();
// ============================================================================
// Read-only Data APIs
// ============================================================================
/**
* @brief Get dungeon room tile data
* @param room_id Room ID (0-295)
* @return JSON with layer1, layer2 tile arrays
*/
static std::string GetRoomTileData(int room_id);
/**
* @brief Get objects in a dungeon room
* @param room_id Room ID (0-295)
* @return JSON array of room objects
*/
static std::string GetRoomObjects(int room_id);
/**
* @brief Get dungeon room properties
* @param room_id Room ID (0-295)
* @return JSON with music, palette, tileset, etc.
*/
static std::string GetRoomProperties(int room_id);
/**
* @brief Get overworld map tile data
* @param map_id Map ID (0-159)
* @return JSON with tile array
*/
static std::string GetMapTileData(int map_id);
/**
* @brief Get entities on an overworld map
* @param map_id Map ID (0-159)
* @return JSON with entrances, exits, items, sprites
*/
static std::string GetMapEntities(int map_id);
/**
* @brief Get overworld map properties
* @param map_id Map ID (0-159)
* @return JSON with gfx_group, palette_group, area_size, etc.
*/
static std::string GetMapProperties(int map_id);
/**
* @brief Get palette colors
* @param group_name Palette group name
* @param palette_id Palette ID within group
* @return JSON with colors array
*/
static std::string GetPaletteData(const std::string& group_name, int palette_id);
/**
* @brief Get list of available palette groups
* @return JSON array of group names
*/
static std::string ListPaletteGroups();
/**
* @brief Load a font from binary data
* @param name Font name
* @param data Binary font data
* @param size Font size
* @return JSON result with success/error
*/
static std::string LoadFont(const std::string& name, const std::string& data, float size);
// ============================================================================
// GUI Automation APIs (for LLM agents)
// ============================================================================
/**
* @brief Get the UI element tree for automation
* @return JSON with UI elements, their bounds, and types
*/
static std::string GetUIElementTree();
/**
* @brief Get bounds of a specific UI element by ID
* @param element_id Element identifier
* @return JSON with x, y, width, height, visible
*/
static std::string GetUIElementBounds(const std::string& element_id);
/**
* @brief Set selection in active editor
* @param ids_json JSON array of IDs to select
* @return JSON result with success/error
*/
static std::string SetSelection(const std::string& ids_json);
// ============================================================================
// Platform Info API
// ============================================================================
/**
* @brief Get platform information for keyboard shortcuts and UI display
* @return JSON with platform name, is_mac, ctrl_name, alt_name
*
* Example response:
* {
* "platform": "WebMac",
* "is_mac": true,
* "ctrl_display": "Cmd",
* "alt_display": "Opt",
* "shift_display": "Shift"
* }
*/
static std::string GetPlatformInfo();
// ============================================================================
// Agent API (for AI/LLM agent integration)
// ============================================================================
/**
* @brief Check if agent system is ready
* @return true if agent is initialized and available
*/
static bool AgentIsReady();
/**
* @brief Send a message to the AI agent
* @param message User message to send
* @return JSON with response, status, proposals (if any)
*/
static std::string AgentSendMessage(const std::string& message);
/**
* @brief Get chat history
* @return JSON array of chat messages
*/
static std::string AgentGetChatHistory();
/**
* @brief Get current agent configuration
* @return JSON with provider, model, host, etc.
*/
static std::string AgentGetConfig();
/**
* @brief Set agent configuration
* @param config_json JSON configuration object
* @return JSON result with success/error
*/
static std::string AgentSetConfig(const std::string& config_json);
/**
* @brief Get available AI providers
* @return JSON array of provider info
*/
static std::string AgentGetProviders();
/**
* @brief Get list of pending/recent proposals
* @return JSON array of proposal info
*/
static std::string AgentGetProposals();
/**
* @brief Accept a proposal by ID
* @param proposal_id Proposal ID to accept
* @return JSON result with success/error
*/
static std::string AgentAcceptProposal(const std::string& proposal_id);
/**
* @brief Reject a proposal by ID
* @param proposal_id Proposal ID to reject
* @return JSON result with success/error
*/
static std::string AgentRejectProposal(const std::string& proposal_id);
/**
* @brief Get detailed proposal information
* @param proposal_id Proposal ID
* @return JSON with proposal details, diff, etc.
*/
static std::string AgentGetProposalDetails(const std::string& proposal_id);
/**
* @brief Open the agent sidebar
* @return JSON result with success/error
*/
static std::string AgentOpenSidebar();
/**
* @brief Close the agent sidebar
* @return JSON result with success/error
*/
static std::string AgentCloseSidebar();
private:
static editor::EditorManager* editor_manager_;
static bool initialized_;
// Helper to get card registry
static editor::PanelManager* GetPanelRegistry();
// Helper to convert EditorType to string
static std::string EditorTypeToString(int type);
// Helper to convert string to EditorType
static int StringToEditorType(const std::string& name);
};
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub for non-WASM builds
namespace yaze {
namespace editor {
class EditorManager;
}
namespace app {
namespace platform {
class WasmControlApi {
public:
static void Initialize(editor::EditorManager*) {}
static bool IsReady() { return false; }
static void SetupJavaScriptBindings() {}
static std::string SwitchEditor(const std::string&) { return "{}"; }
static std::string GetCurrentEditor() { return "{}"; }
static std::string GetAvailableEditors() { return "[]"; }
static std::string OpenPanel(const std::string&) { return "{}"; }
static std::string ClosePanel(const std::string&) { return "{}"; }
static std::string TogglePanel(const std::string&) { return "{}"; }
static std::string GetVisiblePanels() { return "[]"; }
static std::string GetAvailablePanels() { return "[]"; }
static std::string GetPanelsInCategory(const std::string&) { return "[]"; }
static std::string ShowAllPanels() { return "{}"; }
static std::string HideAllPanels() { return "{}"; }
static std::string ShowAllPanelsInCategory(const std::string&) { return "{}"; }
static std::string HideAllPanelsInCategory(const std::string&) { return "{}"; }
static std::string ShowOnlyPanel(const std::string&) { return "{}"; }
static std::string SetPanelLayout(const std::string&) { return "{}"; }
static std::string GetAvailableLayouts() { return "[]"; }
static std::string SaveCurrentLayout(const std::string&) { return "{}"; }
static std::string TriggerMenuAction(const std::string&) { return "{}"; }
static std::string GetAvailableMenuActions() { return "[]"; }
static std::string ToggleMenuBar() { return "{}"; }
static std::string GetSessionInfo() { return "{}"; }
static std::string CreateSession() { return "{}"; }
static std::string SwitchSession(int) { return "{}"; }
static std::string GetRomStatus() { return "{}"; }
static std::string ReadRomBytes(int, int) { return "{}"; }
static std::string WriteRomBytes(int, const std::string&) { return "{}"; }
static std::string SaveRom() { return "{}"; }
// Editor State APIs
static std::string GetEditorSnapshot() { return "{}"; }
static std::string GetCurrentDungeonRoom() { return "{}"; }
static std::string GetCurrentOverworldMap() { return "{}"; }
static std::string GetEditorSelection() { return "{}"; }
// Read-only Data APIs
static std::string GetRoomTileData(int) { return "{}"; }
static std::string GetRoomObjects(int) { return "[]"; }
static std::string GetRoomProperties(int) { return "{}"; }
static std::string GetMapTileData(int) { return "{}"; }
static std::string GetMapEntities(int) { return "{}"; }
static std::string GetMapProperties(int) { return "{}"; }
static std::string GetPaletteData(const std::string&, int) { return "{}"; }
static std::string ListPaletteGroups() { return "[]"; }
// GUI Automation APIs
static std::string GetUIElementTree() { return "{}"; }
static std::string GetUIElementBounds(const std::string&) { return "{}"; }
static std::string SetSelection(const std::string&) { return "{}"; }
// Platform Info API
static std::string GetPlatformInfo() { return "{}"; }
// Agent API
static bool AgentIsReady() { return false; }
static std::string AgentSendMessage(const std::string&) { return "{}"; }
static std::string AgentGetChatHistory() { return "[]"; }
static std::string AgentGetConfig() { return "{}"; }
static std::string AgentSetConfig(const std::string&) { return "{}"; }
static std::string AgentGetProviders() { return "[]"; }
static std::string AgentGetProposals() { return "[]"; }
static std::string AgentAcceptProposal(const std::string&) { return "{}"; }
static std::string AgentRejectProposal(const std::string&) { return "{}"; }
static std::string AgentGetProposalDetails(const std::string&) { return "{}"; }
static std::string AgentOpenSidebar() { return "{}"; }
static std::string AgentCloseSidebar() { return "{}"; }
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_CONTROL_API_H_

View File

@@ -0,0 +1,472 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_drop_handler.h"
#include <emscripten.h>
#include <emscripten/html5.h>
#include <algorithm>
#include <cctype>
#include <memory>
#include "absl/strings/str_format.h"
namespace yaze {
namespace platform {
// Static member initialization
std::unique_ptr<WasmDropHandler> WasmDropHandler::instance_ = nullptr;
// JavaScript interop for drag and drop operations
EM_JS(void, setupDropZone_impl, (const char* element_id), {
var targetElement = document.body;
if (element_id && UTF8ToString(element_id).length > 0) {
var el = document.getElementById(UTF8ToString(element_id));
if (el) {
targetElement = el;
}
}
// Remove existing event listeners if any
if (window.yazeDropListeners) {
window.yazeDropListeners.forEach(function(listener) {
document.removeEventListener(listener.event, listener.handler);
});
}
window.yazeDropListeners = [];
// Create drop zone overlay if it doesn't exist
var overlay = document.getElementById('yaze-drop-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'yaze-drop-overlay';
overlay.className = 'yaze-drop-overlay';
overlay.innerHTML = '<div class="yaze-drop-content"><div class="yaze-drop-icon">📁</div><div class="yaze-drop-text">Drop file here</div><div class="yaze-drop-info">Supported: .sfc, .smc, .zip, .pal, .tpl</div></div>';
document.body.appendChild(overlay);
}
// Helper function to check if file is a ROM or supported asset
function isSupportedFile(filename) {
var ext = filename.toLowerCase().split('.').pop();
return ext === 'sfc' || ext === 'smc' || ext === 'zip' ||
ext === 'pal' || ext === 'tpl';
}
// Helper function to check if dragged items contain files
function containsFiles(e) {
if (e.dataTransfer.types) {
for (var i = 0; i < e.dataTransfer.types.length; i++) {
if (e.dataTransfer.types[i] === "Files") {
return true;
}
}
}
return false;
}
// Drag enter handler
function handleDragEnter(e) {
if (containsFiles(e)) {
e.preventDefault();
e.stopPropagation();
Module._yazeHandleDragEnter();
overlay.classList.add('yaze-drop-active');
}
}
// Drag over handler
function handleDragOver(e) {
if (containsFiles(e)) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
}
}
// Drag leave handler
function handleDragLeave(e) {
if (e.target === document || e.target === overlay) {
e.preventDefault();
e.stopPropagation();
Module._yazeHandleDragLeave();
overlay.classList.remove('yaze-drop-active');
}
}
// Drop handler
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
overlay.classList.remove('yaze-drop-active');
var files = e.dataTransfer.files;
if (!files || files.length === 0) {
var errPtr = allocateUTF8("No files dropped");
Module._yazeHandleDropError(errPtr);
_free(errPtr);
return;
}
var file = files[0]; // Only handle first file
if (!isSupportedFile(file.name)) {
var errPtr = allocateUTF8("Invalid file type. Please drop a ROM (.sfc) or Palette (.pal, .tpl)");
Module._yazeHandleDropError(errPtr);
_free(errPtr);
return;
}
// Show loading state in overlay
overlay.classList.add('yaze-drop-loading');
overlay.querySelector('.yaze-drop-text').textContent = 'Loading file...';
overlay.querySelector('.yaze-drop-info').textContent = file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)';
var reader = new FileReader();
reader.onload = function() {
var filename = file.name;
var filenamePtr = allocateUTF8(filename);
var data = new Uint8Array(reader.result);
var dataPtr = Module._malloc(data.length);
Module.HEAPU8.set(data, dataPtr);
Module._yazeHandleDroppedFile(filenamePtr, dataPtr, data.length);
Module._free(dataPtr);
_free(filenamePtr);
// Hide loading state
setTimeout(function() {
overlay.classList.remove('yaze-drop-loading');
overlay.querySelector('.yaze-drop-text').textContent = 'Drop file here';
overlay.querySelector('.yaze-drop-info').textContent = 'Supported: .sfc, .smc, .zip, .pal, .tpl';
}, 500);
};
reader.onerror = function() {
var errPtr = allocateUTF8("Failed to read file: " + file.name);
Module._yazeHandleDropError(errPtr);
_free(errPtr);
overlay.classList.remove('yaze-drop-loading');
};
reader.readAsArrayBuffer(file);
}
// Register event listeners
var dragEnterHandler = handleDragEnter;
var dragOverHandler = handleDragOver;
var dragLeaveHandler = handleDragLeave;
var dropHandler = handleDrop;
document.addEventListener('dragenter', dragEnterHandler, false);
document.addEventListener('dragover', dragOverHandler, false);
document.addEventListener('dragleave', dragLeaveHandler, false);
document.addEventListener('drop', dropHandler, false);
// Store listeners for cleanup
window.yazeDropListeners = [
{ event: 'dragenter', handler: dragEnterHandler },
{ event: 'dragover', handler: dragOverHandler },
{ event: 'dragleave', handler: dragLeaveHandler },
{ event: 'drop', handler: dropHandler }
];
});
EM_JS(void, disableDropZone_impl, (), {
if (window.yazeDropListeners) {
window.yazeDropListeners.forEach(function(listener) {
document.removeEventListener(listener.event, listener.handler);
});
window.yazeDropListeners = [];
}
var overlay = document.getElementById('yaze-drop-overlay');
if (overlay) {
overlay.classList.remove('yaze-drop-active');
overlay.classList.remove('yaze-drop-loading');
}
});
EM_JS(void, setOverlayVisible_impl, (bool visible), {
var overlay = document.getElementById('yaze-drop-overlay');
if (overlay) {
overlay.style.display = visible ? 'flex' : 'none';
}
});
EM_JS(void, setOverlayText_impl, (const char* text), {
var overlay = document.getElementById('yaze-drop-overlay');
if (overlay) {
var textElement = overlay.querySelector('.yaze-drop-text');
if (textElement) {
textElement.textContent = UTF8ToString(text);
}
}
});
EM_JS(bool, isDragDropSupported, (), {
return (typeof FileReader !== 'undefined' && typeof DataTransfer !== 'undefined' && 'draggable' in document.createElement('div'));
});
EM_JS(void, injectDropZoneStyles, (), {
if (document.getElementById('yaze-drop-styles')) {
return; // Already injected
}
var style = document.createElement('style');
style.id = 'yaze-drop-styles';
style.textContent = `
.yaze-drop-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 10000;
align-items: center;
justify-content: center;
pointer-events: none;
transition: all 0.3s ease;
}
.yaze-drop-overlay.yaze-drop-active {
display: flex;
pointer-events: all;
background: rgba(0, 0, 0, 0.9);
}
.yaze-drop-overlay.yaze-drop-loading {
display: flex;
pointer-events: all;
}
.yaze-drop-content {
text-align: center;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 60px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
border: 3px dashed rgba(255, 255, 255, 0.5);
animation: pulse 2s infinite;
}
.yaze-drop-overlay.yaze-drop-active .yaze-drop-content {
border-color: #4CAF50;
background: rgba(76, 175, 80, 0.2);
animation: pulse-active 1s infinite;
}
.yaze-drop-overlay.yaze-drop-loading .yaze-drop-content {
border-color: #2196F3;
background: rgba(33, 150, 243, 0.2);
border-style: solid;
animation: none;
}
.yaze-drop-icon {
font-size: 72px;
margin-bottom: 20px;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
}
.yaze-drop-text {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.yaze-drop-info {
font-size: 16px;
opacity: 0.8;
font-weight: 400;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.02); opacity: 0.9; }
}
@keyframes pulse-active {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
`;
document.head.appendChild(style);
});
// WasmDropHandler implementation
WasmDropHandler& WasmDropHandler::GetInstance() {
if (!instance_) {
instance_ = std::unique_ptr<WasmDropHandler>(new WasmDropHandler());
}
return *instance_;
}
WasmDropHandler::WasmDropHandler() = default;
WasmDropHandler::~WasmDropHandler() {
if (initialized_ && enabled_) {
disableDropZone_impl();
}
}
absl::Status WasmDropHandler::Initialize(const std::string& element_id,
DropCallback on_drop,
ErrorCallback on_error) {
if (!IsSupported()) {
return absl::FailedPreconditionError(
"Drag and drop not supported in this browser");
}
// Inject CSS styles
injectDropZoneStyles();
// Set callbacks
if (on_drop) {
drop_callback_ = on_drop;
}
if (on_error) {
error_callback_ = on_error;
}
// Setup drop zone
element_id_ = element_id;
setupDropZone_impl(element_id.c_str());
initialized_ = true;
enabled_ = true;
return absl::OkStatus();
}
void WasmDropHandler::SetDropCallback(DropCallback on_drop) {
drop_callback_ = on_drop;
}
void WasmDropHandler::SetErrorCallback(ErrorCallback on_error) {
error_callback_ = on_error;
}
void WasmDropHandler::SetEnabled(bool enabled) {
if (enabled_ != enabled) {
enabled_ = enabled;
if (!enabled) {
disableDropZone_impl();
} else if (initialized_) {
setupDropZone_impl(element_id_.c_str());
}
}
}
void WasmDropHandler::SetOverlayVisible(bool visible) {
setOverlayVisible_impl(visible);
}
void WasmDropHandler::SetOverlayText(const std::string& text) {
setOverlayText_impl(text.c_str());
}
bool WasmDropHandler::IsSupported() {
return isDragDropSupported();
}
bool WasmDropHandler::IsValidRomFile(const std::string& filename) {
// Get file extension
size_t dot_pos = filename.find_last_of('.');
if (dot_pos == std::string::npos) {
return false;
}
std::string ext = filename.substr(dot_pos + 1);
// Convert to lowercase for comparison
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
return ext == "sfc" || ext == "smc" || ext == "zip" ||
ext == "pal" || ext == "tpl";
}
void WasmDropHandler::HandleDroppedFile(const char* filename,
const uint8_t* data, size_t size) {
auto& instance = GetInstance();
// Validate file
if (!IsValidRomFile(filename)) {
HandleDropError("Invalid file format");
return;
}
// Call the drop callback
if (instance.drop_callback_) {
std::vector<uint8_t> file_data(data, data + size);
instance.drop_callback_(filename, file_data);
} else {
emscripten_log(EM_LOG_WARN, "No drop callback registered for file: %s",
filename);
}
// Reset drag counter
instance.drag_counter_ = 0;
}
void WasmDropHandler::HandleDropError(const char* error_message) {
auto& instance = GetInstance();
if (instance.error_callback_) {
instance.error_callback_(error_message);
} else {
emscripten_log(EM_LOG_ERROR, "Drop error: %s", error_message);
}
// Reset drag counter
instance.drag_counter_ = 0;
}
void WasmDropHandler::HandleDragEnter() {
auto& instance = GetInstance();
instance.drag_counter_++;
}
void WasmDropHandler::HandleDragLeave() {
auto& instance = GetInstance();
instance.drag_counter_--;
// Only truly left when counter reaches 0
if (instance.drag_counter_ <= 0) {
instance.drag_counter_ = 0;
}
}
} // namespace platform
} // namespace yaze
// C-style callbacks for JavaScript interop - must be extern "C" with EMSCRIPTEN_KEEPALIVE
extern "C" {
EMSCRIPTEN_KEEPALIVE
void yazeHandleDroppedFile(const char* filename, const uint8_t* data,
size_t size) {
yaze::platform::WasmDropHandler::HandleDroppedFile(filename, data, size);
}
EMSCRIPTEN_KEEPALIVE
void yazeHandleDropError(const char* error_message) {
yaze::platform::WasmDropHandler::HandleDropError(error_message);
}
EMSCRIPTEN_KEEPALIVE
void yazeHandleDragEnter() {
yaze::platform::WasmDropHandler::HandleDragEnter();
}
EMSCRIPTEN_KEEPALIVE
void yazeHandleDragLeave() {
yaze::platform::WasmDropHandler::HandleDragLeave();
}
} // extern "C"
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,169 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_DROP_HANDLER_H_
#define YAZE_APP_PLATFORM_WASM_WASM_DROP_HANDLER_H_
#ifdef __EMSCRIPTEN__
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace platform {
/**
* @class WasmDropHandler
* @brief Handles drag and drop file operations in WASM/browser environment
*
* This class provides drag and drop functionality for ROM files in the browser,
* allowing users to drag ROM files directly onto the web page to load them.
* It supports .sfc, .smc, and .zip files containing ROMs.
*/
class WasmDropHandler {
public:
/**
* @brief Callback type for when ROM data is received via drop
* @param filename Name of the dropped file
* @param data Binary data of the file
*/
using DropCallback = std::function<void(const std::string& filename,
const std::vector<uint8_t>& data)>;
/**
* @brief Callback type for error handling
* @param error_message Description of the error
*/
using ErrorCallback = std::function<void(const std::string& error_message)>;
/**
* @brief Get the singleton instance of WasmDropHandler
* @return Reference to the singleton instance
*/
static WasmDropHandler& GetInstance();
/**
* @brief Initialize the drop zone on a specific DOM element
* @param element_id ID of the DOM element to use as drop zone (default: document body)
* @param on_drop Callback to invoke when a valid file is dropped
* @param on_error Optional callback for error handling
* @return Status indicating success or failure
*/
absl::Status Initialize(const std::string& element_id = "",
DropCallback on_drop = nullptr,
ErrorCallback on_error = nullptr);
/**
* @brief Register or update the drop callback
* @param on_drop Callback to invoke when a file is dropped
*/
void SetDropCallback(DropCallback on_drop);
/**
* @brief Register or update the error callback
* @param on_error Callback to invoke on error
*/
void SetErrorCallback(ErrorCallback on_error);
/**
* @brief Enable or disable the drop zone
* @param enabled true to enable drop zone, false to disable
*/
void SetEnabled(bool enabled);
/**
* @brief Check if drop zone is currently enabled
* @return true if drop zone is active
*/
bool IsEnabled() const { return enabled_; }
/**
* @brief Show or hide the drop zone overlay
* @param visible true to show overlay, false to hide
*/
void SetOverlayVisible(bool visible);
/**
* @brief Update the overlay text displayed when dragging
* @param text Text to display in the overlay
*/
void SetOverlayText(const std::string& text);
/**
* @brief Check if drag and drop is supported in the current browser
* @return true if drag and drop is available
*/
static bool IsSupported();
/**
* @brief Validate if a file is a supported ROM format
* @param filename Name of the file to validate
* @return true if file has valid ROM extension
*/
static bool IsValidRomFile(const std::string& filename);
/**
* @brief Handle a dropped file (called from JavaScript)
* @param filename The dropped filename
* @param data Pointer to file data
* @param size Size of file data
*/
static void HandleDroppedFile(const char* filename, const uint8_t* data,
size_t size);
/**
* @brief Handle drop error (called from JavaScript)
* @param error_message Error description
*/
static void HandleDropError(const char* error_message);
/**
* @brief Handle drag enter event (called from JavaScript)
*/
static void HandleDragEnter();
/**
* @brief Handle drag leave event (called from JavaScript)
*/
static void HandleDragLeave();
public:
~WasmDropHandler();
private:
// Singleton pattern - private constructor
WasmDropHandler();
// Delete copy constructor and assignment operator
WasmDropHandler(const WasmDropHandler&) = delete;
WasmDropHandler& operator=(const WasmDropHandler&) = delete;
// Instance data
bool initialized_ = false;
bool enabled_ = false;
DropCallback drop_callback_;
ErrorCallback error_callback_;
std::string element_id_;
int drag_counter_ = 0; // Track nested drag enter/leave events
// Static singleton instance
static std::unique_ptr<WasmDropHandler> instance_;
};
} // namespace platform
} // namespace yaze
// C-style callbacks for JavaScript interop
extern "C" {
void yazeHandleDroppedFile(const char* filename, const uint8_t* data,
size_t size);
void yazeHandleDropError(const char* error_message);
void yazeHandleDragEnter();
void yazeHandleDragLeave();
}
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_DROP_HANDLER_H_

View File

@@ -0,0 +1,133 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_DROP_INTEGRATION_EXAMPLE_H_
#define YAZE_APP_PLATFORM_WASM_WASM_DROP_INTEGRATION_EXAMPLE_H_
/**
* @file wasm_drop_integration_example.h
* @brief Example integration of WasmDropHandler with EditorManager
*
* This file demonstrates how to integrate the drag & drop ROM loading
* functionality into the main yaze application when building for WASM.
*
* INTEGRATION STEPS:
*
* 1. In your Controller or EditorManager initialization:
* @code
* #ifdef __EMSCRIPTEN__
* #include "app/platform/wasm/wasm_drop_handler.h"
*
* absl::Status InitializeWasmFeatures() {
* // Initialize drop zone with callbacks
* auto& drop_handler = yaze::platform::WasmDropHandler::GetInstance();
*
* return drop_handler.Initialize(
* "", // Use document body as drop zone
* [this](const std::string& filename, const std::vector<uint8_t>& data) {
* // Handle dropped ROM file
* HandleDroppedRom(filename, data);
* },
* [](const std::string& error) {
* // Handle drop errors
* LOG_ERROR("Drop error: %s", error.c_str());
* }
* );
* }
* @endcode
*
* 2. Implement the ROM loading handler:
* @code
* void HandleDroppedRom(const std::string& filename,
* const std::vector<uint8_t>& data) {
* // Create a new ROM instance
* auto rom = std::make_unique<Rom>();
*
* // Load from data instead of file
* auto status = rom->LoadFromData(data);
* if (!status.ok()) {
* toast_manager_.Show("Failed to load ROM: " + status.ToString(),
* ToastType::kError);
* return;
* }
*
* // Set the filename for display
* rom->set_filename(filename);
*
* // Find or create a session
* auto session_id = session_coordinator_->FindEmptySession();
* if (session_id == -1) {
* session_id = session_coordinator_->CreateNewSession();
* }
*
* // Set the ROM in the session
* session_coordinator_->SetSessionRom(session_id, std::move(rom));
* session_coordinator_->SetCurrentSession(session_id);
*
* // Load editor assets
* LoadAssets();
*
* // Update UI
* ui_coordinator_->SetWelcomeScreenVisible(false);
* ui_coordinator_->SetEditorSelectionVisible(true);
*
* toast_manager_.Show("ROM loaded via drag & drop: " + filename,
* ToastType::kSuccess);
* }
* @endcode
*
* 3. Optional: Customize the drop zone appearance:
* @code
* drop_handler.SetOverlayText("Drop your A Link to the Past ROM here!");
* @endcode
*
* 4. Optional: Enable/disable drop zone based on application state:
* @code
* // Disable during ROM operations
* drop_handler.SetEnabled(false);
* PerformRomOperation();
* drop_handler.SetEnabled(true);
* @endcode
*
* HTML INTEGRATION:
*
* Include the CSS in your HTML file:
* @code{.html}
* <link rel="stylesheet" href="drop_zone.css">
* @endcode
*
* The JavaScript is automatically initialized when the Module is ready.
* You can also manually initialize it:
* @code{.html}
* <script src="drop_zone.js"></script>
* <script>
* // Optional: Custom initialization after Module is ready
* Module.onRuntimeInitialized = function() {
* YazeDropZone.init({
* config: {
* maxFileSize: 8 * 1024 * 1024, // 8MB max
* messages: {
* dropHere: 'Drop SNES ROM here',
* loading: 'Loading ROM...'
* }
* }
* });
* };
* </script>
* @endcode
*
* TESTING:
*
* To test the drag & drop functionality:
* 1. Build with Emscripten: cmake --preset wasm-dbg && cmake --build build_wasm
* 2. Serve the files: python3 -m http.server 8000 -d build_wasm
* 3. Open browser: http://localhost:8000/yaze.html
* 4. Drag a .sfc/.smc ROM file onto the page
* 5. The overlay should appear and the ROM should load
*
* TROUBLESHOOTING:
*
* - If overlay doesn't appear: Check browser console for errors
* - If ROM doesn't load: Verify Rom::LoadFromData() implementation
* - If styles are missing: Ensure drop_zone.css is included
* - For debugging: Check Module._yazeHandleDroppedFile in console
*/
#endif // YAZE_APP_PLATFORM_WASM_WASM_DROP_INTEGRATION_EXAMPLE_H_

View File

@@ -0,0 +1,217 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_error_handler.h"
#include <emscripten.h>
#include <emscripten/html5.h>
#include <map>
#include <mutex>
namespace yaze {
namespace platform {
// Static member initialization
std::atomic<bool> WasmErrorHandler::initialized_{false};
std::atomic<int> WasmErrorHandler::callback_counter_{0};
// Store confirmation callbacks with timestamps for timeout cleanup
struct CallbackEntry {
std::function<void(bool)> callback;
double timestamp; // Time when callback was registered (ms since epoch)
};
static std::map<int, CallbackEntry> g_confirm_callbacks;
static std::mutex g_callback_mutex;
// Callback timeout in milliseconds (5 minutes)
constexpr double kCallbackTimeoutMs = 5.0 * 60.0 * 1000.0;
// Helper to get current time in milliseconds
static double GetCurrentTimeMs() {
return EM_ASM_DOUBLE({ return Date.now(); });
}
// Cleanup stale callbacks that have exceeded the timeout
static void CleanupStaleCallbacks() {
std::lock_guard<std::mutex> lock(g_callback_mutex);
const double now = GetCurrentTimeMs();
for (auto it = g_confirm_callbacks.begin(); it != g_confirm_callbacks.end();) {
if (now - it->second.timestamp > kCallbackTimeoutMs) {
it = g_confirm_callbacks.erase(it);
} else {
++it;
}
}
}
// JavaScript function to register cleanup handler for page unload
EM_JS(void, js_register_cleanup_handler, (), {
window.addEventListener('beforeunload', function() {
// Signal C++ to cleanup stale callbacks
if (Module._cleanupConfirmCallbacks) {
Module._cleanupConfirmCallbacks();
}
});
});
// C++ cleanup function called from JavaScript on page unload
extern "C" EMSCRIPTEN_KEEPALIVE void cleanupConfirmCallbacks() {
std::lock_guard<std::mutex> lock(g_callback_mutex);
g_confirm_callbacks.clear();
}
// JavaScript functions for browser UI interaction
EM_JS(void, js_show_modal, (const char* title, const char* message, const char* type), {
var titleStr = UTF8ToString(title);
var messageStr = UTF8ToString(message);
var typeStr = UTF8ToString(type);
if (typeof window.showYazeModal === 'function') {
window.showYazeModal(titleStr, messageStr, typeStr);
} else {
alert(titleStr + '\n\n' + messageStr);
}
});
EM_JS(void, js_show_toast, (const char* message, const char* type, int duration_ms), {
var messageStr = UTF8ToString(message);
var typeStr = UTF8ToString(type);
if (typeof window.showYazeToast === 'function') {
window.showYazeToast(messageStr, typeStr, duration_ms);
} else {
console.log('[' + typeStr + '] ' + messageStr);
}
});
EM_JS(void, js_show_progress, (const char* task, float progress), {
var taskStr = UTF8ToString(task);
if (typeof window.showYazeProgress === 'function') {
window.showYazeProgress(taskStr, progress);
} else {
console.log('Progress: ' + taskStr + ' - ' + (progress * 100).toFixed(0) + '%');
}
});
EM_JS(void, js_hide_progress, (), {
if (typeof window.hideYazeProgress === 'function') {
window.hideYazeProgress();
}
});
EM_JS(void, js_show_confirm, (const char* message, int callback_id), {
var messageStr = UTF8ToString(message);
if (typeof window.showYazeConfirm === 'function') {
window.showYazeConfirm(messageStr, function(result) {
Module._handleConfirmCallback(callback_id, result ? 1 : 0);
});
} else {
var result = confirm(messageStr);
Module._handleConfirmCallback(callback_id, result ? 1 : 0);
}
});
EM_JS(void, js_inject_styles, (), {
if (document.getElementById('yaze-error-handler-styles')) {
return;
}
var link = document.createElement('link');
link.id = 'yaze-error-handler-styles';
link.rel = 'stylesheet';
link.href = 'error_handler.css';
link.onerror = function() {
var style = document.createElement('style');
style.textContent = '.yaze-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000}.yaze-modal-content{background:white;border-radius:8px;padding:24px;max-width:500px;box-shadow:0 4px 6px rgba(0,0,0,0.1)}.yaze-toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:4px;color:white;z-index:10001}.yaze-toast-info{background:#3498db}.yaze-toast-success{background:#2ecc71}.yaze-toast-warning{background:#f39c12}.yaze-toast-error{background:#e74c3c}';
document.head.appendChild(style);
};
document.head.appendChild(link);
});
// C++ callback handler for confirmation dialogs
extern "C" EMSCRIPTEN_KEEPALIVE void handleConfirmCallback(int callback_id, int result) {
std::function<void(bool)> callback;
{
std::lock_guard<std::mutex> lock(g_callback_mutex);
auto it = g_confirm_callbacks.find(callback_id);
if (it != g_confirm_callbacks.end()) {
callback = it->second.callback;
g_confirm_callbacks.erase(it);
}
}
if (callback) {
callback(result != 0);
}
}
void WasmErrorHandler::Initialize() {
// Use compare_exchange for thread-safe initialization
bool expected = false;
if (!initialized_.compare_exchange_strong(expected, true)) {
return; // Already initialized by another thread
}
js_inject_styles();
EM_ASM({
Module._handleConfirmCallback = Module.cwrap('handleConfirmCallback', null, ['number', 'number']);
Module._cleanupConfirmCallbacks = Module.cwrap('cleanupConfirmCallbacks', null, []);
});
js_register_cleanup_handler();
}
void WasmErrorHandler::ShowError(const std::string& title, const std::string& message) {
if (!initialized_.load()) Initialize();
js_show_modal(title.c_str(), message.c_str(), "error");
}
void WasmErrorHandler::ShowWarning(const std::string& title, const std::string& message) {
if (!initialized_.load()) Initialize();
js_show_modal(title.c_str(), message.c_str(), "warning");
}
void WasmErrorHandler::ShowInfo(const std::string& title, const std::string& message) {
if (!initialized_.load()) Initialize();
js_show_modal(title.c_str(), message.c_str(), "info");
}
void WasmErrorHandler::Toast(const std::string& message, ToastType type, int duration_ms) {
if (!initialized_.load()) Initialize();
const char* type_str = "info";
switch (type) {
case ToastType::kSuccess: type_str = "success"; break;
case ToastType::kWarning: type_str = "warning"; break;
case ToastType::kError: type_str = "error"; break;
case ToastType::kInfo:
default: type_str = "info"; break;
}
js_show_toast(message.c_str(), type_str, duration_ms);
}
void WasmErrorHandler::ShowProgress(const std::string& task, float progress) {
if (!initialized_.load()) Initialize();
if (progress < 0.0f) progress = 0.0f;
if (progress > 1.0f) progress = 1.0f;
js_show_progress(task.c_str(), progress);
}
void WasmErrorHandler::HideProgress() {
if (!initialized_.load()) Initialize();
js_hide_progress();
}
void WasmErrorHandler::Confirm(const std::string& message, std::function<void(bool)> callback) {
if (!initialized_.load()) Initialize();
// Cleanup any stale callbacks before adding new one
CleanupStaleCallbacks();
int callback_id = callback_counter_.fetch_add(1) + 1;
{
std::lock_guard<std::mutex> lock(g_callback_mutex);
g_confirm_callbacks[callback_id] = CallbackEntry{callback, GetCurrentTimeMs()};
}
js_show_confirm(message.c_str(), callback_id);
}
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,102 @@
#ifndef YAZE_APP_PLATFORM_WASM_ERROR_HANDLER_H_
#define YAZE_APP_PLATFORM_WASM_ERROR_HANDLER_H_
#ifdef __EMSCRIPTEN__
#include <atomic>
#include <functional>
#include <string>
namespace yaze {
namespace platform {
/**
* @enum ToastType
* @brief Type of toast notification
*/
enum class ToastType { kInfo, kSuccess, kWarning, kError };
/**
* @class WasmErrorHandler
* @brief Browser-based error handling and notification system for WASM builds
*
* This class provides user-friendly error display, toast notifications,
* progress indicators, and confirmation dialogs using browser UI elements.
* All methods are static and thread-safe.
*/
class WasmErrorHandler {
public:
/**
* @brief Display an error dialog in the browser
* @param title Dialog title
* @param message Error message to display
*/
static void ShowError(const std::string& title, const std::string& message);
/**
* @brief Display a warning dialog in the browser
* @param title Dialog title
* @param message Warning message to display
*/
static void ShowWarning(const std::string& title, const std::string& message);
/**
* @brief Display an info dialog in the browser
* @param title Dialog title
* @param message Info message to display
*/
static void ShowInfo(const std::string& title, const std::string& message);
/**
* @brief Show a non-blocking toast notification
* @param message Message to display
* @param type Toast type (affects styling)
* @param duration_ms Duration in milliseconds (default 3000)
*/
static void Toast(const std::string& message,
ToastType type = ToastType::kInfo, int duration_ms = 3000);
/**
* @brief Show a progress indicator
* @param task Task description
* @param progress Progress value (0.0 to 1.0)
*/
static void ShowProgress(const std::string& task, float progress);
/**
* @brief Hide the progress indicator
*/
static void HideProgress();
/**
* @brief Show a confirmation dialog with callback
* @param message Confirmation message
* @param callback Function to call with user's choice (true = confirmed)
*/
static void Confirm(const std::string& message,
std::function<void(bool)> callback);
/**
* @brief Initialize error handler (called once on startup)
* This injects the necessary CSS styles and prepares the DOM
*/
static void Initialize();
private:
// Prevent instantiation
WasmErrorHandler() = delete;
~WasmErrorHandler() = delete;
// Track if handler is initialized (thread-safe)
static std::atomic<bool> initialized_;
// Counter for generating unique callback IDs (thread-safe)
static std::atomic<int> callback_counter_;
};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_ERROR_HANDLER_H_

View File

@@ -0,0 +1,227 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_file_dialog.h"
#include <emscripten.h>
#include <emscripten/html5.h>
#include <memory>
#include <unordered_map>
#include "absl/strings/str_format.h"
namespace yaze {
namespace platform {
// Static member initialization
int WasmFileDialog::next_callback_id_ = 1;
std::unordered_map<int, WasmFileDialog::PendingOperation> WasmFileDialog::pending_operations_;
std::mutex WasmFileDialog::operations_mutex_;
// JavaScript interop for file operations
EM_JS(void, openFileDialog_impl, (const char* accept, int callback_id, bool is_text), {
var input = document.createElement('input');
input.type = 'file';
input.accept = UTF8ToString(accept);
input.style.display = 'none';
input.onchange = function(e) {
var file = e.target.files[0];
if (!file) {
var errPtr = allocateUTF8("No file selected");
Module._yazeHandleFileError(callback_id, errPtr);
_free(errPtr);
return;
}
var reader = new FileReader();
reader.onload = function() {
var filename = file.name;
var filenamePtr = allocateUTF8(filename);
if (is_text) {
var contentPtr = allocateUTF8(reader.result);
Module._yazeHandleTextFileLoaded(callback_id, filenamePtr, contentPtr);
_free(contentPtr);
} else {
var data = new Uint8Array(reader.result);
var dataPtr = Module._malloc(data.length);
Module.HEAPU8.set(data, dataPtr);
Module._yazeHandleFileLoaded(callback_id, filenamePtr, dataPtr, data.length);
Module._free(dataPtr);
}
_free(filenamePtr);
};
reader.onerror = function() {
var errPtr = allocateUTF8("Failed to read file");
Module._yazeHandleFileError(callback_id, errPtr);
_free(errPtr);
};
if (is_text) {
reader.readAsText(file);
} else {
reader.readAsArrayBuffer(file);
}
};
document.body.appendChild(input);
input.click();
setTimeout(function() { document.body.removeChild(input); }, 100);
});
EM_JS(void, downloadFile_impl, (const char* filename, const uint8_t* data, size_t size, const char* mime_type), {
var dataArray = HEAPU8.subarray(data, data + size);
var blob = new Blob([dataArray], { type: UTF8ToString(mime_type) });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = UTF8ToString(filename);
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
EM_JS(void, downloadTextFile_impl, (const char* filename, const char* content, const char* mime_type), {
var blob = new Blob([UTF8ToString(content)], { type: UTF8ToString(mime_type) });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = UTF8ToString(filename);
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
EM_JS(bool, isFileApiSupported, (), {
return (typeof File !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined' && typeof URL !== 'undefined' && typeof URL.createObjectURL !== 'undefined');
});
// Implementation of public methods
void WasmFileDialog::OpenFileDialog(const std::string& accept, FileLoadCallback on_load, ErrorCallback on_error) {
PendingOperation op;
op.binary_callback = on_load;
op.error_callback = on_error;
op.is_text = false;
int callback_id = RegisterCallback(std::move(op));
openFileDialog_impl(accept.c_str(), callback_id, false);
}
void WasmFileDialog::OpenTextFileDialog(const std::string& accept,
std::function<void(const std::string&, const std::string&)> on_load,
ErrorCallback on_error) {
PendingOperation op;
op.text_callback = on_load;
op.error_callback = on_error;
op.is_text = true;
int callback_id = RegisterCallback(std::move(op));
openFileDialog_impl(accept.c_str(), callback_id, true);
}
absl::Status WasmFileDialog::DownloadFile(const std::string& filename, const std::vector<uint8_t>& data) {
if (!IsSupported()) {
return absl::FailedPreconditionError("File API not supported in this browser");
}
if (data.empty()) {
return absl::InvalidArgumentError("Cannot download empty file");
}
downloadFile_impl(filename.c_str(), data.data(), data.size(), "application/octet-stream");
return absl::OkStatus();
}
absl::Status WasmFileDialog::DownloadTextFile(const std::string& filename, const std::string& content, const std::string& mime_type) {
if (!IsSupported()) {
return absl::FailedPreconditionError("File API not supported in this browser");
}
downloadTextFile_impl(filename.c_str(), content.c_str(), mime_type.c_str());
return absl::OkStatus();
}
bool WasmFileDialog::IsSupported() {
return isFileApiSupported();
}
// Private methods
int WasmFileDialog::RegisterCallback(PendingOperation operation) {
std::lock_guard<std::mutex> lock(operations_mutex_);
int id = next_callback_id_++;
operation.id = id;
pending_operations_[id] = std::move(operation);
return id;
}
std::unique_ptr<WasmFileDialog::PendingOperation> WasmFileDialog::GetPendingOperation(int callback_id) {
std::lock_guard<std::mutex> lock(operations_mutex_);
auto it = pending_operations_.find(callback_id);
if (it == pending_operations_.end()) {
return nullptr;
}
auto op = std::make_unique<PendingOperation>(std::move(it->second));
pending_operations_.erase(it);
return op;
}
void WasmFileDialog::HandleFileLoaded(int callback_id, const char* filename, const uint8_t* data, size_t size) {
auto op = GetPendingOperation(callback_id);
if (!op) {
emscripten_log(EM_LOG_WARN, "Unknown callback ID: %d", callback_id);
return;
}
if (op->binary_callback) {
std::vector<uint8_t> file_data(data, data + size);
op->binary_callback(filename, file_data);
}
}
void WasmFileDialog::HandleTextFileLoaded(int callback_id, const char* filename, const char* content) {
auto op = GetPendingOperation(callback_id);
if (!op) {
emscripten_log(EM_LOG_WARN, "Unknown callback ID: %d", callback_id);
return;
}
if (op->text_callback) {
op->text_callback(filename, content);
}
}
void WasmFileDialog::HandleFileError(int callback_id, const char* error_message) {
auto op = GetPendingOperation(callback_id);
if (!op) {
emscripten_log(EM_LOG_WARN, "Unknown callback ID: %d", callback_id);
return;
}
if (op->error_callback) {
op->error_callback(error_message);
} else {
emscripten_log(EM_LOG_ERROR, "File operation error: %s", error_message);
}
}
} // namespace platform
} // namespace yaze
// C-style callbacks for JavaScript interop - must be extern "C" with EMSCRIPTEN_KEEPALIVE
extern "C" {
EMSCRIPTEN_KEEPALIVE
void yazeHandleFileLoaded(int callback_id, const char* filename, const uint8_t* data, size_t size) {
yaze::platform::WasmFileDialog::HandleFileLoaded(callback_id, filename, data, size);
}
EMSCRIPTEN_KEEPALIVE
void yazeHandleTextFileLoaded(int callback_id, const char* filename, const char* content) {
yaze::platform::WasmFileDialog::HandleTextFileLoaded(callback_id, filename, content);
}
EMSCRIPTEN_KEEPALIVE
void yazeHandleFileError(int callback_id, const char* error_message) {
yaze::platform::WasmFileDialog::HandleFileError(callback_id, error_message);
}
} // extern "C"
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,164 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_FILE_DIALOG_H_
#define YAZE_APP_PLATFORM_WASM_WASM_FILE_DIALOG_H_
#ifdef __EMSCRIPTEN__
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace platform {
/**
* @class WasmFileDialog
* @brief File dialog implementation for WASM/browser environment
*
* This class provides file input/output functionality in the browser
* using HTML5 File API and Blob downloads.
*/
class WasmFileDialog {
public:
/**
* @brief Callback type for file load operations
* @param filename Name of the loaded file
* @param data Binary data of the file
*/
using FileLoadCallback = std::function<void(
const std::string& filename, const std::vector<uint8_t>& data)>;
/**
* @brief Callback type for error handling
* @param error_message Description of the error
*/
using ErrorCallback = std::function<void(const std::string& error_message)>;
/**
* @brief Open a file selection dialog
* @param accept File type filter (e.g., ".sfc,.smc" for ROM files)
* @param on_load Callback to invoke when file is loaded
* @param on_error Optional callback for error handling
*/
static void OpenFileDialog(const std::string& accept,
FileLoadCallback on_load,
ErrorCallback on_error = nullptr);
/**
* @brief Open a file selection dialog for text files
* @param accept File type filter (e.g., ".json,.txt")
* @param on_load Callback to invoke with file content as string
* @param on_error Optional callback for error handling
*/
static void OpenTextFileDialog(const std::string& accept,
std::function<void(const std::string& filename,
const std::string& content)>
on_load,
ErrorCallback on_error = nullptr);
/**
* @brief Download a file to the user's downloads folder
* @param filename Suggested filename for the download
* @param data Binary data to download
* @return Status indicating success or failure
*/
static absl::Status DownloadFile(const std::string& filename,
const std::vector<uint8_t>& data);
/**
* @brief Download a text file to the user's downloads folder
* @param filename Suggested filename for the download
* @param content Text content to download
* @param mime_type MIME type (default: "text/plain")
* @return Status indicating success or failure
*/
static absl::Status DownloadTextFile(
const std::string& filename, const std::string& content,
const std::string& mime_type = "text/plain");
/**
* @brief Check if file dialogs are supported in the current environment
* @return true if file operations are available
*/
static bool IsSupported();
/**
* @brief Structure to hold pending file operation data
*/
struct PendingOperation {
int id;
FileLoadCallback binary_callback;
std::function<void(const std::string&, const std::string&)> text_callback;
ErrorCallback error_callback;
bool is_text;
};
private:
// Callback management (thread-safe)
static int next_callback_id_;
static std::unordered_map<int, PendingOperation> pending_operations_;
static std::mutex operations_mutex_;
/**
* @brief Register a callback for async file operations
* @param operation The pending operation to register
* @return Unique callback ID
*/
static int RegisterCallback(PendingOperation operation);
/**
* @brief Get and remove a pending operation
* @param callback_id The callback ID to retrieve
* @return The pending operation or nullptr if not found
*/
static std::unique_ptr<PendingOperation> GetPendingOperation(int callback_id);
public:
// These must be public to be called from extern "C" functions
/**
* @brief Handle file load completion (called from JavaScript)
* @param callback_id The callback ID
* @param filename The loaded filename
* @param data Pointer to file data
* @param size Size of file data
*/
static void HandleFileLoaded(int callback_id, const char* filename,
const uint8_t* data, size_t size);
/**
* @brief Handle text file load completion (called from JavaScript)
* @param callback_id The callback ID
* @param filename The loaded filename
* @param content The text content
*/
static void HandleTextFileLoaded(int callback_id, const char* filename,
const char* content);
/**
* @brief Handle file load error (called from JavaScript)
* @param callback_id The callback ID
* @param error_message Error description
*/
static void HandleFileError(int callback_id, const char* error_message);
};
} // namespace platform
} // namespace yaze
// C-style callbacks for JavaScript interop
extern "C" {
void yazeHandleFileLoaded(int callback_id, const char* filename,
const uint8_t* data, size_t size);
void yazeHandleTextFileLoaded(int callback_id, const char* filename,
const char* content);
void yazeHandleFileError(int callback_id, const char* error_message);
}
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_FILE_DIALOG_H_

View File

@@ -0,0 +1,245 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_loading_manager.h"
#include <emscripten.h>
#include <emscripten/html5.h>
namespace yaze {
namespace app {
namespace platform {
// JavaScript interface functions
// Note: These functions take uint32_t js_id, not the full 64-bit handle.
// The JS layer only sees the low 32 bits which are unique IDs for UI elements.
EM_JS(void, js_create_loading_indicator, (uint32_t id, const char* task_name), {
if (typeof window.createLoadingIndicator === 'function') {
window.createLoadingIndicator(id, UTF8ToString(task_name));
} else {
console.warn('createLoadingIndicator not defined. Include loading_indicator.js');
}
});
EM_JS(void, js_update_loading_progress, (uint32_t id, float progress, const char* message), {
if (typeof window.updateLoadingProgress === 'function') {
window.updateLoadingProgress(id, progress, UTF8ToString(message));
}
});
EM_JS(void, js_remove_loading_indicator, (uint32_t id), {
if (typeof window.removeLoadingIndicator === 'function') {
window.removeLoadingIndicator(id);
}
});
EM_JS(bool, js_check_loading_cancelled, (uint32_t id), {
if (typeof window.isLoadingCancelled === 'function') {
return window.isLoadingCancelled(id);
}
return false;
});
EM_JS(void, js_show_cancel_button, (uint32_t id), {
if (typeof window.showCancelButton === 'function') {
window.showCancelButton(id, function() {});
}
});
// WasmLoadingManager implementation
WasmLoadingManager& WasmLoadingManager::GetInstance() {
static WasmLoadingManager instance;
return instance;
}
WasmLoadingManager::WasmLoadingManager() {}
WasmLoadingManager::~WasmLoadingManager() {
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& [handle, op] : operations_) {
if (op && op->active) {
js_remove_loading_indicator(GetJsId(handle));
}
}
}
WasmLoadingManager::LoadingHandle WasmLoadingManager::BeginLoading(const std::string& task_name) {
auto& instance = GetInstance();
// Generate unique JS ID and generation counter atomically
uint32_t js_id = instance.next_js_id_.fetch_add(1);
uint32_t generation = instance.generation_counter_.fetch_add(1);
// Create the full 64-bit handle
LoadingHandle handle = MakeHandle(js_id, generation);
auto operation = std::make_unique<LoadingOperation>();
operation->task_name = task_name;
operation->active = true;
operation->generation = generation;
{
std::lock_guard<std::mutex> lock(instance.mutex_);
instance.operations_[handle] = std::move(operation);
}
// JS functions receive only the 32-bit ID
js_create_loading_indicator(js_id, task_name.c_str());
js_show_cancel_button(js_id);
return handle;
}
void WasmLoadingManager::UpdateProgress(LoadingHandle handle, float progress) {
if (handle == kInvalidHandle) return;
auto& instance = GetInstance();
std::string message;
uint32_t js_id = GetJsId(handle);
{
std::lock_guard<std::mutex> lock(instance.mutex_);
auto it = instance.operations_.find(handle);
if (it == instance.operations_.end() || !it->second->active) return;
it->second->progress = progress;
message = it->second->message;
}
js_update_loading_progress(js_id, progress, message.c_str());
}
void WasmLoadingManager::UpdateMessage(LoadingHandle handle, const std::string& message) {
if (handle == kInvalidHandle) return;
auto& instance = GetInstance();
float progress = 0.0f;
uint32_t js_id = GetJsId(handle);
{
std::lock_guard<std::mutex> lock(instance.mutex_);
auto it = instance.operations_.find(handle);
if (it == instance.operations_.end() || !it->second->active) return;
it->second->message = message;
progress = it->second->progress;
}
js_update_loading_progress(js_id, progress, message.c_str());
}
bool WasmLoadingManager::IsCancelled(LoadingHandle handle) {
if (handle == kInvalidHandle) return false;
auto& instance = GetInstance();
uint32_t js_id = GetJsId(handle);
// Check JS cancellation state first (outside lock)
bool js_cancelled = js_check_loading_cancelled(js_id);
{
std::lock_guard<std::mutex> lock(instance.mutex_);
auto it = instance.operations_.find(handle);
if (it == instance.operations_.end() || !it->second->active) {
return true;
}
if (js_cancelled && !it->second->cancelled.load()) {
it->second->cancelled.store(true);
}
return it->second->cancelled.load();
}
}
void WasmLoadingManager::EndLoading(LoadingHandle handle) {
if (handle == kInvalidHandle) return;
auto& instance = GetInstance();
uint32_t js_id = GetJsId(handle);
{
std::lock_guard<std::mutex> lock(instance.mutex_);
auto it = instance.operations_.find(handle);
if (it != instance.operations_.end()) {
// Mark inactive and erase immediately - no async delay.
// The 64-bit handle with generation counter ensures that even if
// a new operation starts immediately with the same js_id, it will
// have a different generation and thus a different full handle.
it->second->active = false;
// Clear arena handle if it matches this handle
if (instance.arena_handle_ == handle) {
instance.arena_handle_ = kInvalidHandle;
}
// Erase synchronously - safe because generation counter prevents reuse
instance.operations_.erase(it);
}
}
// Remove the JS UI element after releasing the lock
js_remove_loading_indicator(js_id);
}
bool WasmLoadingManager::ReportArenaProgress(int current, int total, const std::string& item_name) {
auto& instance = GetInstance();
LoadingHandle handle;
uint32_t js_id;
float progress = 0.0f;
bool should_update_progress = false;
bool should_update_message = false;
bool is_cancelled = false;
{
std::lock_guard<std::mutex> lock(instance.mutex_);
handle = instance.arena_handle_;
if (handle == kInvalidHandle) return true;
js_id = GetJsId(handle);
// Check if the operation still exists and is active
auto it = instance.operations_.find(handle);
if (it == instance.operations_.end() || !it->second->active) {
// Handle is stale, clear it and return
instance.arena_handle_ = kInvalidHandle;
return true;
}
// Update progress if applicable
if (total > 0) {
progress = static_cast<float>(current) / static_cast<float>(total);
it->second->progress = progress;
should_update_progress = true;
} else {
progress = it->second->progress;
}
// Update message if applicable
if (!item_name.empty()) {
it->second->message = item_name;
should_update_message = true;
}
// Check cancellation status
is_cancelled = it->second->cancelled.load();
}
// Perform JS calls outside the lock to avoid blocking
if (should_update_progress || should_update_message) {
js_update_loading_progress(js_id, progress,
should_update_message ? item_name.c_str() : "");
}
return !is_cancelled;
}
void WasmLoadingManager::SetArenaHandle(LoadingHandle handle) {
auto& instance = GetInstance();
std::lock_guard<std::mutex> lock(instance.mutex_);
instance.arena_handle_ = handle;
}
void WasmLoadingManager::ClearArenaHandle() {
auto& instance = GetInstance();
std::lock_guard<std::mutex> lock(instance.mutex_);
instance.arena_handle_ = kInvalidHandle;
}
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,229 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_LOADING_MANAGER_H_
#define YAZE_APP_PLATFORM_WASM_WASM_LOADING_MANAGER_H_
#ifdef __EMSCRIPTEN__
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
namespace yaze {
namespace app {
namespace platform {
/**
* @class WasmLoadingManager
* @brief Manages loading operations with progress tracking for WASM builds
*
* This class provides a centralized loading manager for long-running operations
* in the browser environment. It integrates with JavaScript UI to show progress
* indicators with cancel capability.
*
* Handle Design:
* The LoadingHandle is a 64-bit value composed of:
* - High 32 bits: Generation counter (prevents handle reuse after EndLoading)
* - Low 32 bits: Unique ID for JS interop
*
* This design eliminates race conditions where a new operation could reuse
* the same ID as a recently-ended operation. Operations are deleted
* synchronously in EndLoading() rather than using async callbacks.
*
* Example usage:
* @code
* auto handle = WasmLoadingManager::BeginLoading("Loading Graphics");
* for (int i = 0; i < total; i++) {
* if (WasmLoadingManager::IsCancelled(handle)) {
* WasmLoadingManager::EndLoading(handle);
* return absl::CancelledError("User cancelled loading");
* }
* // Do work...
* WasmLoadingManager::UpdateProgress(handle, static_cast<float>(i) / total);
* WasmLoadingManager::UpdateMessage(handle, absl::StrFormat("Sheet %d/%d", i, total));
* }
* WasmLoadingManager::EndLoading(handle);
* @endcode
*/
class WasmLoadingManager {
public:
/**
* @brief Handle for tracking a loading operation
*
* 64-bit handle with generation counter to prevent reuse race conditions:
* - High 32 bits: Generation counter
* - Low 32 bits: JS-visible ID
*/
using LoadingHandle = uint64_t;
/**
* @brief Invalid handle value
*/
static constexpr LoadingHandle kInvalidHandle = 0;
/**
* @brief Extract the JS-visible ID from a handle (low 32 bits)
* @param handle The full 64-bit handle
* @return The 32-bit JS ID
*/
static uint32_t GetJsId(LoadingHandle handle) {
return static_cast<uint32_t>(handle & 0xFFFFFFFF);
}
/**
* @brief Begin a new loading operation
* @param task_name The name of the task to display in the UI
* @return Handle to track this loading operation
*/
static LoadingHandle BeginLoading(const std::string& task_name);
/**
* @brief Update the progress of a loading operation
* @param handle The loading operation handle
* @param progress Progress value between 0.0 and 1.0
*/
static void UpdateProgress(LoadingHandle handle, float progress);
/**
* @brief Update the status message for a loading operation
* @param handle The loading operation handle
* @param message Status message to display
*/
static void UpdateMessage(LoadingHandle handle, const std::string& message);
/**
* @brief Check if the user has requested cancellation
* @param handle The loading operation handle
* @return true if the operation should be cancelled
*/
static bool IsCancelled(LoadingHandle handle);
/**
* @brief End a loading operation and remove UI
* @param handle The loading operation handle
*/
static void EndLoading(LoadingHandle handle);
/**
* @brief Integration point for gfx::Arena progressive loading
*
* This method can be called by gfx::Arena during texture loading
* to report progress without modifying Arena's core logic.
*
* @param current Current item being processed
* @param total Total items to process
* @param item_name Name of the current item (e.g., "Graphics Sheet 42")
* @return true if loading should continue, false if cancelled
*/
static bool ReportArenaProgress(int current, int total,
const std::string& item_name);
/**
* @brief Set the global loading handle for Arena operations
*
* This allows Arena to use a pre-existing loading operation
* instead of creating its own.
*
* @param handle The loading operation handle to use for Arena progress
*/
static void SetArenaHandle(LoadingHandle handle);
/**
* @brief Clear the global Arena loading handle
*/
static void ClearArenaHandle();
private:
/**
* @brief Internal structure to track loading operations
*/
struct LoadingOperation {
std::string task_name;
float progress = 0.0f;
std::string message;
std::atomic<bool> cancelled{false};
bool active = true;
uint32_t generation = 0; // Generation counter for validation
};
/**
* @brief Get the singleton instance
*/
static WasmLoadingManager& GetInstance();
/**
* @brief Constructor (private for singleton)
*/
WasmLoadingManager();
/**
* @brief Destructor
*/
~WasmLoadingManager();
// Disable copy and move
WasmLoadingManager(const WasmLoadingManager&) = delete;
WasmLoadingManager& operator=(const WasmLoadingManager&) = delete;
WasmLoadingManager(WasmLoadingManager&&) = delete;
WasmLoadingManager& operator=(WasmLoadingManager&&) = delete;
/**
* @brief Create a handle from JS ID and generation
*/
static LoadingHandle MakeHandle(uint32_t js_id, uint32_t generation) {
return (static_cast<uint64_t>(generation) << 32) | js_id;
}
/**
* @brief Extract generation from handle (high 32 bits)
*/
static uint32_t GetGeneration(LoadingHandle handle) {
return static_cast<uint32_t>(handle >> 32);
}
/**
* @brief Next available JS ID (low 32 bits of handle)
*
* This counter only increments, never wraps. With 32 bits, even at
* 1000 operations/second, it would take ~50 days to wrap.
*/
std::atomic<uint32_t> next_js_id_{1};
/**
* @brief Generation counter to prevent handle reuse
*
* Incremented each time BeginLoading is called. Combined with js_id
* to create unique 64-bit handles that cannot be accidentally reused.
*/
std::atomic<uint32_t> generation_counter_{1};
/**
* @brief Active loading operations, keyed by full 64-bit handle
*/
std::unordered_map<LoadingHandle, std::unique_ptr<LoadingOperation>>
operations_;
/**
* @brief Mutex for thread safety
*/
std::mutex mutex_;
/**
* @brief Global handle for Arena operations (protected by mutex_)
*
* NOTE: This is a non-static member protected by mutex_ to prevent
* race conditions between ReportArenaProgress() and ClearArenaHandle().
*/
LoadingHandle arena_handle_ = kInvalidHandle;
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_LOADING_MANAGER_H_

View File

@@ -0,0 +1,617 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_message_queue.h"
#include <emscripten.h>
#include <random>
#include <sstream>
#include "absl/strings/str_format.h"
namespace yaze {
namespace app {
namespace platform {
// JavaScript IndexedDB interface for message queue persistence
// All functions use yazeAsyncQueue to serialize async operations
EM_JS(int, mq_save_queue, (const char* key, const char* json_data), {
return Asyncify.handleAsync(function() {
var keyStr = UTF8ToString(key);
var jsonStr = UTF8ToString(json_data);
var operation = function() {
return new Promise(function(resolve) {
try {
// Open or create the database
var request = indexedDB.open('YazeMessageQueue', 1);
request.onerror = function() {
console.error('Failed to open message queue database:', request.error);
resolve(-1);
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
if (!db.objectStoreNames.contains('queues')) {
db.createObjectStore('queues');
}
};
request.onsuccess = function() {
var db = request.result;
var transaction = db.transaction(['queues'], 'readwrite');
var store = transaction.objectStore('queues');
var putRequest = store.put(jsonStr, keyStr);
putRequest.onsuccess = function() {
db.close();
resolve(0);
};
putRequest.onerror = function() {
console.error('Failed to save message queue:', putRequest.error);
db.close();
resolve(-1);
};
};
} catch (e) {
console.error('Exception in mq_save_queue:', e);
resolve(-1);
}
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(char*, mq_load_queue, (const char* key), {
return Asyncify.handleAsync(function() {
var keyStr = UTF8ToString(key);
var operation = function() {
return new Promise(function(resolve) {
try {
var request = indexedDB.open('YazeMessageQueue', 1);
request.onerror = function() {
console.error('Failed to open message queue database:', request.error);
resolve(0);
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
if (!db.objectStoreNames.contains('queues')) {
db.createObjectStore('queues');
}
};
request.onsuccess = function() {
var db = request.result;
var transaction = db.transaction(['queues'], 'readonly');
var store = transaction.objectStore('queues');
var getRequest = store.get(keyStr);
getRequest.onsuccess = function() {
var result = getRequest.result;
db.close();
if (result && typeof result === 'string') {
var len = lengthBytesUTF8(result) + 1;
var ptr = Module._malloc(len);
stringToUTF8(result, ptr, len);
resolve(ptr);
} else {
resolve(0);
}
};
getRequest.onerror = function() {
console.error('Failed to load message queue:', getRequest.error);
db.close();
resolve(0);
};
};
} catch (e) {
console.error('Exception in mq_load_queue:', e);
resolve(0);
}
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(int, mq_clear_queue, (const char* key), {
return Asyncify.handleAsync(function() {
var keyStr = UTF8ToString(key);
var operation = function() {
return new Promise(function(resolve) {
try {
var request = indexedDB.open('YazeMessageQueue', 1);
request.onerror = function() {
console.error('Failed to open message queue database:', request.error);
resolve(-1);
};
request.onsuccess = function() {
var db = request.result;
var transaction = db.transaction(['queues'], 'readwrite');
var store = transaction.objectStore('queues');
var deleteRequest = store.delete(keyStr);
deleteRequest.onsuccess = function() {
db.close();
resolve(0);
};
deleteRequest.onerror = function() {
console.error('Failed to clear message queue:', deleteRequest.error);
db.close();
resolve(-1);
};
};
} catch (e) {
console.error('Exception in mq_clear_queue:', e);
resolve(-1);
}
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
// Get current time in seconds since epoch
static double GetCurrentTime() {
return emscripten_get_now() / 1000.0;
}
WasmMessageQueue::WasmMessageQueue() {
// Attempt to load queue from storage on construction
auto status = LoadFromStorage();
if (!status.ok()) {
emscripten_log(EM_LOG_WARN, "Failed to load message queue from storage: %s",
status.ToString().c_str());
}
}
WasmMessageQueue::~WasmMessageQueue() {
// Persist queue on destruction if auto-persist is enabled
if (auto_persist_ && !queue_.empty()) {
auto status = PersistToStorage();
if (!status.ok()) {
emscripten_log(EM_LOG_ERROR, "Failed to persist message queue on destruction: %s",
status.ToString().c_str());
}
}
}
std::string WasmMessageQueue::Enqueue(const std::string& message_type,
const std::string& payload) {
std::lock_guard<std::mutex> lock(queue_mutex_);
// Check queue size limit
if (queue_.size() >= max_queue_size_) {
// Remove oldest message if at capacity
queue_.pop_front();
}
// Create new message
QueuedMessage msg;
msg.message_type = message_type;
msg.payload = payload;
msg.timestamp = GetCurrentTime();
msg.retry_count = 0;
msg.id = GenerateMessageId();
// Add to queue
queue_.push_back(msg);
total_enqueued_++;
// Notify listeners
NotifyStatusChange();
// Maybe persist to storage
MaybePersist();
return msg.id;
}
void WasmMessageQueue::ReplayAll(MessageSender sender, int max_retries) {
if (is_replaying_) {
emscripten_log(EM_LOG_WARN, "Already replaying messages, skipping replay request");
return;
}
is_replaying_ = true;
int replayed = 0;
int failed = 0;
// Copy queue to avoid holding lock during send operations
std::vector<QueuedMessage> messages_to_send;
{
std::lock_guard<std::mutex> lock(queue_mutex_);
messages_to_send.reserve(queue_.size());
for (const auto& msg : queue_) {
messages_to_send.push_back(msg);
}
}
// Process each message
std::vector<std::string> successful_ids;
std::vector<QueuedMessage> failed_messages;
for (auto& msg : messages_to_send) {
// Check if message has expired
double age = GetCurrentTime() - msg.timestamp;
if (age > message_expiry_seconds_) {
continue; // Skip expired messages
}
// Try to send the message
auto status = sender(msg.message_type, msg.payload);
if (status.ok()) {
successful_ids.push_back(msg.id);
replayed++;
total_replayed_++;
} else {
msg.retry_count++;
if (msg.retry_count >= max_retries) {
// Move to failed list
failed_messages.push_back(msg);
failed++;
total_failed_++;
} else {
// Keep in queue for retry
// Message stays in queue
}
emscripten_log(EM_LOG_WARN, "Failed to replay message %s (attempt %d): %s",
msg.id.c_str(), msg.retry_count, status.ToString().c_str());
}
}
// Update queue with results
{
std::lock_guard<std::mutex> lock(queue_mutex_);
// Remove successful messages
for (const auto& id : successful_ids) {
queue_.erase(
std::remove_if(queue_.begin(), queue_.end(),
[&id](const QueuedMessage& m) { return m.id == id; }),
queue_.end());
}
// Move failed messages to failed list
for (const auto& msg : failed_messages) {
failed_messages_.push_back(msg);
queue_.erase(
std::remove_if(queue_.begin(), queue_.end(),
[&msg](const QueuedMessage& m) { return m.id == msg.id; }),
queue_.end());
}
}
is_replaying_ = false;
// Notify completion
if (replay_complete_callback_) {
replay_complete_callback_(replayed, failed);
}
// Update status
NotifyStatusChange();
// Persist changes
MaybePersist();
}
size_t WasmMessageQueue::PendingCount() const {
std::lock_guard<std::mutex> lock(queue_mutex_);
return queue_.size();
}
WasmMessageQueue::QueueStatus WasmMessageQueue::GetStatus() const {
std::lock_guard<std::mutex> lock(queue_mutex_);
QueueStatus status;
status.pending_count = queue_.size();
status.failed_count = failed_messages_.size();
status.total_bytes = CalculateTotalBytes();
if (!queue_.empty()) {
double now = GetCurrentTime();
status.oldest_message_age = now - queue_.front().timestamp;
}
// Check if queue is persisted (simplified check)
status.is_persisted = auto_persist_;
return status;
}
void WasmMessageQueue::Clear() {
std::lock_guard<std::mutex> lock(queue_mutex_);
queue_.clear();
failed_messages_.clear();
// Clear from storage as well
mq_clear_queue(kStorageKey);
NotifyStatusChange();
}
void WasmMessageQueue::ClearFailed() {
std::lock_guard<std::mutex> lock(queue_mutex_);
failed_messages_.clear();
NotifyStatusChange();
MaybePersist();
}
bool WasmMessageQueue::RemoveMessage(const std::string& message_id) {
std::lock_guard<std::mutex> lock(queue_mutex_);
// Try to remove from main queue
auto it = std::find_if(queue_.begin(), queue_.end(),
[&message_id](const QueuedMessage& m) {
return m.id == message_id;
});
if (it != queue_.end()) {
queue_.erase(it);
NotifyStatusChange();
MaybePersist();
return true;
}
// Try to remove from failed messages
auto failed_it = std::find_if(failed_messages_.begin(), failed_messages_.end(),
[&message_id](const QueuedMessage& m) {
return m.id == message_id;
});
if (failed_it != failed_messages_.end()) {
failed_messages_.erase(failed_it);
NotifyStatusChange();
MaybePersist();
return true;
}
return false;
}
absl::Status WasmMessageQueue::PersistToStorage() {
std::lock_guard<std::mutex> lock(queue_mutex_);
try {
// Create JSON representation
nlohmann::json json_data;
json_data["version"] = 1;
json_data["timestamp"] = GetCurrentTime();
// Serialize main queue
nlohmann::json queue_array = nlohmann::json::array();
for (const auto& msg : queue_) {
nlohmann::json msg_json;
msg_json["id"] = msg.id;
msg_json["type"] = msg.message_type;
msg_json["payload"] = msg.payload;
msg_json["timestamp"] = msg.timestamp;
msg_json["retry_count"] = msg.retry_count;
queue_array.push_back(msg_json);
}
json_data["queue"] = queue_array;
// Serialize failed messages
nlohmann::json failed_array = nlohmann::json::array();
for (const auto& msg : failed_messages_) {
nlohmann::json msg_json;
msg_json["id"] = msg.id;
msg_json["type"] = msg.message_type;
msg_json["payload"] = msg.payload;
msg_json["timestamp"] = msg.timestamp;
msg_json["retry_count"] = msg.retry_count;
failed_array.push_back(msg_json);
}
json_data["failed"] = failed_array;
// Save statistics
json_data["stats"]["total_enqueued"] = total_enqueued_;
json_data["stats"]["total_replayed"] = total_replayed_;
json_data["stats"]["total_failed"] = total_failed_;
// Convert to string and save
std::string json_str = json_data.dump();
int result = mq_save_queue(kStorageKey, json_str.c_str());
if (result != 0) {
return absl::InternalError("Failed to save message queue to IndexedDB");
}
return absl::OkStatus();
} catch (const std::exception& e) {
return absl::InternalError(absl::StrFormat("Failed to serialize message queue: %s", e.what()));
}
}
absl::Status WasmMessageQueue::LoadFromStorage() {
char* json_ptr = mq_load_queue(kStorageKey);
if (!json_ptr) {
// No saved queue, which is fine
return absl::OkStatus();
}
try {
std::string json_str(json_ptr);
free(json_ptr);
nlohmann::json json_data = nlohmann::json::parse(json_str);
// Check version compatibility
int version = json_data.value("version", 0);
if (version != 1) {
return absl::InvalidArgumentError(absl::StrFormat("Unsupported queue version: %d", version));
}
std::lock_guard<std::mutex> lock(queue_mutex_);
// Clear current state
queue_.clear();
failed_messages_.clear();
// Load main queue
if (json_data.contains("queue")) {
for (const auto& msg_json : json_data["queue"]) {
QueuedMessage msg;
msg.id = msg_json.value("id", "");
msg.message_type = msg_json.value("type", "");
msg.payload = msg_json.value("payload", "");
msg.timestamp = msg_json.value("timestamp", 0.0);
msg.retry_count = msg_json.value("retry_count", 0);
// Skip expired messages
double age = GetCurrentTime() - msg.timestamp;
if (age <= message_expiry_seconds_) {
queue_.push_back(msg);
}
}
}
// Load failed messages
if (json_data.contains("failed")) {
for (const auto& msg_json : json_data["failed"]) {
QueuedMessage msg;
msg.id = msg_json.value("id", "");
msg.message_type = msg_json.value("type", "");
msg.payload = msg_json.value("payload", "");
msg.timestamp = msg_json.value("timestamp", 0.0);
msg.retry_count = msg_json.value("retry_count", 0);
// Keep failed messages for review even if expired
failed_messages_.push_back(msg);
}
}
// Load statistics
if (json_data.contains("stats")) {
total_enqueued_ = json_data["stats"].value("total_enqueued", 0);
total_replayed_ = json_data["stats"].value("total_replayed", 0);
total_failed_ = json_data["stats"].value("total_failed", 0);
}
emscripten_log(EM_LOG_INFO, "Loaded %zu messages from storage (%zu failed)",
queue_.size(), failed_messages_.size());
NotifyStatusChange();
return absl::OkStatus();
} catch (const std::exception& e) {
free(json_ptr);
return absl::InvalidArgumentError(absl::StrFormat("Failed to parse saved queue: %s", e.what()));
}
}
std::vector<WasmMessageQueue::QueuedMessage> WasmMessageQueue::GetQueuedMessages() const {
std::lock_guard<std::mutex> lock(queue_mutex_);
return std::vector<QueuedMessage>(queue_.begin(), queue_.end());
}
int WasmMessageQueue::PruneExpiredMessages() {
std::lock_guard<std::mutex> lock(queue_mutex_);
double now = GetCurrentTime();
size_t initial_size = queue_.size();
// Remove expired messages
queue_.erase(
std::remove_if(queue_.begin(), queue_.end(),
[now, this](const QueuedMessage& msg) {
return (now - msg.timestamp) > message_expiry_seconds_;
}),
queue_.end());
int removed = initial_size - queue_.size();
if (removed > 0) {
NotifyStatusChange();
MaybePersist();
}
return removed;
}
std::string WasmMessageQueue::GenerateMessageId() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> dis(0, 15);
static const char* hex_chars = "0123456789abcdef";
std::stringstream ss;
ss << "msg_";
// Add timestamp component
ss << static_cast<long long>(GetCurrentTime() * 1000) << "_";
// Add random component
for (int i = 0; i < 8; i++) {
ss << hex_chars[dis(gen)];
}
return ss.str();
}
size_t WasmMessageQueue::CalculateTotalBytes() const {
size_t total = 0;
for (const auto& msg : queue_) {
total += msg.message_type.size();
total += msg.payload.size();
total += msg.id.size();
total += sizeof(msg.timestamp) + sizeof(msg.retry_count);
}
for (const auto& msg : failed_messages_) {
total += msg.message_type.size();
total += msg.payload.size();
total += msg.id.size();
total += sizeof(msg.timestamp) + sizeof(msg.retry_count);
}
return total;
}
void WasmMessageQueue::NotifyStatusChange() {
if (status_change_callback_) {
status_change_callback_(GetStatus());
}
}
void WasmMessageQueue::MaybePersist() {
if (auto_persist_ && !is_replaying_) {
auto status = PersistToStorage();
if (!status.ok()) {
emscripten_log(EM_LOG_WARN, "Failed to auto-persist message queue: %s",
status.ToString().c_str());
}
}
}
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,279 @@
#ifndef YAZE_APP_PLATFORM_WASM_MESSAGE_QUEUE_H_
#define YAZE_APP_PLATFORM_WASM_MESSAGE_QUEUE_H_
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/val.h>
#include <chrono>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "nlohmann/json.hpp"
namespace yaze {
namespace app {
namespace platform {
/**
* @brief Offline message queue for WebSocket collaboration
*
* This class provides a persistent message queue for the collaboration system,
* allowing messages to be queued when offline and replayed when reconnected.
* Messages are persisted to IndexedDB to survive browser refreshes/crashes.
*/
class WasmMessageQueue {
public:
/**
* @brief Message structure for queued items
*/
struct QueuedMessage {
std::string message_type; // "change", "cursor", etc.
std::string payload; // JSON payload
double timestamp; // When message was queued
int retry_count = 0; // Number of send attempts
std::string id; // Unique message ID
};
/**
* @brief Status information for the queue
*/
struct QueueStatus {
size_t pending_count = 0;
size_t failed_count = 0;
size_t total_bytes = 0;
double oldest_message_age = 0; // Seconds
bool is_persisted = false;
};
// Callback types
using ReplayCompleteCallback = std::function<void(int replayed_count, int failed_count)>;
using MessageSender = std::function<absl::Status(const std::string& type, const std::string& payload)>;
using StatusChangeCallback = std::function<void(const QueueStatus& status)>;
WasmMessageQueue();
~WasmMessageQueue();
/**
* @brief Enqueue a message for later sending
* @param message_type Type of message ("change", "cursor", etc.)
* @param payload JSON payload string
* @return Unique message ID
*/
std::string Enqueue(const std::string& message_type, const std::string& payload);
/**
* @brief Set callback for when replay completes
* @param callback Function to call after replay attempt
*/
void SetOnReplayComplete(ReplayCompleteCallback callback) {
replay_complete_callback_ = callback;
}
/**
* @brief Set callback for queue status changes
* @param callback Function to call when queue status changes
*/
void SetOnStatusChange(StatusChangeCallback callback) {
status_change_callback_ = callback;
}
/**
* @brief Replay all queued messages
* @param sender Function to send each message
* @param max_retries Maximum send attempts per message (default: 3)
*/
void ReplayAll(MessageSender sender, int max_retries = 3);
/**
* @brief Get number of pending messages
* @return Count of messages in queue
*/
size_t PendingCount() const;
/**
* @brief Get detailed queue status
* @return Current queue status information
*/
QueueStatus GetStatus() const;
/**
* @brief Clear all messages from queue
*/
void Clear();
/**
* @brief Clear only failed messages
*/
void ClearFailed();
/**
* @brief Remove a specific message by ID
* @param message_id The message ID to remove
* @return true if message was found and removed
*/
bool RemoveMessage(const std::string& message_id);
/**
* @brief Persist queue to IndexedDB storage
* @return Status of persist operation
*/
absl::Status PersistToStorage();
/**
* @brief Load queue from IndexedDB storage
* @return Status of load operation
*/
absl::Status LoadFromStorage();
/**
* @brief Enable/disable automatic persistence
* @param enable true to auto-persist on changes
*/
void SetAutoPersist(bool enable) {
auto_persist_ = enable;
}
/**
* @brief Set maximum queue size (default: 1000)
* @param max_size Maximum number of messages to queue
*/
void SetMaxQueueSize(size_t max_size) {
max_queue_size_ = max_size;
}
/**
* @brief Set message expiry time (default: 24 hours)
* @param seconds Time in seconds before messages expire
*/
void SetMessageExpiry(double seconds) {
message_expiry_seconds_ = seconds;
}
/**
* @brief Get all queued messages (for inspection/debugging)
* @return Vector of queued messages
*/
std::vector<QueuedMessage> GetQueuedMessages() const;
/**
* @brief Prune expired messages from queue
* @return Number of messages removed
*/
int PruneExpiredMessages();
private:
// Generate unique message ID
std::string GenerateMessageId();
// Calculate total size of queued messages
size_t CalculateTotalBytes() const;
// Notify status change listeners
void NotifyStatusChange();
// Check if we should persist to storage
void MaybePersist();
// Message queue
std::deque<QueuedMessage> queue_;
std::vector<QueuedMessage> failed_messages_;
mutable std::mutex queue_mutex_;
// Configuration
bool auto_persist_ = true;
size_t max_queue_size_ = 1000;
double message_expiry_seconds_ = 86400.0; // 24 hours
// State tracking
bool is_replaying_ = false;
size_t total_enqueued_ = 0;
size_t total_replayed_ = 0;
size_t total_failed_ = 0;
// Callbacks
ReplayCompleteCallback replay_complete_callback_;
StatusChangeCallback status_change_callback_;
// Storage key for IndexedDB
static constexpr const char* kStorageKey = "collaboration_message_queue";
};
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub implementation for non-WASM builds
#include <deque>
#include <functional>
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace yaze {
namespace app {
namespace platform {
class WasmMessageQueue {
public:
struct QueuedMessage {
std::string message_type;
std::string payload;
double timestamp;
int retry_count = 0;
std::string id;
};
struct QueueStatus {
size_t pending_count = 0;
size_t failed_count = 0;
size_t total_bytes = 0;
double oldest_message_age = 0;
bool is_persisted = false;
};
using ReplayCompleteCallback = std::function<void(int, int)>;
using MessageSender = std::function<absl::Status(const std::string&, const std::string&)>;
using StatusChangeCallback = std::function<void(const QueueStatus&)>;
WasmMessageQueue() {}
~WasmMessageQueue() {}
std::string Enqueue(const std::string&, const std::string&) { return ""; }
void SetOnReplayComplete(ReplayCompleteCallback) {}
void SetOnStatusChange(StatusChangeCallback) {}
void ReplayAll(MessageSender, int = 3) {}
size_t PendingCount() const { return 0; }
QueueStatus GetStatus() const { return {}; }
void Clear() {}
void ClearFailed() {}
bool RemoveMessage(const std::string&) { return false; }
absl::Status PersistToStorage() {
return absl::UnimplementedError("Message queue requires WASM build");
}
absl::Status LoadFromStorage() {
return absl::UnimplementedError("Message queue requires WASM build");
}
void SetAutoPersist(bool) {}
void SetMaxQueueSize(size_t) {}
void SetMessageExpiry(double) {}
std::vector<QueuedMessage> GetQueuedMessages() const { return {}; }
int PruneExpiredMessages() { return 0; }
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_MESSAGE_QUEUE_H_

View File

@@ -0,0 +1,483 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_patch_export.h"
#include <algorithm>
#include <cstring>
#include <emscripten.h>
#include <emscripten/html5.h>
#include "absl/strings/str_format.h"
namespace yaze {
namespace platform {
// JavaScript interop for downloading patch files
EM_JS(void, downloadPatchFile_impl, (const char* filename, const uint8_t* data, size_t size, const char* mime_type), {
var dataArray = HEAPU8.subarray(data, data + size);
var blob = new Blob([dataArray], { type: UTF8ToString(mime_type) });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = UTF8ToString(filename);
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
// clang-format on
// CRC32 implementation
static const uint32_t kCRC32Table[256] = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d};
uint32_t WasmPatchExport::CalculateCRC32(const std::vector<uint8_t>& data) {
return CalculateCRC32(data.data(), data.size());
}
uint32_t WasmPatchExport::CalculateCRC32(const uint8_t* data, size_t size) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < size; ++i) {
crc = kCRC32Table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return ~crc;
}
void WasmPatchExport::WriteVariableLength(std::vector<uint8_t>& output,
uint64_t value) {
while (true) {
uint8_t byte = value & 0x7F;
value >>= 7;
if (value == 0) {
output.push_back(byte | 0x80);
break;
}
output.push_back(byte);
}
}
void WasmPatchExport::WriteIPS24BitOffset(std::vector<uint8_t>& output,
uint32_t offset) {
output.push_back((offset >> 16) & 0xFF);
output.push_back((offset >> 8) & 0xFF);
output.push_back(offset & 0xFF);
}
void WasmPatchExport::WriteIPS16BitSize(std::vector<uint8_t>& output,
uint16_t size) {
output.push_back((size >> 8) & 0xFF);
output.push_back(size & 0xFF);
}
std::vector<std::pair<size_t, size_t>> WasmPatchExport::FindChangedRegions(
const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified) {
std::vector<std::pair<size_t, size_t>> regions;
size_t min_size = std::min(original.size(), modified.size());
size_t i = 0;
while (i < min_size) {
// Skip unchanged bytes
while (i < min_size && original[i] == modified[i]) {
++i;
}
if (i < min_size) {
// Found a change, record the start
size_t start = i;
// Find the end of the changed region
while (i < min_size && original[i] != modified[i]) {
++i;
}
regions.push_back({start, i - start});
}
}
// Handle case where modified ROM is larger
if (modified.size() > original.size()) {
regions.push_back({original.size(), modified.size() - original.size()});
}
return regions;
}
std::vector<uint8_t> WasmPatchExport::GenerateBPSPatch(
const std::vector<uint8_t>& source,
const std::vector<uint8_t>& target) {
std::vector<uint8_t> patch;
// BPS header "BPS1"
patch.push_back('B');
patch.push_back('P');
patch.push_back('S');
patch.push_back('1');
// Source size (variable-length encoding)
WriteVariableLength(patch, source.size());
// Target size (variable-length encoding)
WriteVariableLength(patch, target.size());
// Metadata size (0 for no metadata)
WriteVariableLength(patch, 0);
// BPS action types:
// 0 = SourceRead: copy n bytes from source at outputOffset
// 1 = TargetRead: copy n literal bytes from patch
// 2 = SourceCopy: copy n bytes from source at sourceRelativeOffset
// 3 = TargetCopy: copy n bytes from target at targetRelativeOffset
size_t output_offset = 0;
int64_t source_relative_offset = 0;
int64_t target_relative_offset = 0;
while (output_offset < target.size()) {
// Check if we can use SourceRead (bytes match at current position)
size_t source_read_len = 0;
if (output_offset < source.size()) {
while (output_offset + source_read_len < target.size() &&
output_offset + source_read_len < source.size() &&
source[output_offset + source_read_len] ==
target[output_offset + source_read_len]) {
++source_read_len;
}
}
// Try to find a better match elsewhere in source (SourceCopy)
size_t best_source_copy_offset = 0;
size_t best_source_copy_len = 0;
if (source_read_len < 4) { // Only search if SourceRead isn't good enough
for (size_t i = 0; i < source.size(); ++i) {
size_t match_len = 0;
while (i + match_len < source.size() &&
output_offset + match_len < target.size() &&
source[i + match_len] == target[output_offset + match_len]) {
++match_len;
}
if (match_len > best_source_copy_len && match_len >= 4) {
best_source_copy_len = match_len;
best_source_copy_offset = i;
}
}
}
// Decide which action to use
if (source_read_len >= 4 || (source_read_len > 0 && source_read_len >= best_source_copy_len)) {
// Use SourceRead
uint64_t action = ((source_read_len - 1) << 2) | 0;
WriteVariableLength(patch, action);
output_offset += source_read_len;
} else if (best_source_copy_len >= 4) {
// Use SourceCopy
uint64_t action = ((best_source_copy_len - 1) << 2) | 2;
WriteVariableLength(patch, action);
// Write relative offset (signed, encoded as unsigned with sign bit)
int64_t relative = static_cast<int64_t>(best_source_copy_offset) - source_relative_offset;
uint64_t encoded_offset = (relative < 0) ?
((static_cast<uint64_t>(-relative - 1) << 1) | 1) :
(static_cast<uint64_t>(relative) << 1);
WriteVariableLength(patch, encoded_offset);
source_relative_offset = best_source_copy_offset + best_source_copy_len;
output_offset += best_source_copy_len;
} else {
// Use TargetRead - find how many bytes to write literally
size_t target_read_len = 1;
while (output_offset + target_read_len < target.size()) {
// Check if next position has a good match
bool has_good_match = false;
// Check SourceRead at next position
if (output_offset + target_read_len < source.size() &&
source[output_offset + target_read_len] ==
target[output_offset + target_read_len]) {
size_t match_len = 0;
while (output_offset + target_read_len + match_len < target.size() &&
output_offset + target_read_len + match_len < source.size() &&
source[output_offset + target_read_len + match_len] ==
target[output_offset + target_read_len + match_len]) {
++match_len;
}
if (match_len >= 4) {
has_good_match = true;
}
}
if (has_good_match) {
break;
}
++target_read_len;
}
// Write TargetRead action
uint64_t action = ((target_read_len - 1) << 2) | 1;
WriteVariableLength(patch, action);
// Write the literal bytes
for (size_t i = 0; i < target_read_len; ++i) {
patch.push_back(target[output_offset + i]);
}
output_offset += target_read_len;
}
}
// Write checksums (all CRC32, little-endian)
uint32_t source_crc = CalculateCRC32(source);
uint32_t target_crc = CalculateCRC32(target);
// Source CRC32
patch.push_back(source_crc & 0xFF);
patch.push_back((source_crc >> 8) & 0xFF);
patch.push_back((source_crc >> 16) & 0xFF);
patch.push_back((source_crc >> 24) & 0xFF);
// Target CRC32
patch.push_back(target_crc & 0xFF);
patch.push_back((target_crc >> 8) & 0xFF);
patch.push_back((target_crc >> 16) & 0xFF);
patch.push_back((target_crc >> 24) & 0xFF);
// Patch CRC32 (includes everything before this point)
uint32_t patch_crc = CalculateCRC32(patch.data(), patch.size());
patch.push_back(patch_crc & 0xFF);
patch.push_back((patch_crc >> 8) & 0xFF);
patch.push_back((patch_crc >> 16) & 0xFF);
patch.push_back((patch_crc >> 24) & 0xFF);
return patch;
}
std::vector<uint8_t> WasmPatchExport::GenerateIPSPatch(
const std::vector<uint8_t>& source,
const std::vector<uint8_t>& target) {
std::vector<uint8_t> patch;
// IPS header
patch.push_back('P');
patch.push_back('A');
patch.push_back('T');
patch.push_back('C');
patch.push_back('H');
// Find all changed regions
auto regions = FindChangedRegions(source, target);
for (const auto& region : regions) {
size_t offset = region.first;
size_t length = region.second;
// IPS has a maximum offset of 16MB (0xFFFFFF)
if (offset > 0xFFFFFF) {
break; // Can't encode offsets beyond 16MB
}
// Process the region, splitting if necessary (max 65535 bytes per record)
size_t remaining = length;
size_t current_offset = offset;
while (remaining > 0) {
size_t chunk_size = std::min(remaining, static_cast<size_t>(0xFFFF));
// Check for RLE opportunity (same byte repeated)
bool use_rle = false;
uint8_t rle_byte = 0;
if (chunk_size >= 3 && current_offset < target.size()) {
rle_byte = target[current_offset];
use_rle = true;
for (size_t i = 1; i < chunk_size; ++i) {
if (current_offset + i >= target.size() ||
target[current_offset + i] != rle_byte) {
use_rle = false;
break;
}
}
}
if (use_rle && chunk_size >= 3) {
// RLE record
WriteIPS24BitOffset(patch, current_offset);
WriteIPS16BitSize(patch, 0); // RLE marker
WriteIPS16BitSize(patch, chunk_size);
patch.push_back(rle_byte);
} else {
// Normal record
WriteIPS24BitOffset(patch, current_offset);
WriteIPS16BitSize(patch, chunk_size);
for (size_t i = 0; i < chunk_size; ++i) {
if (current_offset + i < target.size()) {
patch.push_back(target[current_offset + i]);
} else {
patch.push_back(0); // Pad with zeros if target is shorter
}
}
}
current_offset += chunk_size;
remaining -= chunk_size;
}
}
// IPS EOF marker
patch.push_back('E');
patch.push_back('O');
patch.push_back('F');
return patch;
}
absl::Status WasmPatchExport::DownloadPatchFile(const std::string& filename,
const std::vector<uint8_t>& data,
const std::string& mime_type) {
if (data.empty()) {
return absl::InvalidArgumentError("Cannot download empty patch file");
}
if (filename.empty()) {
return absl::InvalidArgumentError("Filename cannot be empty");
}
// clang-format off
downloadPatchFile_impl(filename.c_str(), data.data(), data.size(), mime_type.c_str());
// clang-format on
return absl::OkStatus();
}
absl::Status WasmPatchExport::ExportBPS(const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified,
const std::string& filename) {
if (original.empty()) {
return absl::InvalidArgumentError("Original ROM data is empty");
}
if (modified.empty()) {
return absl::InvalidArgumentError("Modified ROM data is empty");
}
if (filename.empty()) {
return absl::InvalidArgumentError("Filename cannot be empty");
}
// Generate the BPS patch
std::vector<uint8_t> patch = GenerateBPSPatch(original, modified);
if (patch.empty()) {
return absl::InternalError("Failed to generate BPS patch");
}
// Download the patch file
return DownloadPatchFile(filename, patch, "application/octet-stream");
}
absl::Status WasmPatchExport::ExportIPS(const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified,
const std::string& filename) {
if (original.empty()) {
return absl::InvalidArgumentError("Original ROM data is empty");
}
if (modified.empty()) {
return absl::InvalidArgumentError("Modified ROM data is empty");
}
if (filename.empty()) {
return absl::InvalidArgumentError("Filename cannot be empty");
}
// Check for IPS size limitation
if (modified.size() > 0xFFFFFF) {
return absl::InvalidArgumentError(
absl::StrFormat("ROM size (%zu bytes) exceeds IPS format limit (16MB)",
modified.size()));
}
// Generate the IPS patch
std::vector<uint8_t> patch = GenerateIPSPatch(original, modified);
if (patch.empty()) {
return absl::InternalError("Failed to generate IPS patch");
}
// Download the patch file
return DownloadPatchFile(filename, patch, "application/octet-stream");
}
PatchInfo WasmPatchExport::GetPatchPreview(const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified) {
PatchInfo info;
info.changed_bytes = 0;
info.num_regions = 0;
if (original.empty() || modified.empty()) {
return info;
}
// Find all changed regions
info.changed_regions = FindChangedRegions(original, modified);
info.num_regions = info.changed_regions.size();
// Calculate total changed bytes
for (const auto& region : info.changed_regions) {
info.changed_bytes += region.second;
}
return info;
}
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,151 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_PATCH_EXPORT_H_
#define YAZE_APP_PLATFORM_WASM_WASM_PATCH_EXPORT_H_
#ifdef __EMSCRIPTEN__
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace platform {
/**
* @struct PatchInfo
* @brief Information about the differences between original and modified ROMs
*/
struct PatchInfo {
size_t changed_bytes; // Total number of changed bytes
size_t num_regions; // Number of distinct changed regions
std::vector<std::pair<size_t, size_t>> changed_regions; // List of (offset, length) pairs
};
/**
* @class WasmPatchExport
* @brief Patch export functionality for WASM/browser environment
*
* This class provides functionality to generate and export BPS and IPS patches
* from ROM modifications, allowing users to save their changes as patch files
* that can be applied to clean ROMs.
*/
class WasmPatchExport {
public:
/**
* @brief Export modifications as a BPS (Beat) patch file
*
* BPS is a modern patching format that supports:
* - Variable-length encoding for efficient storage
* - Delta encoding for changed regions
* - CRC32 checksums for source, target, and patch validation
*
* @param original Original ROM data
* @param modified Modified ROM data
* @param filename Suggested filename for the download (e.g., "hack.bps")
* @return Status indicating success or failure
*/
static absl::Status ExportBPS(const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified,
const std::string& filename);
/**
* @brief Export modifications as an IPS patch file
*
* IPS is a classic patching format that supports:
* - Simple record-based structure
* - RLE encoding for repeated bytes
* - Maximum file size of 16MB (24-bit addressing)
*
* @param original Original ROM data
* @param modified Modified ROM data
* @param filename Suggested filename for the download (e.g., "hack.ips")
* @return Status indicating success or failure
*/
static absl::Status ExportIPS(const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified,
const std::string& filename);
/**
* @brief Get a preview of changes between original and modified ROMs
*
* Analyzes the differences and returns summary information about
* what would be included in a patch file.
*
* @param original Original ROM data
* @param modified Modified ROM data
* @return PatchInfo structure containing change statistics
*/
static PatchInfo GetPatchPreview(const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified);
private:
// CRC32 calculation
static uint32_t CalculateCRC32(const std::vector<uint8_t>& data);
static uint32_t CalculateCRC32(const uint8_t* data, size_t size);
// BPS format helpers
static void WriteVariableLength(std::vector<uint8_t>& output, uint64_t value);
static std::vector<uint8_t> GenerateBPSPatch(const std::vector<uint8_t>& source,
const std::vector<uint8_t>& target);
// IPS format helpers
static void WriteIPS24BitOffset(std::vector<uint8_t>& output, uint32_t offset);
static void WriteIPS16BitSize(std::vector<uint8_t>& output, uint16_t size);
static std::vector<uint8_t> GenerateIPSPatch(const std::vector<uint8_t>& source,
const std::vector<uint8_t>& target);
// Common helpers
static std::vector<std::pair<size_t, size_t>> FindChangedRegions(
const std::vector<uint8_t>& original,
const std::vector<uint8_t>& modified);
// Browser download functionality (implemented via EM_JS)
static absl::Status DownloadPatchFile(const std::string& filename,
const std::vector<uint8_t>& data,
const std::string& mime_type);
};
} // namespace platform
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub implementation for non-WASM builds
namespace yaze {
namespace platform {
struct PatchInfo {
size_t changed_bytes = 0;
size_t num_regions = 0;
std::vector<std::pair<size_t, size_t>> changed_regions;
};
class WasmPatchExport {
public:
static absl::Status ExportBPS(const std::vector<uint8_t>&,
const std::vector<uint8_t>&,
const std::string&) {
return absl::UnimplementedError("Patch export is only available in WASM builds");
}
static absl::Status ExportIPS(const std::vector<uint8_t>&,
const std::vector<uint8_t>&,
const std::string&) {
return absl::UnimplementedError("Patch export is only available in WASM builds");
}
static PatchInfo GetPatchPreview(const std::vector<uint8_t>&,
const std::vector<uint8_t>&) {
return PatchInfo{};
}
};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_PATCH_EXPORT_H_

View File

@@ -0,0 +1,497 @@
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_secure_storage.h"
#include <emscripten.h>
#include <emscripten/val.h>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
namespace yaze {
namespace app {
namespace platform {
namespace {
// JavaScript interop functions for sessionStorage
EM_JS(void, js_session_storage_set, (const char* key, const char* value), {
try {
if (typeof(Storage) !== "undefined" && sessionStorage) {
sessionStorage.setItem(UTF8ToString(key), UTF8ToString(value));
}
} catch (e) {
console.error('Failed to set sessionStorage:', e);
}
});
EM_JS(char*, js_session_storage_get, (const char* key), {
try {
if (typeof(Storage) !== "undefined" && sessionStorage) {
const value = sessionStorage.getItem(UTF8ToString(key));
if (value === null) return null;
const len = lengthBytesUTF8(value) + 1;
const ptr = _malloc(len);
stringToUTF8(value, ptr, len);
return ptr;
}
} catch (e) {
console.error('Failed to get from sessionStorage:', e);
}
return null;
});
EM_JS(void, js_session_storage_remove, (const char* key), {
try {
if (typeof(Storage) !== "undefined" && sessionStorage) {
sessionStorage.removeItem(UTF8ToString(key));
}
} catch (e) {
console.error('Failed to remove from sessionStorage:', e);
}
});
EM_JS(int, js_session_storage_has, (const char* key), {
try {
if (typeof(Storage) !== "undefined" && sessionStorage) {
return sessionStorage.getItem(UTF8ToString(key)) !== null ? 1 : 0;
}
} catch (e) {
console.error('Failed to check sessionStorage:', e);
}
return 0;
});
// JavaScript interop functions for localStorage
EM_JS(void, js_local_storage_set, (const char* key, const char* value), {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
localStorage.setItem(UTF8ToString(key), UTF8ToString(value));
}
} catch (e) {
console.error('Failed to set localStorage:', e);
}
});
EM_JS(char*, js_local_storage_get, (const char* key), {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
const value = localStorage.getItem(UTF8ToString(key));
if (value === null) return null;
const len = lengthBytesUTF8(value) + 1;
const ptr = _malloc(len);
stringToUTF8(value, ptr, len);
return ptr;
}
} catch (e) {
console.error('Failed to get from localStorage:', e);
}
return null;
});
EM_JS(void, js_local_storage_remove, (const char* key), {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
localStorage.removeItem(UTF8ToString(key));
}
} catch (e) {
console.error('Failed to remove from localStorage:', e);
}
});
EM_JS(int, js_local_storage_has, (const char* key), {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
return localStorage.getItem(UTF8ToString(key)) !== null ? 1 : 0;
}
} catch (e) {
console.error('Failed to check localStorage:', e);
}
return 0;
});
// Get all keys from storage
EM_JS(char*, js_session_storage_keys, (), {
try {
if (typeof(Storage) !== "undefined" && sessionStorage) {
const keys = [];
for (let i = 0; i < sessionStorage.length; i++) {
keys.push(sessionStorage.key(i));
}
const keysStr = keys.join('|');
const len = lengthBytesUTF8(keysStr) + 1;
const ptr = _malloc(len);
stringToUTF8(keysStr, ptr, len);
return ptr;
}
} catch (e) {
console.error('Failed to get sessionStorage keys:', e);
}
return null;
});
EM_JS(char*, js_local_storage_keys, (), {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
keys.push(localStorage.key(i));
}
const keysStr = keys.join('|');
const len = lengthBytesUTF8(keysStr) + 1;
const ptr = _malloc(len);
stringToUTF8(keysStr, ptr, len);
return ptr;
}
} catch (e) {
console.error('Failed to get localStorage keys:', e);
}
return null;
});
// Clear all keys with prefix
EM_JS(void, js_session_storage_clear_prefix, (const char* prefix), {
try {
if (typeof(Storage) !== "undefined" && sessionStorage) {
const prefixStr = UTF8ToString(prefix);
const keysToRemove = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith(prefixStr)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => sessionStorage.removeItem(key));
}
} catch (e) {
console.error('Failed to clear sessionStorage prefix:', e);
}
});
EM_JS(void, js_local_storage_clear_prefix, (const char* prefix), {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
const prefixStr = UTF8ToString(prefix);
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(prefixStr)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}
} catch (e) {
console.error('Failed to clear localStorage prefix:', e);
}
});
// Check if storage is available
EM_JS(int, js_is_storage_available, (), {
try {
if (typeof(Storage) !== "undefined") {
// Test both storage types
const testKey = '__yaze_storage_test__';
// Test sessionStorage
if (sessionStorage) {
sessionStorage.setItem(testKey, 'test');
sessionStorage.removeItem(testKey);
}
// Test localStorage
if (localStorage) {
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
}
return 1;
}
} catch (e) {
console.error('Storage not available:', e);
}
return 0;
});
// Helper to split string by delimiter
std::vector<std::string> SplitString(const std::string& str, char delimiter) {
std::vector<std::string> result;
size_t start = 0;
size_t end = str.find(delimiter);
while (end != std::string::npos) {
result.push_back(str.substr(start, end - start));
start = end + 1;
end = str.find(delimiter, start);
}
if (start < str.length()) {
result.push_back(str.substr(start));
}
return result;
}
} // namespace
// Public methods implementation
absl::Status WasmSecureStorage::StoreApiKey(const std::string& service,
const std::string& key,
StorageType storage_type) {
if (service.empty()) {
return absl::InvalidArgumentError("Service name cannot be empty");
}
if (key.empty()) {
return absl::InvalidArgumentError("API key cannot be empty");
}
std::string storage_key = BuildApiKeyStorageKey(service);
if (storage_type == StorageType::kSession) {
js_session_storage_set(storage_key.c_str(), key.c_str());
} else {
js_local_storage_set(storage_key.c_str(), key.c_str());
}
return absl::OkStatus();
}
absl::StatusOr<std::string> WasmSecureStorage::RetrieveApiKey(
const std::string& service,
StorageType storage_type) {
if (service.empty()) {
return absl::InvalidArgumentError("Service name cannot be empty");
}
std::string storage_key = BuildApiKeyStorageKey(service);
char* value = nullptr;
if (storage_type == StorageType::kSession) {
value = js_session_storage_get(storage_key.c_str());
} else {
value = js_local_storage_get(storage_key.c_str());
}
if (value == nullptr) {
return absl::NotFoundError(
absl::StrFormat("No API key found for service: %s", service));
}
std::string result(value);
free(value); // Free the allocated memory from JS
return result;
}
absl::Status WasmSecureStorage::ClearApiKey(const std::string& service,
StorageType storage_type) {
if (service.empty()) {
return absl::InvalidArgumentError("Service name cannot be empty");
}
std::string storage_key = BuildApiKeyStorageKey(service);
if (storage_type == StorageType::kSession) {
js_session_storage_remove(storage_key.c_str());
} else {
js_local_storage_remove(storage_key.c_str());
}
return absl::OkStatus();
}
bool WasmSecureStorage::HasApiKey(const std::string& service,
StorageType storage_type) {
if (service.empty()) {
return false;
}
std::string storage_key = BuildApiKeyStorageKey(service);
if (storage_type == StorageType::kSession) {
return js_session_storage_has(storage_key.c_str()) != 0;
} else {
return js_local_storage_has(storage_key.c_str()) != 0;
}
}
absl::Status WasmSecureStorage::StoreSecret(const std::string& key,
const std::string& value,
StorageType storage_type) {
if (key.empty()) {
return absl::InvalidArgumentError("Key cannot be empty");
}
if (value.empty()) {
return absl::InvalidArgumentError("Value cannot be empty");
}
std::string storage_key = BuildSecretStorageKey(key);
if (storage_type == StorageType::kSession) {
js_session_storage_set(storage_key.c_str(), value.c_str());
} else {
js_local_storage_set(storage_key.c_str(), value.c_str());
}
return absl::OkStatus();
}
absl::StatusOr<std::string> WasmSecureStorage::RetrieveSecret(
const std::string& key,
StorageType storage_type) {
if (key.empty()) {
return absl::InvalidArgumentError("Key cannot be empty");
}
std::string storage_key = BuildSecretStorageKey(key);
char* value = nullptr;
if (storage_type == StorageType::kSession) {
value = js_session_storage_get(storage_key.c_str());
} else {
value = js_local_storage_get(storage_key.c_str());
}
if (value == nullptr) {
return absl::NotFoundError(absl::StrFormat("No secret found for key: %s", key));
}
std::string result(value);
free(value);
return result;
}
absl::Status WasmSecureStorage::ClearSecret(const std::string& key,
StorageType storage_type) {
if (key.empty()) {
return absl::InvalidArgumentError("Key cannot be empty");
}
std::string storage_key = BuildSecretStorageKey(key);
if (storage_type == StorageType::kSession) {
js_session_storage_remove(storage_key.c_str());
} else {
js_local_storage_remove(storage_key.c_str());
}
return absl::OkStatus();
}
std::vector<std::string> WasmSecureStorage::ListStoredApiKeys(
StorageType storage_type) {
std::vector<std::string> services;
char* keys_str = nullptr;
if (storage_type == StorageType::kSession) {
keys_str = js_session_storage_keys();
} else {
keys_str = js_local_storage_keys();
}
if (keys_str != nullptr) {
std::string keys(keys_str);
free(keys_str);
// Split keys by delimiter and filter for API keys
auto all_keys = SplitString(keys, '|');
std::string api_prefix = kApiKeyPrefix;
for (const auto& key : all_keys) {
if (key.find(api_prefix) == 0) {
// Extract service name from key
std::string service = ExtractServiceFromKey(key);
if (!service.empty()) {
services.push_back(service);
}
}
}
}
return services;
}
absl::Status WasmSecureStorage::ClearAllApiKeys(StorageType storage_type) {
if (storage_type == StorageType::kSession) {
js_session_storage_clear_prefix(kApiKeyPrefix);
} else {
js_local_storage_clear_prefix(kApiKeyPrefix);
}
return absl::OkStatus();
}
bool WasmSecureStorage::IsStorageAvailable() {
return js_is_storage_available() != 0;
}
absl::StatusOr<WasmSecureStorage::StorageQuota> WasmSecureStorage::GetStorageQuota(
StorageType storage_type) {
// Browser storage APIs don't provide direct quota information
// We can estimate based on typical limits
StorageQuota quota;
if (storage_type == StorageType::kSession) {
// sessionStorage typically has 5-10MB limit
quota.available_bytes = 5 * 1024 * 1024; // 5MB estimate
} else {
// localStorage typically has 5-10MB limit
quota.available_bytes = 10 * 1024 * 1024; // 10MB estimate
}
// Estimate used bytes by summing all stored values
char* keys_str = nullptr;
if (storage_type == StorageType::kSession) {
keys_str = js_session_storage_keys();
} else {
keys_str = js_local_storage_keys();
}
size_t used = 0;
if (keys_str != nullptr) {
std::string keys(keys_str);
free(keys_str);
auto all_keys = SplitString(keys, '|');
for (const auto& key : all_keys) {
char* value = nullptr;
if (storage_type == StorageType::kSession) {
value = js_session_storage_get(key.c_str());
} else {
value = js_local_storage_get(key.c_str());
}
if (value != nullptr) {
used += strlen(value) + key.length();
free(value);
}
}
}
quota.used_bytes = used;
return quota;
}
// Private methods implementation
std::string WasmSecureStorage::BuildApiKeyStorageKey(const std::string& service) {
return absl::StrCat(kApiKeyPrefix, service);
}
std::string WasmSecureStorage::BuildSecretStorageKey(const std::string& key) {
return absl::StrCat(kSecretPrefix, key);
}
std::string WasmSecureStorage::ExtractServiceFromKey(const std::string& storage_key) {
std::string prefix = kApiKeyPrefix;
if (storage_key.find(prefix) == 0) {
return storage_key.substr(prefix.length());
}
return "";
}
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,262 @@
#ifndef YAZE_APP_PLATFORM_WASM_SECURE_STORAGE_H_
#define YAZE_APP_PLATFORM_WASM_SECURE_STORAGE_H_
#ifdef __EMSCRIPTEN__
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace app {
namespace platform {
/**
* @class WasmSecureStorage
* @brief Secure storage for sensitive data in browser environment
*
* This class provides secure storage for API keys and other sensitive
* data using browser storage APIs. It uses sessionStorage by default
* (cleared when tab closes) with optional localStorage for persistence.
*
* Security considerations:
* - API keys are stored in sessionStorage by default (memory only)
* - localStorage option available for user convenience (less secure)
* - Keys are prefixed with "yaze_secure_" to avoid conflicts
* - No encryption currently (future enhancement)
*
* Storage format:
* - Key: "yaze_secure_<service>_<type>"
* - Value: Raw string value (API key, token, etc.)
*/
class WasmSecureStorage {
public:
/**
* @enum StorageType
* @brief Type of browser storage to use
*/
enum class StorageType {
kSession, // sessionStorage (cleared on tab close)
kLocal // localStorage (persistent)
};
/**
* @brief Store an API key for a service
* @param service Service name (e.g., "gemini", "openai")
* @param key API key value
* @param storage_type Storage type to use
* @return Success status
*/
static absl::Status StoreApiKey(const std::string& service,
const std::string& key,
StorageType storage_type = StorageType::kSession);
/**
* @brief Retrieve an API key for a service
* @param service Service name
* @param storage_type Storage type to check
* @return API key or NotFound error
*/
static absl::StatusOr<std::string> RetrieveApiKey(
const std::string& service,
StorageType storage_type = StorageType::kSession);
/**
* @brief Clear an API key for a service
* @param service Service name
* @param storage_type Storage type to clear from
* @return Success status
*/
static absl::Status ClearApiKey(const std::string& service,
StorageType storage_type = StorageType::kSession);
/**
* @brief Check if an API key exists for a service
* @param service Service name
* @param storage_type Storage type to check
* @return True if key exists
*/
static bool HasApiKey(const std::string& service,
StorageType storage_type = StorageType::kSession);
/**
* @brief Store a generic secret value
* @param key Storage key
* @param value Secret value
* @param storage_type Storage type to use
* @return Success status
*/
static absl::Status StoreSecret(const std::string& key,
const std::string& value,
StorageType storage_type = StorageType::kSession);
/**
* @brief Retrieve a generic secret value
* @param key Storage key
* @param storage_type Storage type to check
* @return Secret value or NotFound error
*/
static absl::StatusOr<std::string> RetrieveSecret(
const std::string& key,
StorageType storage_type = StorageType::kSession);
/**
* @brief Clear a generic secret value
* @param key Storage key
* @param storage_type Storage type to clear from
* @return Success status
*/
static absl::Status ClearSecret(const std::string& key,
StorageType storage_type = StorageType::kSession);
/**
* @brief List all stored API keys (service names only)
* @param storage_type Storage type to check
* @return List of service names with stored keys
*/
static std::vector<std::string> ListStoredApiKeys(
StorageType storage_type = StorageType::kSession);
/**
* @brief Clear all stored API keys
* @param storage_type Storage type to clear
* @return Success status
*/
static absl::Status ClearAllApiKeys(StorageType storage_type = StorageType::kSession);
/**
* @brief Check if browser storage is available
* @return True if storage APIs are available
*/
static bool IsStorageAvailable();
/**
* @brief Get storage quota information
* @param storage_type Storage type to check
* @return Used and available bytes, or error if not supported
*/
struct StorageQuota {
size_t used_bytes = 0;
size_t available_bytes = 0;
};
static absl::StatusOr<StorageQuota> GetStorageQuota(
StorageType storage_type = StorageType::kSession);
private:
// Key prefixes for different types of data
static constexpr const char* kApiKeyPrefix = "yaze_secure_api_";
static constexpr const char* kSecretPrefix = "yaze_secure_secret_";
/**
* @brief Build storage key for API keys
* @param service Service name
* @return Full storage key
*/
static std::string BuildApiKeyStorageKey(const std::string& service);
/**
* @brief Build storage key for secrets
* @param key User-provided key
* @return Full storage key
*/
static std::string BuildSecretStorageKey(const std::string& key);
/**
* @brief Extract service name from storage key
* @param storage_key Full storage key
* @return Service name or empty if not an API key
*/
static std::string ExtractServiceFromKey(const std::string& storage_key);
};
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub for non-WASM builds
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace app {
namespace platform {
/**
* Non-WASM stub for WasmSecureStorage.
* All methods return Unimplemented/NotFound as secure browser storage is not available.
*/
class WasmSecureStorage {
public:
enum class StorageType { kSession, kLocal };
static absl::Status StoreApiKey(const std::string&, const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Secure storage requires WASM build");
}
static absl::StatusOr<std::string> RetrieveApiKey(
const std::string&, StorageType = StorageType::kSession) {
return absl::NotFoundError("Secure storage requires WASM build");
}
static absl::Status ClearApiKey(const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Secure storage requires WASM build");
}
static bool HasApiKey(const std::string&,
StorageType = StorageType::kSession) {
return false;
}
static absl::Status StoreSecret(const std::string&, const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Secure storage requires WASM build");
}
static absl::StatusOr<std::string> RetrieveSecret(
const std::string&, StorageType = StorageType::kSession) {
return absl::NotFoundError("Secure storage requires WASM build");
}
static absl::Status ClearSecret(const std::string&,
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Secure storage requires WASM build");
}
static std::vector<std::string> ListStoredApiKeys(
StorageType = StorageType::kSession) {
return {};
}
static absl::Status ClearAllApiKeys(StorageType = StorageType::kSession) {
return absl::UnimplementedError("Secure storage requires WASM build");
}
static bool IsStorageAvailable() { return false; }
struct StorageQuota {
size_t used_bytes = 0;
size_t available_bytes = 0;
};
static absl::StatusOr<StorageQuota> GetStorageQuota(
StorageType = StorageType::kSession) {
return absl::UnimplementedError("Secure storage requires WASM build");
}
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_SECURE_STORAGE_H_

View File

@@ -0,0 +1,681 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_session_bridge.h"
#include <emscripten.h>
#include <emscripten/bind.h>
#include "absl/strings/str_format.h"
#include "app/editor/editor.h"
#include "app/editor/editor_manager.h"
#include "rom/rom.h"
#include "nlohmann/json.hpp"
#include "util/log.h"
namespace yaze {
namespace app {
namespace platform {
// Static member initialization
editor::EditorManager* WasmSessionBridge::editor_manager_ = nullptr;
bool WasmSessionBridge::initialized_ = false;
SharedSessionState WasmSessionBridge::current_state_;
std::mutex WasmSessionBridge::state_mutex_;
std::vector<WasmSessionBridge::StateChangeCallback> WasmSessionBridge::state_callbacks_;
WasmSessionBridge::CommandCallback WasmSessionBridge::command_handler_;
std::string WasmSessionBridge::pending_command_;
std::string WasmSessionBridge::pending_result_;
bool WasmSessionBridge::command_pending_ = false;
// ============================================================================
// SharedSessionState Implementation
// ============================================================================
std::string SharedSessionState::ToJson() const {
nlohmann::json j;
// ROM state
j["rom"]["loaded"] = rom_loaded;
j["rom"]["filename"] = rom_filename;
j["rom"]["title"] = rom_title;
j["rom"]["size"] = rom_size;
j["rom"]["dirty"] = rom_dirty;
// Editor state
j["editor"]["current"] = current_editor;
j["editor"]["type"] = current_editor_type;
j["editor"]["visible_cards"] = visible_cards;
// Session info
j["session"]["id"] = session_id;
j["session"]["count"] = session_count;
j["session"]["name"] = session_name;
// Feature flags
j["flags"]["save_all_palettes"] = flag_save_all_palettes;
j["flags"]["save_gfx_groups"] = flag_save_gfx_groups;
j["flags"]["save_overworld_maps"] = flag_save_overworld_maps;
j["flags"]["load_custom_overworld"] = flag_load_custom_overworld;
j["flags"]["apply_zscustom_asm"] = flag_apply_zscustom_asm;
// Project info
j["project"]["name"] = project_name;
j["project"]["path"] = project_path;
j["project"]["has_project"] = has_project;
// Z3ed state
j["z3ed"]["last_command"] = last_z3ed_command;
j["z3ed"]["last_result"] = last_z3ed_result;
j["z3ed"]["command_pending"] = z3ed_command_pending;
return j.dump();
}
SharedSessionState SharedSessionState::FromJson(const std::string& json) {
SharedSessionState state;
try {
auto j = nlohmann::json::parse(json);
// ROM state
if (j.contains("rom")) {
state.rom_loaded = j["rom"].value("loaded", false);
state.rom_filename = j["rom"].value("filename", "");
state.rom_title = j["rom"].value("title", "");
state.rom_size = j["rom"].value("size", 0);
state.rom_dirty = j["rom"].value("dirty", false);
}
// Editor state
if (j.contains("editor")) {
state.current_editor = j["editor"].value("current", "");
state.current_editor_type = j["editor"].value("type", 0);
if (j["editor"].contains("visible_cards")) {
state.visible_cards = j["editor"]["visible_cards"].get<std::vector<std::string>>();
}
}
// Session info
if (j.contains("session")) {
state.session_id = j["session"].value("id", 0);
state.session_count = j["session"].value("count", 1);
state.session_name = j["session"].value("name", "");
}
// Feature flags
if (j.contains("flags")) {
state.flag_save_all_palettes = j["flags"].value("save_all_palettes", false);
state.flag_save_gfx_groups = j["flags"].value("save_gfx_groups", false);
state.flag_save_overworld_maps = j["flags"].value("save_overworld_maps", true);
state.flag_load_custom_overworld = j["flags"].value("load_custom_overworld", false);
state.flag_apply_zscustom_asm = j["flags"].value("apply_zscustom_asm", false);
}
// Project info
if (j.contains("project")) {
state.project_name = j["project"].value("name", "");
state.project_path = j["project"].value("path", "");
state.has_project = j["project"].value("has_project", false);
}
} catch (const std::exception& e) {
LOG_ERROR("SharedSessionState", "Failed to parse JSON: %s", e.what());
}
return state;
}
void SharedSessionState::UpdateFromEditor(editor::EditorManager* manager) {
if (!manager) return;
// ROM state
auto* rom = manager->GetCurrentRom();
if (rom && rom->is_loaded()) {
rom_loaded = true;
rom_filename = rom->filename();
rom_title = rom->title();
rom_size = rom->size();
rom_dirty = rom->dirty();
} else {
rom_loaded = false;
rom_filename = "";
rom_title = "";
rom_size = 0;
rom_dirty = false;
}
// Editor state
auto* current = manager->GetCurrentEditor();
if (current) {
current_editor_type = static_cast<int>(current->type());
if (current_editor_type >= 0 &&
current_editor_type < static_cast<int>(editor::kEditorNames.size())) {
current_editor = editor::kEditorNames[current_editor_type];
}
}
// Session info
session_id = manager->GetCurrentSessionIndex();
session_count = manager->GetActiveSessionCount();
// Feature flags from global
auto& flags = core::FeatureFlags::get();
flag_save_all_palettes = flags.kSaveAllPalettes;
flag_save_gfx_groups = flags.kSaveGfxGroups;
flag_save_overworld_maps = flags.overworld.kSaveOverworldMaps;
flag_load_custom_overworld = flags.overworld.kLoadCustomOverworld;
flag_apply_zscustom_asm = flags.overworld.kApplyZSCustomOverworldASM;
}
absl::Status SharedSessionState::ApplyToEditor(editor::EditorManager* manager) {
if (!manager) {
return absl::InvalidArgumentError("EditorManager is null");
}
// Apply feature flags to global
auto& flags = core::FeatureFlags::get();
flags.kSaveAllPalettes = flag_save_all_palettes;
flags.kSaveGfxGroups = flag_save_gfx_groups;
flags.overworld.kSaveOverworldMaps = flag_save_overworld_maps;
flags.overworld.kLoadCustomOverworld = flag_load_custom_overworld;
flags.overworld.kApplyZSCustomOverworldASM = flag_apply_zscustom_asm;
// Switch editor if changed
if (!current_editor.empty()) {
for (size_t i = 0; i < editor::kEditorNames.size(); ++i) {
if (editor::kEditorNames[i] == current_editor) {
manager->SwitchToEditor(static_cast<editor::EditorType>(i));
break;
}
}
}
return absl::OkStatus();
}
// ============================================================================
// JavaScript Bindings Setup
// ============================================================================
EM_JS(void, SetupYazeSessionApi, (), {
if (typeof Module === 'undefined') return;
// Create unified window.yaze namespace if not exists
if (!window.yaze) {
window.yaze = {};
}
// Session API namespace
window.yaze.session = {
// State management
getState: function() {
if (Module.sessionGetState) {
try { return JSON.parse(Module.sessionGetState()); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
setState: function(state) {
if (Module.sessionSetState) {
try { return JSON.parse(Module.sessionSetState(JSON.stringify(state))); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
getProperty: function(name) {
if (Module.sessionGetProperty) {
try { return JSON.parse(Module.sessionGetProperty(name)); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
setProperty: function(name, value) {
if (Module.sessionSetProperty) {
try { return JSON.parse(Module.sessionSetProperty(name, JSON.stringify(value))); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
refresh: function() {
if (Module.sessionRefreshState) {
try { return JSON.parse(Module.sessionRefreshState()); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
// Feature flags
getFlags: function() {
if (Module.sessionGetFeatureFlags) {
try { return JSON.parse(Module.sessionGetFeatureFlags()); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
setFlag: function(name, value) {
if (Module.sessionSetFeatureFlag) {
try { return JSON.parse(Module.sessionSetFeatureFlag(name, value)); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
getAvailableFlags: function() {
if (Module.sessionGetAvailableFlags) {
try { return JSON.parse(Module.sessionGetAvailableFlags()); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
// Z3ed integration
executeCommand: function(command) {
if (Module.sessionExecuteZ3edCommand) {
try { return JSON.parse(Module.sessionExecuteZ3edCommand(command)); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
getPendingCommand: function() {
if (Module.sessionGetPendingCommand) {
try { return JSON.parse(Module.sessionGetPendingCommand()); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
setCommandResult: function(result) {
if (Module.sessionSetCommandResult) {
try { return JSON.parse(Module.sessionSetCommandResult(JSON.stringify(result))); }
catch(e) { return {error: e.message}; }
}
return {error: "API not ready"};
},
isReady: function() {
return Module.sessionIsReady ? Module.sessionIsReady() : false;
}
};
console.log("[yaze] window.yaze.session API initialized");
});
// ============================================================================
// WasmSessionBridge Implementation
// ============================================================================
void WasmSessionBridge::Initialize(editor::EditorManager* editor_manager) {
editor_manager_ = editor_manager;
initialized_ = (editor_manager_ != nullptr);
if (initialized_) {
SetupJavaScriptBindings();
// Initialize state from editor
std::lock_guard<std::mutex> lock(state_mutex_);
current_state_.UpdateFromEditor(editor_manager_);
LOG_INFO("WasmSessionBridge", "Session bridge initialized");
}
}
bool WasmSessionBridge::IsReady() {
return initialized_ && editor_manager_ != nullptr;
}
void WasmSessionBridge::SetupJavaScriptBindings() {
SetupYazeSessionApi();
}
std::string WasmSessionBridge::GetState() {
if (!IsReady()) {
return R"({"error": "Session bridge not initialized"})";
}
std::lock_guard<std::mutex> lock(state_mutex_);
current_state_.UpdateFromEditor(editor_manager_);
return current_state_.ToJson();
}
std::string WasmSessionBridge::SetState(const std::string& state_json) {
nlohmann::json result;
if (!IsReady()) {
result["success"] = false;
result["error"] = "Session bridge not initialized";
return result.dump();
}
try {
std::lock_guard<std::mutex> lock(state_mutex_);
auto new_state = SharedSessionState::FromJson(state_json);
auto status = new_state.ApplyToEditor(editor_manager_);
if (status.ok()) {
current_state_ = new_state;
result["success"] = true;
} else {
result["success"] = false;
result["error"] = status.ToString();
}
} catch (const std::exception& e) {
result["success"] = false;
result["error"] = e.what();
}
return result.dump();
}
std::string WasmSessionBridge::GetProperty(const std::string& property_name) {
nlohmann::json result;
if (!IsReady()) {
result["error"] = "Session bridge not initialized";
return result.dump();
}
std::lock_guard<std::mutex> lock(state_mutex_);
current_state_.UpdateFromEditor(editor_manager_);
if (property_name == "rom.loaded") {
result["value"] = current_state_.rom_loaded;
} else if (property_name == "rom.filename") {
result["value"] = current_state_.rom_filename;
} else if (property_name == "rom.title") {
result["value"] = current_state_.rom_title;
} else if (property_name == "rom.size") {
result["value"] = current_state_.rom_size;
} else if (property_name == "rom.dirty") {
result["value"] = current_state_.rom_dirty;
} else if (property_name == "editor.current") {
result["value"] = current_state_.current_editor;
} else if (property_name == "session.id") {
result["value"] = current_state_.session_id;
} else if (property_name == "session.count") {
result["value"] = current_state_.session_count;
} else {
result["error"] = "Unknown property: " + property_name;
}
return result.dump();
}
std::string WasmSessionBridge::SetProperty(const std::string& property_name,
const std::string& value) {
nlohmann::json result;
if (!IsReady()) {
result["success"] = false;
result["error"] = "Session bridge not initialized";
return result.dump();
}
// Most properties are read-only from external sources
// Only feature flags can be set
if (property_name.find("flags.") == 0) {
std::string flag_name = property_name.substr(6);
try {
bool flag_value = nlohmann::json::parse(value).get<bool>();
return SetFeatureFlag(flag_name, flag_value);
} catch (const std::exception& e) {
result["success"] = false;
result["error"] = "Invalid boolean value";
}
} else {
result["success"] = false;
result["error"] = "Property is read-only: " + property_name;
}
return result.dump();
}
// ============================================================================
// Feature Flags
// ============================================================================
std::string WasmSessionBridge::GetFeatureFlags() {
nlohmann::json result;
auto& flags = core::FeatureFlags::get();
result["save_all_palettes"] = flags.kSaveAllPalettes;
result["save_gfx_groups"] = flags.kSaveGfxGroups;
result["save_with_change_queue"] = flags.kSaveWithChangeQueue;
result["save_dungeon_maps"] = flags.kSaveDungeonMaps;
result["save_graphics_sheet"] = flags.kSaveGraphicsSheet;
result["log_to_console"] = flags.kLogToConsole;
result["enable_performance_monitoring"] = flags.kEnablePerformanceMonitoring;
result["enable_tiered_gfx_architecture"] = flags.kEnableTieredGfxArchitecture;
result["use_native_file_dialog"] = flags.kUseNativeFileDialog;
// Overworld flags
result["overworld"]["draw_sprites"] = flags.overworld.kDrawOverworldSprites;
result["overworld"]["save_maps"] = flags.overworld.kSaveOverworldMaps;
result["overworld"]["save_entrances"] = flags.overworld.kSaveOverworldEntrances;
result["overworld"]["save_exits"] = flags.overworld.kSaveOverworldExits;
result["overworld"]["save_items"] = flags.overworld.kSaveOverworldItems;
result["overworld"]["save_properties"] = flags.overworld.kSaveOverworldProperties;
result["overworld"]["load_custom"] = flags.overworld.kLoadCustomOverworld;
result["overworld"]["apply_zscustom_asm"] = flags.overworld.kApplyZSCustomOverworldASM;
return result.dump();
}
std::string WasmSessionBridge::SetFeatureFlag(const std::string& flag_name, bool value) {
nlohmann::json result;
auto& flags = core::FeatureFlags::get();
bool found = true;
if (flag_name == "save_all_palettes") {
flags.kSaveAllPalettes = value;
} else if (flag_name == "save_gfx_groups") {
flags.kSaveGfxGroups = value;
} else if (flag_name == "save_with_change_queue") {
flags.kSaveWithChangeQueue = value;
} else if (flag_name == "save_dungeon_maps") {
flags.kSaveDungeonMaps = value;
} else if (flag_name == "save_graphics_sheet") {
flags.kSaveGraphicsSheet = value;
} else if (flag_name == "log_to_console") {
flags.kLogToConsole = value;
} else if (flag_name == "enable_performance_monitoring") {
flags.kEnablePerformanceMonitoring = value;
} else if (flag_name == "overworld.draw_sprites") {
flags.overworld.kDrawOverworldSprites = value;
} else if (flag_name == "overworld.save_maps") {
flags.overworld.kSaveOverworldMaps = value;
} else if (flag_name == "overworld.save_entrances") {
flags.overworld.kSaveOverworldEntrances = value;
} else if (flag_name == "overworld.save_exits") {
flags.overworld.kSaveOverworldExits = value;
} else if (flag_name == "overworld.save_items") {
flags.overworld.kSaveOverworldItems = value;
} else if (flag_name == "overworld.save_properties") {
flags.overworld.kSaveOverworldProperties = value;
} else if (flag_name == "overworld.load_custom") {
flags.overworld.kLoadCustomOverworld = value;
} else if (flag_name == "overworld.apply_zscustom_asm") {
flags.overworld.kApplyZSCustomOverworldASM = value;
} else {
found = false;
}
if (found) {
result["success"] = true;
result["flag"] = flag_name;
result["value"] = value;
LOG_INFO("WasmSessionBridge", "Set flag %s = %s", flag_name.c_str(), value ? "true" : "false");
} else {
result["success"] = false;
result["error"] = "Unknown flag: " + flag_name;
}
return result.dump();
}
std::string WasmSessionBridge::GetAvailableFlags() {
nlohmann::json result = nlohmann::json::array();
result.push_back("save_all_palettes");
result.push_back("save_gfx_groups");
result.push_back("save_with_change_queue");
result.push_back("save_dungeon_maps");
result.push_back("save_graphics_sheet");
result.push_back("log_to_console");
result.push_back("enable_performance_monitoring");
result.push_back("enable_tiered_gfx_architecture");
result.push_back("overworld.draw_sprites");
result.push_back("overworld.save_maps");
result.push_back("overworld.save_entrances");
result.push_back("overworld.save_exits");
result.push_back("overworld.save_items");
result.push_back("overworld.save_properties");
result.push_back("overworld.load_custom");
result.push_back("overworld.apply_zscustom_asm");
return result.dump();
}
// ============================================================================
// Z3ed Command Integration
// ============================================================================
std::string WasmSessionBridge::ExecuteZ3edCommand(const std::string& command) {
nlohmann::json result;
if (!IsReady()) {
result["success"] = false;
result["error"] = "Session bridge not initialized";
return result.dump();
}
// If we have a command handler, use it directly
if (command_handler_) {
std::string output = command_handler_(command);
result["success"] = true;
result["output"] = output;
std::lock_guard<std::mutex> lock(state_mutex_);
current_state_.last_z3ed_command = command;
current_state_.last_z3ed_result = output;
return result.dump();
}
// Otherwise, queue for external CLI
{
std::lock_guard<std::mutex> lock(state_mutex_);
pending_command_ = command;
command_pending_ = true;
current_state_.z3ed_command_pending = true;
current_state_.last_z3ed_command = command;
}
result["success"] = true;
result["queued"] = true;
result["command"] = command;
LOG_INFO("WasmSessionBridge", "Queued z3ed command: %s", command.c_str());
return result.dump();
}
std::string WasmSessionBridge::GetPendingCommand() {
nlohmann::json result;
std::lock_guard<std::mutex> lock(state_mutex_);
if (command_pending_) {
result["pending"] = true;
result["command"] = pending_command_;
} else {
result["pending"] = false;
}
return result.dump();
}
std::string WasmSessionBridge::SetCommandResult(const std::string& result_str) {
nlohmann::json result;
std::lock_guard<std::mutex> lock(state_mutex_);
pending_result_ = result_str;
command_pending_ = false;
current_state_.z3ed_command_pending = false;
current_state_.last_z3ed_result = result_str;
result["success"] = true;
return result.dump();
}
void WasmSessionBridge::SetCommandHandler(CommandCallback handler) {
command_handler_ = handler;
}
// ============================================================================
// Event System
// ============================================================================
void WasmSessionBridge::OnStateChange(StateChangeCallback callback) {
state_callbacks_.push_back(callback);
}
void WasmSessionBridge::NotifyStateChange() {
std::lock_guard<std::mutex> lock(state_mutex_);
current_state_.UpdateFromEditor(editor_manager_);
for (const auto& callback : state_callbacks_) {
if (callback) {
callback(current_state_);
}
}
}
std::string WasmSessionBridge::RefreshState() {
if (!IsReady()) {
return R"({"error": "Session bridge not initialized"})";
}
std::lock_guard<std::mutex> lock(state_mutex_);
current_state_.UpdateFromEditor(editor_manager_);
nlohmann::json result;
result["success"] = true;
result["state"] = nlohmann::json::parse(current_state_.ToJson());
return result.dump();
}
// ============================================================================
// Emscripten Bindings
// ============================================================================
EMSCRIPTEN_BINDINGS(wasm_session_bridge) {
emscripten::function("sessionIsReady", &WasmSessionBridge::IsReady);
emscripten::function("sessionGetState", &WasmSessionBridge::GetState);
emscripten::function("sessionSetState", &WasmSessionBridge::SetState);
emscripten::function("sessionGetProperty", &WasmSessionBridge::GetProperty);
emscripten::function("sessionSetProperty", &WasmSessionBridge::SetProperty);
emscripten::function("sessionGetFeatureFlags", &WasmSessionBridge::GetFeatureFlags);
emscripten::function("sessionSetFeatureFlag", &WasmSessionBridge::SetFeatureFlag);
emscripten::function("sessionGetAvailableFlags", &WasmSessionBridge::GetAvailableFlags);
emscripten::function("sessionExecuteZ3edCommand", &WasmSessionBridge::ExecuteZ3edCommand);
emscripten::function("sessionGetPendingCommand", &WasmSessionBridge::GetPendingCommand);
emscripten::function("sessionSetCommandResult", &WasmSessionBridge::SetCommandResult);
emscripten::function("sessionRefreshState", &WasmSessionBridge::RefreshState);
}
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,267 @@
#ifndef YAZE_APP_PLATFORM_WASM_SESSION_BRIDGE_H_
#define YAZE_APP_PLATFORM_WASM_SESSION_BRIDGE_H_
#ifdef __EMSCRIPTEN__
#include <functional>
#include <mutex>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "core/features.h"
namespace yaze {
// Forward declarations
class Rom;
namespace editor {
class EditorManager;
} // namespace editor
namespace app {
namespace platform {
/**
* @brief Shared session state structure for IPC between WASM and z3ed
*
* This structure contains all the state that needs to be synchronized
* between the browser-based WASM app and the z3ed CLI tool.
*/
struct SharedSessionState {
// ROM state
bool rom_loaded = false;
std::string rom_filename;
std::string rom_title;
size_t rom_size = 0;
bool rom_dirty = false;
// Editor state
std::string current_editor;
int current_editor_type = 0;
std::vector<std::string> visible_cards;
// Session info
size_t session_id = 0;
size_t session_count = 1;
std::string session_name;
// Feature flags (serializable subset)
bool flag_save_all_palettes = false;
bool flag_save_gfx_groups = false;
bool flag_save_overworld_maps = true;
bool flag_load_custom_overworld = false;
bool flag_apply_zscustom_asm = false;
// Project info
std::string project_name;
std::string project_path;
bool has_project = false;
// Z3ed integration
std::string last_z3ed_command;
std::string last_z3ed_result;
bool z3ed_command_pending = false;
// Serialize to JSON string
std::string ToJson() const;
// Deserialize from JSON string
static SharedSessionState FromJson(const std::string& json);
// Update from current EditorManager state
void UpdateFromEditor(editor::EditorManager* manager);
// Apply changes to EditorManager
absl::Status ApplyToEditor(editor::EditorManager* manager);
};
/**
* @brief Session bridge for bidirectional state sync
*
* Provides:
* - window.yaze.session.* JavaScript API
* - State change event notifications
* - z3ed command execution and result retrieval
* - Feature flag synchronization
*/
class WasmSessionBridge {
public:
// Callback types
using StateChangeCallback = std::function<void(const SharedSessionState&)>;
using CommandCallback = std::function<std::string(const std::string&)>;
/**
* @brief Initialize the session bridge
* @param editor_manager Pointer to the main editor manager
*/
static void Initialize(editor::EditorManager* editor_manager);
/**
* @brief Check if the session bridge is ready
*/
static bool IsReady();
/**
* @brief Setup JavaScript bindings for window.yaze.session
*/
static void SetupJavaScriptBindings();
/**
* @brief Get current session state as JSON
*/
static std::string GetState();
/**
* @brief Set session state from JSON (for external updates)
*/
static std::string SetState(const std::string& state_json);
/**
* @brief Get specific state property
*/
static std::string GetProperty(const std::string& property_name);
/**
* @brief Set specific state property
*/
static std::string SetProperty(const std::string& property_name,
const std::string& value);
// ============================================================================
// Feature Flags
// ============================================================================
/**
* @brief Get all feature flags as JSON
*/
static std::string GetFeatureFlags();
/**
* @brief Set a feature flag by name
*/
static std::string SetFeatureFlag(const std::string& flag_name, bool value);
/**
* @brief Get available feature flag names
*/
static std::string GetAvailableFlags();
// ============================================================================
// Z3ed Command Integration
// ============================================================================
/**
* @brief Execute a z3ed command
* @param command The z3ed command string
* @return JSON result with output or error
*/
static std::string ExecuteZ3edCommand(const std::string& command);
/**
* @brief Get pending z3ed command (for CLI polling)
*/
static std::string GetPendingCommand();
/**
* @brief Set z3ed command result (from CLI)
*/
static std::string SetCommandResult(const std::string& result);
/**
* @brief Register callback for z3ed commands
*/
static void SetCommandHandler(CommandCallback handler);
// ============================================================================
// Event System
// ============================================================================
/**
* @brief Subscribe to state changes
*/
static void OnStateChange(StateChangeCallback callback);
/**
* @brief Notify subscribers of state change
*/
static void NotifyStateChange();
/**
* @brief Force state refresh from EditorManager
*/
static std::string RefreshState();
private:
static editor::EditorManager* editor_manager_;
static bool initialized_;
static SharedSessionState current_state_;
static std::mutex state_mutex_;
static std::vector<StateChangeCallback> state_callbacks_;
static CommandCallback command_handler_;
// Pending z3ed command queue
static std::string pending_command_;
static std::string pending_result_;
static bool command_pending_;
};
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub for non-WASM builds
#include <string>
#include <functional>
namespace yaze {
namespace editor {
class EditorManager;
}
namespace app {
namespace platform {
struct SharedSessionState {
bool rom_loaded = false;
std::string rom_filename;
std::string ToJson() const { return "{}"; }
static SharedSessionState FromJson(const std::string&) { return {}; }
void UpdateFromEditor(editor::EditorManager*) {}
};
class WasmSessionBridge {
public:
using StateChangeCallback = std::function<void(const SharedSessionState&)>;
using CommandCallback = std::function<std::string(const std::string&)>;
static void Initialize(editor::EditorManager*) {}
static bool IsReady() { return false; }
static void SetupJavaScriptBindings() {}
static std::string GetState() { return "{}"; }
static std::string SetState(const std::string&) { return "{}"; }
static std::string GetProperty(const std::string&) { return "{}"; }
static std::string SetProperty(const std::string&, const std::string&) { return "{}"; }
static std::string GetFeatureFlags() { return "{}"; }
static std::string SetFeatureFlag(const std::string&, bool) { return "{}"; }
static std::string GetAvailableFlags() { return "[]"; }
static std::string ExecuteZ3edCommand(const std::string&) { return "{}"; }
static std::string GetPendingCommand() { return "{}"; }
static std::string SetCommandResult(const std::string&) { return "{}"; }
static void SetCommandHandler(CommandCallback) {}
static void OnStateChange(StateChangeCallback) {}
static void NotifyStateChange() {}
static std::string RefreshState() { return "{}"; }
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_SESSION_BRIDGE_H_

View File

@@ -0,0 +1,501 @@
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_settings.h"
#include <emscripten.h>
#include <algorithm>
#include <sstream>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "app/gui/core/theme_manager.h"
#include "app/platform/font_loader.h"
#include "app/platform/wasm/wasm_storage.h"
namespace yaze {
namespace platform {
// JavaScript localStorage interface using EM_JS
EM_JS(void, localStorage_setItem, (const char* key, const char* value), {
try {
localStorage.setItem(UTF8ToString(key), UTF8ToString(value));
} catch (e) {
console.error('Failed to save to localStorage:', e);
}
});
EM_JS(char*, localStorage_getItem, (const char* key), {
try {
const value = localStorage.getItem(UTF8ToString(key));
if (value === null) return null;
const len = lengthBytesUTF8(value) + 1;
const ptr = _malloc(len);
stringToUTF8(value, ptr, len);
return ptr;
} catch (e) {
console.error('Failed to read from localStorage:', e);
return null;
}
});
EM_JS(void, localStorage_removeItem, (const char* key), {
try {
localStorage.removeItem(UTF8ToString(key));
} catch (e) {
console.error('Failed to remove from localStorage:', e);
}
});
EM_JS(int, localStorage_hasItem, (const char* key), {
try {
return localStorage.getItem(UTF8ToString(key)) !== null ? 1 : 0;
} catch (e) {
console.error('Failed to check localStorage:', e);
return 0;
}
});
EM_JS(void, localStorage_clear, (), {
try {
// Only clear yaze-specific keys
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('yaze_')) {
keys.push(key);
}
}
keys.forEach(key => localStorage.removeItem(key));
} catch (e) {
console.error('Failed to clear localStorage:', e);
}
});
// Theme Management
absl::Status WasmSettings::SaveTheme(const std::string& theme) {
localStorage_setItem(kThemeKey, theme.c_str());
return absl::OkStatus();
}
std::string WasmSettings::LoadTheme() {
char* theme = localStorage_getItem(kThemeKey);
if (!theme) {
return "dark"; // Default theme
}
std::string result(theme);
free(theme);
return result;
}
std::string WasmSettings::GetCurrentThemeData() {
return gui::ThemeManager::Get().ExportCurrentThemeJson();
}
absl::Status WasmSettings::LoadUserFont(const std::string& name,
const std::string& data, float size) {
return LoadFontFromMemory(name, data, size);
}
// Recent Files Management
nlohmann::json WasmSettings::RecentFilesToJson(
const std::vector<RecentFile>& files) {
nlohmann::json json_array = nlohmann::json::array();
for (const auto& file : files) {
nlohmann::json entry;
entry["filename"] = file.filename;
entry["timestamp"] =
std::chrono::duration_cast<std::chrono::milliseconds>(
file.timestamp.time_since_epoch())
.count();
json_array.push_back(entry);
}
return json_array;
}
std::vector<WasmSettings::RecentFile> WasmSettings::JsonToRecentFiles(
const nlohmann::json& json) {
std::vector<RecentFile> files;
if (!json.is_array()) return files;
for (const auto& entry : json) {
if (entry.contains("filename") && entry.contains("timestamp")) {
RecentFile file;
file.filename = entry["filename"].get<std::string>();
auto ms = std::chrono::milliseconds(entry["timestamp"].get<int64_t>());
file.timestamp = std::chrono::system_clock::time_point(ms);
files.push_back(file);
}
}
return files;
}
absl::Status WasmSettings::AddRecentFile(
const std::string& filename,
std::chrono::system_clock::time_point timestamp) {
// Load existing recent files
char* json_str = localStorage_getItem(kRecentFilesKey);
std::vector<RecentFile> files;
if (json_str) {
try {
nlohmann::json json = nlohmann::json::parse(json_str);
files = JsonToRecentFiles(json);
} catch (const std::exception& e) {
// Ignore parse errors and start fresh
emscripten_log(EM_LOG_WARN, "Failed to parse recent files: %s", e.what());
}
free(json_str);
}
// Remove existing entry if present
files.erase(
std::remove_if(files.begin(), files.end(),
[&filename](const RecentFile& f) {
return f.filename == filename;
}),
files.end());
// Add new entry at the beginning
files.insert(files.begin(), {filename, timestamp});
// Limit to 20 recent files
if (files.size() > 20) {
files.resize(20);
}
// Save back to localStorage
nlohmann::json json = RecentFilesToJson(files);
localStorage_setItem(kRecentFilesKey, json.dump().c_str());
return absl::OkStatus();
}
std::vector<std::string> WasmSettings::GetRecentFiles(size_t max_count) {
std::vector<std::string> result;
char* json_str = localStorage_getItem(kRecentFilesKey);
if (!json_str) {
return result;
}
try {
nlohmann::json json = nlohmann::json::parse(json_str);
std::vector<RecentFile> files = JsonToRecentFiles(json);
size_t count = std::min(max_count, files.size());
for (size_t i = 0; i < count; ++i) {
result.push_back(files[i].filename);
}
} catch (const std::exception& e) {
emscripten_log(EM_LOG_WARN, "Failed to parse recent files: %s", e.what());
}
free(json_str);
return result;
}
absl::Status WasmSettings::ClearRecentFiles() {
localStorage_removeItem(kRecentFilesKey);
return absl::OkStatus();
}
absl::Status WasmSettings::RemoveRecentFile(const std::string& filename) {
char* json_str = localStorage_getItem(kRecentFilesKey);
if (!json_str) {
return absl::OkStatus(); // Nothing to remove
}
try {
nlohmann::json json = nlohmann::json::parse(json_str);
std::vector<RecentFile> files = JsonToRecentFiles(json);
files.erase(
std::remove_if(files.begin(), files.end(),
[&filename](const RecentFile& f) {
return f.filename == filename;
}),
files.end());
nlohmann::json new_json = RecentFilesToJson(files);
localStorage_setItem(kRecentFilesKey, new_json.dump().c_str());
} catch (const std::exception& e) {
free(json_str);
return absl::InternalError(
absl::StrFormat("Failed to remove recent file: %s", e.what()));
}
free(json_str);
return absl::OkStatus();
}
// Workspace Layout Management
absl::Status WasmSettings::SaveWorkspace(const std::string& name,
const std::string& layout_json) {
std::string key = absl::StrCat(kWorkspacePrefix, name);
return WasmStorage::SaveProject(key, layout_json);
}
absl::StatusOr<std::string> WasmSettings::LoadWorkspace(const std::string& name) {
std::string key = absl::StrCat(kWorkspacePrefix, name);
return WasmStorage::LoadProject(key);
}
std::vector<std::string> WasmSettings::ListWorkspaces() {
std::vector<std::string> all_projects = WasmStorage::ListProjects();
std::vector<std::string> workspaces;
const std::string prefix(kWorkspacePrefix);
for (const auto& project : all_projects) {
if (project.find(prefix) == 0) {
workspaces.push_back(project.substr(prefix.length()));
}
}
return workspaces;
}
absl::Status WasmSettings::DeleteWorkspace(const std::string& name) {
std::string key = absl::StrCat(kWorkspacePrefix, name);
return WasmStorage::DeleteProject(key);
}
absl::Status WasmSettings::SetActiveWorkspace(const std::string& name) {
localStorage_setItem(kActiveWorkspaceKey, name.c_str());
return absl::OkStatus();
}
std::string WasmSettings::GetActiveWorkspace() {
char* workspace = localStorage_getItem(kActiveWorkspaceKey);
if (!workspace) {
return "default";
}
std::string result(workspace);
free(workspace);
return result;
}
// Undo History Persistence
absl::Status WasmSettings::SaveUndoHistory(const std::string& editor_id,
const std::vector<uint8_t>& history) {
std::string key = absl::StrCat(kUndoHistoryPrefix, editor_id);
return WasmStorage::SaveRom(key, history); // Use binary storage
}
absl::StatusOr<std::vector<uint8_t>> WasmSettings::LoadUndoHistory(
const std::string& editor_id) {
std::string key = absl::StrCat(kUndoHistoryPrefix, editor_id);
return WasmStorage::LoadRom(key);
}
absl::Status WasmSettings::ClearUndoHistory(const std::string& editor_id) {
std::string key = absl::StrCat(kUndoHistoryPrefix, editor_id);
return WasmStorage::DeleteRom(key);
}
absl::Status WasmSettings::ClearAllUndoHistory() {
std::vector<std::string> all_roms = WasmStorage::ListRoms();
const std::string prefix(kUndoHistoryPrefix);
for (const auto& rom : all_roms) {
if (rom.find(prefix) == 0) {
auto status = WasmStorage::DeleteRom(rom);
if (!status.ok()) {
return status;
}
}
}
return absl::OkStatus();
}
// General Settings
absl::Status WasmSettings::SaveSetting(const std::string& key,
const nlohmann::json& value) {
std::string storage_key = absl::StrCat(kSettingsPrefix, key);
localStorage_setItem(storage_key.c_str(), value.dump().c_str());
// Update last save time
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
localStorage_setItem(kLastSaveTimeKey, std::to_string(ms).c_str());
return absl::OkStatus();
}
absl::StatusOr<nlohmann::json> WasmSettings::LoadSetting(const std::string& key) {
std::string storage_key = absl::StrCat(kSettingsPrefix, key);
char* value = localStorage_getItem(storage_key.c_str());
if (!value) {
return absl::NotFoundError(absl::StrFormat("Setting '%s' not found", key));
}
try {
nlohmann::json json = nlohmann::json::parse(value);
free(value);
return json;
} catch (const std::exception& e) {
free(value);
return absl::InvalidArgumentError(
absl::StrFormat("Failed to parse setting '%s': %s", key, e.what()));
}
}
bool WasmSettings::HasSetting(const std::string& key) {
std::string storage_key = absl::StrCat(kSettingsPrefix, key);
return localStorage_hasItem(storage_key.c_str()) == 1;
}
absl::Status WasmSettings::SaveAllSettings(const nlohmann::json& settings) {
if (!settings.is_object()) {
return absl::InvalidArgumentError("Settings must be a JSON object");
}
for (auto it = settings.begin(); it != settings.end(); ++it) {
auto status = SaveSetting(it.key(), it.value());
if (!status.ok()) {
return status;
}
}
return absl::OkStatus();
}
absl::StatusOr<nlohmann::json> WasmSettings::LoadAllSettings() {
nlohmann::json settings = nlohmann::json::object();
// This is a simplified implementation since we can't easily iterate localStorage
// from C++. In practice, you'd maintain a list of known setting keys.
// For now, we'll just return common settings if they exist.
std::vector<std::string> common_keys = {
"show_grid", "grid_size", "auto_save", "auto_save_interval",
"show_tooltips", "confirm_on_delete", "default_editor",
"animation_speed", "zoom_level", "show_minimap"
};
for (const auto& key : common_keys) {
if (HasSetting(key)) {
auto result = LoadSetting(key);
if (result.ok()) {
settings[key] = *result;
}
}
}
return settings;
}
absl::Status WasmSettings::ClearAllSettings() {
localStorage_clear();
return absl::OkStatus();
}
// Utility
absl::StatusOr<std::string> WasmSettings::ExportSettings() {
nlohmann::json export_data = nlohmann::json::object();
// Export theme
export_data["theme"] = LoadTheme();
// Export recent files
char* recent_json = localStorage_getItem(kRecentFilesKey);
if (recent_json) {
try {
export_data["recent_files"] = nlohmann::json::parse(recent_json);
} catch (...) {
// Ignore parse errors
}
free(recent_json);
}
// Export active workspace
export_data["active_workspace"] = GetActiveWorkspace();
// Export workspaces
nlohmann::json workspaces = nlohmann::json::object();
for (const auto& name : ListWorkspaces()) {
auto workspace_data = LoadWorkspace(name);
if (workspace_data.ok()) {
workspaces[name] = nlohmann::json::parse(*workspace_data);
}
}
export_data["workspaces"] = workspaces;
// Export general settings
auto all_settings = LoadAllSettings();
if (all_settings.ok()) {
export_data["settings"] = *all_settings;
}
return export_data.dump(2); // Pretty print with 2 spaces
}
absl::Status WasmSettings::ImportSettings(const std::string& json_str) {
try {
nlohmann::json import_data = nlohmann::json::parse(json_str);
// Import theme
if (import_data.contains("theme")) {
SaveTheme(import_data["theme"].get<std::string>());
}
// Import recent files
if (import_data.contains("recent_files")) {
localStorage_setItem(kRecentFilesKey,
import_data["recent_files"].dump().c_str());
}
// Import active workspace
if (import_data.contains("active_workspace")) {
SetActiveWorkspace(import_data["active_workspace"].get<std::string>());
}
// Import workspaces
if (import_data.contains("workspaces") && import_data["workspaces"].is_object()) {
for (auto it = import_data["workspaces"].begin();
it != import_data["workspaces"].end(); ++it) {
SaveWorkspace(it.key(), it.value().dump());
}
}
// Import general settings
if (import_data.contains("settings") && import_data["settings"].is_object()) {
SaveAllSettings(import_data["settings"]);
}
return absl::OkStatus();
} catch (const std::exception& e) {
return absl::InvalidArgumentError(
absl::StrFormat("Failed to import settings: %s", e.what()));
}
}
absl::StatusOr<std::chrono::system_clock::time_point> WasmSettings::GetLastSaveTime() {
char* time_str = localStorage_getItem(kLastSaveTimeKey);
if (!time_str) {
return absl::NotFoundError("No save time recorded");
}
try {
int64_t ms = std::stoll(time_str);
free(time_str);
return std::chrono::system_clock::time_point(std::chrono::milliseconds(ms));
} catch (const std::exception& e) {
free(time_str);
return absl::InvalidArgumentError(
absl::StrFormat("Failed to parse save time: %s", e.what()));
}
}
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,263 @@
#ifndef YAZE_APP_PLATFORM_WASM_SETTINGS_H_
#define YAZE_APP_PLATFORM_WASM_SETTINGS_H_
#ifdef __EMSCRIPTEN__
#include <string>
#include <vector>
#include <chrono>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "nlohmann/json.hpp"
namespace yaze {
namespace platform {
/**
* @class WasmSettings
* @brief Browser-based settings persistence for WASM builds
*
* This class provides persistent storage for user preferences, recent files,
* workspace layouts, and undo history using browser localStorage and IndexedDB.
* All methods are static and thread-safe.
*/
class WasmSettings {
public:
// Theme Management
/**
* @brief Save the current theme selection
* @param theme Theme name (e.g., "dark", "light", "classic")
* @return Status indicating success or failure
*/
static absl::Status SaveTheme(const std::string& theme);
/**
* @brief Load the saved theme selection
* @return Theme name or default if not found
*/
static std::string LoadTheme();
/**
* @brief Get the full JSON data for the current theme
* @return JSON string containing all theme colors and style settings
*/
static std::string GetCurrentThemeData();
/**
* @brief Load a user-provided font from binary data
* @param name Font name
* @param data Binary font data (TTF/OTF)
* @param size Font size in pixels
*/
static absl::Status LoadUserFont(const std::string& name,
const std::string& data, float size);
// Recent Files Management
/**
* @brief Add a file to the recent files list
* @param filename Name/identifier of the file
* @param timestamp Optional timestamp (uses current time if not provided)
* @return Status indicating success or failure
*/
static absl::Status AddRecentFile(
const std::string& filename,
std::chrono::system_clock::time_point timestamp =
std::chrono::system_clock::now());
/**
* @brief Get the list of recent files
* @param max_count Maximum number of files to return (default 10)
* @return Vector of recent file names, newest first
*/
static std::vector<std::string> GetRecentFiles(size_t max_count = 10);
/**
* @brief Clear the recent files list
* @return Status indicating success or failure
*/
static absl::Status ClearRecentFiles();
/**
* @brief Remove a specific file from the recent files list
* @param filename Name of the file to remove
* @return Status indicating success or failure
*/
static absl::Status RemoveRecentFile(const std::string& filename);
// Workspace Layout Management
/**
* @brief Save a workspace layout configuration
* @param name Workspace name (e.g., "default", "debugging", "art")
* @param layout_json JSON string containing layout configuration
* @return Status indicating success or failure
*/
static absl::Status SaveWorkspace(const std::string& name,
const std::string& layout_json);
/**
* @brief Load a workspace layout configuration
* @param name Workspace name to load
* @return JSON string containing layout or error
*/
static absl::StatusOr<std::string> LoadWorkspace(const std::string& name);
/**
* @brief List all saved workspace names
* @return Vector of workspace names
*/
static std::vector<std::string> ListWorkspaces();
/**
* @brief Delete a workspace layout
* @param name Workspace name to delete
* @return Status indicating success or failure
*/
static absl::Status DeleteWorkspace(const std::string& name);
/**
* @brief Set the active workspace
* @param name Name of the workspace to make active
* @return Status indicating success or failure
*/
static absl::Status SetActiveWorkspace(const std::string& name);
/**
* @brief Get the name of the active workspace
* @return Name of active workspace or "default" if none set
*/
static std::string GetActiveWorkspace();
// Undo History Persistence (for crash recovery)
/**
* @brief Save undo history for an editor
* @param editor_id Editor identifier (e.g., "overworld", "dungeon")
* @param history Serialized undo history data
* @return Status indicating success or failure
*/
static absl::Status SaveUndoHistory(const std::string& editor_id,
const std::vector<uint8_t>& history);
/**
* @brief Load undo history for an editor
* @param editor_id Editor identifier
* @return Undo history data or error
*/
static absl::StatusOr<std::vector<uint8_t>> LoadUndoHistory(
const std::string& editor_id);
/**
* @brief Clear undo history for an editor
* @param editor_id Editor identifier
* @return Status indicating success or failure
*/
static absl::Status ClearUndoHistory(const std::string& editor_id);
/**
* @brief Clear all undo histories
* @return Status indicating success or failure
*/
static absl::Status ClearAllUndoHistory();
// General Settings
/**
* @brief Save a general setting value
* @param key Setting key
* @param value Setting value as JSON
* @return Status indicating success or failure
*/
static absl::Status SaveSetting(const std::string& key,
const nlohmann::json& value);
/**
* @brief Load a general setting value
* @param key Setting key
* @return Setting value as JSON or error
*/
static absl::StatusOr<nlohmann::json> LoadSetting(const std::string& key);
/**
* @brief Check if a setting exists
* @param key Setting key
* @return true if setting exists
*/
static bool HasSetting(const std::string& key);
/**
* @brief Save all settings as a batch
* @param settings JSON object containing all settings
* @return Status indicating success or failure
*/
static absl::Status SaveAllSettings(const nlohmann::json& settings);
/**
* @brief Load all settings
* @return JSON object containing all settings or error
*/
static absl::StatusOr<nlohmann::json> LoadAllSettings();
/**
* @brief Clear all settings (reset to defaults)
* @return Status indicating success or failure
*/
static absl::Status ClearAllSettings();
// Utility
/**
* @brief Export all settings to a JSON string for backup
* @return JSON string containing all settings and data
*/
static absl::StatusOr<std::string> ExportSettings();
/**
* @brief Import settings from a JSON string
* @param json_str JSON string containing settings to import
* @return Status indicating success or failure
*/
static absl::Status ImportSettings(const std::string& json_str);
/**
* @brief Get the last time settings were saved
* @return Timestamp of last save or error
*/
static absl::StatusOr<std::chrono::system_clock::time_point> GetLastSaveTime();
private:
// Storage keys for localStorage
static constexpr const char* kThemeKey = "yaze_theme";
static constexpr const char* kRecentFilesKey = "yaze_recent_files";
static constexpr const char* kActiveWorkspaceKey = "yaze_active_workspace";
static constexpr const char* kSettingsPrefix = "yaze_setting_";
static constexpr const char* kLastSaveTimeKey = "yaze_last_save_time";
// Storage keys for IndexedDB (via WasmStorage)
static constexpr const char* kWorkspacePrefix = "workspace_";
static constexpr const char* kUndoHistoryPrefix = "undo_";
// Helper structure for recent files
struct RecentFile {
std::string filename;
std::chrono::system_clock::time_point timestamp;
};
// Helper to serialize/deserialize recent files
static nlohmann::json RecentFilesToJson(const std::vector<RecentFile>& files);
static std::vector<RecentFile> JsonToRecentFiles(const nlohmann::json& json);
// Prevent instantiation
WasmSettings() = delete;
~WasmSettings() = delete;
};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_SETTINGS_H_

View File

@@ -0,0 +1,543 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_storage.h"
#include <emscripten.h>
#include <emscripten/val.h>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include "absl/strings/str_format.h"
namespace yaze {
namespace platform {
// Static member initialization
std::atomic<bool> WasmStorage::initialized_{false};
// JavaScript IndexedDB interface using EM_JS
// All functions use yazeAsyncQueue to serialize async operations
EM_JS(int, idb_open_database, (const char* db_name, int version), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
var dbName = UTF8ToString(db_name); // Must convert before queueing!
var operation = function() {
return new Promise(function(resolve, reject) {
var request = indexedDB.open(dbName, version);
request.onerror = function() {
console.error('Failed to open IndexedDB:', request.error);
resolve(-1);
};
request.onsuccess = function() {
var db = request.result;
Module._yazeDB = db;
resolve(0);
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
if (!db.objectStoreNames.contains('roms')) {
db.createObjectStore('roms');
}
if (!db.objectStoreNames.contains('projects')) {
db.createObjectStore('projects');
}
if (!db.objectStoreNames.contains('preferences')) {
db.createObjectStore('preferences');
}
};
});
};
// Use async queue if available to prevent concurrent Asyncify operations
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(int, idb_save_binary, (const char* store_name, const char* key, const uint8_t* data, size_t size), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
var storeName = UTF8ToString(store_name);
var keyStr = UTF8ToString(key);
var dataArray = new Uint8Array(HEAPU8.subarray(data, data + size));
var operation = function() {
return new Promise(function(resolve, reject) {
if (!Module._yazeDB) {
console.error('Database not initialized');
resolve(-1);
return;
}
var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
var store = transaction.objectStore(storeName);
var request = store.put(dataArray, keyStr);
request.onsuccess = function() { resolve(0); };
request.onerror = function() {
console.error('Failed to save data:', request.error);
resolve(-1);
};
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(int, idb_load_binary, (const char* store_name, const char* key, uint8_t** out_data, size_t* out_size), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
if (!Module._yazeDB) {
console.error('Database not initialized');
return -1;
}
var storeName = UTF8ToString(store_name);
var keyStr = UTF8ToString(key);
var operation = function() {
return new Promise(function(resolve) {
var transaction = Module._yazeDB.transaction([storeName], 'readonly');
var store = transaction.objectStore(storeName);
var request = store.get(keyStr);
request.onsuccess = function() {
var result = request.result;
if (result && result instanceof Uint8Array) {
var size = result.length;
var ptr = Module._malloc(size);
Module.HEAPU8.set(result, ptr);
Module.HEAPU32[out_data >> 2] = ptr;
Module.HEAPU32[out_size >> 2] = size;
resolve(0);
} else {
resolve(-2);
}
};
request.onerror = function() {
console.error('Failed to load data:', request.error);
resolve(-1);
};
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(int, idb_save_string, (const char* store_name, const char* key, const char* value), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
var storeName = UTF8ToString(store_name);
var keyStr = UTF8ToString(key);
var valueStr = UTF8ToString(value);
var operation = function() {
return new Promise(function(resolve, reject) {
if (!Module._yazeDB) {
console.error('Database not initialized');
resolve(-1);
return;
}
var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
var store = transaction.objectStore(storeName);
var request = store.put(valueStr, keyStr);
request.onsuccess = function() { resolve(0); };
request.onerror = function() {
console.error('Failed to save string:', request.error);
resolve(-1);
};
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(char*, idb_load_string, (const char* store_name, const char* key), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
if (!Module._yazeDB) {
console.error('Database not initialized');
return 0;
}
var storeName = UTF8ToString(store_name);
var keyStr = UTF8ToString(key);
var operation = function() {
return new Promise(function(resolve) {
var transaction = Module._yazeDB.transaction([storeName], 'readonly');
var store = transaction.objectStore(storeName);
var request = store.get(keyStr);
request.onsuccess = function() {
var result = request.result;
if (result && typeof result === 'string') {
var len = lengthBytesUTF8(result) + 1;
var ptr = Module._malloc(len);
stringToUTF8(result, ptr, len);
resolve(ptr);
} else {
resolve(0);
}
};
request.onerror = function() {
console.error('Failed to load string:', request.error);
resolve(0);
};
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(int, idb_delete_entry, (const char* store_name, const char* key), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
var storeName = UTF8ToString(store_name);
var keyStr = UTF8ToString(key);
var operation = function() {
return new Promise(function(resolve, reject) {
if (!Module._yazeDB) {
console.error('Database not initialized');
resolve(-1);
return;
}
var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
var store = transaction.objectStore(storeName);
var request = store.delete(keyStr);
request.onsuccess = function() { resolve(0); };
request.onerror = function() {
console.error('Failed to delete entry:', request.error);
resolve(-1);
};
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(char*, idb_list_keys, (const char* store_name), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
if (!Module._yazeDB) {
console.error('Database not initialized');
return 0;
}
var storeName = UTF8ToString(store_name);
var operation = function() {
return new Promise(function(resolve) {
var transaction = Module._yazeDB.transaction([storeName], 'readonly');
var store = transaction.objectStore(storeName);
var request = store.getAllKeys();
request.onsuccess = function() {
var keys = request.result;
var jsonStr = JSON.stringify(keys);
var len = lengthBytesUTF8(jsonStr) + 1;
var ptr = Module._malloc(len);
stringToUTF8(jsonStr, ptr, len);
resolve(ptr);
};
request.onerror = function() {
console.error('Failed to list keys:', request.error);
resolve(0);
};
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
EM_JS(size_t, idb_get_storage_usage, (), {
const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
return asyncify.handleAsync(function() {
if (!Module._yazeDB) {
console.error('Database not initialized');
return 0;
}
var operation = function() {
return new Promise(function(resolve) {
var totalSize = 0;
var storeNames = ['roms', 'projects', 'preferences'];
var completed = 0;
storeNames.forEach(function(storeName) {
var transaction = Module._yazeDB.transaction([storeName], 'readonly');
var store = transaction.objectStore(storeName);
var request = store.openCursor();
request.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
var value = cursor.value;
if (value instanceof Uint8Array) {
totalSize += value.length;
} else if (typeof value === 'string') {
totalSize += value.length * 2; // UTF-16 estimation
} else if (value) {
totalSize += JSON.stringify(value).length * 2;
}
cursor.continue();
} else {
completed++;
if (completed === storeNames.length) {
resolve(totalSize);
}
}
};
request.onerror = function() {
completed++;
if (completed === storeNames.length) {
resolve(totalSize);
}
};
});
});
};
if (window.yazeAsyncQueue) {
return window.yazeAsyncQueue.enqueue(operation);
}
return operation();
});
});
// Implementation of WasmStorage methods
absl::Status WasmStorage::Initialize() {
// Use compare_exchange for thread-safe initialization
bool expected = false;
if (!initialized_.compare_exchange_strong(expected, true)) {
return absl::OkStatus(); // Already initialized by another thread
}
int result = idb_open_database(kDatabaseName, kDatabaseVersion);
if (result != 0) {
initialized_.store(false); // Reset on failure
return absl::InternalError("Failed to initialize IndexedDB");
}
return absl::OkStatus();
}
void WasmStorage::EnsureInitialized() {
if (!initialized_.load()) {
auto status = Initialize();
if (!status.ok()) {
emscripten_log(EM_LOG_ERROR, "Failed to initialize WasmStorage: %s", status.ToString().c_str());
}
}
}
bool WasmStorage::IsStorageAvailable() {
EnsureInitialized();
return initialized_.load();
}
bool WasmStorage::IsWebContext() {
return EM_ASM_INT({
return (typeof window !== 'undefined' && typeof indexedDB !== 'undefined') ? 1 : 0;
}) == 1;
}
// ROM Storage Operations
absl::Status WasmStorage::SaveRom(const std::string& name, const std::vector<uint8_t>& data) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
int result = idb_save_binary(kRomStoreName, name.c_str(), data.data(), data.size());
if (result != 0) {
return absl::InternalError(absl::StrFormat("Failed to save ROM '%s'", name));
}
return absl::OkStatus();
}
absl::StatusOr<std::vector<uint8_t>> WasmStorage::LoadRom(const std::string& name) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
uint8_t* data_ptr = nullptr;
size_t data_size = 0;
int result = idb_load_binary(kRomStoreName, name.c_str(), &data_ptr, &data_size);
if (result == -2) {
if (data_ptr) free(data_ptr);
return absl::NotFoundError(absl::StrFormat("ROM '%s' not found", name));
} else if (result != 0) {
if (data_ptr) free(data_ptr);
return absl::InternalError(absl::StrFormat("Failed to load ROM '%s'", name));
}
std::vector<uint8_t> data(data_ptr, data_ptr + data_size);
free(data_ptr);
return data;
}
absl::Status WasmStorage::DeleteRom(const std::string& name) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
int result = idb_delete_entry(kRomStoreName, name.c_str());
if (result != 0) {
return absl::InternalError(absl::StrFormat("Failed to delete ROM '%s'", name));
}
return absl::OkStatus();
}
std::vector<std::string> WasmStorage::ListRoms() {
EnsureInitialized();
if (!initialized_.load()) {
return {};
}
char* keys_json = idb_list_keys(kRomStoreName);
if (!keys_json) {
return {};
}
std::vector<std::string> result;
try {
nlohmann::json keys = nlohmann::json::parse(keys_json);
for (const auto& key : keys) {
if (key.is_string()) {
result.push_back(key.get<std::string>());
}
}
} catch (const std::exception& e) {
emscripten_log(EM_LOG_ERROR, "Failed to parse ROM list: %s", e.what());
}
free(keys_json);
return result;
}
// Project Storage Operations
absl::Status WasmStorage::SaveProject(const std::string& name, const std::string& json) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
int result = idb_save_string(kProjectStoreName, name.c_str(), json.c_str());
if (result != 0) {
return absl::InternalError(absl::StrFormat("Failed to save project '%s'", name));
}
return absl::OkStatus();
}
absl::StatusOr<std::string> WasmStorage::LoadProject(const std::string& name) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
char* json_ptr = idb_load_string(kProjectStoreName, name.c_str());
if (!json_ptr) {
// Note: idb_load_string returns 0 (null) on not found or error,
// no memory is allocated in that case, so no free needed here.
return absl::NotFoundError(absl::StrFormat("Project '%s' not found", name));
}
std::string json(json_ptr);
free(json_ptr);
return json;
}
absl::Status WasmStorage::DeleteProject(const std::string& name) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
int result = idb_delete_entry(kProjectStoreName, name.c_str());
if (result != 0) {
return absl::InternalError(absl::StrFormat("Failed to delete project '%s'", name));
}
return absl::OkStatus();
}
std::vector<std::string> WasmStorage::ListProjects() {
EnsureInitialized();
if (!initialized_.load()) {
return {};
}
char* keys_json = idb_list_keys(kProjectStoreName);
if (!keys_json) {
return {};
}
std::vector<std::string> result;
try {
nlohmann::json keys = nlohmann::json::parse(keys_json);
for (const auto& key : keys) {
if (key.is_string()) {
result.push_back(key.get<std::string>());
}
}
} catch (const std::exception& e) {
emscripten_log(EM_LOG_ERROR, "Failed to parse project list: %s", e.what());
}
free(keys_json);
return result;
}
// User Preferences Storage
absl::Status WasmStorage::SavePreferences(const nlohmann::json& prefs) {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
std::string json_str = prefs.dump();
int result = idb_save_string(kPreferencesStoreName, kPreferencesKey, json_str.c_str());
if (result != 0) {
return absl::InternalError("Failed to save preferences");
}
return absl::OkStatus();
}
absl::StatusOr<nlohmann::json> WasmStorage::LoadPreferences() {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
char* json_ptr = idb_load_string(kPreferencesStoreName, kPreferencesKey);
if (!json_ptr) {
return nlohmann::json::object();
}
try {
nlohmann::json prefs = nlohmann::json::parse(json_ptr);
free(json_ptr);
return prefs;
} catch (const std::exception& e) {
free(json_ptr);
return absl::InvalidArgumentError(absl::StrFormat("Failed to parse preferences: %s", e.what()));
}
}
absl::Status WasmStorage::ClearPreferences() {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
int result = idb_delete_entry(kPreferencesStoreName, kPreferencesKey);
if (result != 0) {
return absl::InternalError("Failed to clear preferences");
}
return absl::OkStatus();
}
// Utility Operations
absl::StatusOr<size_t> WasmStorage::GetStorageUsage() {
EnsureInitialized();
if (!initialized_.load()) {
return absl::FailedPreconditionError("Storage not initialized");
}
return idb_get_storage_usage();
}
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,167 @@
#ifndef YAZE_APP_PLATFORM_WASM_WASM_STORAGE_H_
#define YAZE_APP_PLATFORM_WASM_WASM_STORAGE_H_
#ifdef __EMSCRIPTEN__
#include <atomic>
#include <functional>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "nlohmann/json.hpp"
namespace yaze {
namespace platform {
/**
* @class WasmStorage
* @brief WASM storage implementation using Emscripten IndexedDB
*
* This class provides persistent storage in the browser using IndexedDB
* for ROM files, project data, and user preferences.
*
* All operations are asynchronous but exposed as synchronous for ease of use.
* The implementation uses condition variables to wait for completion.
*/
class WasmStorage {
public:
// ROM Storage Operations
/**
* @brief Save ROM data to IndexedDB
* @param name Unique name/identifier for the ROM
* @param data Binary ROM data
* @return Status indicating success or failure
*/
static absl::Status SaveRom(const std::string& name,
const std::vector<uint8_t>& data);
/**
* @brief Load ROM data from IndexedDB
* @param name Name of the ROM to load
* @return ROM data or error status
*/
static absl::StatusOr<std::vector<uint8_t>> LoadRom(const std::string& name);
/**
* @brief Delete a ROM from IndexedDB
* @param name Name of the ROM to delete
* @return Status indicating success or failure
*/
static absl::Status DeleteRom(const std::string& name);
/**
* @brief List all saved ROM names
* @return Vector of ROM names in storage
*/
static std::vector<std::string> ListRoms();
// Project Storage Operations
/**
* @brief Save project JSON data
* @param name Project name/identifier
* @param json JSON string containing project data
* @return Status indicating success or failure
*/
static absl::Status SaveProject(const std::string& name,
const std::string& json);
/**
* @brief Load project JSON data
* @param name Project name to load
* @return JSON string or error status
*/
static absl::StatusOr<std::string> LoadProject(const std::string& name);
/**
* @brief Delete a project from storage
* @param name Project name to delete
* @return Status indicating success or failure
*/
static absl::Status DeleteProject(const std::string& name);
/**
* @brief List all saved project names
* @return Vector of project names in storage
*/
static std::vector<std::string> ListProjects();
// User Preferences Storage
/**
* @brief Save user preferences as JSON
* @param prefs JSON object containing preferences
* @return Status indicating success or failure
*/
static absl::Status SavePreferences(const nlohmann::json& prefs);
/**
* @brief Load user preferences
* @return JSON preferences or error status
*/
static absl::StatusOr<nlohmann::json> LoadPreferences();
/**
* @brief Clear all preferences
* @return Status indicating success or failure
*/
static absl::Status ClearPreferences();
// Utility Operations
/**
* @brief Get total storage used (in bytes)
* @return Total bytes used or error status
*/
static absl::StatusOr<size_t> GetStorageUsage();
/**
* @brief Check if storage is available and initialized
* @return true if IndexedDB is available and ready
*/
static bool IsStorageAvailable();
/**
* @brief Initialize IndexedDB (called automatically on first use)
* @return Status indicating success or failure
*/
static absl::Status Initialize();
private:
// Database constants
static constexpr const char* kDatabaseName = "YazeStorage";
static constexpr int kDatabaseVersion = 1;
static constexpr const char* kRomStoreName = "roms";
static constexpr const char* kProjectStoreName = "projects";
static constexpr const char* kPreferencesStoreName = "preferences";
static constexpr const char* kPreferencesKey = "user_preferences";
// Internal helper for async operations
struct AsyncResult {
bool completed = false;
bool success = false;
std::string error_message;
std::vector<uint8_t> binary_data;
std::string string_data;
std::vector<std::string> string_list;
};
// Ensure database is initialized
static void EnsureInitialized();
// Check if we're running in a web context
static bool IsWebContext();
// Database initialized flag (thread-safe)
static std::atomic<bool> initialized_;
};
} // namespace platform
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_WASM_STORAGE_H_

View File

@@ -0,0 +1,626 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_storage_quota.h"
#include <emscripten.h>
#include <emscripten/bind.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <iostream>
#include <queue>
namespace yaze {
namespace app {
namespace platform {
namespace {
// Callback management for async operations
struct CallbackManager {
static CallbackManager& Get() {
static CallbackManager instance;
return instance;
}
int RegisterStorageCallback(
std::function<void(const WasmStorageQuota::StorageInfo&)> cb) {
std::lock_guard<std::mutex> lock(mutex_);
int id = next_id_++;
storage_callbacks_[id] = cb;
return id;
}
int RegisterCompressCallback(
std::function<void(std::vector<uint8_t>)> cb) {
std::lock_guard<std::mutex> lock(mutex_);
int id = next_id_++;
compress_callbacks_[id] = cb;
return id;
}
void InvokeStorageCallback(int id, size_t used, size_t quota, bool persistent) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = storage_callbacks_.find(id);
if (it != storage_callbacks_.end()) {
WasmStorageQuota::StorageInfo info;
info.used_bytes = used;
info.quota_bytes = quota;
info.usage_percent = quota > 0 ? (float(used) / float(quota) * 100.0f) : 0.0f;
info.persistent = persistent;
it->second(info);
storage_callbacks_.erase(it);
}
}
void InvokeCompressCallback(int id, uint8_t* data, size_t size) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = compress_callbacks_.find(id);
if (it != compress_callbacks_.end()) {
std::vector<uint8_t> result;
if (data && size > 0) {
result.assign(data, data + size);
free(data); // Free the allocated memory from JS
}
it->second(result);
compress_callbacks_.erase(it);
}
}
private:
std::mutex mutex_;
int next_id_ = 1;
std::map<int, std::function<void(const WasmStorageQuota::StorageInfo&)>> storage_callbacks_;
std::map<int, std::function<void(std::vector<uint8_t>)>> compress_callbacks_;
};
} // namespace
// External C functions called from JavaScript
extern "C" {
EMSCRIPTEN_KEEPALIVE
void wasm_storage_quota_estimate_callback(int callback_id, double used,
double quota, int persistent) {
CallbackManager::Get().InvokeStorageCallback(
callback_id, size_t(used), size_t(quota), persistent != 0);
}
EMSCRIPTEN_KEEPALIVE
void wasm_compress_callback(int callback_id, uint8_t* data, size_t size) {
CallbackManager::Get().InvokeCompressCallback(callback_id, data, size);
}
EMSCRIPTEN_KEEPALIVE
void wasm_decompress_callback(int callback_id, uint8_t* data, size_t size) {
CallbackManager::Get().InvokeCompressCallback(callback_id, data, size);
}
} // extern "C"
// External JS functions declared in header
extern void wasm_storage_quota_estimate(int callback_id);
extern void wasm_compress_data(const uint8_t* data, size_t size, int callback_id);
extern void wasm_decompress_data(const uint8_t* data, size_t size, int callback_id);
extern double wasm_get_timestamp_ms();
extern int wasm_compression_available();
// WasmStorageQuota implementation
WasmStorageQuota& WasmStorageQuota::Get() {
static WasmStorageQuota instance;
return instance;
}
bool WasmStorageQuota::IsSupported() {
// Check for required APIs
return EM_ASM_INT({
return (navigator.storage &&
navigator.storage.estimate &&
indexedDB) ? 1 : 0;
}) != 0;
}
void WasmStorageQuota::GetStorageInfo(
std::function<void(const StorageInfo&)> callback) {
if (!callback) return;
// Check if we recently checked (within 5 seconds)
double now = wasm_get_timestamp_ms();
if (now - last_quota_check_time_.load() < 5000.0 &&
last_storage_info_.quota_bytes > 0) {
callback(last_storage_info_);
return;
}
int callback_id = CallbackManager::Get().RegisterStorageCallback(
[this, callback](const StorageInfo& info) {
last_storage_info_ = info;
last_quota_check_time_.store(wasm_get_timestamp_ms());
callback(info);
});
wasm_storage_quota_estimate(callback_id);
}
void WasmStorageQuota::CompressData(
const std::vector<uint8_t>& input,
std::function<void(std::vector<uint8_t>)> callback) {
if (!callback || input.empty()) {
if (callback) callback(std::vector<uint8_t>());
return;
}
int callback_id = CallbackManager::Get().RegisterCompressCallback(callback);
wasm_compress_data(input.data(), input.size(), callback_id);
}
void WasmStorageQuota::DecompressData(
const std::vector<uint8_t>& input,
std::function<void(std::vector<uint8_t>)> callback) {
if (!callback || input.empty()) {
if (callback) callback(std::vector<uint8_t>());
return;
}
int callback_id = CallbackManager::Get().RegisterCompressCallback(callback);
wasm_decompress_data(input.data(), input.size(), callback_id);
}
void WasmStorageQuota::CompressAndStore(
const std::string& key,
const std::vector<uint8_t>& data,
std::function<void(bool success)> callback) {
if (key.empty() || data.empty()) {
if (callback) callback(false);
return;
}
size_t original_size = data.size();
// First compress the data
CompressData(data, [this, key, original_size, callback](
const std::vector<uint8_t>& compressed) {
if (compressed.empty()) {
if (callback) callback(false);
return;
}
// Check quota before storing
CheckQuotaAndEvict(compressed.size(), [this, key, compressed, original_size, callback](
bool quota_ok) {
if (!quota_ok) {
if (callback) callback(false);
return;
}
// Store the compressed data
StoreCompressedData(key, compressed, original_size, callback);
});
});
}
void WasmStorageQuota::LoadAndDecompress(
const std::string& key,
std::function<void(std::vector<uint8_t>)> callback) {
if (key.empty()) {
if (callback) callback(std::vector<uint8_t>());
return;
}
// Load compressed data from storage
LoadCompressedData(key, [this, key, callback](
const std::vector<uint8_t>& compressed,
size_t original_size) {
if (compressed.empty()) {
if (callback) callback(std::vector<uint8_t>());
return;
}
// Update access time
UpdateAccessTime(key);
// Decompress the data
DecompressData(compressed, callback);
});
}
void WasmStorageQuota::StoreCompressedData(
const std::string& key,
const std::vector<uint8_t>& compressed_data,
size_t original_size,
std::function<void(bool)> callback) {
// Use the existing WasmStorage for actual storage
EM_ASM({
var key = UTF8ToString($0);
var dataPtr = $1;
var dataSize = $2;
var originalSize = $3;
var callbackPtr = $4;
if (!Module._yazeDB) {
console.error('[StorageQuota] Database not initialized');
Module.dynCall_vi(callbackPtr, 0);
return;
}
var data = new Uint8Array(Module.HEAPU8.buffer, dataPtr, dataSize);
var metadata = {
compressed_size: dataSize,
original_size: originalSize,
last_access: Date.now(),
compression_ratio: originalSize > 0 ? (dataSize / originalSize) : 1.0
};
var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readwrite');
var romStore = transaction.objectStore('roms');
var metaStore = transaction.objectStore('metadata');
// Store compressed data
var dataRequest = romStore.put(data, key);
// Store metadata
var metaRequest = metaStore.put(metadata, key);
transaction.oncomplete = function() {
Module.dynCall_vi(callbackPtr, 1);
};
transaction.onerror = function() {
console.error('[StorageQuota] Failed to store compressed data');
Module.dynCall_vi(callbackPtr, 0);
};
}, key.c_str(), compressed_data.data(), compressed_data.size(),
original_size, callback ? new std::function<void(bool)>(callback) : nullptr);
// Update local metadata cache
UpdateMetadata(key, compressed_data.size(), original_size);
}
void WasmStorageQuota::LoadCompressedData(
const std::string& key,
std::function<void(std::vector<uint8_t>, size_t)> callback) {
EM_ASM({
var key = UTF8ToString($0);
var callbackPtr = $1;
if (!Module._yazeDB) {
console.error('[StorageQuota] Database not initialized');
Module.dynCall_viii(callbackPtr, 0, 0, 0);
return;
}
var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readonly');
var romStore = transaction.objectStore('roms');
var metaStore = transaction.objectStore('metadata');
var dataRequest = romStore.get(key);
var metaRequest = metaStore.get(key);
var romData = null;
var metadata = null;
dataRequest.onsuccess = function() {
romData = dataRequest.result;
checkComplete();
};
metaRequest.onsuccess = function() {
metadata = metaRequest.result;
checkComplete();
};
function checkComplete() {
if (romData !== null && metadata !== null) {
if (romData && metadata) {
var ptr = Module._malloc(romData.length);
Module.HEAPU8.set(romData, ptr);
Module.dynCall_viii(callbackPtr, ptr, romData.length,
metadata.original_size || romData.length);
} else {
Module.dynCall_viii(callbackPtr, 0, 0, 0);
}
}
}
transaction.onerror = function() {
console.error('[StorageQuota] Failed to load compressed data');
Module.dynCall_viii(callbackPtr, 0, 0, 0);
};
}, key.c_str(), callback ? new std::function<void(std::vector<uint8_t>, size_t)>(
callback) : nullptr);
}
void WasmStorageQuota::UpdateAccessTime(const std::string& key) {
double now = wasm_get_timestamp_ms();
{
std::lock_guard<std::mutex> lock(mutex_);
access_times_[key] = now;
if (item_metadata_.count(key)) {
item_metadata_[key].last_access_time = now;
}
}
// Update in IndexedDB
EM_ASM({
var key = UTF8ToString($0);
var timestamp = $1;
if (!Module._yazeDB) return;
var transaction = Module._yazeDB.transaction(['metadata'], 'readwrite');
var store = transaction.objectStore('metadata');
var request = store.get(key);
request.onsuccess = function() {
var metadata = request.result || {};
metadata.last_access = timestamp;
store.put(metadata, key);
};
}, key.c_str(), now);
}
void WasmStorageQuota::UpdateMetadata(const std::string& key,
size_t compressed_size,
size_t original_size) {
std::lock_guard<std::mutex> lock(mutex_);
StorageItem item;
item.key = key;
item.compressed_size = compressed_size;
item.original_size = original_size;
item.last_access_time = wasm_get_timestamp_ms();
item.compression_ratio = original_size > 0 ?
float(compressed_size) / float(original_size) : 1.0f;
item_metadata_[key] = item;
access_times_[key] = item.last_access_time;
}
void WasmStorageQuota::GetStoredItems(
std::function<void(std::vector<StorageItem>)> callback) {
if (!callback) return;
LoadMetadata([this, callback]() {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<StorageItem> items;
items.reserve(item_metadata_.size());
for (const auto& [key, item] : item_metadata_) {
items.push_back(item);
}
// Sort by last access time (most recent first)
std::sort(items.begin(), items.end(),
[](const StorageItem& a, const StorageItem& b) {
return a.last_access_time > b.last_access_time;
});
callback(items);
});
}
void WasmStorageQuota::LoadMetadata(std::function<void()> callback) {
if (metadata_loaded_.load()) {
if (callback) callback();
return;
}
EM_ASM({
var callbackPtr = $0;
if (!Module._yazeDB) {
if (callbackPtr) Module.dynCall_v(callbackPtr);
return;
}
var transaction = Module._yazeDB.transaction(['metadata'], 'readonly');
var store = transaction.objectStore('metadata');
var request = store.getAllKeys();
request.onsuccess = function() {
var keys = request.result;
var metadata = {};
var pending = keys.length;
if (pending === 0) {
if (callbackPtr) Module.dynCall_v(callbackPtr);
return;
}
keys.forEach(function(key) {
var getRequest = store.get(key);
getRequest.onsuccess = function() {
metadata[key] = getRequest.result;
pending--;
if (pending === 0) {
// Pass metadata back to C++
Module.storageQuotaMetadata = metadata;
if (callbackPtr) Module.dynCall_v(callbackPtr);
}
};
});
};
}, callback ? new std::function<void()>(callback) : nullptr);
// After JS callback, process the metadata
std::lock_guard<std::mutex> lock(mutex_);
// Access JS metadata object and populate C++ structures
emscripten::val metadata = emscripten::val::global("Module")["storageQuotaMetadata"];
if (metadata.as<bool>()) {
// Process each key in the metadata
// Note: This is simplified - in production you'd iterate the JS object properly
metadata_loaded_.store(true);
}
}
void WasmStorageQuota::EvictOldest(int count,
std::function<void(int evicted)> callback) {
if (count <= 0) {
if (callback) callback(0);
return;
}
GetStoredItems([this, count, callback](const std::vector<StorageItem>& items) {
// Items are already sorted by access time (newest first)
// We want to evict from the end (oldest)
int to_evict = std::min(count, static_cast<int>(items.size()));
int evicted = 0;
for (int i = items.size() - to_evict; i < items.size(); ++i) {
DeleteItem(items[i].key, [&evicted](bool success) {
if (success) evicted++;
});
}
if (callback) callback(evicted);
});
}
void WasmStorageQuota::EvictToTarget(float target_percent,
std::function<void(int evicted)> callback) {
if (target_percent <= 0 || target_percent >= 100) {
if (callback) callback(0);
return;
}
GetStorageInfo([this, target_percent, callback](const StorageInfo& info) {
if (info.usage_percent <= target_percent) {
if (callback) callback(0);
return;
}
// Calculate how much space we need to free
size_t target_bytes = size_t(info.quota_bytes * target_percent / 100.0f);
size_t bytes_to_free = info.used_bytes - target_bytes;
GetStoredItems([this, bytes_to_free, callback](
const std::vector<StorageItem>& items) {
size_t freed = 0;
int evicted = 0;
// Evict oldest items until we've freed enough space
for (auto it = items.rbegin(); it != items.rend(); ++it) {
if (freed >= bytes_to_free) break;
DeleteItem(it->key, [&evicted, &freed, it](bool success) {
if (success) {
evicted++;
freed += it->compressed_size;
}
});
}
if (callback) callback(evicted);
});
});
}
void WasmStorageQuota::CheckQuotaAndEvict(size_t new_size_bytes,
std::function<void(bool)> callback) {
GetStorageInfo([this, new_size_bytes, callback](const StorageInfo& info) {
// Check if we have enough space
size_t projected_usage = info.used_bytes + new_size_bytes;
float projected_percent = info.quota_bytes > 0 ?
(float(projected_usage) / float(info.quota_bytes) * 100.0f) : 100.0f;
if (projected_percent <= kWarningThreshold) {
// Plenty of space available
if (callback) callback(true);
return;
}
if (projected_percent > kCriticalThreshold) {
// Need to evict to make space
std::cerr << "[StorageQuota] Approaching quota limit, evicting old ROMs..."
<< std::endl;
EvictToTarget(kDefaultTargetUsage, [callback](int evicted) {
std::cerr << "[StorageQuota] Evicted " << evicted << " items"
<< std::endl;
if (callback) callback(evicted > 0);
});
} else {
// Warning zone but still ok
std::cerr << "[StorageQuota] Warning: Storage at " << projected_percent
<< "% after this operation" << std::endl;
if (callback) callback(true);
}
});
}
void WasmStorageQuota::DeleteItem(const std::string& key,
std::function<void(bool success)> callback) {
EM_ASM({
var key = UTF8ToString($0);
var callbackPtr = $1;
if (!Module._yazeDB) {
if (callbackPtr) Module.dynCall_vi(callbackPtr, 0);
return;
}
var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readwrite');
var romStore = transaction.objectStore('roms');
var metaStore = transaction.objectStore('metadata');
romStore.delete(key);
metaStore.delete(key);
transaction.oncomplete = function() {
if (callbackPtr) Module.dynCall_vi(callbackPtr, 1);
};
transaction.onerror = function() {
if (callbackPtr) Module.dynCall_vi(callbackPtr, 0);
};
}, key.c_str(), callback ? new std::function<void(bool)>(callback) : nullptr);
// Update local cache
{
std::lock_guard<std::mutex> lock(mutex_);
access_times_.erase(key);
item_metadata_.erase(key);
}
}
void WasmStorageQuota::ClearAll(std::function<void()> callback) {
EM_ASM({
var callbackPtr = $0;
if (!Module._yazeDB) {
if (callbackPtr) Module.dynCall_v(callbackPtr);
return;
}
var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readwrite');
var romStore = transaction.objectStore('roms');
var metaStore = transaction.objectStore('metadata');
romStore.clear();
metaStore.clear();
transaction.oncomplete = function() {
if (callbackPtr) Module.dynCall_v(callbackPtr);
};
}, callback ? new std::function<void()>(callback) : nullptr);
// Clear local cache
{
std::lock_guard<std::mutex> lock(mutex_);
access_times_.clear();
item_metadata_.clear();
}
}
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
// clang-format on

View File

@@ -0,0 +1,428 @@
#ifndef YAZE_APP_PLATFORM_WASM_STORAGE_QUOTA_H_
#define YAZE_APP_PLATFORM_WASM_STORAGE_QUOTA_H_
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/val.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
namespace yaze {
namespace app {
namespace platform {
/**
* @brief Manages browser storage quota, compression, and LRU eviction for ROMs
*
* This class provides efficient storage management for ROM files in the browser:
* - Monitors IndexedDB storage usage via navigator.storage.estimate()
* - Compresses ROM data using zlib before storage (typically 30-50% compression ratio)
* - Implements LRU (Least Recently Used) eviction when approaching quota limits
* - Tracks last-access times for intelligent cache management
*
* Storage Strategy:
* - Keep storage usage below 80% of quota to avoid browser warnings
* - Compress all ROMs before storage (reduces ~3MB to ~1.5MB typically)
* - Evict least recently used ROMs when nearing quota
* - Update access times on every ROM load
*
* Usage Example:
* @code
* WasmStorageQuota::Get().GetStorageInfo([](const StorageInfo& info) {
* printf("Storage: %.1f%% used (%.1fMB / %.1fMB)\n",
* info.usage_percent,
* info.used_bytes / 1024.0 / 1024.0,
* info.quota_bytes / 1024.0 / 1024.0);
* });
*
* // Compress and store a ROM
* std::vector<uint8_t> rom_data = LoadRomFromFile();
* WasmStorageQuota::Get().CompressAndStore("zelda3.sfc", rom_data,
* [](bool success) {
* if (success) {
* printf("ROM stored successfully with compression\n");
* }
* });
* @endcode
*/
class WasmStorageQuota {
public:
/**
* @brief Storage information from the browser
*/
struct StorageInfo {
size_t used_bytes = 0; ///< Bytes currently used
size_t quota_bytes = 0; ///< Total quota available
float usage_percent = 0.0f; ///< Percentage of quota used
bool persistent = false; ///< Whether storage is persistent
};
/**
* @brief Metadata for stored items
*/
struct StorageItem {
std::string key;
size_t compressed_size = 0;
size_t original_size = 0;
double last_access_time = 0.0; ///< Timestamp in milliseconds
float compression_ratio = 0.0f;
};
/**
* @brief Get current storage quota and usage information
* @param callback Called with storage info when available
*
* Uses navigator.storage.estimate() to get current usage and quota.
* Note: Browsers may report conservative estimates for privacy reasons.
*/
void GetStorageInfo(std::function<void(const StorageInfo&)> callback);
/**
* @brief Compress ROM data and store in IndexedDB
* @param key Unique identifier for the ROM
* @param data Raw ROM data to compress and store
* @param callback Called with success/failure status
*
* Compresses the ROM using zlib (deflate) before storage.
* Updates access time and manages quota automatically.
* If storage would exceed 80% quota, triggers LRU eviction first.
*/
void CompressAndStore(const std::string& key,
const std::vector<uint8_t>& data,
std::function<void(bool success)> callback);
/**
* @brief Load ROM from storage and decompress
* @param key ROM identifier
* @param callback Called with decompressed ROM data (empty if not found)
*
* Automatically updates access time for LRU tracking.
* Returns empty vector if key not found or decompression fails.
*/
void LoadAndDecompress(const std::string& key,
std::function<void(std::vector<uint8_t>)> callback);
/**
* @brief Evict the oldest (least recently used) items from storage
* @param count Number of items to evict
* @param callback Called with actual number of items evicted
*
* Removes items based on last_access_time, oldest first.
* Useful for making space when approaching quota limits.
*/
void EvictOldest(int count, std::function<void(int evicted)> callback);
/**
* @brief Evict items until storage usage is below target percentage
* @param target_percent Target usage percentage (0-100)
* @param callback Called with number of items evicted
*
* Intelligently evicts LRU items until usage drops below target.
* Default target is 70% to leave headroom for new saves.
*/
void EvictToTarget(float target_percent,
std::function<void(int evicted)> callback);
/**
* @brief Update the last access time for a stored item
* @param key Item identifier
*
* Call this when accessing a ROM through other means to keep
* LRU tracking accurate. CompressAndStore and LoadAndDecompress
* update times automatically.
*/
void UpdateAccessTime(const std::string& key);
/**
* @brief Get metadata for all stored items
* @param callback Called with vector of storage items
*
* Returns information about all stored ROMs including sizes,
* compression ratios, and access times for management UI.
*/
void GetStoredItems(
std::function<void(std::vector<StorageItem>)> callback);
/**
* @brief Delete a specific item from storage
* @param key Item identifier
* @param callback Called with success status
*/
void DeleteItem(const std::string& key,
std::function<void(bool success)> callback);
/**
* @brief Clear all stored ROMs and metadata
* @param callback Called when complete
*
* Use with caution - removes all compressed ROM data.
*/
void ClearAll(std::function<void()> callback);
/**
* @brief Check if browser supports required storage APIs
* @return true if navigator.storage and compression APIs are available
*/
static bool IsSupported();
/**
* @brief Get singleton instance
* @return Reference to the global storage quota manager
*/
static WasmStorageQuota& Get();
// Configuration constants
static constexpr float kDefaultTargetUsage = 70.0f; ///< Target usage %
static constexpr float kWarningThreshold = 80.0f; ///< Warning at this %
static constexpr float kCriticalThreshold = 90.0f; ///< Critical at this %
static constexpr size_t kMinQuotaBytes = 50 * 1024 * 1024; ///< 50MB minimum
private:
WasmStorageQuota() = default;
// Compression helpers (using browser's CompressionStream API)
void CompressData(const std::vector<uint8_t>& input,
std::function<void(std::vector<uint8_t>)> callback);
void DecompressData(const std::vector<uint8_t>& input,
std::function<void(std::vector<uint8_t>)> callback);
// Internal storage operations
void StoreCompressedData(const std::string& key,
const std::vector<uint8_t>& compressed_data,
size_t original_size,
std::function<void(bool)> callback);
void LoadCompressedData(const std::string& key,
std::function<void(std::vector<uint8_t>, size_t)> callback);
// Metadata management
void UpdateMetadata(const std::string& key, size_t compressed_size,
size_t original_size);
void LoadMetadata(std::function<void()> callback);
void SaveMetadata(std::function<void()> callback);
// Storage monitoring
void CheckQuotaAndEvict(size_t new_size_bytes,
std::function<void(bool)> callback);
// Thread safety
mutable std::mutex mutex_;
// Cached metadata (key -> last access time in ms)
std::map<std::string, double> access_times_;
std::map<std::string, StorageItem> item_metadata_;
std::atomic<bool> metadata_loaded_{false};
// Current storage state
StorageInfo last_storage_info_;
std::atomic<double> last_quota_check_time_{0.0};
};
// clang-format off
// JavaScript bridge functions for storage quota API
EM_JS(void, wasm_storage_quota_estimate, (int callback_id), {
if (!navigator.storage || !navigator.storage.estimate) {
// Call back with error values
Module.ccall('wasm_storage_quota_estimate_callback',
null, ['number', 'number', 'number', 'number'],
[callback_id, 0, 0, 0]);
return;
}
navigator.storage.estimate().then(function(estimate) {
var used = estimate.usage || 0;
var quota = estimate.quota || 0;
var persistent = estimate.persistent ? 1 : 0;
Module.ccall('wasm_storage_quota_estimate_callback',
null, ['number', 'number', 'number', 'number'],
[callback_id, used, quota, persistent]);
}).catch(function(error) {
console.error('[StorageQuota] Error estimating storage:', error);
Module.ccall('wasm_storage_quota_estimate_callback',
null, ['number', 'number', 'number', 'number'],
[callback_id, 0, 0, 0]);
});
});
// Compression using browser's CompressionStream API
EM_JS(void, wasm_compress_data, (const uint8_t* data, size_t size, int callback_id), {
var input = new Uint8Array(Module.HEAPU8.buffer, data, size);
// Use CompressionStream if available (Chrome 80+, Firefox 113+)
if (typeof CompressionStream !== 'undefined') {
var stream = new CompressionStream('deflate');
var writer = stream.writable.getWriter();
writer.write(input).then(function() {
return writer.close();
}).then(function() {
return new Response(stream.readable).arrayBuffer();
}).then(function(compressed) {
var compressedArray = new Uint8Array(compressed);
var ptr = Module._malloc(compressedArray.length);
Module.HEAPU8.set(compressedArray, ptr);
Module.ccall('wasm_compress_callback',
null, ['number', 'number', 'number'],
[callback_id, ptr, compressedArray.length]);
}).catch(function(error) {
console.error('[StorageQuota] Compression error:', error);
Module.ccall('wasm_compress_callback',
null, ['number', 'number', 'number'],
[callback_id, 0, 0]);
});
} else {
// Fallback: No compression, return original data
console.warn('[StorageQuota] CompressionStream not available, storing uncompressed');
var ptr = Module._malloc(size);
Module.HEAPU8.set(input, ptr);
Module.ccall('wasm_compress_callback',
null, ['number', 'number', 'number'],
[callback_id, ptr, size]);
}
});
EM_JS(void, wasm_decompress_data, (const uint8_t* data, size_t size, int callback_id), {
var input = new Uint8Array(Module.HEAPU8.buffer, data, size);
if (typeof DecompressionStream !== 'undefined') {
var stream = new DecompressionStream('deflate');
var writer = stream.writable.getWriter();
writer.write(input).then(function() {
return writer.close();
}).then(function() {
return new Response(stream.readable).arrayBuffer();
}).then(function(decompressed) {
var decompressedArray = new Uint8Array(decompressed);
var ptr = Module._malloc(decompressedArray.length);
Module.HEAPU8.set(decompressedArray, ptr);
Module.ccall('wasm_decompress_callback',
null, ['number', 'number', 'number'],
[callback_id, ptr, decompressedArray.length]);
}).catch(function(error) {
console.error('[StorageQuota] Decompression error:', error);
Module.ccall('wasm_decompress_callback',
null, ['number', 'number', 'number'],
[callback_id, 0, 0]);
});
} else {
// Fallback: Assume data is uncompressed
var ptr = Module._malloc(size);
Module.HEAPU8.set(input, ptr);
Module.ccall('wasm_decompress_callback',
null, ['number', 'number', 'number'],
[callback_id, ptr, size]);
}
});
// Get current timestamp in milliseconds
EM_JS(double, wasm_get_timestamp_ms, (), {
return Date.now();
});
// Check if compression APIs are available
EM_JS(int, wasm_compression_available, (), {
return (typeof CompressionStream !== 'undefined' &&
typeof DecompressionStream !== 'undefined') ? 1 : 0;
});
// clang-format on
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub implementation for non-WASM builds
#include <functional>
#include <string>
#include <vector>
namespace yaze {
namespace app {
namespace platform {
class WasmStorageQuota {
public:
struct StorageInfo {
size_t used_bytes = 0;
size_t quota_bytes = 100 * 1024 * 1024; // 100MB default
float usage_percent = 0.0f;
bool persistent = false;
};
struct StorageItem {
std::string key;
size_t compressed_size = 0;
size_t original_size = 0;
double last_access_time = 0.0;
float compression_ratio = 1.0f;
};
void GetStorageInfo(std::function<void(const StorageInfo&)> callback) {
StorageInfo info;
callback(info);
}
void CompressAndStore(const std::string& key,
const std::vector<uint8_t>& data,
std::function<void(bool success)> callback) {
callback(false);
}
void LoadAndDecompress(const std::string& key,
std::function<void(std::vector<uint8_t>)> callback) {
callback(std::vector<uint8_t>());
}
void EvictOldest(int count, std::function<void(int evicted)> callback) {
callback(0);
}
void EvictToTarget(float target_percent,
std::function<void(int evicted)> callback) {
callback(0);
}
void UpdateAccessTime(const std::string& key) {}
void GetStoredItems(
std::function<void(std::vector<StorageItem>)> callback) {
callback(std::vector<StorageItem>());
}
void DeleteItem(const std::string& key,
std::function<void(bool success)> callback) {
callback(false);
}
void ClearAll(std::function<void()> callback) { callback(); }
static bool IsSupported() { return false; }
static WasmStorageQuota& Get() {
static WasmStorageQuota instance;
return instance;
}
static constexpr float kDefaultTargetUsage = 70.0f;
static constexpr float kWarningThreshold = 80.0f;
static constexpr float kCriticalThreshold = 90.0f;
static constexpr size_t kMinQuotaBytes = 50 * 1024 * 1024;
};
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_PLATFORM_WASM_STORAGE_QUOTA_H_

View File

@@ -0,0 +1,599 @@
// clang-format off
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_worker_pool.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <iostream>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include "absl/strings/str_format.h"
namespace yaze {
namespace app {
namespace platform {
namespace wasm {
namespace {
// Get optimal worker count based on hardware
size_t GetOptimalWorkerCount() {
#ifdef __EMSCRIPTEN__
// In Emscripten, check navigator.hardwareConcurrency
EM_ASM({
if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
Module['_yaze_hardware_concurrency'] = navigator.hardwareConcurrency;
} else {
Module['_yaze_hardware_concurrency'] = 4; // Default fallback
}
});
// Read the value set by JavaScript
int concurrency = EM_ASM_INT({
return Module['_yaze_hardware_concurrency'] || 4;
});
// Use half the available cores for workers, minimum 2, maximum 8
return std::max(2, std::min(8, concurrency / 2));
#else
// Native platform
unsigned int hw_threads = std::thread::hardware_concurrency();
if (hw_threads == 0) hw_threads = 4; // Fallback
return std::max(2u, std::min(8u, hw_threads / 2));
#endif
}
} // namespace
WasmWorkerPool::WasmWorkerPool(size_t num_workers)
: num_workers_(num_workers == 0 ? GetOptimalWorkerCount() : num_workers) {
worker_stats_.resize(num_workers_);
}
WasmWorkerPool::~WasmWorkerPool() {
if (initialized_) {
Shutdown();
}
}
bool WasmWorkerPool::Initialize() {
if (initialized_) {
return true;
}
#ifdef __EMSCRIPTEN__
// Check if SharedArrayBuffer is available (required for pthreads)
bool has_shared_array_buffer = EM_ASM_INT({
return typeof SharedArrayBuffer !== 'undefined';
});
if (!has_shared_array_buffer) {
std::cerr << "WasmWorkerPool: SharedArrayBuffer not available. "
<< "Workers will run in degraded mode.\n";
// Could fall back to single-threaded mode or use postMessage-based workers
// For now, we'll proceed but with reduced functionality
num_workers_ = 0;
initialized_ = true;
return true;
}
// Log initialization
EM_ASM({
console.log('WasmWorkerPool: Initializing with', $0, 'workers');
}, num_workers_);
#endif
// Create worker threads
workers_.reserve(num_workers_);
for (size_t i = 0; i < num_workers_; ++i) {
workers_.emplace_back(&WasmWorkerPool::WorkerThread, this, i);
}
initialized_ = true;
return true;
}
void WasmWorkerPool::Shutdown() {
if (!initialized_) {
return;
}
// Signal shutdown
{
std::lock_guard<std::mutex> lock(queue_mutex_);
shutting_down_ = true;
}
queue_cv_.notify_all();
// Wait for all workers to finish
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
// Clear any remaining tasks
{
std::lock_guard<std::mutex> lock(queue_mutex_);
while (!task_queue_.empty()) {
task_queue_.pop();
}
active_tasks_.clear();
}
initialized_ = false;
}
uint32_t WasmWorkerPool::SubmitTask(TaskType type,
const std::vector<uint8_t>& input_data,
TaskCallback callback,
Priority priority) {
return SubmitTaskWithProgress(type, input_data, callback, nullptr, priority);
}
uint32_t WasmWorkerPool::SubmitCustomTask(const std::string& type_string,
const std::vector<uint8_t>& input_data,
TaskCallback callback,
Priority priority) {
auto task = std::make_shared<Task>();
task->id = next_task_id_++;
task->type = TaskType::kCustom;
task->type_string = type_string;
task->priority = priority;
task->input_data = input_data;
task->completion_callback = callback;
{
std::lock_guard<std::mutex> lock(queue_mutex_);
active_tasks_[task->id] = task;
task_queue_.push(task);
total_tasks_submitted_++;
}
queue_cv_.notify_one();
return task->id;
}
uint32_t WasmWorkerPool::SubmitTaskWithProgress(TaskType type,
const std::vector<uint8_t>& input_data,
TaskCallback completion_callback,
ProgressCallback progress_callback,
Priority priority) {
// If no workers available, execute synchronously
if (num_workers_ == 0 || !initialized_) {
if (completion_callback) {
try {
auto task = std::make_shared<Task>();
task->type = type;
task->input_data = input_data;
auto result = ExecuteTask(*task);
completion_callback(true, result);
} catch (const std::exception& e) {
completion_callback(false, std::vector<uint8_t>());
}
}
// Return special ID to indicate synchronous execution (task already completed)
return kSynchronousTaskId;
}
auto task = std::make_shared<Task>();
task->id = next_task_id_++;
task->type = type;
task->priority = priority;
task->input_data = input_data;
task->completion_callback = completion_callback;
task->progress_callback = progress_callback;
{
std::lock_guard<std::mutex> lock(queue_mutex_);
active_tasks_[task->id] = task;
task_queue_.push(task);
total_tasks_submitted_++;
}
queue_cv_.notify_one();
return task->id;
}
bool WasmWorkerPool::Cancel(uint32_t task_id) {
std::lock_guard<std::mutex> lock(queue_mutex_);
auto it = active_tasks_.find(task_id);
if (it != active_tasks_.end()) {
it->second->cancelled = true;
return true;
}
return false;
}
void WasmWorkerPool::CancelAllOfType(TaskType type) {
std::lock_guard<std::mutex> lock(queue_mutex_);
for (auto& [id, task] : active_tasks_) {
if (task->type == type) {
task->cancelled = true;
}
}
}
bool WasmWorkerPool::WaitAll(uint32_t timeout_ms) {
auto start = std::chrono::steady_clock::now();
while (true) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
if (task_queue_.empty() && active_workers_ == 0) {
return true;
}
}
if (timeout_ms > 0) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeout_ms) {
return false;
}
}
std::unique_lock<std::mutex> lock(queue_mutex_);
completion_cv_.wait_for(lock, std::chrono::milliseconds(100));
}
}
size_t WasmWorkerPool::GetPendingCount() const {
std::lock_guard<std::mutex> lock(queue_mutex_);
return task_queue_.size();
}
size_t WasmWorkerPool::GetActiveWorkerCount() const {
return active_workers_.load();
}
std::vector<WasmWorkerPool::WorkerStats> WasmWorkerPool::GetWorkerStats() const {
std::lock_guard<std::mutex> lock(queue_mutex_);
return worker_stats_;
}
void WasmWorkerPool::SetMaxWorkers(size_t count) {
// This would require stopping and restarting workers
// For simplicity, we'll just store the value for next initialization
if (!initialized_) {
num_workers_ = count;
}
}
void WasmWorkerPool::ProcessCallbacks() {
std::queue<std::function<void()>> callbacks_to_process;
{
std::lock_guard<std::mutex> lock(callback_mutex_);
callbacks_to_process.swap(callback_queue_);
}
while (!callbacks_to_process.empty()) {
callbacks_to_process.front()();
callbacks_to_process.pop();
}
}
void WasmWorkerPool::WorkerThread(size_t worker_id) {
#ifdef __EMSCRIPTEN__
// Set thread name for debugging
emscripten_set_thread_name(pthread_self(),
absl::StrFormat("YazeWorker%zu", worker_id).c_str());
#endif
while (true) {
std::shared_ptr<Task> task;
// Get next task from queue
{
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait(lock, [this] {
return shutting_down_ || !task_queue_.empty();
});
if (shutting_down_ && task_queue_.empty()) {
break;
}
if (!task_queue_.empty()) {
task = task_queue_.top();
task_queue_.pop();
active_workers_++;
worker_stats_[worker_id].is_busy = true;
worker_stats_[worker_id].current_task_type =
task->type == TaskType::kCustom ? task->type_string :
absl::StrFormat("Type%d", static_cast<int>(task->type));
}
}
if (task && !task->cancelled) {
ProcessTask(*task, worker_id);
}
// Clean up
if (task) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
active_tasks_.erase(task->id);
active_workers_--;
worker_stats_[worker_id].is_busy = false;
worker_stats_[worker_id].current_task_type.clear();
}
completion_cv_.notify_all();
}
}
}
void WasmWorkerPool::ProcessTask(const Task& task, size_t worker_id) {
auto start_time = std::chrono::steady_clock::now();
bool success = false;
std::vector<uint8_t> result;
try {
// Report starting
if (task.progress_callback) {
ReportProgress(task.id, 0.0f, "Starting task...");
}
// Execute the task
result = ExecuteTask(task);
success = true;
// Update stats
worker_stats_[worker_id].tasks_completed++;
// Report completion
if (task.progress_callback) {
ReportProgress(task.id, 1.0f, "Task completed");
}
} catch (const std::exception& e) {
std::cerr << "Worker " << worker_id << " task failed: " << e.what() << std::endl;
worker_stats_[worker_id].tasks_failed++;
success = false;
}
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time).count();
worker_stats_[worker_id].total_processing_time_ms += elapsed;
// Queue callback for main thread execution
if (task.completion_callback && !task.cancelled) {
QueueCallback([callback = task.completion_callback, success, result]() {
callback(success, result);
});
}
total_tasks_completed_++;
}
std::vector<uint8_t> WasmWorkerPool::ExecuteTask(const Task& task) {
switch (task.type) {
case TaskType::kRomDecompression:
return ProcessRomDecompression(task.input_data);
case TaskType::kGraphicsDecoding:
return ProcessGraphicsDecoding(task.input_data);
case TaskType::kPaletteCalculation:
return ProcessPaletteCalculation(task.input_data);
case TaskType::kAsarCompilation:
return ProcessAsarCompilation(task.input_data);
case TaskType::kCustom:
// For custom tasks, just return the input as we don't know how to process it
// Real implementation would need a registry of custom processors
return task.input_data;
default:
throw std::runtime_error("Unknown task type");
}
}
std::vector<uint8_t> WasmWorkerPool::ProcessRomDecompression(const std::vector<uint8_t>& input) {
// Placeholder for LC-LZ2 decompression
// In real implementation, this would call the actual decompression routine
// from src/app/gfx/compression.cc
// For now, simulate some work
std::vector<uint8_t> result;
result.reserve(input.size() * 2); // Assume 2x expansion
// Simulate decompression (just duplicate data for testing)
for (size_t i = 0; i < input.size(); ++i) {
result.push_back(input[i]);
result.push_back(input[i] ^ 0xFF); // Inverted copy
// Simulate progress reporting
if (i % 1000 == 0) {
float progress = static_cast<float>(i) / input.size();
// Would call ReportProgress here if we had the task ID
}
}
return result;
}
std::vector<uint8_t> WasmWorkerPool::ProcessGraphicsDecoding(const std::vector<uint8_t>& input) {
// Placeholder for graphics sheet decoding
// In real implementation, this would decode SNES tile formats
std::vector<uint8_t> result;
result.reserve(input.size());
// Simulate processing
for (uint8_t byte : input) {
// Simple transformation to simulate work
result.push_back((byte << 1) | (byte >> 7));
}
return result;
}
std::vector<uint8_t> WasmWorkerPool::ProcessPaletteCalculation(const std::vector<uint8_t>& input) {
// Placeholder for palette calculations
// In real implementation, this would process SNES color formats
std::vector<uint8_t> result;
// Process in groups of 2 bytes (SNES color format)
for (size_t i = 0; i + 1 < input.size(); i += 2) {
uint16_t snes_color = (input[i + 1] << 8) | input[i];
// Extract RGB components (5 bits each)
uint8_t r = (snes_color & 0x1F) << 3;
uint8_t g = ((snes_color >> 5) & 0x1F) << 3;
uint8_t b = ((snes_color >> 10) & 0x1F) << 3;
// Store as RGB24
result.push_back(r);
result.push_back(g);
result.push_back(b);
}
return result;
}
std::vector<uint8_t> WasmWorkerPool::ProcessAsarCompilation(const std::vector<uint8_t>& input) {
// Placeholder for Asar assembly compilation
// In real implementation, this would call the Asar wrapper
// For now, return empty result (compilation succeeded with no output)
return std::vector<uint8_t>();
}
void WasmWorkerPool::ReportProgress(uint32_t task_id, float progress, const std::string& message) {
// Find the task
std::shared_ptr<Task> task;
{
std::lock_guard<std::mutex> lock(queue_mutex_);
auto it = active_tasks_.find(task_id);
if (it != active_tasks_.end()) {
task = it->second;
}
}
if (task && task->progress_callback && !task->cancelled) {
QueueCallback([callback = task->progress_callback, progress, message]() {
callback(progress, message);
});
}
}
void WasmWorkerPool::QueueCallback(std::function<void()> callback) {
#ifdef __EMSCRIPTEN__
// In Emscripten, we need to execute callbacks on the main thread
// Use emscripten_async_run_in_main_runtime_thread for thread safety
auto* callback_ptr = new std::function<void()>(std::move(callback));
emscripten_async_run_in_main_runtime_thread(
EM_FUNC_SIG_VI,
&WasmWorkerPool::MainThreadCallbackHandler,
callback_ptr);
#else
// For non-Emscripten builds, just queue for later processing
std::lock_guard<std::mutex> lock(callback_mutex_);
callback_queue_.push(callback);
#endif
}
#ifdef __EMSCRIPTEN__
void WasmWorkerPool::MainThreadCallbackHandler(void* arg) {
auto* callback_ptr = static_cast<std::function<void()>*>(arg);
if (callback_ptr) {
(*callback_ptr)();
delete callback_ptr;
}
}
#endif
} // namespace wasm
} // namespace platform
} // namespace app
} // namespace yaze
#else // !__EMSCRIPTEN__
// Stub implementation for non-Emscripten builds
#include "app/platform/wasm/wasm_worker_pool.h"
namespace yaze {
namespace app {
namespace platform {
namespace wasm {
WasmWorkerPool::WasmWorkerPool(size_t num_workers) : num_workers_(0) {}
WasmWorkerPool::~WasmWorkerPool() {}
bool WasmWorkerPool::Initialize() { return false; }
void WasmWorkerPool::Shutdown() {}
uint32_t WasmWorkerPool::SubmitTask(TaskType type,
const std::vector<uint8_t>& input_data,
TaskCallback callback,
Priority priority) {
// No-op in non-WASM builds
if (callback) {
callback(false, std::vector<uint8_t>());
}
return 0;
}
uint32_t WasmWorkerPool::SubmitCustomTask(const std::string& type_string,
const std::vector<uint8_t>& input_data,
TaskCallback callback,
Priority priority) {
if (callback) {
callback(false, std::vector<uint8_t>());
}
return 0;
}
uint32_t WasmWorkerPool::SubmitTaskWithProgress(TaskType type,
const std::vector<uint8_t>& input_data,
TaskCallback completion_callback,
ProgressCallback progress_callback,
Priority priority) {
if (completion_callback) {
completion_callback(false, std::vector<uint8_t>());
}
return 0;
}
bool WasmWorkerPool::Cancel(uint32_t task_id) { return false; }
void WasmWorkerPool::CancelAllOfType(TaskType type) {}
bool WasmWorkerPool::WaitAll(uint32_t timeout_ms) { return true; }
size_t WasmWorkerPool::GetPendingCount() const { return 0; }
size_t WasmWorkerPool::GetActiveWorkerCount() const { return 0; }
std::vector<WasmWorkerPool::WorkerStats> WasmWorkerPool::GetWorkerStats() const { return {}; }
void WasmWorkerPool::SetMaxWorkers(size_t count) {}
void WasmWorkerPool::ProcessCallbacks() {}
void WasmWorkerPool::WorkerThread(size_t worker_id) {}
void WasmWorkerPool::ProcessTask(const Task& task, size_t worker_id) {}
std::vector<uint8_t> WasmWorkerPool::ExecuteTask(const Task& task) { return {}; }
std::vector<uint8_t> WasmWorkerPool::ProcessRomDecompression(const std::vector<uint8_t>& input) { return {}; }
std::vector<uint8_t> WasmWorkerPool::ProcessGraphicsDecoding(const std::vector<uint8_t>& input) { return {}; }
std::vector<uint8_t> WasmWorkerPool::ProcessPaletteCalculation(const std::vector<uint8_t>& input) { return {}; }
std::vector<uint8_t> WasmWorkerPool::ProcessAsarCompilation(const std::vector<uint8_t>& input) { return {}; }
void WasmWorkerPool::ReportProgress(uint32_t task_id, float progress, const std::string& message) {}
void WasmWorkerPool::QueueCallback(std::function<void()> callback) {
if (callback) callback();
}
} // namespace wasm
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,275 @@
// clang-format off
#ifndef YAZE_APP_PLATFORM_WASM_WORKER_POOL_H
#define YAZE_APP_PLATFORM_WASM_WORKER_POOL_H
#include <cstddef>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <unordered_map>
#include <emscripten.h>
#include <emscripten/threading.h>
#endif
namespace yaze {
namespace app {
namespace platform {
namespace wasm {
/**
* @brief Web Worker pool for offloading CPU-intensive operations.
*
* This class manages a pool of background workers (using pthreads in Emscripten)
* to handle heavy processing tasks without blocking the UI thread. Supported
* task types include:
* - ROM decompression (LC-LZ2)
* - Graphics sheet decoding
* - Palette calculations
* - Asar assembly compilation
*
* The implementation uses Emscripten's pthread support which maps to Web Workers
* in the browser. Callbacks are executed on the main thread to ensure safe
* UI updates.
*/
class WasmWorkerPool {
public:
// Task types that can be processed in background
enum class TaskType {
kRomDecompression,
kGraphicsDecoding,
kPaletteCalculation,
kAsarCompilation,
kCustom
};
// Task priority levels
enum class Priority {
kLow = 0,
kNormal = 1,
kHigh = 2,
kCritical = 3
};
// Callback type for task completion
using TaskCallback = std::function<void(bool success, const std::vector<uint8_t>& result)>;
// Progress callback for long-running tasks
using ProgressCallback = std::function<void(float progress, const std::string& message)>;
// Task structure
struct Task {
uint32_t id;
TaskType type;
Priority priority;
std::vector<uint8_t> input_data;
TaskCallback completion_callback;
ProgressCallback progress_callback;
std::string type_string; // For custom task types
bool cancelled = false;
};
// Worker statistics
struct WorkerStats {
uint32_t tasks_completed = 0;
uint32_t tasks_failed = 0;
uint64_t total_processing_time_ms = 0;
std::string current_task_type;
bool is_busy = false;
};
// Special task ID returned when task is executed synchronously (no workers available)
static constexpr uint32_t kSynchronousTaskId = UINT32_MAX;
WasmWorkerPool(size_t num_workers = 0); // 0 = auto-detect optimal count
~WasmWorkerPool();
// Initialize the worker pool
bool Initialize();
// Shutdown the worker pool
void Shutdown();
/**
* @brief Submit a task to the worker pool.
*
* @param type The type of task to process
* @param input_data The input data for the task
* @param callback Callback to invoke on completion (executed on main thread)
* @param priority Task priority (higher priority tasks are processed first)
* @return Task ID that can be used for cancellation
*/
uint32_t SubmitTask(TaskType type,
const std::vector<uint8_t>& input_data,
TaskCallback callback,
Priority priority = Priority::kNormal);
/**
* @brief Submit a custom task type.
*
* @param type_string Custom task type identifier
* @param input_data The input data for the task
* @param callback Callback to invoke on completion
* @param priority Task priority
* @return Task ID
*/
uint32_t SubmitCustomTask(const std::string& type_string,
const std::vector<uint8_t>& input_data,
TaskCallback callback,
Priority priority = Priority::kNormal);
/**
* @brief Submit a task with progress reporting.
*/
uint32_t SubmitTaskWithProgress(TaskType type,
const std::vector<uint8_t>& input_data,
TaskCallback completion_callback,
ProgressCallback progress_callback,
Priority priority = Priority::kNormal);
/**
* @brief Cancel a pending task.
*
* @param task_id The task ID to cancel
* @return true if task was cancelled, false if already running or completed
*/
bool Cancel(uint32_t task_id);
/**
* @brief Cancel all pending tasks of a specific type.
*/
void CancelAllOfType(TaskType type);
/**
* @brief Wait for all pending tasks to complete.
*
* @param timeout_ms Maximum time to wait in milliseconds (0 = infinite)
* @return true if all tasks completed, false if timeout
*/
bool WaitAll(uint32_t timeout_ms = 0);
/**
* @brief Get the number of pending tasks.
*/
size_t GetPendingCount() const;
/**
* @brief Get the number of active workers.
*/
size_t GetActiveWorkerCount() const;
/**
* @brief Get statistics for all workers.
*/
std::vector<WorkerStats> GetWorkerStats() const;
/**
* @brief Check if the worker pool is initialized.
*/
bool IsInitialized() const { return initialized_; }
/**
* @brief Set the maximum number of concurrent workers.
*/
void SetMaxWorkers(size_t count);
/**
* @brief Process any pending callbacks on the main thread.
* Should be called periodically from the main loop.
*/
void ProcessCallbacks();
private:
// Worker thread function
void WorkerThread(size_t worker_id);
// Process a single task
void ProcessTask(const Task& task, size_t worker_id);
// Execute task based on type
std::vector<uint8_t> ExecuteTask(const Task& task);
// Task-specific processing functions
std::vector<uint8_t> ProcessRomDecompression(const std::vector<uint8_t>& input);
std::vector<uint8_t> ProcessGraphicsDecoding(const std::vector<uint8_t>& input);
std::vector<uint8_t> ProcessPaletteCalculation(const std::vector<uint8_t>& input);
std::vector<uint8_t> ProcessAsarCompilation(const std::vector<uint8_t>& input);
// Report progress from worker thread
void ReportProgress(uint32_t task_id, float progress, const std::string& message);
// Queue a callback for execution on main thread
void QueueCallback(std::function<void()> callback);
#ifdef __EMSCRIPTEN__
// Emscripten-specific callback handler
static void MainThreadCallbackHandler(void* arg);
#endif
// Member variables
bool initialized_ = false;
bool shutting_down_ = false;
size_t num_workers_;
#ifdef __EMSCRIPTEN__
std::atomic<uint32_t> next_task_id_{1};
// Worker threads
std::vector<std::thread> workers_;
std::vector<WorkerStats> worker_stats_;
// Task queue (priority queue)
struct TaskCompare {
bool operator()(const std::shared_ptr<Task>& a, const std::shared_ptr<Task>& b) {
// Higher priority first, then lower ID (FIFO within priority)
if (a->priority != b->priority) {
return static_cast<int>(a->priority) < static_cast<int>(b->priority);
}
return a->id > b->id;
}
};
std::priority_queue<std::shared_ptr<Task>,
std::vector<std::shared_ptr<Task>>,
TaskCompare> task_queue_;
// Active tasks map (task_id -> task)
std::unordered_map<uint32_t, std::shared_ptr<Task>> active_tasks_;
// Synchronization
mutable std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::condition_variable completion_cv_;
// Callback queue for main thread execution
std::queue<std::function<void()>> callback_queue_;
mutable std::mutex callback_mutex_;
// Statistics
std::atomic<size_t> active_workers_{0};
std::atomic<size_t> total_tasks_submitted_{0};
std::atomic<size_t> total_tasks_completed_{0};
#else
// Stub members for non-Emscripten builds
uint32_t next_task_id_{1};
std::vector<WorkerStats> worker_stats_;
size_t active_workers_{0};
size_t total_tasks_submitted_{0};
size_t total_tasks_completed_{0};
#endif
};
} // namespace wasm
} // namespace platform
} // namespace app
} // namespace yaze
#endif // YAZE_APP_PLATFORM_WASM_WORKER_POOL_H

View File

@@ -7,12 +7,15 @@
#include "app/gfx/resource/arena.h"
#include "app/gui/core/style.h"
#include "app/platform/font_loader.h"
#include "imgui/backends/imgui_impl_sdl2.h"
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
#include "imgui/imgui.h"
#include "util/log.h"
#include "util/sdl_deleter.h"
#ifndef YAZE_USE_SDL3
#include "imgui/backends/imgui_impl_sdl2.h"
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
#endif
namespace {
// Custom ImGui assertion handler to prevent crashes
void ImGuiAssertionHandler(const char* expr, const char* file, int line,
@@ -57,6 +60,10 @@ namespace core {
bool g_window_is_resizing = false;
absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer, int flags) {
#ifdef YAZE_USE_SDL3
return absl::FailedPreconditionError(
"Legacy SDL2 window path is unavailable when building with SDL3");
#else
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) {
return absl::InternalError(
absl::StrFormat("SDL_Init: %s\n", SDL_GetError()));
@@ -90,6 +97,11 @@ absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer, int flags) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Ensure macOS-style behavior (Cmd acts as Ctrl for shortcuts)
#ifdef __APPLE__
io.ConfigMacOSXBehaviors = true;
#endif
// Set custom assertion handler to prevent crashes
#ifdef IMGUI_DISABLE_DEFAULT_ASSERT_HANDLER
ImGui::SetAssertHandler(ImGuiAssertionHandler);
@@ -128,18 +140,23 @@ absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer, int flags) {
window.audio_buffer_ = std::shared_ptr<int16_t>(
new int16_t[buffer_size], std::default_delete<int16_t[]>());
// Note: Actual audio device is created by Emulator's IAudioBackend
// This maintains compatibility with existing code paths
LOG_INFO(
"Window",
"Audio buffer allocated: %zu int16_t samples (backend in Emulator)",
buffer_size);
// Note: Actual audio device is created by Emulator's IAudioBackend
// This maintains compatibility with existing code paths
LOG_INFO(
"Window",
"Audio buffer allocated: %zu int16_t samples (backend in Emulator)",
buffer_size);
}
return absl::OkStatus();
#endif // YAZE_USE_SDL3
}
absl::Status ShutdownWindow(Window& window) {
#ifdef YAZE_USE_SDL3
return absl::FailedPreconditionError(
"Legacy SDL2 window path is unavailable when building with SDL3");
#else
SDL_PauseAudioDevice(window.audio_device_, 1);
SDL_CloseAudioDevice(window.audio_device_);
@@ -177,9 +194,14 @@ absl::Status ShutdownWindow(Window& window) {
LOG_INFO("Window", "Shutdown complete");
return absl::OkStatus();
#endif // YAZE_USE_SDL3
}
absl::Status HandleEvents(Window& window) {
#ifdef YAZE_USE_SDL3
return absl::FailedPreconditionError(
"Legacy SDL2 window path is unavailable when building with SDL3");
#else
SDL_Event event;
ImGuiIO& io = ImGui::GetIO();
@@ -188,14 +210,9 @@ absl::Status HandleEvents(Window& window) {
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event);
switch (event.type) {
case SDL_KEYDOWN:
case SDL_KEYUP: {
io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0);
io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0);
io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0);
io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0);
break;
}
// Note: Keyboard modifiers are handled by ImGui_ImplSDL2_ProcessEvent
// which respects ConfigMacOSXBehaviors for Cmd/Ctrl swapping on macOS.
// Do NOT manually override io.KeyCtrl/KeySuper here.
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_CLOSE:
@@ -236,7 +253,8 @@ absl::Status HandleEvents(Window& window) {
int wheel = 0;
io.MouseWheel = static_cast<float>(wheel);
return absl::OkStatus();
#endif // YAZE_USE_SDL3
}
} // namespace core
} // namespace yaze
} // namespace yaze