feat: Support for building ImHex for the web (#1328)

Co-authored-by: WerWolv <werwolv98@gmail.com>
Co-authored-by: AnnsAnn <git@annsann.eu>
This commit is contained in:
iTrooz
2023-10-04 12:00:32 +02:00
committed by GitHub
parent a62ede7840
commit d15bd4771d
84 changed files with 1825 additions and 676 deletions

View File

@@ -8,11 +8,13 @@ add_executable(main ${APPLICATION_TYPE}
source/window/win_window.cpp
source/window/macos_window.cpp
source/window/linux_window.cpp
source/window/web_window.cpp
source/messaging/common.cpp
source/messaging/linux.cpp
source/messaging/macos.cpp
source/messaging/win.cpp
source/messaging/web.cpp
source/init/splash_window.cpp
source/init/tasks.cpp
@@ -29,6 +31,18 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../lib/external/libromfs ${CMAKE
set_target_properties(${LIBROMFS_LIBRARY} PROPERTIES POSITION_INDEPENDENT_CODE ON)
add_dependencies(imhex_all main)
if (EMSCRIPTEN)
target_link_options(main PRIVATE -sUSE_GLFW=3 -sUSE_PTHREADS=1 -sALLOW_MEMORY_GROWTH=1)
target_link_options(main PRIVATE -sTOTAL_MEMORY=134217728)
target_link_options(main PRIVATE -sMAX_WEBGL_VERSION=2)
target_link_options(main PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall)
target_link_options(main PRIVATE -sFETCH)
target_link_options(main PRIVATE -sWASM_BIGINT)
target_link_options(main PRIVATE -O1)
target_link_options(main PRIVATE -sLEGACY_GL_EMULATION)
target_link_libraries(main PRIVATE idbfs.js)
endif ()
set_target_properties(main PROPERTIES
OUTPUT_NAME ${IMHEX_APPLICATION_NAME}
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../..

View File

@@ -28,6 +28,8 @@ namespace hex {
static void initNative();
void resize(i32 width, i32 height);
private:
void setupNativeWindow();
void beginNativeWindowFrame();

View File

@@ -16,7 +16,6 @@
#include <hex/ui/imgui_imhex_extensions.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_opengl3_loader.h>
#include <fonts/fontawesome_font.h>
#include <GLFW/glfw3.h>
@@ -29,6 +28,14 @@
#include <numeric>
#include <random>
#if defined(OS_WEB)
#define GLFW_INCLUDE_ES3
#include <GLES3/gl3.h>
#include <emscripten/html5.h>
#else
#include <imgui_impl_opengl3_loader.h>
#endif
using namespace std::literals::chrono_literals;
namespace hex::init {
@@ -324,6 +331,8 @@ namespace hex::init {
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
glfwWindowHint(GLFW_FLOATING, GLFW_FALSE);
glfwWindowHint(GLFW_SAMPLES, 1);
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
// Create the splash screen window
this->m_window = glfwCreateWindow(1, 400, "Starting ImHex...", nullptr, nullptr);
@@ -373,6 +382,8 @@ namespace hex::init {
#if defined(OS_MACOS)
ImGui_ImplOpenGL3_Init("#version 150");
#elif defined(OS_WEB)
ImGui_ImplOpenGL3_Init();
#else
ImGui_ImplOpenGL3_Init("#version 130");
#endif

View File

@@ -80,6 +80,13 @@ namespace hex::init {
}
TaskManager::createBackgroundTask("Sending statistics...", [uuid, versionString](auto&) {
// To avoid potentially flooding our database with lots of dead users
// from people just visiting the website, don't send telemetry data from
// the web version
#if defined(OS_WEB)
return;
#endif
// Make telemetry request
nlohmann::json telemetry = {
{ "uuid", uuid },
@@ -145,7 +152,7 @@ namespace hex::init {
wolv::io::File newConfigFile(newConfigPath / "settings.json", wolv::io::File::Mode::Read);
if (!newConfigFile.isValid()) {
// find an old config
// Find an old config
std::fs::path oldConfigPath;
for (const auto &dir : hex::fs::appendPath(hex::fs::getDataPaths(), "config")) {
wolv::io::File oldConfigFile(dir / "settings.json", wolv::io::File::Mode::Read);
@@ -493,15 +500,17 @@ namespace hex::init {
// ImHex requires exactly one built-in plugin
// If no built-in plugin or more than one was found, something's wrong and we can't continue
if (builtinPlugins == 0) {
log::error("Built-in plugin not found!");
ImHexApi::System::impl::addInitArgument("no-builtin-plugin");
return false;
} else if (builtinPlugins > 1) {
log::error("Found more than one built-in plugin!");
ImHexApi::System::impl::addInitArgument("multiple-builtin-plugins");
return false;
}
#if !defined(EMSCRIPTEN)
if (builtinPlugins == 0) {
log::error("Built-in plugin not found!");
ImHexApi::System::impl::addInitArgument("no-builtin-plugin");
return false;
} else if (builtinPlugins > 1) {
log::error("Found more than one built-in plugin!");
ImHexApi::System::impl::addInitArgument("multiple-builtin-plugins");
return false;
}
#endif
return true;
}

View File

@@ -19,6 +19,11 @@
#include <wolv/io/fs.hpp>
#include <wolv/utils/guards.hpp>
#if defined(OS_WEB)
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
using namespace hex;
namespace {
@@ -66,6 +71,7 @@ namespace {
/**
* @brief Displays ImHex's splash screen and runs all initialization tasks. The splash screen will be displayed until all tasks have finished.
*/
[[maybe_unused]]
void initializeImHex() {
init::WindowSplash splashWindow;
@@ -105,6 +111,103 @@ namespace {
}
}
#if defined(OS_WEB)
using namespace hex::init;
void saveFsData() {
EM_ASM({
FS.syncfs(function (err) {
if (!err)
return;
alert("Failed to save permanent file system: "+err);
});
});
}
int runImHex() {
auto splashWindow = new WindowSplash();
log::info("Using '{}' GPU", ImHexApi::System::getGPUVendor());
// Add initialization tasks to run
TaskManager::init();
for (const auto &[name, task, async] : init::getInitTasks())
splashWindow->addStartupTask(name, task, async);
splashWindow->startStartupTasks();
// Draw the splash window while tasks are running
emscripten_set_main_loop_arg([](void *arg) {
auto splashWindow = reinterpret_cast<WindowSplash*>(arg);
FrameResult res = splashWindow->fullFrame();
if (res == FrameResult::success) {
handleFileOpenRequest();
// Clean up everything after the main window is closed
emscripten_set_beforeunload_callback(nullptr, [](int eventType, const void *reserved, void *userData) {
hex::unused(eventType, reserved, userData);
try {
saveFsData();
deinitializeImHex();
return "";
} catch (const std::exception &ex) {
std::string *msg = new std::string("Failed to deinitialize ImHex. This is just a message warning you of this, the application has already closed, you probably can't do anything about it. Message: ");
msg->append(std::string(ex.what()));
log::fatal("{}", *msg);
return msg->c_str();
}
});
// Delete splash window (do it before creating the main window so glfw destroys the window)
delete splashWindow;
emscripten_cancel_main_loop();
// Main window
static Window window;
emscripten_set_main_loop([]() {
window.fullFrame();
}, 60, 0);
}
}, splashWindow, 60, 0);
return -1;
}
#else
int runImHex() {
bool shouldRestart = false;
do {
// Register an event handler that will make ImHex restart when requested
shouldRestart = false;
EventManager::subscribe<RequestRestartImHex>([&] {
shouldRestart = true;
});
initializeImHex();
handleFileOpenRequest();
// Clean up everything after the main window is closed
ON_SCOPE_EXIT {
deinitializeImHex();
};
// Main window
Window window;
window.loop();
} while (shouldRestart);
return EXIT_SUCCESS;
}
#endif
}
/**
@@ -127,27 +230,5 @@ int main(int argc, char **argv) {
ImHexApi::System::impl::setPortableVersion(isPortableVersion());
bool shouldRestart = false;
do {
// Register an event handler that will make ImHex restart when requested
shouldRestart = false;
EventManager::subscribe<RequestRestartImHex>([&] {
shouldRestart = true;
});
initializeImHex();
handleFileOpenRequest();
// Clean up everything after the main window is closed
ON_SCOPE_EXIT {
deinitializeImHex();
};
// Main window
Window window;
window.loop();
} while (shouldRestart);
return EXIT_SUCCESS;
}
return runImHex();
};

View File

@@ -0,0 +1,24 @@
#if defined(OS_WEB)
#include<stdexcept>
#include <hex/helpers/intrinsics.hpp>
#include <hex/helpers/logger.hpp>
#include "messaging.hpp"
namespace hex::messaging {
void sendToOtherInstance(const std::string &eventName, const std::vector<u8> &args) {
hex::unused(eventName);
hex::unused(args);
log::error("Unimplemented function 'sendToOtherInstance()' called");
}
// Not implemented, so lets say we are the main instance every time so events are forwarded to ourselves
bool setupNative() {
return true;
}
}
#endif

View File

@@ -22,13 +22,13 @@ namespace hex::messaging {
// Check if the window is visible and if it's an ImHex window
if (::IsWindowVisible(hWnd) == TRUE && length != 0) {
if (windowName.starts_with("ImHex")) {
// it's our window, return it and stop iteration
// It's our window, return it and stop iteration
*reinterpret_cast<HWND*>(ret) = hWnd;
return FALSE;
}
}
// continue iteration
// Continue iteration
return TRUE;
}, reinterpret_cast<LPARAM>(&imhexWindow));
@@ -69,7 +69,7 @@ namespace hex::messaging {
constexpr static auto UniqueMutexId = "ImHex/a477ea68-e334-4d07-a439-4f159c683763";
// check if an ImHex instance is already running by opening a global mutex
// Check if an ImHex instance is already running by opening a global mutex
HANDLE globalMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, UniqueMutexId);
if (globalMutex == nullptr) {
// If no ImHex instance is running, create a new global mutex

View File

@@ -44,7 +44,7 @@ namespace hex {
executeCmd({"zenity", "--error", "--text", message});
} else if(isFileInPath("notify-send")) {
executeCmd({"notify-send", "-i", "script-error", "Error", message});
} // hopefully one of these commands is installed
} // Hopefully one of these commands is installed
}
void Window::initNative() {

View File

@@ -0,0 +1,71 @@
#include "window.hpp"
#if defined(OS_WEB)
#include <emscripten.h>
#include <emscripten/html5.h>
// Function used by c++ to get the size of the html canvas
EM_JS(int, canvas_get_width, (), {
return Module.canvas.width;
});
// Function used by c++ to get the size of the html canvas
EM_JS(int, canvas_get_height, (), {
return Module.canvas.height;
});
// Function called by javascript
EM_JS(void, resizeCanvas, (), {
js_resizeCanvas();
});
namespace hex {
void nativeErrorMessage(const std::string &message) {
log::fatal(message);
EM_ASM({
alert(UTF8ToString($0));
}, message.c_str());
}
void Window::initNative() {
EM_ASM({
// Save data directory
FS.mkdir("/home/web_user/.local");
FS.mount(IDBFS, {}, '/home/web_user/.local');
FS.syncfs(true, function (err) {
if (!err)
return;
alert("Failed to load permanent file system: "+err);
});
});
}
void Window::setupNativeWindow() {
resizeCanvas();
}
void Window::beginNativeWindowFrame() {
static i32 prevWidth = 0;
static i32 prevHeight = 0;
auto width = canvas_get_width();
auto height = canvas_get_height();
if (prevWidth != width || prevHeight != height) {
// Size has changed
prevWidth = width;
prevHeight = height;
this->resize(width, height);
}
}
void Window::endNativeWindowFrame() {
}
}
#endif

View File

@@ -156,6 +156,8 @@ namespace hex {
}
void Window::fullFrame() {
this->m_lastFrameTime = glfwGetTime();
glfwPollEvents();
// Render frame
@@ -255,7 +257,7 @@ namespace hex {
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabActive));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabHovered));
// custom titlebar buttons implementation for borderless window mode
// Custom titlebar buttons implementation for borderless window mode
auto &titleBarButtons = ContentRegistry::Interface::impl::getTitleBarButtons();
// Draw custom title bar buttons
@@ -812,6 +814,8 @@ namespace hex {
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_SAMPLES, 1);
if (restoreWindowPos) {
int maximized = ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.window.maximized", GLFW_FALSE);
@@ -924,30 +928,32 @@ namespace hex {
win->processEvent();
});
// Register key press callback
glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) {
hex::unused(mods);
#if !defined(OS_WEB)
// Register key press callback
glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) {
hex::unused(mods);
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
if (action == GLFW_RELEASE) {
win->m_buttonDown = false;
} else {
win->m_buttonDown = true;
}
if (action == GLFW_RELEASE) {
win->m_buttonDown = false;
} else {
win->m_buttonDown = true;
}
if (key == GLFW_KEY_UNKNOWN) return;
if (key == GLFW_KEY_UNKNOWN) return;
auto keyName = glfwGetKeyName(key, scancode);
if (keyName != nullptr)
key = std::toupper(keyName[0]);
auto keyName = glfwGetKeyName(key, scancode);
if (keyName != nullptr)
key = std::toupper(keyName[0]);
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
win->m_pressedKeys.push_back(key);
}
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
win->m_pressedKeys.push_back(key);
}
win->processEvent();
});
win->processEvent();
});
#endif
// Register cursor position callback
glfwSetCursorPosCallback(this->m_window, [](GLFWwindow *window, double x, double y) {
@@ -995,6 +1001,10 @@ namespace hex {
glfwShowWindow(this->m_window);
}
void Window::resize(i32 width, i32 height) {
glfwSetWindowSize(this->m_window, width, height);
}
void Window::initImGui() {
IMGUI_CHECKVERSION();
@@ -1094,10 +1104,13 @@ namespace hex {
}
}
ImGui_ImplGlfw_InitForOpenGL(this->m_window, true);
#if defined(OS_MACOS)
ImGui_ImplOpenGL3_Init("#version 150");
#elif defined(OS_WEB)
ImGui_ImplOpenGL3_Init();
#else
ImGui_ImplOpenGL3_Init("#version 130");
#endif