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..11ca765 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,22 @@ elseif(UNIX) "-lwayland-client" "-lwayland-cursor" ) + elseif(EMSCRIPTEN) + 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" + "--no-entry" + "-sSINGLE_FILE" + ) else() target_link_libraries(minifb "-lX11" @@ -329,6 +357,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/README.md b/README.md index 3c63ff7..4d95dd6 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,116 @@ 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 +``` + +> *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. + +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.: + +``` +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 + +```` + +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/`: diff --git a/include/MiniFB.h b/include/MiniFB.h index 62ea2ba..59e1c4b 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 @@ -78,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 diff --git a/src/web/WebMiniFB.c b/src/web/WebMiniFB.c new file mode 100644 index 0000000..6a9f1a0 --- /dev/null +++ b/src/web/WebMiniFB.c @@ -0,0 +1,549 @@ +#include +#include +#include +#include +#include +#include +#include + +#define EM_EXPORT __attribute__((used)) + +static bool g_initialized = false; + +EM_ASYNC_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; + 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"; + if (!canvas.style["outline-style"]) 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) { + 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) { + // 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, 1); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousebutton", button: event.button + 1, 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 + 1, 0); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousebutton", button: event.button + 1, 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 + 1, 0); + Module._window_data_set_mod_keys(windowData, mod); + w.events.push({ type: "mousebutton", button: event.button + 1, 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); + + canvas.addEventListener("touchstart", (event) => { + if (!w.activeTouchId) { + let touch = event.changedTouches[0]; + 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; + w.events.push({ type: "mousebutton", button: 1, 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 = 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; + } + } + } + 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 = 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; + w.events.push({ type: "mousebutton", button: 1, 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); + Module._reverse_color_channels(buffer, buffer, width, height); + 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