From 24344aa506e0ac5fbd7908d832f93e5502ad0358 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Dec 2022 15:23:32 +0100 Subject: [PATCH 01/14] Web support --- .gitignore | 6 +- CMakeLists.txt | 61 +++- include/MiniFB.h | 9 +- src/web/WebMiniFB.c | 548 ++++++++++++++++++++++++++++++++ tests/hidpi.c | 11 +- tests/input_events.c | 2 +- tests/input_events_cpp.cpp | 2 +- tests/multiple_windows.c | 18 +- tests/noise.c | 2 +- tests/timer.c | 61 ++++ tests/web/hidpi.html | 21 ++ tests/web/index.html | 25 ++ tests/web/input_events.html | 21 ++ tests/web/multiple_windows.html | 21 ++ tests/web/noise.html | 20 ++ tests/web/timer.html | 21 ++ 16 files changed, 825 insertions(+), 24 deletions(-) create mode 100644 src/web/WebMiniFB.c create mode 100644 tests/timer.c create mode 100644 tests/web/hidpi.html create mode 100644 tests/web/index.html create mode 100644 tests/web/input_events.html create mode 100644 tests/web/multiple_windows.html create mode 100644 tests/web/noise.html create mode 100644 tests/web/timer.html diff --git a/.gitignore b/.gitignore index dca0aad..e7e82ec 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,8 @@ t2-output .cxx build kk -vc \ No newline at end of file +vc +cmake-build-debug +cmake-build-debug-emscripten +cmake-build-release +cmake-build-release-emscripten diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e58c07..c6fa4d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,11 @@ set(SrcGL src/gl/MiniFB_GL.c ) +#-- +set(SrcWeb + src/web/WebMiniFB.c +) + # Avoid RelWithDebInfo and MinSizeRel #-------------------------------------- set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) @@ -110,9 +115,11 @@ if(APPLE AND NOT IOS) option(USE_METAL_API "Build the project using metal API code" ON) option(USE_INVERTED_Y_ON_MACOS "Use default mouse position: (0, 0) at (left, down)" OFF) elseif(UNIX) - option(USE_WAYLAND_API "Build the project using wayland API code" OFF) - if(NOT USE_WAYLAND_API) - option(USE_OPENGL_API "Build the project using OpenGL API code" ON) + if (NOT EMSCRIPTEN) + option(USE_WAYLAND_API "Build the project using wayland API code" OFF) + if(NOT USE_WAYLAND_API) + option(USE_OPENGL_API "Build the project using OpenGL API code" ON) + endif() endif() elseif(WIN32) option(USE_OPENGL_API "Build the project using OpenGL API code" ON) @@ -183,6 +190,9 @@ endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_definitions(-D_DEBUG) add_definitions(-DDEBUG) + if(EMSCRIPTEN) + add_link_options(-g) + endif() endif() # Set compiler/platform specific flags and dependencies @@ -222,6 +232,8 @@ elseif(UNIX) if(USE_WAYLAND_API) list(APPEND SrcLib ${SrcWayland}) + elseif(EMSCRIPTEN) + list(APPEND SrcLib ${SrcWeb}) else() if(USE_OPENGL_API) list(APPEND SrcLib ${SrcGL}) @@ -266,6 +278,27 @@ elseif(UNIX) "-lwayland-client" "-lwayland-cursor" ) + elseif(EMSCRIPTEN) + add_compile_options( + "-DEMSCRIPTEN" + "-sSTRICT=1" + "--no-entry" + ) + add_link_options( + "-sSTRICT=1" + "-sENVIRONMENT=web" + "-sLLD_REPORT_UNDEFINED" + "-sMODULARIZE=1" + "-sALLOW_MEMORY_GROWTH=1" + "-sALLOW_TABLE_GROWTH" + "-sMALLOC=emmalloc" + "-sEXPORT_ALL=1" + "-sEXPORTED_FUNCTIONS=[\"_malloc\",\"_free\",\"_main\"]" + "-sEXPORTED_RUNTIME_METHODS=ccall,cwrap" + "-sASYNCIFY" + "-sASYNCIFY_IMPORTS=[\"setup_web_mfb\"]" + "--no-entry" + ) else() target_link_libraries(minifb "-lX11" @@ -329,6 +362,28 @@ if(MINIFB_BUILD_EXAMPLES) tests/fullscreen.c ) + add_executable(timer + tests/timer.c + ) + + if(EMSCRIPTEN) + add_custom_target(web_assets + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/tests/web + ${CMAKE_CURRENT_BINARY_DIR} + ) + add_dependencies(noise web_assets) + target_link_options(noise PRIVATE "-sEXPORT_NAME=noise") + add_dependencies(input_events web_assets) + target_link_options(input_events PRIVATE "-sEXPORT_NAME=input_events") + add_dependencies(hidpi web_assets) + target_link_options(hidpi PRIVATE "-sEXPORT_NAME=hidpi") + add_dependencies(multiple_windows web_assets) + target_link_options(multiple_windows PRIVATE "-sEXPORT_NAME=multiple_windows") + add_dependencies(timer web_assets) + target_link_options(timer PRIVATE "-sEXPORT_NAME=timer") + endif() + else() add_executable(noise diff --git a/include/MiniFB.h b/include/MiniFB.h index 62ea2ba..c11e9b1 100644 --- a/include/MiniFB.h +++ b/include/MiniFB.h @@ -9,12 +9,15 @@ extern "C" { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #ifndef __ANDROID__ -#define MFB_RGB(r, g, b) (((uint32_t) r) << 16) | (((uint32_t) g) << 8) | ((uint32_t) b) +#define MFB_RGB(r, g, b) (((uint32_t) r) << 16) | (((uint32_t) g) << 8) | ((uint32_t) b) +#define MFB_ARGB(a, r, g, b) (((uint32_t) a) << 24) | (((uint32_t) r) << 16) | (((uint32_t) g) << 8) | ((uint32_t) b) #else #ifdef HOST_WORDS_BIGENDIAN - #define MFB_RGB(r, g, b) (((uint32_t) r) << 16) | (((uint32_t) g) << 8) | ((uint32_t) b) + #define MFB_RGB(r, g, b) (((uint32_t) r) << 16) | (((uint32_t) g) << 8) | ((uint32_t) b) + #define MFB_ARGB(a, r, g, b) (((uint32_t) a) << 24) | (((uint32_t) r) << 16) | (((uint32_t) g) << 8) | ((uint32_t) b) #else - #define MFB_RGB(r, g, b) (((uint32_t) b) << 16) | (((uint32_t) g) << 8) | ((uint32_t) r) + #define MFB_ARGB(r, g, b) (((uint32_t) a) << 24) | (((uint32_t) b) << 16) | (((uint32_t) g) << 8) | ((uint32_t) r) + #define MFB_RGB(r, g, b) (((uint32_t) b) << 16) | (((uint32_t) g) << 8) | ((uint32_t) r) #endif #endif diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c new file mode 100644 index 0000000..0c1a723 --- /dev/null +++ b/src/web/WebMiniFB.c @@ -0,0 +1,548 @@ +#include +#include +#include +#include +#include +#include +#include + +#define EM_EXPORT __attribute__((used)) + +static bool g_initialized = false; + +EM_JS(void, setup_web_mfb, (), { + // Use requestAnimationFrame instead of setTimeout for async processing. + Asyncify.handleSleep(wakeUp => { + requestAnimationFrame(wakeUp); + }); + + window._minifb.keyMap = { + "Space": 32, + "Quote": 39, + "Comma": 44, + "Minus": 45, + "Period": 46, + "Slash": 47, + "Digit0": 48, + "Digit1": 49, + "Digit2": 50, + "Digit3": 51, + "Digit4": 52, + "Digit5": 53, + "Digit6": 54, + "Digit7": 55, + "Digit8": 56, + "Digit9": 57, + "Semicolon": 59, + "Equal": 61, + "NumpadEqual": 61, + "KeyA": 65, + "KeyB": 66, + "KeyC": 67, + "KeyD": 68, + "KeyE": 69, + "KeyF": 70, + "KeyG": 71, + "KeyH": 72, + "KeyI": 73, + "KeyJ": 74, + "KeyK": 75, + "KeyL": 76, + "KeyM": 77, + "KeyN": 78, + "KeyO": 79, + "KeyP": 80, + "KeyQ": 81, + "KeyR": 82, + "KeyS": 83, + "KeyT": 84, + "KeyU": 85, + "KeyV": 86, + "KeyW": 87, + "KeyX": 88, + "KeyY": 89, + "KeyZ": 90, + "BracketLeft": 91, + "Backslash": 92, + "BracketRight": 93, + "Backquote": 96, + + "Escape": 256, + "Enter": 257, + "Tab": 258, + "Backspace": 259, + "Insert": 260, + "Delete": 261, + "ArrowRight": 262, + "ArrowLeft": 263, + "ArrowDown": 264, + "ArrowUp": 265, + "PageUp": 266, + "PageDown": 267, + "Home": 268, + "End": 269, + "CapsLock": 280, + "ScrollLock": 281, + "NumLock": 282, + "PrintScreen": 283, + "Pause": 284, + "F1": 290, + "F2": 291, + "F3": 292, + "F4": 293, + "F5": 294, + "F6": 295, + "F7": 296, + "F8": 297, + "F9": 298, + "F10": 299, + "F11": 300, + "F12": 301, + "F13": 302, + "F14": 303, + "F15": 304, + "F16": 305, + "F17": 306, + "F18": 307, + "F19": 308, + "F20": 309, + "F21": 310, + "F22": 311, + "F23": 312, + "F24": 313, + "F25": 314, + "Numpad0": 320, + "Numpad1": 321, + "Numpad2": 322, + "Numpad3": 323, + "Numpad4": 324, + "Numpad5": 325, + "Numpad6": 326, + "Numpad7": 327, + "Numpad8": 328, + "Numpad9": 329, + "NumpadComma": 330, + "NumpadDivide": 331, + "NumpadMultiply": 332, + "NumpadSubtract": 333, + "NumpadAdd": 334, + "NumpadEnter": 335, + "NumpadEqual": 336, + "ShiftLeft": 340, + "ControlLeft": 341, + "AltLeft": 342, + "MetaLeft": 343, + "ShiftRight": 344, + "ControlRight": 345, + "AltRight": 346, + "MetaRight": 347, + "ContextMenu": 348 + }; +}) + +EM_EXPORT void reverse_color_channels(uint8_t *src, uint8_t *dst, int width, int height) { + int32_t numPixels = (width * height) << 2; + for (int i = 0; i < numPixels; i += 4) { + uint8_t b = src[i]; + uint8_t g = src[i + 1]; + uint8_t r = src[i + 2]; + uint8_t a = src[i + 3]; + dst[i] = r; + dst[i + 1] = g; + dst[i + 2] = b; + dst[i + 3] = a; + } +} + +EM_EXPORT void window_data_set_mouse_pos(SWindowData *windowData, int x, int y) { + if (!windowData) return; + windowData->mouse_pos_x = x; + windowData->mouse_pos_y = y; +} + +EM_EXPORT void window_data_set_mouse_wheel(SWindowData *windowData, float x, float y) { + if (!windowData) return; + windowData->mouse_wheel_x = x; + windowData->mouse_wheel_y = y; +} + +EM_EXPORT void window_data_set_mouse_button(SWindowData *windowData, uint8_t button, bool is_pressed) { + if (!windowData) return; + if (button > 7) return; + windowData->mouse_button_status[button] = is_pressed; +} + +EM_EXPORT void window_data_set_key(SWindowData *windowData, unsigned key, bool is_pressed) { + if (!windowData) return; + if (key > 512) return; + windowData->key_status[key] = is_pressed; +} + +EM_EXPORT void window_data_set_mod_keys(SWindowData *windowData, uint32_t mod) { + if (!windowData) return; + windowData->mod_keys = mod; +} + +EM_EXPORT void *window_data_get_specific(SWindowData *windowData) { + if (!windowData) return 0; + return windowData->specific; +} + +EM_EXPORT void window_data_call_active_func(SWindowData *windowData, bool is_active) { + if (windowData == 0x0) return; + if (windowData->active_func) windowData->active_func((struct mfb_window*)windowData, is_active); +} + +EM_EXPORT void window_data_call_resize_func(SWindowData *windowData, int width, int height) { + if (windowData == 0x0) return; + if (windowData->resize_func) windowData->resize_func((struct mfb_window*)windowData, width, height); +} + +EM_EXPORT void window_data_call_close_func(SWindowData *windowData) { + if (windowData == 0x0) return; + if(windowData->close_func) windowData->close_func((struct mfb_window*)windowData); +} + +EM_EXPORT void window_data_call_keyboard_func(SWindowData *windowData, mfb_key key, mfb_key_mod mod, bool is_pressed) { + if (windowData == 0x0) return; + if (windowData->keyboard_func) windowData->keyboard_func((struct mfb_window*)windowData, key, mod, is_pressed); +} + +EM_EXPORT void window_data_call_char_input_func(SWindowData *windowData, unsigned int code) { + if (windowData == 0x0) return; + if(windowData->char_input_func) windowData->char_input_func((struct mfb_window*)windowData, code); +} + +EM_EXPORT void window_data_call_mouse_btn_func(SWindowData *windowData, mfb_mouse_button button, mfb_key_mod mod, bool is_pressed) { + if (windowData == 0x0) return; + if (windowData->mouse_btn_func) windowData->mouse_btn_func((struct mfb_window*)windowData, button, mod, is_pressed); +} + +EM_EXPORT void window_data_call_mouse_move_func(SWindowData *windowData, int x, int y) { + if (windowData == 0x0) return; + if (windowData->mouse_move_func) windowData->mouse_move_func((struct mfb_window*)windowData, x, y); +} + +EM_EXPORT void window_data_call_mouse_wheel_func(SWindowData *windowData, mfb_key_mod mod, float x, float y) { + if (windowData == 0x0) return; + if (windowData->mouse_wheel_func) windowData->mouse_wheel_func((struct mfb_window*)windowData, mod, x, y); +} + +EM_EXPORT bool window_data_get_close(SWindowData *windowData) { + return windowData->close; +} + +EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigned width, unsigned height, unsigned flags), { + let canvas = document.getElementById(UTF8ToString(title)); + if (!canvas) return 0; + + if (!window._minifb) { + window._minifb = { + nextId: 1, + windows: [], + }; + } + + let id = window._minifb.nextId++; + canvas.width = width; + canvas.height = height; + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; + canvas.style["image-rendering"] = "pixelated"; + canvas.style["user-select"] = "none"; + canvas.style["border"] = "none"; + canvas.style["outline-style"] = "none"; + canvas.tabIndex = -1; + + let w = { + id: id, + canvas: canvas, + windowData: windowData, + activeTouchId: null, + events: [ + { type: "active" } + ] + }; + + function toMfbCode(code) { + return window._minifb.keyMap[code] ? window._minifb.keyMap[code] : -1; + } + + + function getMousePos(event) { + var rect = canvas.getBoundingClientRect(); + return { x: event.clientX - rect.left, y: event.clientY - rect.top }; + }; + + function getMfbKeyModFromEvent(event) { + // FIXME can we make these global somehow? --pre-js maybe? + // FIXME need to lookup caps and num lock keystates in windowData->key_status + const KB_MOD_SHIFT = 0x0001; + const KB_MOD_CONTROL = 0x0002; + const KB_MOD_ALT = 0x0004; + const KB_MOD_SUPER = 0x0008; + + let mod = 0; + if (event.shiftKey) mod = mod | KB_MOD_SHIFT; + if (event.ctrlKey) mod = mod | KB_MOD_CONTROL; + if (event.altKey) mod = mod | KB_MOD_ALT; + if (event.metaKey) mod = mod | KB_MOD_SUPER; + return mod; + }; + + canvas.addEventListener("keydown", (event) => { + let code = toMfbCode(event.code); + Module._window_data_set_key(windowData, code, 1); + let mod = getMfbKeyModFromEvent(event); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "keydown", code: code, mod: mod }); + }); + + canvas.addEventListener("keyup", (event) => { + let code = toMfbCode(event.code); + Module._window_data_set_key(windowData, code, 0); + let mod = getMfbKeyModFromEvent(event); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "keydown", code: code, mod: mod }); + }); + + canvas.addEventListener("mousedown", (event) => { + if (event.button > 8) return; + let pos = getMousePos(event); + let mod = getMfbKeyModFromEvent(event); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + Module._window_data_set_mouse_button(windowData, event.button, 1); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousebutton", button: event.button, mod: mod, isPressed: true}); + }, false); + + canvas.addEventListener("mousemove", (event) => { + let pos = getMousePos(event); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + w.events.push({ type: "mousemove", x: pos.x, y: pos.y}); + }, false); + + canvas.addEventListener("mouseup", (event) => { + if (event.button > 8) return; + let pos = getMousePos(event); + let mod = getMfbKeyModFromEvent(event); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + Module._window_data_set_mouse_button(windowData, event.button, 0); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousebutton", button: event.button, mod: mod, isPressed: false}); + }, false); + + document.body.addEventListener("mouseup", (event) => { + if (event.button > 8) return; + let pos = getMousePos(event); + let mod = getMfbKeyModFromEvent(event); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + Module._window_data_set_mouse_button(windowData, event.button, 0); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousebutton", button: event.button, mod: mod, isPressed: false}); + }, false); + + canvas.addEventListener('wheel', (event) => { + event.preventDefault(); + let mod = getMfbKeyModFromEvent(event); + Module._window_data_set_mouse_wheel(windowData, event.deltaX, event.deltaY); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousescroll", mod: mod, x: event.deltaX, y: event.deltaY}); + }, false); + + function getTouchPos(event) { + var rect = canvas.getBoundingClientRect(); + return { x: event.clientX - rect.left, y: event.clientY - rect.top }; + }; + + canvas.addEventListener("touchstart", (event) => { + if (!w.activeTouchId) { + let touch = event.changedTouches[0]; + let pos = getTouchPos(touch); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + Module._window_data_set_mouse_button(windowData, 0, 1); + w.activeTouchId = touch.identifier; + w.events.push({ type: "mousebutton", button: 0, mod: 0, isPressed: true}); + } + event.preventDefault(); + }, false); + + canvas.addEventListener("touchmove", (event) => { + if (w.activeTouchId != null) { + for (let i = 0; i < event.changedTouches.length; i++) { + let touch = event.changedTouches[i]; + if (w.activeTouchId === touch.identifier) { + let pos = getTouchPos(touch); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + w.events.push({ type: "mousemove", x: pos.x, y: pos.y}); + break; + } + } + } + event.preventDefault(); + }, false); + + function touchEndOrCancel(event) { + if (w.activeTouchId != null) { + for (let i = 0; i < event.changedTouches.length; i++) { + let touch = event.changedTouches[i]; + if (w.activeTouchId === touch.identifier) { + let pos = getTouchPos(touch); + Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); + Module._window_data_set_mouse_button(windowData, 0, 0); + w.activeTouchId = null; + w.events.push({ type: "mousebutton", button: 0, mod: 0, isPressed: false}); + break; + } + } + } + event.preventDefault(); + } + canvas.addEventListener("touchend", touchEndOrCancel, false); + canvas.addEventListener("touchcancel", touchEndOrCancel, false); + + window._minifb.windows[id] = w; + return id; +}); + +struct mfb_window *mfb_open_ex(const char *title, unsigned width, unsigned height, unsigned flags) { + SWindowData *window_data; + + window_data = malloc(sizeof(SWindowData)); + if(window_data == 0x0) { + printf("Cannot allocate window data\n"); + return 0x0; + } + memset(window_data, 0, sizeof(SWindowData)); + + void *specific = mfb_open_ex_js(window_data, title, width, height, 0); + if (!specific) { + printf("Cannot allocate JavaScript window data\n"); + return 0x0; + } + window_data->specific = specific; + + // setup key map if not initialized yet + if (!g_initialized) { + setup_web_mfb(); + g_initialized = true; + } + + mfb_set_keyboard_callback((struct mfb_window *) window_data, keyboard_default); + + window_data->is_active = true; + window_data->is_initialized = true; + + return (struct mfb_window*)window_data; +} + +bool mfb_set_viewport(struct mfb_window *window, unsigned offset_x, unsigned offset_y, unsigned width, unsigned height) { + return false; +} + +EM_JS(mfb_update_state, mfb_update_events_js, (SWindowData * windowData), { + // FIXME can we make these global somehow? --pre-js maybe? + const STATE_OK = 0; + const STATE_EXIT = -1; + const STATE_INVALID_WINDOW = -2; + const STATE_INVALID_BUFFER = -3; + const STATE_INTERNAL_ERROR = -4; + if (windowData == 0) return STATE_INVALID_WINDOW; + let windowId = Module._window_data_get_specific(windowData); + if (!window._minifb) return STATE_INTERNAL_ERROR; + if (!window._minifb.windows[windowId]) return STATE_INVALID_WINDOW; + let w = window._minifb.windows[windowId]; + let events = w.events; + w.events = []; + for (let i = 0; i < events.length; i++) { + let event = events[i]; + if (event.type == "active") { + Module._window_data_call_active_func(windowData, 1); + } else if (event.type == "mousebutton") { + Module._window_data_call_mouse_btn_func(windowData, event.button, event.mod, event.isPressed ? 1 : 0); + } else if (event.type == "mousemove") { + Module._window_data_call_mouse_move_func(windowData, event.x, event.y); + } else if (event.type == "mousescroll") { + Module._window_data_call_mouse_wheel_func(windowData, event.mod, event.x, event.y); + } else if (event.type == "keydown") { + Module._window_data_call_keyboard_func(windowData, event.code, event.mod, 1); + } else if (event.type == "keyup") { + Module._window_data_call_keyboard_func(windowData, event.code, event.mod, 0); + } + } + return Module._window_data_get_close(windowData); +}); + +mfb_update_state mfb_update_events(struct mfb_window *window) { + if (window == 0x0) return STATE_INVALID_WINDOW; + return mfb_update_events_js((SWindowData *)window); +} + +EM_JS(mfb_update_state, mfb_update_js, (struct mfb_window * windowData, void *buffer, int width, int height), { + // FIXME can we make these global somehow? preamble.js maybe? + const STATE_OK = 0; + const STATE_EXIT = -1; + const STATE_INVALID_WINDOW = -2; + const STATE_INVALID_BUFFER = -3; + const STATE_INTERNAL_ERROR = -4; + if (windowData == 0) return STATE_INVALID_WINDOW; + let windowId = Module._window_data_get_specific(windowData); + if (!window._minifb) return STATE_INTERNAL_ERROR; + if (!window._minifb.windows[windowId]) return STATE_INVALID_WINDOW; + if (buffer == 0) return STATE_INVALID_BUFFER; + let w = window._minifb.windows[windowId]; + let canvas = w.canvas; + if (width <= 0) { + width = canvas.width; + height = canvas.height; + } else { + if (canvas.width != width) canvas.width = width; + if (canvas.height != height) canvas.height = height; + } + Module._reverse_color_channels(buffer, buffer, width, height); + let framePixels = new Uint8ClampedArray(HEAPU8.buffer, buffer, width * height * 4); + let imageData = new ImageData(framePixels, width, height); + canvas.getContext("2d").putImageData(imageData, 0, 0); + return Module._window_data_get_close(windowData); +}); + +mfb_update_state mfb_update_ex(struct mfb_window *window, void *buffer, unsigned width, unsigned height) { + mfb_update_state state = mfb_update_js(window, buffer, width, height); + if (state != STATE_OK) return state; + state = mfb_update_events_js((SWindowData *)window); + return state; +} + +EM_JS(void, emscripten_sleep_using_raf, (), { + Asyncify.handleSleep(wakeUp => { + requestAnimationFrame(wakeUp); + }); +}); + +bool mfb_wait_sync(struct mfb_window *window) { + emscripten_sleep(0); + return true; +} + +void mfb_get_monitor_scale(struct mfb_window *window, float *scale_x, float *scale_y) { + if (!window) return; + if (scale_x) *scale_x = 1.0f; + if (scale_y) *scale_y = 1.0f; +} + +extern double g_timer_frequency; +extern double g_timer_resolution; + +void mfb_timer_init(void) { + g_timer_frequency = 1e+9; + g_timer_resolution = 1.0 / g_timer_frequency; +} + +EM_JS(double, mfb_timer_tick_js, (), { + return performance.now(); +}); + +uint64_t mfb_timer_tick(void) { + uint64_t now = (uint64_t)(mfb_timer_tick_js() * 1e+6); + return now; +} diff --git a/tests/hidpi.c b/tests/hidpi.c index 4053a89..018b657 100644 --- a/tests/hidpi.c +++ b/tests/hidpi.c @@ -17,7 +17,7 @@ pretty_square(unsigned int *p, int dimen) { const int three_quarter_dimen = one_half_dimen + one_quarter_dimen; for (int x = one_quarter_dimen; x < three_quarter_dimen; x++) for (int y = one_quarter_dimen; y < three_quarter_dimen; y++) - p[y * dimen + x] = (x & 1) ? MFB_RGB(223, 0, (255 * (x - one_quarter_dimen)) / one_half_dimen) : MFB_RGB(0, 0, 0); + p[y * dimen + x] = (x & 1) ? MFB_ARGB(0xff, 223, 0, (255 * (x - one_quarter_dimen)) / one_half_dimen) : MFB_ARGB(0xff, 0, 0, 0); } int @@ -30,14 +30,15 @@ main() { while (window_high || window_low) { if (window_low) - if (mfb_update_ex(window_low, g_buffer_low, DIMEN_LOW, DIMEN_LOW) != STATE_OK - || !mfb_wait_sync(window_low)) + if (mfb_update_ex(window_low, g_buffer_low, DIMEN_LOW, DIMEN_LOW) != STATE_OK) window_low = NULL; if (window_high) - if (mfb_update_ex(window_high, g_buffer_high, DIMEN_HIGH, DIMEN_HIGH) != STATE_OK - || !mfb_wait_sync(window_high)) + if (mfb_update_ex(window_high, g_buffer_high, DIMEN_HIGH, DIMEN_HIGH) != STATE_OK) window_high = NULL; + + if (window_high) mfb_wait_sync(window_high); + else if(window_low) mfb_wait_sync(window_low); } return 0; diff --git a/tests/input_events.c b/tests/input_events.c index acb7315..4c347d8 100644 --- a/tests/input_events.c +++ b/tests/input_events.c @@ -145,7 +145,7 @@ main() seed >>= 1; seed |= (carry << 30); noise &= 0xFF; - g_buffer[i] = MFB_RGB(noise, noise, noise); + g_buffer[i] = MFB_ARGB(0xff, noise, noise, noise); } state = mfb_update(window, g_buffer); diff --git a/tests/input_events_cpp.cpp b/tests/input_events_cpp.cpp index b78a1cf..08589b1 100644 --- a/tests/input_events_cpp.cpp +++ b/tests/input_events_cpp.cpp @@ -248,7 +248,7 @@ main() seed >>= 1; seed |= (carry << 30); noise &= 0xFF; - g_buffer[i] = MFB_RGB(noise, noise, noise); + g_buffer[i] = MFB_ARGB(0xff, noise, noise, noise); } state = mfb_update(window, g_buffer); diff --git a/tests/multiple_windows.c b/tests/multiple_windows.c index caab598..eb0874f 100644 --- a/tests/multiple_windows.c +++ b/tests/multiple_windows.c @@ -129,14 +129,14 @@ main() float inc = 90.0f / 64.0f; for(uint32_t c=0; c<64; ++c) { int32_t col = (int32_t) ((255.0f * sinf(c * inc * kPI / 180.0f)) + 0.5f); - pallete[64*0 + c] = MFB_RGB(col, 0, 0); - pallete[64*1 + c] = MFB_RGB(255, col, 0); - pallete[64*2 + c] = MFB_RGB(255-col, 255, 0); - pallete[64*3 + c] = MFB_RGB(0, 255, col); - pallete[64*4 + c] = MFB_RGB(0, 255-col, 255); - pallete[64*5 + c] = MFB_RGB(col, 0, 255); - pallete[64*6 + c] = MFB_RGB(255, 0, 255-col); - pallete[64*7 + c] = MFB_RGB(255-col, 0, 0); + pallete[64*0 + c] = MFB_ARGB(255, col, 0, 0); + pallete[64*1 + c] = MFB_ARGB(255, 255, col, 0); + pallete[64*2 + c] = MFB_ARGB(255, 255-col, 255, 0); + pallete[64*3 + c] = MFB_ARGB(255, 0, 255, col); + pallete[64*4 + c] = MFB_ARGB(255, 0, 255-col, 255); + pallete[64*5 + c] = MFB_ARGB(255, col, 0, 255); + pallete[64*6 + c] = MFB_ARGB(255, 255, 0, 255-col); + pallete[64*7 + c] = MFB_ARGB(255, 255-col, 0, 0); } mfb_set_target_fps(10); @@ -162,7 +162,7 @@ main() seed >>= 1; seed |= (carry << 30); noise &= 0xFF; - g_buffer_a[i] = MFB_RGB(noise, noise, noise); + g_buffer_a[i] = MFB_ARGB(255, noise, noise, noise); } //-- diff --git a/tests/noise.c b/tests/noise.c index 573f194..27f6194 100644 --- a/tests/noise.c +++ b/tests/noise.c @@ -45,7 +45,7 @@ main() seed >>= 1; seed |= (carry << 30); noise &= 0xFF; - g_buffer[i] = MFB_RGB(noise, noise, noise); + g_buffer[i] = MFB_ARGB(0xff, noise, noise, noise); } state = mfb_update_ex(window, g_buffer, g_width, g_height); diff --git a/tests/timer.c b/tests/timer.c new file mode 100644 index 0000000..19afeae --- /dev/null +++ b/tests/timer.c @@ -0,0 +1,61 @@ +#include +#include +#include +#include + +static uint32_t g_width = 800; +static uint32_t g_height = 600; +static uint32_t *g_buffer = 0x0; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void +resize(struct mfb_window *window, int width, int height) { + (void) window; + g_width = width; + g_height = height; + g_buffer = realloc(g_buffer, g_width * g_height * 4); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +int +main() +{ + uint32_t i, noise, carry, seed = 0xbeef; + + struct mfb_window *window = mfb_open_ex("Timer Test", g_width, g_height, WF_RESIZABLE); + if (!window) + return 0; + + g_buffer = (uint32_t *) malloc(g_width * g_height * 4); + mfb_set_resize_callback(window, resize); + + mfb_set_viewport(window, 50, 50, g_width - 50 - 50, g_height - 50 - 50); + resize(window, g_width - 100, g_height - 100); // to resize buffer + + struct mfb_timer *timer = mfb_timer_create(); + mfb_update_state state; + do { + mfb_timer_now(timer); + for (i = 0; i < g_width * g_height; ++i) { + noise = seed; + noise >>= 3; + noise ^= seed; + carry = noise & 1; + noise >>= 1; + seed >>= 1; + seed |= (carry << 30); + noise &= 0xFF; + g_buffer[i] = MFB_ARGB(0xff, noise, noise, noise); + } + state = mfb_update_ex(window, g_buffer, g_width, g_height); + if (state != STATE_OK) { + window = 0x0; + break; + } + printf("frame time: %f\n", mfb_timer_delta(timer)); + } while(mfb_wait_sync(window)); + + return 0; +} diff --git a/tests/web/hidpi.html b/tests/web/hidpi.html new file mode 100644 index 0000000..d1d50af --- /dev/null +++ b/tests/web/hidpi.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+

