diff --git a/source/EsUuid.cpp b/source/EsUuid.cpp index 0182f32..15679c5 100644 --- a/source/EsUuid.cpp +++ b/source/EsUuid.cpp @@ -117,8 +117,13 @@ EsUuid::UuidType EsUuid::MakeUuid() Logger::Log("Created locally unique UUID only", Logger::Level::WARNING); else if(status == RPC_S_UUID_NO_ADDRESS) throw runtime_error("Failed to create UUID"); +#else +#ifdef __EMSCRIPTEN__ + // Emscripten's uuid_generate makes random (version 4) UUIDs. + uuid_generate(value.id); #else uuid_generate_random(value.id); +#endif #endif return value; } diff --git a/source/FrameTimer.cpp b/source/FrameTimer.cpp index dcdf0e6..bf5a93a 100644 --- a/source/FrameTimer.cpp +++ b/source/FrameTimer.cpp @@ -21,6 +21,10 @@ this program. If not, see . #include #else #include + +#ifdef __EMSCRIPTEN__ +#include +#endif #endif using namespace std; @@ -70,6 +74,9 @@ void FrameTimer::Wait() // See the thread starting with https://sourceforge.net/p/mingw-w64/mailman/message/37013810/. #ifdef _WIN32 Sleep(chrono::duration_cast(next - now).count()); +#elif defined(__EMSCRIPTEN__) + // Hand the browser its turn (Asyncify) instead of blocking it. + emscripten_sleep(chrono::duration_cast(next - now).count()); #else this_thread::sleep_until(next); #endif diff --git a/source/GameWindow.cpp b/source/GameWindow.cpp index cd82653..3490afd 100644 --- a/source/GameWindow.cpp +++ b/source/GameWindow.cpp @@ -31,6 +31,10 @@ this program. If not, see . using namespace std; +#ifdef __EMSCRIPTEN__ +#include +#endif + namespace { // The minimal screen resolution requirements. constexpr int minWidth = 1024; @@ -188,9 +192,15 @@ bool GameWindow::Init(bool headless) SDL_EnableScreenSaver(); // Settings that must be declared before the window creation. +#ifdef __EMSCRIPTEN__ + // The page sizes the canvas (wg_resize below) and owns full screen. + Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_ALLOW_HIGHDPI; + if(false) +#else Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; if(Preferences::ScreenModeSetting() == "fullscreen") +#endif flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; else if(Preferences::Has("maximized")) flags |= SDL_WINDOW_MAXIMIZED; @@ -489,6 +499,9 @@ bool GameWindow::IsMaximized() bool GameWindow::IsFullscreen() { +#ifdef __EMSCRIPTEN__ + return EM_ASM_INT(return !!document.fullscreenElement); +#endif return (SDL_GetWindowFlags(mainWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP); } @@ -496,6 +509,11 @@ bool GameWindow::IsFullscreen() void GameWindow::ToggleFullscreen() { +#ifdef __EMSCRIPTEN__ + // The page's own full screen. + EM_ASM(Module.wgToggleFullscreen && Module.wgToggleFullscreen()); + return; +#endif // This will generate a window size change event, // no need to adjust the viewport here. if(IsFullscreen()) @@ -568,3 +586,14 @@ void GameWindow::UpdateWindowRounding() WinWindow::UpdateWindowRounding(mainWindow); } #endif + + + +#ifdef __EMSCRIPTEN__ +// The page calls this with the size the game should have, in CSS pixels. +extern "C" EMSCRIPTEN_KEEPALIVE void wg_resize(int width, int height) +{ + if(mainWindow) + SDL_SetWindowSize(mainWindow, width, height); +} +#endif diff --git a/source/TaskQueue.cpp b/source/TaskQueue.cpp index 7f17774..5ba0389 100644 --- a/source/TaskQueue.cpp +++ b/source/TaskQueue.cpp @@ -23,6 +23,109 @@ this program. If not, see . using namespace std; +#ifdef __EMSCRIPTEN__ +// The web build has no threads (they would need cross-origin isolation): the +// "async" tasks are queued and run on the main thread, a few milliseconds' +// worth at a time whenever a queue processes its sync tasks, and all of a +// queue's tasks when it is waited on. +#include +#include + +namespace { + deque tasks; +} + + + +void TaskQueue::Execute(Task &task) +{ + try { + if(task.async) + task.async(); + } + catch(...) + { + auto exception = current_exception(); + task.sync = [exception] { rethrow_exception(exception); }; + } + if(task.sync) + task.queue->syncTasks.push(std::move(task.sync)); + task.futurePromise.set_value(); + task.queue->futures.erase(task.futureIt); +} + + + +void TaskQueue::SetWorkerThreadCount(uint64_t) +{ +} + + + +TaskQueue::~TaskQueue() +{ + Wait(); +} + + + +shared_future TaskQueue::Run(function asyncTask, function syncTask) +{ + tasks.push_back(Task{this, std::move(asyncTask), std::move(syncTask)}); + shared_future result = futures.emplace_back(tasks.back().futurePromise.get_future()); + tasks.back().futureIt = prev(futures.end()); + return result; +} + + + +void TaskQueue::ProcessSyncTasks() +{ + // Run queued tasks (of any queue), then this queue's sync tasks: for up to + // 8 ms a call, or 40 ms while there are many (the game is loading). + auto end = chrono::steady_clock::now() + chrono::milliseconds(tasks.size() > 50 ? 40 : 8); + while(!tasks.empty() && chrono::steady_clock::now() < end) + { + Task task = std::move(tasks.front()); + tasks.pop_front(); + Execute(task); + } + for(int i = 0; !syncTasks.empty() && i < MAX_SYNC_TASKS; ++i) + { + auto task = std::move(syncTasks.front()); + syncTasks.pop(); + task(); + } +} + + + +void TaskQueue::Wait() +{ + while(!IsDone()) + for(auto it = tasks.begin(); it != tasks.end(); ++it) + if(it->queue == this) + { + Task task = std::move(*it); + tasks.erase(it); + Execute(task); + break; + } +} + + + +bool TaskQueue::IsDone() const +{ + return futures.empty(); +} + + + +void TaskQueue::ThreadLoop() noexcept +{ +} +#else namespace { // The main task queue used by the worker threads. @@ -195,3 +298,4 @@ void TaskQueue::ThreadLoop() noexcept asyncCondition.wait(lock, [] { return shouldQuit || !tasks.empty(); }); } } +#endif diff --git a/source/TaskQueue.h b/source/TaskQueue.h index aa42292..5063748 100644 --- a/source/TaskQueue.h +++ b/source/TaskQueue.h @@ -84,6 +84,10 @@ private: public: // Thread entry point. static void ThreadLoop() noexcept; +#ifdef __EMSCRIPTEN__ + // Runs a task on the main thread (the web build has no worker threads). + static void Execute(Task &task); +#endif private: diff --git a/source/audio/Audio.cpp b/source/audio/Audio.cpp index aa2df8b..9def51a 100644 --- a/source/audio/Audio.cpp +++ b/source/audio/Audio.cpp @@ -168,7 +168,14 @@ void Audio::LoadSounds(const vector &sources) } // Begin loading the files. if(!loadQueue.empty()) + { +#ifdef __EMSCRIPTEN__ + // No threads on the web: load them now. + Load(); +#else loadThread = thread(&Load); +#endif + } } diff --git a/source/audio/Music.cpp b/source/audio/Music.cpp index efa8242..e5f743d 100644 --- a/source/audio/Music.cpp +++ b/source/audio/Music.cpp @@ -16,9 +16,11 @@ this program. If not, see . #include "Music.h" #include "../Files.h" -#include "supplier/FlacSupplier.h" #include "../text/Format.h" +#ifndef __EMSCRIPTEN__ +#include "supplier/FlacSupplier.h" #include "supplier/Mp3Supplier.h" +#endif #include @@ -59,6 +61,11 @@ void Music::Init(const vector &sources) unique_ptr Music::CreateSupplier(const string &name, bool looping) { +#ifdef __EMSCRIPTEN__ + // The web build plays no music: its decoders stream on threads. + (void)name; + (void)looping; +#else if(paths.contains(name)) switch(paths[name].second) { @@ -69,5 +76,6 @@ unique_ptr Music::CreateSupplier(const string &name, bool looping return unique_ptr{ new FlacSupplier{Files::Open(paths[name].first), looping}}; } +#endif return {}; } diff --git a/source/image/ImageBuffer.cpp b/source/image/ImageBuffer.cpp index b4dfd4e..6302d11 100644 --- a/source/image/ImageBuffer.cpp +++ b/source/image/ImageBuffer.cpp @@ -19,7 +19,9 @@ this program. If not, see . #include "ImageFileData.h" #include "../Logger.h" +#ifndef __EMSCRIPTEN__ #include +#endif #include #include @@ -31,6 +33,19 @@ this program. If not, see . using namespace std; +#ifdef __EMSCRIPTEN__ +#include + +// Images the page downloads when they are first drawn start out as stand-ins +// of the same size; these wait for the real file. +EM_JS(int, wg_image_pending, (const char *path), { + return Module.wgImagePending ? Module.wgImagePending(UTF8ToString(path)) : 0; +}); +EM_ASYNC_JS(void, wg_fetch_image, (const char *path), { + await Module.wgFetchImage(UTF8ToString(path)); +}); +#endif + namespace { const set PNG_EXTENSIONS{".png"}; const set JPG_EXTENSIONS{".jpg", ".jpeg", ".jpe"}; @@ -218,6 +233,11 @@ int ImageBuffer::Read(const ImageFileData &data, int frame, bool onlyDimensions) if(!isPNG && !isJPG && !isAVIF) return false; +#ifdef __EMSCRIPTEN__ + if(!onlyDimensions && wg_image_pending(data.path.c_str())) + wg_fetch_image(data.path.c_str()); +#endif + bool isAlphaPreMultiplied = data.blendingMode == BlendingMode::PREMULTIPLIED_ALPHA; int loaded; if(isPNG) @@ -433,6 +453,10 @@ namespace { int ReadAVIF(const filesystem::path &path, ImageBuffer &buffer, int frame, bool alphaPreMultiplied, bool onlyDimensions) { +#ifdef __EMSCRIPTEN__ + // The web build has no AVIF decoder (the game's own images are PNG and JPEG). + return 0; +#else unique_ptr decoder(avifDecoderCreate(), avifDecoderDestroy); if(!decoder) { @@ -554,6 +578,7 @@ namespace { Logger::Log("Skipped corrupted frames for \"" + path.generic_string() + "\"", Logger::Level::WARNING); return bufferFrameCount; +#endif } diff --git a/source/image/SpriteLoadManager.cpp b/source/image/SpriteLoadManager.cpp index a0e3449..ca7c38e 100644 --- a/source/image/SpriteLoadManager.cpp +++ b/source/image/SpriteLoadManager.cpp @@ -173,8 +173,13 @@ void SpriteLoadManager::PreventSpriteUpload() void SpriteLoadManager::FindDeferredFolders() { - // Landscape images are always deferred. + // Landscape images are always deferred. The web build always defers these + // folders: the page downloads their images when they are first drawn. +#ifdef __EMSCRIPTEN__ + if(true) +#else if(Preferences::Has("Defer loading images")) +#endif deferredFolders = {"land", "thumbnail", "outfit", "scene", "star", "planet"}; else deferredFolders = {"land"}; diff --git a/source/main.cpp b/source/main.cpp index e16edb7..e841318 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -63,6 +63,10 @@ this program. If not, see . #include #include #include +#ifdef __EMSCRIPTEN__ +#include +#include +#endif #include #ifdef _WIN32 @@ -239,6 +243,10 @@ int main(int argc, char *argv[]) if(node.Token(0) == "conditions") GameData::GlobalConditions().Load(node); +#ifdef __EMSCRIPTEN__ + // Keys reach the game while its canvas has focus, not anywhere on the page. + SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#canvas"); +#endif if(!GameWindow::Init(isTesting && !debugMode)) return 1; @@ -286,6 +294,10 @@ int main(int argc, char *argv[]) Audio::Quit(); GameWindow::Quit(); +#ifdef __EMSCRIPTEN__ + // Tell the page the player quit. + EM_ASM(Module.wgQuit && Module.wgQuit()); +#endif return 0; } @@ -548,6 +560,9 @@ void GameLoop(PlayerInfo &player, TaskQueue &queue, const Conversation &conversa static mach_msg_type_number_t infoSize = MACH_TASK_BASIC_INFO_COUNT; task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast(&info), &infoSize); virtualMemoryUse = info.virtual_size; +#elif defined(__EMSCRIPTEN__) + // There is no /proc on the web: the size of the game's memory. + virtualMemoryUse = emscripten_get_heap_size(); #else static string statmStr; ifstream statm{"/proc/self/statm"};