hidpi

+ + +
+ + + \ No newline at end of file diff --git a/tests/web/index.html b/tests/web/index.html new file mode 100644 index 0000000..7cd4a13 --- /dev/null +++ b/tests/web/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + +
+

MiniFB Web Tests

+ hidpi + input events + multiple windows + noise + timer +
+ + \ No newline at end of file diff --git a/tests/web/input_events.html b/tests/web/input_events.html new file mode 100644 index 0000000..3526a7f --- /dev/null +++ b/tests/web/input_events.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+

Input Events Test

+

Open the development console to see events.

+ +
+ + + \ No newline at end of file diff --git a/tests/web/multiple_windows.html b/tests/web/multiple_windows.html new file mode 100644 index 0000000..50deccc --- /dev/null +++ b/tests/web/multiple_windows.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+

Multiple Windows Test

+ + +
+ + + \ No newline at end of file diff --git a/tests/web/noise.html b/tests/web/noise.html new file mode 100644 index 0000000..5f3843c --- /dev/null +++ b/tests/web/noise.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+

Noise

+ +
+ + + \ No newline at end of file diff --git a/tests/web/timer.html b/tests/web/timer.html new file mode 100644 index 0000000..da847e0 --- /dev/null +++ b/tests/web/timer.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+

Timer

+

Open the development console to see log output.

+ +
+ + + \ No newline at end of file From 6dfa434004b25ad8f5db88db2a195ebc2a09b2df Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Dec 2022 15:30:24 +0100 Subject: [PATCH 02/14] Full prototype for mfb_get_target_fps. --- include/MiniFB.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/MiniFB.h b/include/MiniFB.h index c11e9b1..59e1c4b 100644 --- a/include/MiniFB.h +++ b/include/MiniFB.h @@ -81,7 +81,7 @@ const uint8_t * mfb_get_key_buffer(struct mfb_window *window); // O // FPS void mfb_set_target_fps(uint32_t fps); -unsigned mfb_get_target_fps(); +unsigned mfb_get_target_fps(void); bool mfb_wait_sync(struct mfb_window *window); // Timer From d8d68d7b9aad8950e1ee7ca9b13dce550d3aa517 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Dec 2022 15:51:26 +0100 Subject: [PATCH 03/14] Use ES_ASYNC_JS for setup_web_mfb to be able to remove compile and link options. --- CMakeLists.txt | 6 ------ src/web/WebMiniFB.c | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c6fa4d9..0bc04c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,11 +279,6 @@ elseif(UNIX) "-lwayland-cursor" ) elseif(EMSCRIPTEN) - add_compile_options( - "-DEMSCRIPTEN" - "-sSTRICT=1" - "--no-entry" - ) add_link_options( "-sSTRICT=1" "-sENVIRONMENT=web" @@ -296,7 +291,6 @@ elseif(UNIX) "-sEXPORTED_FUNCTIONS=[\"_malloc\",\"_free\",\"_main\"]" "-sEXPORTED_RUNTIME_METHODS=ccall,cwrap" "-sASYNCIFY" - "-sASYNCIFY_IMPORTS=[\"setup_web_mfb\"]" "--no-entry" ) else() diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index 0c1a723..b3664f4 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -10,7 +10,7 @@ static bool g_initialized = false; -EM_JS(void, setup_web_mfb, (), { +EM_ASYNC_JS(void, setup_web_mfb, (), { // Use requestAnimationFrame instead of setTimeout for async processing. Asyncify.handleSleep(wakeUp => { requestAnimationFrame(wakeUp); From 8d08d065fd0345894d2f320302f6c05568bc0a09 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Dec 2022 23:49:08 +0100 Subject: [PATCH 04/14] Reverse color channels after drawing to canvas. --- src/web/WebMiniFB.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index b3664f4..73bf664 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -503,6 +503,7 @@ EM_JS(mfb_update_state, mfb_update_js, (struct mfb_window * windowData, void *bu let framePixels = new Uint8ClampedArray(HEAPU8.buffer, buffer, width * height * 4); let imageData = new ImageData(framePixels, width, height); canvas.getContext("2d").putImageData(imageData, 0, 0); + Module._reverse_color_channels(buffer, buffer, width, height); return Module._window_data_get_close(windowData); }); From c126b823e0713d4f1ca7478578a6e0d606ef23d2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 3 Dec 2022 01:42:24 +0100 Subject: [PATCH 05/14] Fix mouse button indexing. --- src/web/WebMiniFB.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index 73bf664..c415ab8 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -311,9 +311,9 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne let pos = getMousePos(event); let mod = getMfbKeyModFromEvent(event); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); - Module._window_data_set_mouse_button(windowData, event.button, 1); + Module._window_data_set_mouse_button(windowData, event.button + 1, 1); Module._window_data_set_mod_keys(windowData, mod); - w.events.push({ type: "mousebutton", button: event.button, mod: mod, isPressed: true}); + w.events.push({ type: "mousebutton", button: event.button + 1, mod: mod, isPressed: true}); }, false); canvas.addEventListener("mousemove", (event) => { @@ -327,9 +327,9 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne let pos = getMousePos(event); let mod = getMfbKeyModFromEvent(event); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); - Module._window_data_set_mouse_button(windowData, event.button, 0); + Module._window_data_set_mouse_button(windowData, event.button + 1, 0); Module._window_data_set_mod_keys(windowData, mod); - w.events.push({ type: "mousebutton", button: event.button, mod: mod, isPressed: false}); + w.events.push({ type: "mousebutton", button: event.button + 1, mod: mod, isPressed: false}); }, false); document.body.addEventListener("mouseup", (event) => { @@ -337,9 +337,9 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne let pos = getMousePos(event); let mod = getMfbKeyModFromEvent(event); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); - Module._window_data_set_mouse_button(windowData, event.button, 0); + Module._window_data_set_mouse_button(windowData, event.button + 1, 0); Module._window_data_set_mod_keys(windowData, mod); - w.events.push({ type: "mousebutton", button: event.button, mod: mod, isPressed: false}); + w.events.push({ type: "mousebutton", button: event.button + 1, mod: mod, isPressed: false}); }, false); canvas.addEventListener('wheel', (event) => { @@ -360,9 +360,9 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne let touch = event.changedTouches[0]; let pos = getTouchPos(touch); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); - Module._window_data_set_mouse_button(windowData, 0, 1); + Module._window_data_set_mouse_button(windowData, 1, 1); w.activeTouchId = touch.identifier; - w.events.push({ type: "mousebutton", button: 0, mod: 0, isPressed: true}); + w.events.push({ type: "mousebutton", button: 1, mod: 0, isPressed: true}); } event.preventDefault(); }, false); @@ -389,9 +389,9 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne if (w.activeTouchId === touch.identifier) { let pos = getTouchPos(touch); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); - Module._window_data_set_mouse_button(windowData, 0, 0); + Module._window_data_set_mouse_button(windowData, 1, 0); w.activeTouchId = null; - w.events.push({ type: "mousebutton", button: 0, mod: 0, isPressed: false}); + w.events.push({ type: "mousebutton", button: 1, mod: 0, isPressed: false}); break; } } From 6415ed221da18c45cb454958134f9986f6eea669 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 4 Dec 2022 00:32:05 +0100 Subject: [PATCH 06/14] Take client width/height into account when deriving mouse positions. --- src/web/WebMiniFB.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index c415ab8..579b27a 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -270,8 +270,11 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne function getMousePos(event) { - var rect = canvas.getBoundingClientRect(); - return { x: event.clientX - rect.left, y: event.clientY - rect.top }; + let rect = canvas.getBoundingClientRect(); + let pos = { x: event.clientX - rect.left, y: event.clientY - rect.top }; + pos.x = pos.x / canvas.clientWidth * canvas.width; + pos.y = pos.y / canvas.clientHeight * canvas.height; + return pos; }; function getMfbKeyModFromEvent(event) { From ed48ad9aa1d021abd11461445b9f60b14306a3d2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 4 Dec 2022 19:57:12 +0100 Subject: [PATCH 07/14] Fix touch position handling. --- src/web/WebMiniFB.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index 579b27a..509ea03 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -353,15 +353,10 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne w.events.push({ type: "mousescroll", mod: mod, x: event.deltaX, y: event.deltaY}); }, false); - function getTouchPos(event) { - var rect = canvas.getBoundingClientRect(); - return { x: event.clientX - rect.left, y: event.clientY - rect.top }; - }; - canvas.addEventListener("touchstart", (event) => { if (!w.activeTouchId) { let touch = event.changedTouches[0]; - let pos = getTouchPos(touch); + let pos = getMousePos(touch); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); Module._window_data_set_mouse_button(windowData, 1, 1); w.activeTouchId = touch.identifier; @@ -375,7 +370,7 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne for (let i = 0; i < event.changedTouches.length; i++) { let touch = event.changedTouches[i]; if (w.activeTouchId === touch.identifier) { - let pos = getTouchPos(touch); + let pos = getMousePos(touch); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); w.events.push({ type: "mousemove", x: pos.x, y: pos.y}); break; @@ -390,7 +385,7 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne for (let i = 0; i < event.changedTouches.length; i++) { let touch = event.changedTouches[i]; if (w.activeTouchId === touch.identifier) { - let pos = getTouchPos(touch); + let pos = getMousePos(touch); Module._window_data_set_mouse_pos(windowData, pos.x, pos.y); Module._window_data_set_mouse_button(windowData, 1, 0); w.activeTouchId = null; From 4566702bb848deede07dc39c17361dcc0968f96b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 5 Dec 2022 09:14:38 +0100 Subject: [PATCH 08/14] Only apply styles if not already given by user --- src/web/WebMiniFB.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index 509ea03..19098c9 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -246,12 +246,12 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne let id = window._minifb.nextId++; canvas.width = width; canvas.height = height; - canvas.style.width = width + "px"; - canvas.style.height = height + "px"; - canvas.style["image-rendering"] = "pixelated"; - canvas.style["user-select"] = "none"; - canvas.style["border"] = "none"; - canvas.style["outline-style"] = "none"; + if (!canvas.style.width) canvas.style.width = width + "px"; + if (!canvas.style.height) canvas.style.height = height + "px"; + if (!canvas.style["image-rendering"]) canvas.style["image-rendering"] = "pixelated"; + if (!canvas.style["user-select"]) canvas.style["user-select"] = "none"; + if (!canvas.style["border"]) canvas.style["border"] = "none"; + if (!canvas.style["outline-style"]) canvas.style["outline-style"] = "none"; canvas.tabIndex = -1; let w = { From 5719677a63b97540f2ea7d3631612463e1ae8592 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 7 Dec 2022 09:22:07 +0100 Subject: [PATCH 09/14] Only set css width/height if neither are given. --- src/web/WebMiniFB.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c index 19098c9..6a9f1a0 100644 --- a/src/web/WebMiniFB.c +++ b/src/web/WebMiniFB.c @@ -246,8 +246,10 @@ EM_JS(void*, mfb_open_ex_js,(SWindowData *windowData, const char *title, unsigne let id = window._minifb.nextId++; canvas.width = width; canvas.height = height; - if (!canvas.style.width) canvas.style.width = width + "px"; - if (!canvas.style.height) canvas.style.height = height + "px"; + if (!canvas.style.width && !canvas.style.height) { + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; + } if (!canvas.style["image-rendering"]) canvas.style["image-rendering"] = "pixelated"; if (!canvas.style["user-select"]) canvas.style["user-select"] = "none"; if (!canvas.style["border"]) canvas.style["border"] = "none"; From 0702fbef0fc46d62764e49ddb139d284141c7bb2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 18 Dec 2022 13:05:24 +0100 Subject: [PATCH 10/14] Update README.md with info on web backend. --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c63ff7..858869e 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ See https://github.com/emoon/minifb/blob/master/tests/noise.c for a complete exa - Wayland (Linux) [there are some issues] - iOS (beta) - Android (beta) + - Web (WASM) (beta) -MiniFB has been tested on Windows, Mac OS X, Linux, iOS and Android but may of course have trouble depending on your setup. Currently the code will not do any converting of data if not a proper 32-bit display can be created. +MiniFB has been tested on Windows, Mac OS X, Linux, iOS, Android and web but may of course have trouble depending on your setup. Currently the code will not do any converting of data if not a proper 32-bit display can be created. # Features: @@ -512,6 +513,100 @@ cd build cmake .. -DUSE_WAYLAND_API=ON ``` +## Web (WASM) +Download and install [Emscripten](https://emscripten.org/). When configuring your CMake build, specify the Emscripten toolchain file. Then proceed to build as usual. + +### Building and running the examples + +```bash +cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/emsdk//emscripten/cmake/Modules/Platform/Emscripten.cmake -S . -B build +cmake --build build +python3 -m http.server --directory build/ +``` + +Then open [http://localhost:8000](http://localhost:8000) in your browser to view the example index. + +### Integrating a MiniFB app in a website +To build an executable target for the web, you need to add a linker option specifying its module name, e.g.: + +``` +target_link_options(my_app PRIVATE "-sEXPORT_NAME=my_app") +``` + +The Emscripten toolchain will then build a `my_app.wasm` and `my_app.js` file containing your app's WASM code and JavaScript glue code to load the WASM file and run it. To load and run your app, you need to: + +1. Create a `` element with an `id` attribute matching the `title` you specify when calling `mfb_open_window()` or `mfb_open_window_ex()`. +2. Call the `()` in JavaScript. + +Example app: + +```c +int main() { + struct mfb_window *window = mfb_open_ex("my_app", 320, 240); + if (!window) + return 0; + uint32_t *buffer = (uint32_t *) malloc(g_width * g_height * 4); + mfb_update_state state; + do { + state = mfb_update_ex(window, buffer, 320, 200); + if (state != STATE_OK) { + break; + } + } while(mfb_wait_sync(window)); + return 0; +} +``` + +Assuming the build will generate `my_app.wasm` and `my_app.js`, the simplest `.html` file to load and run the app would look like this: + +```html + + + + + + + + + + + +
+ +
+ + + +``` + +### Limitations & caveats +The web backend currently does not support the following MiniFB features: + +* The flags to `mfb_open_ex()` are ignored +* `mfb_set_viewport()` (no-op) +* `mfb_set_viewport_best_fit()` (no-op) +* `mfb_get_monitor_dpi()` (reports a fixed value) +* `mfb_get_monitor_scale()` (reports a fixed value) +* `mfb_set_target_fps()` (no-op) +* `mfb_get_target_fps()` (no-op) + +Everything else is supported. + +When calling `mfb_open()` or `mfb_open_ex()`, the specified title must match the `id` attribute of a `` element in the DOM. The functions will modify the `width` and `height` attribute of the `` element. If not already set, then the functions will also modify the CSS style `width` and `height` attributes of the canvas. + +Setting the CSS width and height of the canvas allows you to up-scale the framebuffer arbitrarily: + +``` +// Request a 320x240 window +mfb_open("my_app", 320, 240); + +// Up-scale 2x via CSS + +```` + # How to add it to your project First add this **repository as a submodule** in your dependencies folder. Something like `dependencies/`: From ab81e5465dc8e9ab5a2eb9b010dbcb2af347a316 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 18 Dec 2022 13:08:54 +0100 Subject: [PATCH 11/14] More on CSS styles for web backend. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 858869e..1851887 100644 --- a/README.md +++ b/README.md @@ -607,6 +607,13 @@ mfb_open("my_app", 320, 240); ```` +If not already set, the backend will also set a handfull of CSS styles on the canvas that are good defaults for pixel graphics. + +* `image-rendering: pixelated` +* `user-select: none` +* `border: none` +* `outline-style: none`; + # How to add it to your project First add this **repository as a submodule** in your dependencies folder. Something like `dependencies/`: From fc2ccc8347a940b0142668f98eb01305eb85a187 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 29 Dec 2022 23:44:06 +0100 Subject: [PATCH 12/14] Use -sSINGLE_FILE for web examples. --- CMakeLists.txt | 1 + README.md | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bc04c4..11ca765 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -292,6 +292,7 @@ elseif(UNIX) "-sEXPORTED_RUNTIME_METHODS=ccall,cwrap" "-sASYNCIFY" "--no-entry" + "-sSINGLE_FILE" ) else() target_link_libraries(minifb diff --git a/README.md b/README.md index 1851887..1ff495d 100644 --- a/README.md +++ b/README.md @@ -521,10 +521,9 @@ Download and install [Emscripten](https://emscripten.org/). When configuring you ```bash cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/emsdk//emscripten/cmake/Modules/Platform/Emscripten.cmake -S . -B build cmake --build build -python3 -m http.server --directory build/ ``` -Then open [http://localhost:8000](http://localhost:8000) in your browser to view the example index. +Then open the file `build/index.html` in your browser to view the example index. ### Integrating a MiniFB app in a website To build an executable target for the web, you need to add a linker option specifying its module name, e.g.: From 5f4c21d267b89423498eb0d4ff2f5fcf1c120290 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 29 Dec 2022 23:57:43 +0100 Subject: [PATCH 13/14] Update README.md with instructions for web builds on Windows. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1ff495d..961fcbb 100644 --- a/README.md +++ b/README.md @@ -523,6 +523,8 @@ cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/emsdk//emscripten/cmake/Modules/P cmake --build build ``` +> *Note*: On Windows, you will need a build tool other than Visual Studio. [Ninja](https://ninja-build.org/) is the best and easiest option. Simply download it, put the `ninja.exe` executable somewhere, and make it available on the command line via your `PATH` environment variable. Then invoke the first command above with the addition of `-G Ninja` at the end. + Then open the file `build/index.html` in your browser to view the example index. ### Integrating a MiniFB app in a website From 42b91d5f97ecf8aa1aa052851692cf8f8dc94fff Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 30 Dec 2022 09:37:55 +0100 Subject: [PATCH 14/14] Added info on serving the Web build output via Python's http.server module. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 961fcbb..4d95dd6 100644 --- a/README.md +++ b/README.md @@ -527,6 +527,14 @@ cmake --build build Then open the file `build/index.html` in your browser to view the example index. +The examples are build using the Emscripten flag `-sSINGLE_FILE`, which will coalesce the `.js` and `.wasm` files into a single `.js` file. If you build your own apps without the `-sSINGLE_FILE` flag, you can not simply open the `.html` file in the browser from disk. Instead, you need an HTTP server to serve the build output. The simplest solution for that is Python's `http.server` module: + +``` +python3 -m http.server build/ +``` + +You can then open the index at [http://localhost:8000](http://localhost:8000) in your browser. + ### Integrating a MiniFB app in a website To build an executable target for the web, you need to add a linker option specifying its module name, e.g.: