diff --git a/Makefile b/Makefile
index 544b0c25..7ec6b1fc 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,21 @@ ifeq ($(filter $(check_goal),config defconfig),)
endif
endif
+# Detect Emscripten early (before including toolchain.mk)
+CC_VERSION := $(shell $(CC) --version 2>/dev/null)
+ifneq ($(findstring emcc,$(CC_VERSION)),)
+ CC_IS_EMCC := 1
+endif
+
+# Enforce SDL backend for Emscripten builds (skip during config targets)
+ifeq ($(filter $(check_goal),config defconfig),)
+ ifeq ($(CC_IS_EMCC),1)
+ ifneq ($(CONFIG_BACKEND_SDL),y)
+ $(error Emscripten (WebAssembly) builds require SDL backend. Please run: env CC=emcc make defconfig)
+ endif
+ endif
+endif
+
# Target variables initialization
target-y :=
@@ -23,6 +38,8 @@ target.a-y += libtwin.a
# Core library
libtwin.a_cflags-y :=
+# Emscripten size optimization
+libtwin.a_cflags-$(CC_IS_EMCC) += -Oz
libtwin.a_files-y = \
src/box.c \
@@ -85,14 +102,26 @@ endif
ifeq ($(CONFIG_LOADER_JPEG), y)
libtwin.a_files-y += src/image-jpeg.c
+ifneq ($(CC_IS_EMCC), 1)
libtwin.a_cflags-y += $(shell pkg-config --cflags libjpeg)
TARGET_LIBS += $(shell pkg-config --libs libjpeg)
+else
+# Emscripten libjpeg port - flags needed for both compile and link
+libtwin.a_cflags-y += -sUSE_LIBJPEG=1
+TARGET_LIBS += -sUSE_LIBJPEG=1
+endif
endif
ifeq ($(CONFIG_LOADER_PNG), y)
libtwin.a_files-y += src/image-png.c
+ifneq ($(CC_IS_EMCC), 1)
libtwin.a_cflags-y += $(shell pkg-config --cflags libpng)
TARGET_LIBS += $(shell pkg-config --libs libpng)
+else
+# Emscripten libpng port (includes zlib) - flags needed for both compile and link
+libtwin.a_cflags-y += -sUSE_LIBPNG=1 -sUSE_ZLIB=1
+TARGET_LIBS += -sUSE_LIBPNG=1 -sUSE_ZLIB=1
+endif
endif
ifeq ($(CONFIG_LOADER_GIF), y)
@@ -116,6 +145,8 @@ libapps.a_files-$(CONFIG_DEMO_ANIMATION) += apps/animation.c
libapps.a_files-$(CONFIG_DEMO_IMAGE) += apps/image.c
libapps.a_includes-y := include
+# Emscripten size optimization
+libapps.a_cflags-$(CC_IS_EMCC) += -Oz
# Graphical backends
@@ -124,8 +155,15 @@ BACKEND := none
ifeq ($(CONFIG_BACKEND_SDL), y)
BACKEND = sdl
libtwin.a_files-y += backend/sdl.c
+# Emscripten uses ports system for SDL2
+ifneq ($(CC_IS_EMCC), 1)
libtwin.a_cflags-y += $(shell sdl2-config --cflags)
TARGET_LIBS += $(shell sdl2-config --libs)
+else
+# Emscripten SDL2 port - flags needed for both compile and link
+libtwin.a_cflags-y += -sUSE_SDL=2
+TARGET_LIBS += -sUSE_SDL=2
+endif
endif
ifeq ($(CONFIG_BACKEND_FBDEV), y)
@@ -149,7 +187,6 @@ libtwin.a_files-y += backend/headless.c
endif
# Performance tester
-
ifeq ($(CONFIG_PERF_TEST), y)
target-$(CONFIG_PERF_TEST) += mado-perf
mado-perf_depends-y += $(target.a-y)
@@ -167,13 +204,34 @@ target-y += demo-$(BACKEND)
demo-$(BACKEND)_depends-y += $(target.a-y)
demo-$(BACKEND)_files-y = apps/main.c
demo-$(BACKEND)_includes-y := include
+# Emscripten size optimization
+demo-$(BACKEND)_cflags-$(CC_IS_EMCC) += -Oz
demo-$(BACKEND)_ldflags-y := \
$(target.a-y) \
$(TARGET_LIBS)
+
+# Emscripten-specific linker flags for WebAssembly builds
+ifeq ($(CC_IS_EMCC), 1)
+demo-$(BACKEND)_ldflags-y += \
+ -sINITIAL_MEMORY=33554432 \
+ -sALLOW_MEMORY_GROWTH=1 \
+ -sSTACK_SIZE=1048576 \
+ -sUSE_SDL=2 \
+ -sMINIMAL_RUNTIME=0 \
+ -sDYNAMIC_EXECUTION=0 \
+ -sASSERTIONS=0 \
+ -sEXPORTED_FUNCTIONS=_main,_malloc,_free \
+ -sEXPORTED_RUNTIME_METHODS=ccall,cwrap \
+ -sNO_EXIT_RUNTIME=1 \
+ -Oz \
+ --preload-file assets \
+ --exclude-file assets/web
+endif
endif
# Font editor tool
-
+# Tools should not be built for WebAssembly
+ifneq ($(CC_IS_EMCC), 1)
ifeq ($(CONFIG_TOOLS), y)
target-$(CONFIG_TOOL_FONTEDIT) += font-edit
font-edit_files-y = \
@@ -194,6 +252,7 @@ headless-ctl_files-y = tools/headless-ctl.c
headless-ctl_includes-y := include
headless-ctl_ldflags-y := # -lrt
endif
+endif
# Build system integration
@@ -237,3 +296,26 @@ defconfig: $(KCONFIGLIB)
config: $(KCONFIGLIB) configs/Kconfig
@tools/kconfig/menuconfig.py configs/Kconfig
@tools/kconfig/genconfig.py configs/Kconfig
+
+# WebAssembly post-build: Copy artifacts to assets/web/
+.PHONY: wasm-install
+wasm-install:
+ @if [ "$(CC_IS_EMCC)" = "1" ]; then \
+ echo "Installing WebAssembly artifacts to assets/web/..."; \
+ mkdir -p assets/web; \
+ cp -f .demo-$(BACKEND)/demo-$(BACKEND) .demo-$(BACKEND)/demo-$(BACKEND).wasm .demo-$(BACKEND)/demo-$(BACKEND).data assets/web/ 2>/dev/null || true; \
+ echo "✓ WebAssembly build artifacts copied to assets/web/"; \
+ echo ""; \
+ echo "\033[1;32m========================================\033[0m"; \
+ echo "\033[1;32mWebAssembly build complete!\033[0m"; \
+ echo "\033[1;32m========================================\033[0m"; \
+ echo ""; \
+ echo "To test in browser, run:"; \
+ echo " \033[1;34m./scripts/serve-wasm.py --open\033[0m"; \
+ echo ""; \
+ fi
+
+# Override all target to add post-build hook for Emscripten
+ifeq ($(CC_IS_EMCC), 1)
+all: wasm-install
+endif
diff --git a/apps/main.c b/apps/main.c
index f746704f..56ba79d5 100644
--- a/apps/main.c
+++ b/apps/main.c
@@ -82,6 +82,39 @@ static void sigint_handler(int sig)
exit(1);
}
+/* Initialize demo applications based on build configuration.
+ * Shared by both native and WebAssembly builds.
+ */
+static void init_demo_apps(twin_context_t *ctx)
+{
+ twin_screen_t *screen = ctx->screen;
+#if defined(CONFIG_DEMO_MULTI)
+ apps_multi_start(screen, "Demo", 100, 100, 400, 400);
+#endif
+#if defined(CONFIG_DEMO_HELLO)
+ apps_hello_start(screen, "Hello, World", 0, 0, 200, 200);
+#endif
+#if defined(CONFIG_DEMO_CLOCK)
+ apps_clock_start(screen, "Clock", 10, 10, 200, 200);
+#endif
+#if defined(CONFIG_DEMO_CALCULATOR)
+ apps_calc_start(screen, "Calculator", 100, 100, 200, 200);
+#endif
+#if defined(CONFIG_DEMO_LINE)
+ apps_line_start(screen, "Line", 0, 0, 200, 200);
+#endif
+#if defined(CONFIG_DEMO_SPLINE)
+ apps_spline_start(screen, "Spline", 20, 20, 400, 400);
+#endif
+#if defined(CONFIG_DEMO_ANIMATION)
+ apps_animation_start(screen, "Viewer", ASSET_PATH "nyancat.gif", 20, 20);
+#endif
+#if defined(CONFIG_DEMO_IMAGE)
+ apps_image_start(screen, "Viewer", 20, 20);
+#endif
+ twin_screen_set_active(screen, screen->top);
+}
+
int main(void)
{
tx = twin_create(WIDTH, HEIGHT);
@@ -106,34 +139,8 @@ int main(void)
twin_screen_set_background(
tx->screen, load_background(tx->screen, ASSET_PATH "/tux.png"));
-#if defined(CONFIG_DEMO_MULTI)
- apps_multi_start(tx->screen, "Demo", 100, 100, 400, 400);
-#endif
-#if defined(CONFIG_DEMO_HELLO)
- apps_hello_start(tx->screen, "Hello, World", 0, 0, 200, 200);
-#endif
-#if defined(CONFIG_DEMO_CLOCK)
- apps_clock_start(tx->screen, "Clock", 10, 10, 200, 200);
-#endif
-#if defined(CONFIG_DEMO_CALCULATOR)
- apps_calc_start(tx->screen, "Calculator", 100, 100, 200, 200);
-#endif
-#if defined(CONFIG_DEMO_LINE)
- apps_line_start(tx->screen, "Line", 0, 0, 200, 200);
-#endif
-#if defined(CONFIG_DEMO_SPLINE)
- apps_spline_start(tx->screen, "Spline", 20, 20, 400, 400);
-#endif
-#if defined(CONFIG_DEMO_ANIMATION)
- apps_animation_start(tx->screen, "Viewer", ASSET_PATH "nyancat.gif", 20,
- 20);
-#endif
-#if defined(CONFIG_DEMO_IMAGE)
- apps_image_start(tx->screen, "Viewer", 20, 20);
-#endif
-
- twin_screen_set_active(tx->screen, tx->screen->top);
- twin_dispatch(tx);
+ /* Start application with unified API (handles native and WebAssembly) */
+ twin_run(tx, init_demo_apps);
return 0;
}
diff --git a/assets/web/.gitignore b/assets/web/.gitignore
new file mode 100644
index 00000000..d7117239
--- /dev/null
+++ b/assets/web/.gitignore
@@ -0,0 +1,4 @@
+# WebAssembly runtime files (generated by build)
+demo-sdl
+demo-sdl.wasm
+demo-sdl.data
diff --git a/assets/web/index.html b/assets/web/index.html
new file mode 100644
index 00000000..fbdca003
--- /dev/null
+++ b/assets/web/index.html
@@ -0,0 +1,176 @@
+
+
+
+
+
+ Mado Window System (WebAssembly)
+
+
+
+ Mado Window System
+
+ WebAssembly build running in your browser
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/backend/fbdev.c b/backend/fbdev.c
index a90777ea..4206d0c4 100644
--- a/backend/fbdev.c
+++ b/backend/fbdev.c
@@ -236,7 +236,7 @@ twin_context_t *twin_fbdev_init(int width, int height)
return NULL;
ctx->priv = calloc(1, sizeof(twin_fbdev_t));
if (!ctx->priv)
- return NULL;
+ goto bail;
twin_fbdev_t *tx = ctx->priv;
@@ -264,7 +264,7 @@ twin_context_t *twin_fbdev_init(int width, int height)
/* Examine if framebuffer mapping is valid */
if (tx->fb_base == MAP_FAILED) {
log_error("Failed to map framebuffer memory");
- return;
+ goto bail_vt_fd;
}
const twin_put_span_t fbdev_put_spans[] = {
@@ -276,6 +276,10 @@ twin_context_t *twin_fbdev_init(int width, int height)
ctx->screen = twin_screen_create(
width, height, NULL, fbdev_put_spans[tx->fb_var.bits_per_pixel / 8 - 2],
ctx);
+ if (!ctx->screen) {
+ log_error("Failed to create screen");
+ goto bail_fb_unmap;
+ }
/* Create Linux input system object */
tx->input = twin_linux_input_create(ctx->screen);
@@ -294,6 +298,9 @@ twin_context_t *twin_fbdev_init(int width, int height)
bail_screen:
twin_screen_destroy(ctx->screen);
+bail_fb_unmap:
+ if (tx->fb_base != MAP_FAILED)
+ munmap(tx->fb_base, tx->fb_len);
bail_vt_fd:
close(tx->vt_fd);
bail_fb_fd:
@@ -327,9 +334,27 @@ static void twin_fbdev_exit(twin_context_t *ctx)
free(ctx);
}
+/* Start function for fbdev backend
+ * Note: fbdev uses Linux input system with background thread for events,
+ * so we use the standard dispatcher for work queue and timeout processing.
+ */
+static void twin_fbdev_start(twin_context_t *ctx,
+ void (*init_callback)(twin_context_t *))
+{
+ if (init_callback)
+ init_callback(ctx);
+
+ /* Use standard dispatcher to ensure work queue and timeouts run.
+ * Events are handled by linux_input background thread.
+ */
+ while (twin_dispatch_once(ctx))
+ ;
+}
+
/* Register the Linux framebuffer backend */
const twin_backend_t g_twin_backend = {
.init = twin_fbdev_init,
.configure = twin_fbdev_configure,
+ .start = twin_fbdev_start,
.exit = twin_fbdev_exit,
};
diff --git a/backend/headless.c b/backend/headless.c
index 12d0e7c8..e295b206 100644
--- a/backend/headless.c
+++ b/backend/headless.c
@@ -4,21 +4,36 @@
* All rights reserved.
*/
-#include
-#include
-#include
+/* Detect POSIX environment for shared memory support
+ * Bare-metal and embedded systems lack sys/mman.h and related POSIX APIs
+ */
+#if defined(__unix__) || defined(__unix) || defined(unix) || \
+ (defined(__APPLE__) && defined(__MACH__)) || defined(__linux__) || \
+ defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \
+ defined(__HAIKU__)
+#define HAVE_POSIX_SHM 1
+#endif
+
#include
#include
#include
+#include
+
+#ifdef HAVE_POSIX_SHM
+#include
+#include
+#include
#include
#include
#include
-#include
#include
-
#include "headless-shm.h"
+#endif
+
#include "twin_private.h"
+#ifdef HAVE_POSIX_SHM
+
typedef struct {
twin_headless_shm_t *shm;
twin_argb32_t *framebuffer; /* Points to current back buffer */
@@ -361,10 +376,67 @@ twin_context_t *twin_headless_init(int width, int height)
return NULL;
}
+#else /* !HAVE_POSIX_SHM - Bare-metal stub implementation */
+/* Provides minimal no-op backend for embedded systems without POSIX support */
+
+static void twin_headless_config_dummy(twin_context_t *ctx)
+{
+ (void) ctx;
+}
+
+static bool twin_headless_poll_dummy(twin_context_t *ctx)
+{
+ (void) ctx;
+ return false; /* Immediately exit */
+}
+
+static void twin_headless_exit_dummy(twin_context_t *ctx)
+{
+ free(ctx);
+}
+
+static twin_context_t *twin_headless_init_dummy(int width, int height)
+{
+ (void) width;
+ (void) height;
+ return NULL;
+}
+
+static void twin_headless_start_dummy(twin_context_t *ctx,
+ void (*init_callback)(twin_context_t *))
+{
+ (void) ctx;
+ (void) init_callback;
+}
+#endif /* HAVE_POSIX_SHM */
+
+#ifdef HAVE_POSIX_SHM
+/* Start function for headless backend */
+static void twin_headless_start(twin_context_t *ctx,
+ void (*init_callback)(twin_context_t *))
+{
+ if (init_callback)
+ init_callback(ctx);
+
+ /* Use standard dispatcher to ensure work queue and timeouts run */
+ while (twin_dispatch_once(ctx))
+ ;
+}
+#endif
+
/* Register the headless backend */
const twin_backend_t g_twin_backend = {
+#ifdef HAVE_POSIX_SHM
.init = twin_headless_init,
.configure = twin_headless_configure,
.poll = twin_headless_poll,
+ .start = twin_headless_start,
.exit = twin_headless_exit,
+#else
+ .init = twin_headless_init_dummy,
+ .configure = twin_headless_config_dummy,
+ .poll = twin_headless_poll_dummy,
+ .start = twin_headless_start_dummy,
+ .exit = twin_headless_exit_dummy,
+#endif
};
diff --git a/backend/sdl.c b/backend/sdl.c
index 90a19d73..a56eb18c 100644
--- a/backend/sdl.c
+++ b/backend/sdl.c
@@ -1,6 +1,6 @@
/*
* Twin - A Tiny Window System
- * Copyright (c) 2024 National Cheng Kung University, Taiwan
+ * Copyright (c) 2024-2025 National Cheng Kung University, Taiwan
* All rights reserved.
*/
@@ -9,6 +9,10 @@
#include
#include
+#ifdef __EMSCRIPTEN__
+#include
+#endif
+
#include "twin_private.h"
typedef struct {
@@ -59,12 +63,30 @@ static void _twin_sdl_put_span(twin_coord_t left,
static void _twin_sdl_destroy(twin_screen_t *screen maybe_unused,
twin_sdl_t *tx)
{
- SDL_DestroyTexture(tx->texture);
- SDL_DestroyRenderer(tx->render);
- SDL_DestroyWindow(tx->win);
+ /* Destroy SDL resources and mark as freed to prevent double-destroy */
+ if (tx->texture) {
+ SDL_DestroyTexture(tx->texture);
+ tx->texture = NULL;
+ }
+ if (tx->render) {
+ SDL_DestroyRenderer(tx->render);
+ tx->render = NULL;
+ }
+ if (tx->win) {
+ SDL_DestroyWindow(tx->win);
+ tx->win = NULL;
+ }
SDL_Quit();
}
+#ifdef __EMSCRIPTEN__
+/* Placeholder main loop to prevent SDL from complaining during initialization.
+ * This will be replaced by the real main loop in main().
+ */
+static void twin_sdl_placeholder_loop(void) {}
+static bool twin_sdl_placeholder_set = false;
+#endif
+
static void twin_sdl_damage(twin_screen_t *screen, twin_sdl_t *tx)
{
int width, height;
@@ -92,7 +114,17 @@ twin_context_t *twin_sdl_init(int width, int height)
return NULL;
}
- if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+#ifdef __EMSCRIPTEN__
+ /* Tell SDL we will manage the main loop externally via
+ * emscripten_set_main_loop, preventing SDL from trying to set up its own
+ * timing before we are ready.
+ */
+ SDL_SetMainReady(); // Prevent SDL from taking over main()
+ SDL_SetHint(SDL_HINT_EMSCRIPTEN_ASYNCIFY, "0");
+ SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#canvas");
+#endif
+
+ if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
log_error("%s", SDL_GetError());
goto bail;
}
@@ -109,8 +141,23 @@ twin_context_t *twin_sdl_init(int width, int height)
}
tx->pixels = malloc(width * height * sizeof(*tx->pixels));
+ if (!tx->pixels) {
+ log_error("Failed to allocate pixel buffer");
+ goto bail_window;
+ }
memset(tx->pixels, 255, width * height * sizeof(*tx->pixels));
+#ifdef __EMSCRIPTEN__
+ /* Set up a placeholder main loop to prevent SDL_CreateRenderer from
+ * complaining about missing main loop. The real main loop will be set
+ * up in main() after all initialization is complete.
+ */
+ if (!twin_sdl_placeholder_set) {
+ emscripten_set_main_loop(twin_sdl_placeholder_loop, 0, 0);
+ twin_sdl_placeholder_set = true;
+ }
+#endif
+
tx->render = SDL_CreateRenderer(tx->win, -1, SDL_RENDERER_ACCELERATED);
if (!tx->render) {
log_error("%s", SDL_GetError());
@@ -121,16 +168,30 @@ twin_context_t *twin_sdl_init(int width, int height)
tx->texture = SDL_CreateTexture(tx->render, SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING, width, height);
+ if (!tx->texture) {
+ log_error("%s", SDL_GetError());
+ goto bail_renderer;
+ }
ctx->screen = twin_screen_create(width, height, _twin_sdl_put_begin,
_twin_sdl_put_span, ctx);
+ if (!ctx->screen) {
+ log_error("Failed to create screen");
+ goto bail_texture;
+ }
twin_set_work(twin_sdl_work, TWIN_WORK_REDISPLAY, ctx);
return ctx;
+bail_texture:
+ SDL_DestroyTexture(tx->texture);
+bail_renderer:
+ SDL_DestroyRenderer(tx->render);
bail_pixels:
free(tx->pixels);
+bail_window:
+ SDL_DestroyWindow(tx->win);
bail:
free(ctx->priv);
free(ctx);
@@ -150,7 +211,9 @@ static bool twin_sdl_poll(twin_context_t *ctx)
twin_sdl_t *tx = PRIV(ctx);
SDL_Event ev;
+ bool has_event = false;
while (SDL_PollEvent(&ev)) {
+ has_event = true;
twin_event_t tev;
switch (ev.type) {
case SDL_WINDOWEVENT:
@@ -188,6 +251,14 @@ static bool twin_sdl_poll(twin_context_t *ctx)
break;
}
}
+
+ /* Yield CPU when idle to avoid busy-waiting.
+ * Skip delay if events were processed or screen needs update.
+ */
+ if (!has_event && !twin_screen_damaged(screen)) {
+ SDL_Delay(1); /* 1ms sleep reduces CPU usage when idle */
+ }
+
return true;
}
@@ -195,16 +266,73 @@ static void twin_sdl_exit(twin_context_t *ctx)
{
if (!ctx)
return;
- free(PRIV(ctx)->pixels);
+
+ twin_sdl_t *tx = PRIV(ctx);
+
+ /* Clean up SDL resources */
+ if (tx->texture)
+ SDL_DestroyTexture(tx->texture);
+ if (tx->render)
+ SDL_DestroyRenderer(tx->render);
+ if (tx->win)
+ SDL_DestroyWindow(tx->win);
+ SDL_Quit();
+
+ /* Free memory */
+ free(tx->pixels);
free(ctx->priv);
free(ctx);
}
+#ifdef __EMSCRIPTEN__
+/* Emscripten main loop state */
+static void (*g_wasm_init_callback)(twin_context_t *) = NULL;
+static bool g_wasm_initialized = false;
+
+/* Main loop callback for Emscripten */
+static void twin_sdl_wasm_loop(void *arg)
+{
+ twin_context_t *ctx = (twin_context_t *) arg;
+
+ /* Perform one-time initialization on first iteration */
+ if (!g_wasm_initialized && g_wasm_init_callback) {
+ g_wasm_init_callback(ctx);
+ g_wasm_initialized = true;
+ }
+
+ twin_dispatch_once(ctx);
+}
+#endif
+
+/* Backend start function: unified entry point for both native and WebAssembly
+ */
+static void twin_sdl_start(twin_context_t *ctx,
+ void (*init_callback)(twin_context_t *))
+{
+#ifdef __EMSCRIPTEN__
+ /* WebAssembly: Set up Emscripten main loop */
+ g_wasm_init_callback = init_callback;
+ g_wasm_initialized = false;
+
+ emscripten_cancel_main_loop(); /* Cancel placeholder from init */
+ emscripten_set_main_loop_arg(twin_sdl_wasm_loop, ctx, 0, 1);
+#else
+ /* Native: Initialize immediately and enter standard dispatch loop */
+ if (init_callback)
+ init_callback(ctx);
+
+ /* Use twin_dispatch_once() to ensure work queue and timeouts run */
+ while (twin_dispatch_once(ctx))
+ ;
+#endif
+}
+
/* Register the SDL backend */
const twin_backend_t g_twin_backend = {
.init = twin_sdl_init,
.configure = twin_sdl_configure,
.poll = twin_sdl_poll,
+ .start = twin_sdl_start,
.exit = twin_sdl_exit,
};
diff --git a/backend/vnc.c b/backend/vnc.c
index 8c080296..123aa3c3 100644
--- a/backend/vnc.c
+++ b/backend/vnc.c
@@ -1,6 +1,6 @@
/*
Twin - A Tiny Window System
-Copyright (c) 2024 National Cheng Kung University, Taiwan
+Copyright (c) 2024-2025 National Cheng Kung University, Taiwan
All rights reserved.
*/
#define AML_UNSTABLE_API 1
@@ -289,9 +289,22 @@ static void twin_vnc_exit(twin_context_t *ctx)
free(ctx);
}
+/* Start function for VNC backend */
+static void twin_vnc_start(twin_context_t *ctx,
+ void (*init_callback)(twin_context_t *))
+{
+ if (init_callback)
+ init_callback(ctx);
+
+ /* Use standard dispatcher to ensure work queue and timeouts run */
+ while (twin_dispatch_once(ctx))
+ ;
+}
+
const twin_backend_t g_twin_backend = {
.init = twin_vnc_init,
.poll = twin_vnc_poll,
.configure = twin_vnc_configure,
+ .start = twin_vnc_start,
.exit = twin_vnc_exit,
};
diff --git a/configs/Kconfig b/configs/Kconfig
index 1df10a7b..34e924ca 100644
--- a/configs/Kconfig
+++ b/configs/Kconfig
@@ -4,21 +4,69 @@ config CONFIGURED
bool
default y
+menu "Toolchain Configuration"
+
+# Compiler detection using scripts/detect-compiler.py
+config COMPILER_TYPE
+ string
+ default "$(shell,scripts/detect-compiler.py 2>/dev/null || echo Unknown)"
+
+config CC_IS_EMCC
+ def_bool $(shell,scripts/detect-compiler.py --is Emscripten 2>/dev/null && echo y || echo n)
+
+config CC_IS_CLANG
+ def_bool $(shell,scripts/detect-compiler.py --is Clang 2>/dev/null && echo y || echo n)
+
+config CC_IS_GCC
+ def_bool $(shell,scripts/detect-compiler.py --is GCC 2>/dev/null && echo y || echo n)
+
+# Cross-compilation support detection
+config CROSS_COMPILE_ENABLED
+ def_bool $(shell,test -n "$(CROSS_COMPILE)" && echo y || echo n)
+
+config CROSS_COMPILE_PREFIX
+ string
+ default "$(CROSS_COMPILE)"
+
+comment "Toolchain Information"
+
+comment "Build mode: WebAssembly (Emscripten)"
+ depends on CC_IS_EMCC
+
+comment "Build mode: Cross-compilation"
+ depends on CROSS_COMPILE_ENABLED && !CC_IS_EMCC
+
+comment "Build mode: Native"
+ depends on !CROSS_COMPILE_ENABLED && !CC_IS_EMCC
+
+endmenu
+
# Dependency detection using Kconfiglib shell function
+# For Emscripten builds, libraries are provided via ports system (-sUSE_*)
+# and do not require host pkg-config detection
+
config HAVE_SDL2
- def_bool $(shell,pkg-config --exists sdl2 && echo y || echo n)
+ bool
+ default y if CC_IS_EMCC
+ default $(shell,pkg-config --exists sdl2 && echo y || echo n) if !CC_IS_EMCC
config HAVE_PIXMAN
- def_bool $(shell,pkg-config --exists pixman-1 && echo y || echo n)
+ default n if CC_IS_EMCC
+ def_bool $(shell,pkg-config --exists pixman-1 && echo y || echo n) if !CC_IS_EMCC
config HAVE_LIBPNG
- def_bool $(shell,pkg-config --exists libpng && echo y || echo n)
+ bool
+ default y if CC_IS_EMCC
+ default $(shell,pkg-config --exists libpng && echo y || echo n) if !CC_IS_EMCC
config HAVE_LIBJPEG
- def_bool $(shell,pkg-config --exists libjpeg && echo y || echo n)
+ bool
+ default y if CC_IS_EMCC
+ default $(shell,pkg-config --exists libjpeg && echo y || echo n) if !CC_IS_EMCC
config HAVE_CAIRO
- def_bool $(shell,pkg-config --exists cairo && echo y || echo n)
+ default n if CC_IS_EMCC
+ def_bool $(shell,pkg-config --exists cairo && echo y || echo n) if !CC_IS_EMCC
choice
prompt "Backend Selection"
@@ -27,34 +75,44 @@ choice
Select the display backend for window system rendering.
Only one backend can be active at a time.
+config BACKEND_SDL
+ bool "SDL video output support"
+ depends on HAVE_SDL2
+ help
+ Cross-platform SDL2 backend for development and testing.
+
+ For Emscripten (WebAssembly): This is the only supported backend.
+ Emscripten provides SDL2 via its ports system (-sUSE_SDL=2).
+
config BACKEND_FBDEV
bool "Linux framebuffer support"
- select CURSOR
+ depends on !CC_IS_EMCC
+ imply CURSOR
help
Direct Linux framebuffer backend for embedded systems.
Renders directly to /dev/fb0 without X11/Wayland.
Requires Linux input subsystem for keyboard/mouse events.
-config BACKEND_SDL
- bool "SDL video output support"
- depends on HAVE_SDL2
- help
- Cross-platform SDL2 backend for development and testing.
-
config BACKEND_VNC
bool "VNC server output support"
+ depends on !CC_IS_EMCC
help
Remote access backend using neatvnc library.
Allows headless operation with VNC client connections.
config BACKEND_HEADLESS
bool "Headless backend"
+ depends on !CC_IS_EMCC
help
Shared memory backend for testing and automation.
Renders to memory without display hardware or VNC.
Designed for CI/CD pipelines and automated testing.
+
endchoice
+comment "WebAssembly builds use SDL2 backend only"
+ depends on CC_IS_EMCC
+
choice
prompt "Renderer Selection"
default RENDERER_BUILTIN
@@ -284,11 +342,15 @@ config TOOLS
config TOOL_FONTEDIT
bool "Build scalable font editor"
default y
- depends on TOOLS && HAVE_SDL2 && HAVE_CAIRO
+ depends on TOOLS && HAVE_SDL2 && HAVE_CAIRO && !CROSS_COMPILE_ENABLED && !CC_IS_EMCC
help
Interactive font editor for Mado font format.
Allows creation and modification of scalable fonts.
+ Note: Only available for native builds. Not supported for
+ cross-compilation or WebAssembly (Emscripten) builds due to
+ Cairo library dependencies.
+
config PERF_TEST
bool "Build performance tester"
default y
diff --git a/include/twin.h b/include/twin.h
index 7b88653b..10c2ce01 100644
--- a/include/twin.h
+++ b/include/twin.h
@@ -735,12 +735,36 @@ twin_button_t *twin_button_create(twin_box_t *parent,
*/
twin_pixmap_t *twin_make_cursor(int *hx, int *hy);
+/**
+ * Start the Twin application with initialization callback
+ * @ctx : Twin context containing screen and backend data
+ * @init_callback : Application initialization function (called once before
+ * event loop)
+ *
+ * Unified application startup function that handles platform differences
+ * internally.
+ *
+ * For native builds:
+ * - Calls init_callback immediately
+ * - Enters infinite event loop via twin_dispatch()
+ *
+ * For WebAssembly builds:
+ * - Sets up browser-compatible event loop
+ * - Defers init_callback to first iteration (works around SDL timing issues)
+ *
+ * This is the recommended way to start Twin applications.
+ */
+void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *));
+
/**
* Process pending events and work items
* @ctx : Twin context containing screen and backend data
*
* Main event loop function that processes input events,
* executes work procedures, and handles timeouts.
+ *
+ * For native builds, this runs an infinite loop until termination.
+ * For WebAssembly builds, use twin_run() instead.
*/
void twin_dispatch(twin_context_t *ctx);
diff --git a/mk/toolchain.mk b/mk/toolchain.mk
index ff0c89a7..24416527 100644
--- a/mk/toolchain.mk
+++ b/mk/toolchain.mk
@@ -1,56 +1,122 @@
+# Linux Kernel-style Toolchain Configuration
+# ============================================
+#
+# This file implements toolchain detection and configuration following
+# Linux kernel conventions. Supports native and cross-compilation builds.
+#
+# Usage:
+# make # Native build with system compiler
+# make CROSS_COMPILE=arm-none-eabi- # Cross-compile for ARM bare-metal
+# make CC=clang # Use specific compiler
+# make CC=emcc # Build for WebAssembly with Emscripten
+#
+# Reference: https://www.kernel.org/doc/html/latest/kbuild/kbuild.html
+
+# Include guard to prevent double inclusion
+ifndef TOOLCHAIN_MK_INCLUDED
+TOOLCHAIN_MK_INCLUDED := 1
+
+# Compiler identification flags
CC_IS_CLANG :=
CC_IS_GCC :=
+CC_IS_EMCC :=
-# FIXME: Cross-compilation using Clang is not supported.
-ifdef CROSS_COMPILE
-CC := $(CROSS_COMPILE)gcc
+# Detect host architecture for platform-specific optimizations
+HOSTARCH := $(shell uname -m)
+
+# Set CC based on CROSS_COMPILE or default compiler
+# Check if CC comes from Make's default or command line/environment
+ifeq ($(origin CC),default)
+ # CC is Make's built-in default, override it
+ ifdef CROSS_COMPILE
+ CC := $(CROSS_COMPILE)gcc
+ else
+ CC := cc
+ endif
endif
-override CC := $(shell which $(CC))
-ifndef CC
-$(error "Valid C compiler not found.")
+# Resolve CC to absolute path and validate
+CC_PATH := $(shell which $(CC) 2>/dev/null)
+ifeq ($(CC_PATH),)
+ $(error Compiler '$(CC)' not found. Please install the toolchain or check your PATH.)
endif
+override CC := $(CC_PATH)
-ifneq ($(shell $(CC) --version | head -n 1 | grep clang),)
-CC_IS_CLANG := 1
-else ifneq ($(shell $(CC) --version | grep "Free Software Foundation"),)
-CC_IS_GCC := 1
+# Compiler type detection (Emscripten, Clang, GCC)
+CC_VERSION := $(shell $(CC) --version 2>/dev/null)
+ifneq ($(findstring emcc,$(CC_VERSION)),)
+ CC_IS_EMCC := 1
+else ifneq ($(findstring clang,$(CC_VERSION)),)
+ CC_IS_CLANG := 1
+else ifneq ($(findstring Free Software Foundation,$(CC_VERSION)),)
+ CC_IS_GCC := 1
endif
-ifeq ("$(CC_IS_CLANG)$(CC_IS_GCC)", "")
-$(warning Unsupported C compiler)
+# Validate compiler type
+ifeq ($(CC_IS_CLANG)$(CC_IS_GCC)$(CC_IS_EMCC),)
+ $(warning Unsupported C compiler: $(CC))
endif
+# C++ compiler setup
ifndef CXX
-CXX := $(CROSS_COMPILE)g++
-endif
-ifeq ("$(CC_IS_CLANG)", "1")
-override CXX := $(subst clang,clang++,$(CC))
+ ifeq ($(CC_IS_CLANG),1)
+ override CXX := $(subst clang,clang++,$(CC))
+ else ifeq ($(CC_IS_EMCC),1)
+ override CXX := em++
+ else
+ CXX := $(CROSS_COMPILE)g++
+ endif
endif
+# Target toolchain configuration
+# These tools are used for building the target binaries (potentially cross-compiled)
+
ifndef CPP
CPP := $(CC) -E
endif
-ifndef LD
-LD := $(CROSS_COMPILE)ld
-endif
-
-ifndef AR
-AR := $(CROSS_COMPILE)ar
-endif
-
-ifndef RANLIB
-RANLIB := $(CROSS_COMPILE)ranlib
-endif
-
-ifndef STRIP
-STRIP := $(CROSS_COMPILE)strip -sx
-endif
-
-ifndef OBJCOPY
-OBJCOPY := $(CROSS_COMPILE)objcopy
-endif
+# Emscripten uses its own toolchain (emar, emranlib, etc.)
+# For other compilers, use CROSS_COMPILE prefix
+ifeq ($(CC_IS_EMCC),1)
+ # Emscripten toolchain
+ # Use $(origin) to detect if variables come from Make's built-in defaults
+ ifeq ($(origin AR),default)
+ AR := emar
+ endif
+ # RANLIB has no built-in default in Make, check if undefined or default
+ ifeq ($(filter-out undefined default,$(origin RANLIB)),)
+ RANLIB := emranlib
+ endif
+ ifndef STRIP
+ STRIP := emstrip
+ endif
+ # Emscripten doesn't use traditional ld/objcopy
+ LD := emcc
+ OBJCOPY := true
+else
+ # Traditional GCC/Clang toolchain
+ # Use $(origin) to detect if variables come from Make's built-in defaults
+ ifeq ($(origin LD),default)
+ LD := $(CROSS_COMPILE)ld
+ endif
+ ifeq ($(origin AR),default)
+ AR := $(CROSS_COMPILE)ar
+ endif
+ # RANLIB may have origin 'default' or 'undefined' depending on Make version
+ ifeq ($(filter-out undefined default,$(origin RANLIB)),)
+ RANLIB := $(CROSS_COMPILE)ranlib
+ endif
+ ifndef STRIP
+ STRIP := $(CROSS_COMPILE)strip -sx
+ endif
+ ifndef OBJCOPY
+ OBJCOPY := $(CROSS_COMPILE)objcopy
+ endif
+endif
+
+# Host toolchain configuration
+# These tools are used for building host utilities (always native compilation)
+# Useful when cross-compiling: target tools build for embedded, host tools for development machine
ifndef HOSTCC
HOSTCC := $(HOST_COMPILE)gcc
@@ -83,3 +149,34 @@ endif
ifndef HOSTOBJCOPY
HOSTOBJCOPY := $(HOST_COMPILE)objcopy
endif
+
+# Platform-specific compiler optimizations
+# Based on target architecture and compiler capabilities
+
+# macOS-specific linker flags
+# Xcode 15+ generates warnings about duplicate library options
+# Only apply to native macOS builds (not cross-compilation or Emscripten)
+ifeq ($(shell uname -s),Darwin)
+ ifeq ($(CROSS_COMPILE)$(CC_IS_EMCC),)
+ # Check if ld supports -no_warn_duplicate_libraries
+ # Use /dev/null as input to ensure compiler invokes the linker
+ LD_SUPPORTS_NO_WARN := $(shell printf '' | $(CC) -Wl,-no_warn_duplicate_libraries -x c - -o /dev/null 2>&1 | grep -q "unknown option" && echo 0 || echo 1)
+ ifeq ($(LD_SUPPORTS_NO_WARN),1)
+ LDFLAGS += -Wl,-no_warn_duplicate_libraries
+ endif
+ endif
+endif
+
+# Toolchain information display
+ifdef CROSS_COMPILE
+ $(info Cross-compiling with: $(CROSS_COMPILE))
+ $(info Target toolchain: $(CC))
+endif
+
+ifeq ($(CC_IS_EMCC),1)
+ $(info WebAssembly build with Emscripten)
+ $(info Compiler: $(CC))
+ $(info Toolchain: emar, emranlib, emstrip)
+endif
+
+endif # TOOLCHAIN_MK_INCLUDED
diff --git a/scripts/detect-compiler.py b/scripts/detect-compiler.py
new file mode 100755
index 00000000..fb104789
--- /dev/null
+++ b/scripts/detect-compiler.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+"""
+Compiler Detection Script for Mado Build System
+
+This script detects the compiler type (GCC, Clang, Emscripten) based on
+CROSS_COMPILE and CC environment variables, following Linux kernel Kbuild
+conventions.
+
+Usage:
+ python3 detect-compiler.py [--is COMPILER]
+
+Default behavior (no arguments):
+ Output compiler type (GCC/Clang/Emscripten/Unknown)
+
+Options:
+ --is COMPILER Check if compiler matches specified type (GCC/Clang/Emscripten)
+ Returns exit code 0 if match, 1 otherwise
+ Example: --is Emscripten
+
+Exit codes:
+ 0: Success (or match when using --is)
+ 1: Compiler not found, detection failed, or no match (when using --is)
+
+Environment variables:
+ CROSS_COMPILE: Toolchain prefix (e.g., "arm-none-eabi-")
+ CC: C compiler command (e.g., "gcc", "clang", "emcc")
+"""
+
+import os
+import shlex
+import subprocess
+import sys
+
+
+def get_compiler_path():
+ """
+ Determine compiler path based on CROSS_COMPILE and CC variables.
+
+ Priority:
+ 1. CC environment variable (explicit override)
+ 2. CROSS_COMPILE + gcc (when CC not set)
+ 3. Default: cc
+
+ Returns:
+ str or list: Compiler command to execute (may be list for wrapped compilers)
+ """
+ cross_compile = os.environ.get('CROSS_COMPILE', '')
+ cc = os.environ.get('CC', '')
+
+ # Priority: explicit CC overrides CROSS_COMPILE
+ if cc:
+ return cc
+ elif cross_compile:
+ return f"{cross_compile}gcc"
+ else:
+ return 'cc'
+
+
+def get_compiler_version(compiler):
+ """
+ Get compiler version string.
+
+ Args:
+ compiler: Compiler command (string, may contain spaces for wrapped compilers)
+
+ Returns:
+ str: Full version output, or empty string if command fails
+ """
+ try:
+ # Split command into argv tokens to support wrapped compilers like "ccache clang"
+ cmd_argv = shlex.split(compiler) if isinstance(compiler, str) else [compiler]
+
+ result = subprocess.run(
+ cmd_argv + ['--version'],
+ capture_output=True,
+ text=True,
+ timeout=5
+ )
+ return result.stdout if result.returncode == 0 else ''
+ except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):
+ return ''
+
+
+def detect_compiler_type(version_output):
+ """
+ Detect compiler type from version string.
+
+ Priority order:
+ 1. Emscripten (check for "emcc")
+ 2. Clang (check for "clang" but not "emcc")
+ 3. GCC (check for "gcc" or "Free Software Foundation")
+ 4. Unknown
+
+ Args:
+ version_output: Compiler version string
+
+ Returns:
+ str: Compiler type (Emscripten/Clang/GCC/Unknown)
+ """
+ lower = version_output.lower()
+
+ # Check Emscripten first (it may contain "clang" in output)
+ if 'emcc' in lower:
+ return 'Emscripten'
+
+ # Check Clang (but not if it's Emscripten)
+ if 'clang' in lower:
+ return 'Clang'
+
+ # Check GCC
+ if 'gcc' in lower or 'free software foundation' in lower:
+ return 'GCC'
+
+ return 'Unknown'
+
+
+def main():
+ """Main entry point."""
+ # Parse arguments
+ check_is = '--is' in sys.argv
+
+ # Get expected compiler type for --is check
+ expected_type = None
+ if check_is:
+ try:
+ is_index = sys.argv.index('--is')
+ if is_index + 1 < len(sys.argv):
+ expected_type = sys.argv[is_index + 1]
+ else:
+ print("Error: --is requires a compiler type argument", file=sys.stderr)
+ sys.exit(1)
+ except (ValueError, IndexError):
+ print("Error: --is requires a compiler type argument", file=sys.stderr)
+ sys.exit(1)
+
+ # Get compiler path
+ compiler = get_compiler_path()
+
+ # Get version output
+ version_output = get_compiler_version(compiler)
+
+ if not version_output:
+ print('Unknown')
+ sys.exit(1)
+
+ # Detect compiler type
+ compiler_type = detect_compiler_type(version_output)
+
+ # Handle --is option
+ if check_is:
+ # Case-insensitive comparison
+ matches = compiler_type.lower() == expected_type.lower()
+ sys.exit(0 if matches else 1)
+
+ # Default: output compiler type
+ print(compiler_type)
+ sys.exit(0 if compiler_type != 'Unknown' else 1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/serve-wasm.py b/scripts/serve-wasm.py
new file mode 100755
index 00000000..2fbc34f4
--- /dev/null
+++ b/scripts/serve-wasm.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+"""
+WebAssembly Development Server for Mado
+Serves the web/ directory and provides instructions for testing WebAssembly builds.
+"""
+
+import http.server
+import socketserver
+import os
+import sys
+import argparse
+import webbrowser
+import subprocess
+import signal
+from pathlib import Path
+
+# ANSI color codes
+BOLD = '\033[1m'
+GREEN = '\033[92m'
+YELLOW = '\033[93m'
+BLUE = '\033[94m'
+RED = '\033[91m'
+RESET = '\033[0m'
+
+def find_process_using_port(port):
+ """Find process ID using the given port."""
+ try:
+ # macOS/BSD: use lsof
+ result = subprocess.run(
+ ['lsof', '-ti', f'tcp:{port}'],
+ capture_output=True,
+ text=True
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ pids = result.stdout.strip().split('\n')
+ return [int(pid) for pid in pids if pid]
+ except (subprocess.SubprocessError, FileNotFoundError):
+ # Linux: use ss or netstat as fallback
+ try:
+ result = subprocess.run(
+ ['ss', '-tlnp', f'sport = :{port}'],
+ capture_output=True,
+ text=True
+ )
+ # Parse ss output for PID
+ for line in result.stdout.split('\n'):
+ if 'pid=' in line:
+ pid_part = line.split('pid=')[1].split(',')[0]
+ return [int(pid_part)]
+ except (subprocess.SubprocessError, FileNotFoundError):
+ pass
+ return []
+
+def kill_process(pid):
+ """Kill process by PID."""
+ try:
+ os.kill(pid, signal.SIGTERM)
+ return True
+ except (ProcessLookupError, PermissionError):
+ return False
+
+def check_required_files(web_dir):
+ """Check if required WebAssembly files exist."""
+ required_files = [
+ 'index.html',
+ 'demo-sdl.wasm',
+ 'demo-sdl',
+ 'demo-sdl.data'
+ ]
+
+ missing = []
+ for filename in required_files:
+ filepath = web_dir / filename
+ if not filepath.exists():
+ missing.append(filename)
+
+ return missing
+
+def print_banner(port, web_dir, open_browser):
+ """Print informative banner with instructions."""
+ print(f"\n{BOLD}{GREEN}{'='*70}{RESET}")
+ print(f"{BOLD}{GREEN} Mado WebAssembly Development Server{RESET}")
+ print(f"{BOLD}{GREEN}{'='*70}{RESET}\n")
+
+ print(f"{BOLD}Serving directory:{RESET} {web_dir}")
+ print(f"{BOLD}Server address:{RESET} http://localhost:{port}")
+ print(f"{BOLD}Direct URL:{RESET} http://localhost:{port}/index.html\n")
+
+ if open_browser:
+ print(f"{YELLOW}→ Opening browser automatically...{RESET}\n")
+ else:
+ print(f"{YELLOW}→ Open the URL above in your browser to test the WebAssembly build{RESET}\n")
+
+ print(f"{BOLD}Instructions:{RESET}")
+ print(f" 1. Make sure you have built the WebAssembly version:")
+ print(f" {BLUE}env CC=emcc make{RESET}")
+ print(f" 2. Build artifacts are automatically copied to assets/web/")
+ print(f" 3. Open the URL above in a modern browser\n")
+
+ print(f"{BOLD}Supported browsers:{RESET}")
+ print(f" • Chrome/Chromium (recommended)")
+ print(f" • Firefox")
+ print(f" • Safari (macOS 11.3+)\n")
+
+ print(f"{RED}Press Ctrl+C to stop the server{RESET}")
+ print(f"{BOLD}{GREEN}{'='*70}{RESET}\n")
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Serve Mado WebAssembly build for testing',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog='''
+Examples:
+ %(prog)s # Serve on default port 8000
+ %(prog)s -p 8080 # Serve on port 8080
+ %(prog)s --open # Serve and open browser automatically
+ %(prog)s -p 3000 --open # Custom port with auto-open
+ '''
+ )
+
+ parser.add_argument(
+ '-p', '--port',
+ type=int,
+ default=8000,
+ help='Port to serve on (default: 8000)'
+ )
+
+ parser.add_argument(
+ '--open',
+ action='store_true',
+ help='Automatically open browser'
+ )
+
+ parser.add_argument(
+ '--host',
+ default='127.0.0.1',
+ help='Host to bind to (default: 127.0.0.1)'
+ )
+
+ args = parser.parse_args()
+
+ # Determine project root and web directory
+ script_dir = Path(__file__).parent.absolute()
+ project_root = script_dir.parent
+ web_dir = project_root / 'assets' / 'web'
+
+ # Check if web directory exists
+ if not web_dir.exists():
+ print(f"{RED}Error: assets/web/ directory not found at {web_dir}{RESET}", file=sys.stderr)
+ print(f"{YELLOW}Creating assets/web/ directory...{RESET}")
+ web_dir.mkdir(parents=True, exist_ok=True)
+ print(f"{GREEN}Created: {web_dir}{RESET}")
+
+ # Check for required files
+ missing_files = check_required_files(web_dir)
+ if missing_files:
+ print(f"{YELLOW}Warning: Some required files are missing:{RESET}")
+ for filename in missing_files:
+ print(f" {RED}✗{RESET} {filename}")
+ print(f"\n{YELLOW}Build the WebAssembly version first:{RESET}")
+ print(f" {BLUE}env CC=emcc make defconfig{RESET}")
+ print(f" {BLUE}env CC=emcc make{RESET}")
+ print(f"\n{YELLOW}Build artifacts will be automatically copied to assets/web/{RESET}\n")
+
+ response = input(f"Continue anyway? [y/N]: ").strip().lower()
+ if response not in ['y', 'yes']:
+ print(f"{RED}Aborted.{RESET}")
+ sys.exit(1)
+ print()
+ else:
+ print(f"{GREEN}✓ All required files found{RESET}\n")
+
+ # Change to web directory
+ os.chdir(web_dir)
+
+ # Print banner
+ print_banner(args.port, web_dir, args.open)
+
+ # Open browser if requested
+ if args.open:
+ url = f"http://{args.host}:{args.port}/index.html"
+ webbrowser.open(url)
+
+ # Start HTTP server
+ Handler = http.server.SimpleHTTPRequestHandler
+
+ # Add custom headers for COOP/COEP if needed for SharedArrayBuffer
+ class CustomHandler(Handler):
+ def end_headers(self):
+ # Enable SharedArrayBuffer support (required for some Emscripten features)
+ self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
+ self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
+ super().end_headers()
+
+ # Enable socket reuse to allow quick restart
+ socketserver.TCPServer.allow_reuse_address = True
+
+ try:
+ with socketserver.TCPServer((args.host, args.port), CustomHandler) as httpd:
+ print(f"{GREEN}Server running...{RESET}\n")
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print(f"\n\n{YELLOW}Shutting down server...{RESET}")
+ print(f"{GREEN}Server stopped.{RESET}\n")
+ sys.exit(0)
+ except OSError as e:
+ if e.errno == 48 or e.errno == 98: # Address already in use (macOS: 48, Linux: 98)
+ print(f"\n{RED}Error: Port {args.port} is already in use{RESET}", file=sys.stderr)
+
+ # Try to find the process using the port
+ pids = find_process_using_port(args.port)
+
+ if pids:
+ print(f"{YELLOW}Found process(es) using port {args.port}: {pids}{RESET}")
+ print(f"{YELLOW}This might be a previous instance of this server.{RESET}\n")
+
+ response = input(f"Kill the process(es) and restart? [y/N]: ").strip().lower()
+ if response in ['y', 'yes']:
+ killed = []
+ for pid in pids:
+ if kill_process(pid):
+ killed.append(pid)
+ print(f"{GREEN}✓ Killed process {pid}{RESET}")
+ else:
+ print(f"{RED}✗ Failed to kill process {pid}{RESET}")
+
+ if killed:
+ import time
+ print(f"{YELLOW}Waiting for port to be released...{RESET}")
+ time.sleep(1)
+
+ # Retry starting server
+ try:
+ with socketserver.TCPServer((args.host, args.port), CustomHandler) as httpd:
+ print_banner(args.port, web_dir, False)
+ if args.open:
+ url = f"http://{args.host}:{args.port}/index.html"
+ webbrowser.open(url)
+ print(f"{GREEN}Server running...{RESET}\n")
+ httpd.serve_forever()
+ except OSError:
+ print(f"{RED}Still cannot bind to port {args.port}{RESET}", file=sys.stderr)
+ print(f"{YELLOW}Try manually: {BLUE}lsof -ti tcp:{args.port} | xargs kill{RESET}\n", file=sys.stderr)
+ sys.exit(1)
+ else:
+ sys.exit(1)
+ else:
+ print(f"{YELLOW}Try a different port with: {BLUE}--port XXXX{RESET}\n", file=sys.stderr)
+ sys.exit(1)
+ else:
+ print(f"{YELLOW}Could not identify the process using the port.{RESET}")
+ print(f"{YELLOW}Try manually: {BLUE}lsof -ti tcp:{args.port} | xargs kill{RESET}")
+ print(f"{YELLOW}Or use a different port with: {BLUE}--port XXXX{RESET}\n", file=sys.stderr)
+ sys.exit(1)
+ else:
+ raise
+
+if __name__ == '__main__':
+ main()
diff --git a/src/api.c b/src/api.c
index d7d808d6..2f019720 100644
--- a/src/api.c
+++ b/src/api.c
@@ -1,6 +1,6 @@
/*
* Twin - A Tiny Window System
- * Copyright (c) 2024 National Cheng Kung University, Taiwan
+ * Copyright (c) 2024-2025 National Cheng Kung University, Taiwan
* All rights reserved.
*/
@@ -9,16 +9,60 @@
#include "twin_private.h"
-/* FIXME: Refine API design */
-
extern twin_backend_t g_twin_backend;
+/**
+ * Create a new Twin context with specified dimensions.
+ *
+ * Initializes the backend and creates a screen context for rendering.
+ *
+ * @width : Screen width in pixels
+ * @height : Screen height in pixels
+ * @return : Twin context, or NULL on failure
+ */
twin_context_t *twin_create(int width, int height)
{
- return g_twin_backend.init(width, height);
+ assert(g_twin_backend.init && "Backend not registered");
+
+ twin_context_t *ctx = g_twin_backend.init(width, height);
+ if (!ctx) {
+#ifdef CONFIG_LOGGING
+ log_error("Failed to initialize Twin context (%dx%d)", width, height);
+#endif
+ }
+ return ctx;
}
+/**
+ * Destroy a Twin context and release all resources.
+ *
+ * Cleans up backend resources, frees memory, and terminates the context.
+ *
+ * @ctx : Twin context to destroy
+ */
void twin_destroy(twin_context_t *ctx)
{
- return g_twin_backend.exit(ctx);
+ assert(g_twin_backend.exit && "Backend not registered");
+
+ if (ctx)
+ g_twin_backend.exit(ctx);
+}
+
+/**
+ * Start the Twin application with initialization callback.
+ *
+ * This function provides a platform-independent way to start Twin applications.
+ * It delegates to the backend's start function, which handles platform-specific
+ * main loop setup.
+ *
+ * @ctx : Twin context to run
+ * @init_callback : Application initialization function (called once before
+ * event loop)
+ */
+void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *))
+{
+ assert(ctx && "NULL context passed to twin_run");
+ assert(g_twin_backend.start && "Backend start function not registered");
+
+ g_twin_backend.start(ctx, init_callback);
}
diff --git a/src/dispatch.c b/src/dispatch.c
index 1eef8f8b..eae6cd0b 100644
--- a/src/dispatch.c
+++ b/src/dispatch.c
@@ -7,21 +7,112 @@
#include
+#ifdef __EMSCRIPTEN__
+#include
+#endif
+
#include "twin_private.h"
extern twin_backend_t g_twin_backend;
+/* Execute a single iteration of the event dispatch loop.
+ *
+ * This function processes one iteration of the event loop and is designed
+ * for integration with external event loops (e.g., browser, GUI toolkits).
+ *
+ * Processing order:
+ * 1. Run pending timeout callbacks
+ * 2. Execute queued work items
+ * 3. Poll backend for new events
+ *
+ * @ctx : The Twin context to dispatch events for (must not be NULL)
+ * Return false if backend requested termination (e.g., window closed),
+ * true otherwise
+ *
+ * Example usage with Emscripten:
+ * static void main_loop(void *arg)
+ * {
+ * if (!twin_dispatch_once((twin_context_t *) arg))
+ * emscripten_cancel_main_loop();
+ * }
+ * emscripten_set_main_loop_arg(main_loop, ctx, 0, 1);
+ */
+bool twin_dispatch_once(twin_context_t *ctx)
+{
+ /* Validate context to prevent null pointer dereference in callbacks */
+ if (!ctx) {
+#ifdef CONFIG_LOGGING
+ log_error("twin_dispatch_once: NULL context");
+#endif
+ return false;
+ }
+
+ _twin_run_timeout();
+ _twin_run_work();
+
+ /* Poll backend for events. Returns false when user requests exit.
+ * If no poll function is registered, assume no event source and yield CPU.
+ */
+ if (g_twin_backend.poll) {
+ if (!g_twin_backend.poll(ctx))
+ return false;
+ } else {
+#ifdef CONFIG_LOGGING
+ log_warn("twin_dispatch_once: No backend poll function registered");
+#endif
+ /* Yield CPU to avoid busy-waiting when no event source available */
+#ifdef __EMSCRIPTEN__
+ emscripten_sleep(0);
+#elif defined(_POSIX_VERSION)
+ usleep(1000); /* 1ms sleep */
+#endif
+ }
+
+ return true;
+}
+
+/* Run the main event dispatch loop (native platforms only).
+ *
+ * This function runs an infinite event loop, processing timeouts, work items,
+ * and backend events. The loop exits when the backend's poll function returns
+ * false, typically when the user closes the window or requests termination.
+ *
+ * @ctx : The Twin context to dispatch events for (must not be NULL)
+ *
+ * Platform notes:
+ * - Native builds: Blocks until backend terminates
+ * - Emscripten: This function is not available. Use twin_dispatch_once()
+ * with emscripten_set_main_loop_arg() instead.
+ *
+ * See also: twin_dispatch_once() for single-iteration event processing
+ */
void twin_dispatch(twin_context_t *ctx)
{
- for (;;) {
- _twin_run_timeout();
- _twin_run_work();
+#ifdef __EMSCRIPTEN__
+ /* Emscripten builds must use emscripten_set_main_loop_arg() with
+ * twin_dispatch_once() to integrate with the browser's event loop.
+ * Calling twin_dispatch() directly will not work in WebAssembly.
+ */
+ (void) ctx; /* Unused in Emscripten builds */
+#ifdef CONFIG_LOGGING
+ log_error(
+ "twin_dispatch() called in Emscripten build - use "
+ "twin_dispatch_once() with emscripten_set_main_loop_arg()");
+#endif
+ return;
+#else
+ /* Validate context before entering event loop */
+ if (!ctx) {
+#ifdef CONFIG_LOGGING
+ log_error("twin_dispatch: NULL context");
+#endif
+ return;
+ }
- if (g_twin_backend.poll && !g_twin_backend.poll(ctx)) {
- twin_time_t delay = _twin_timeout_delay();
- if (delay > 0)
- usleep(delay * 1000);
+ /* Main event loop - runs until backend requests termination */
+ for (;;) {
+ if (!twin_dispatch_once(ctx))
break;
- }
}
+#endif
}
diff --git a/src/draw-builtin.c b/src/draw-builtin.c
index 9788b235..64262eac 100644
--- a/src/draw-builtin.c
+++ b/src/draw-builtin.c
@@ -7,10 +7,35 @@
#include
-#if defined(__APPLE__)
+/* Cross-platform endianness detection
+ *
+ * Priority:
+ * 1. Linux/glibc:
+ * 2. macOS/BSD:
+ * 3. Bare-metal/embedded: Manual detection via compiler macros
+ *
+ * Note: We don't actually use any endian.h macros in this file currently,
+ * but keep the includes for future compatibility.
+ */
+#if defined(__linux__) || defined(__GLIBC__)
+#include
+#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
+ defined(__OpenBSD__)
#include
+#elif defined(__BYTE_ORDER__)
+/* Compiler-provided endianness (GCC/Clang bare-metal) */
+#define __LITTLE_ENDIAN 1234
+#define __BIG_ENDIAN 4321
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define __BYTE_ORDER __LITTLE_ENDIAN
+#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+#define __BYTE_ORDER __BIG_ENDIAN
+#endif
#else
-#include
+/* Fallback: Assume little-endian (x86, ARM, RISC-V default) */
+#define __LITTLE_ENDIAN 1234
+#define __BIG_ENDIAN 4321
+#define __BYTE_ORDER __LITTLE_ENDIAN
#endif
#include "twin_private.h"
diff --git a/src/twin_private.h b/src/twin_private.h
index 723280c7..da6a4d4c 100644
--- a/src/twin_private.h
+++ b/src/twin_private.h
@@ -140,10 +140,101 @@ typedef int64_t twin_xfixed_t;
#define twin_dfixed_div(a, b) \
(twin_dfixed_t)((((int64_t) (a)) << 8) / ((int64_t) (b)))
+/* 64-bit fixed-point multiplication and division
+ * These require 128-bit intermediate results for full precision.
+ *
+ * For 64-bit platforms with __int128_t support, use native 128-bit arithmetic.
+ * For 32-bit platforms (RISC-V 32, ARM32, i386), use software implementation.
+ */
+#if defined(__SIZEOF_INT128__) || \
+ (defined(__GNUC__) && !defined(__i386__) && \
+ (defined(__x86_64__) || defined(__aarch64__)))
+/* Native 128-bit integer support (x86-64, AArch64, etc.) */
#define twin_xfixed_mul(a, b) \
(twin_xfixed_t)((((__int128_t) (a)) * ((__int128_t) (b))) >> 32)
#define twin_xfixed_div(a, b) \
(twin_xfixed_t)((((__int128_t) (a)) << 32) / ((__int128_t) (b)))
+#else
+/* Software implementation for 32-bit platforms
+ * Uses 64x64 -> 128 multiplication split into 32x32 -> 64 parts
+ *
+ * For a * b where a and b are 64-bit:
+ * a = ah << 32 | al
+ * b = bh << 32 | bl
+ * a * b = (ah*bh << 64) + (ah*bl << 32) + (al*bh << 32) + (al*bl)
+ *
+ * After right shift by 32:
+ * result = (ah*bh << 32) + (ah*bl) + (al*bh) + (al*bl >> 32)
+ */
+static inline twin_xfixed_t _twin_xfixed_mul_32bit(twin_xfixed_t a,
+ twin_xfixed_t b)
+{
+ /* Handle sign */
+ int neg = ((a < 0) != (b < 0));
+ uint64_t ua = (a < 0) ? -a : a;
+ uint64_t ub = (b < 0) ? -b : b;
+
+ /* Split into high and low 32-bit parts */
+ uint64_t ah = ua >> 32;
+ uint64_t al = ua & 0xFFFFFFFFULL;
+ uint64_t bh = ub >> 32;
+ uint64_t bl = ub & 0xFFFFFFFFULL;
+
+ /* Compute partial products */
+ uint64_t p_hh = ah * bh, p_hl = ah * bl;
+ uint64_t p_lh = al * bh, p_ll = al * bl;
+
+ /* Combine with appropriate shifts, handling carry correctly
+ * Result = (p_hh << 32) + p_hl + p_lh + (p_ll >> 32)
+ * Split middle terms to prevent overflow: add low 32 bits first,
+ * then high 32 bits with carries
+ */
+ uint64_t mid_low =
+ (p_hl & 0xFFFFFFFFULL) + (p_lh & 0xFFFFFFFFULL) + (p_ll >> 32);
+ uint64_t mid_high = (p_hl >> 32) + (p_lh >> 32) + (mid_low >> 32);
+ uint64_t result =
+ (p_hh << 32) + (mid_high << 32) + (mid_low & 0xFFFFFFFFULL);
+
+ return neg ? -(int64_t) result : (int64_t) result;
+}
+
+static inline twin_xfixed_t _twin_xfixed_div_32bit(twin_xfixed_t a,
+ twin_xfixed_t b)
+{
+ /* Handle sign */
+ int neg = ((a < 0) != (b < 0));
+ uint64_t ua = (a < 0) ? -a : a;
+ uint64_t ub = (b < 0) ? -b : b;
+
+ /* Fixed-point division: (a / b) in Q32.32 = (a << 32) / b in integer
+ * arithmetic Since ua is 64-bit, (ua << 32) is 96-bit. Perform long
+ * division: Split ua into 32-bit parts: ua = (ah << 32) | al Then (ua <<
+ * 32) = (ah << 64) | (al << 32)
+ *
+ * Step 1: Divide high part (ah << 32) by ub to get high 32 bits of quotient
+ * Step 2: Divide ((remainder << 32) | al) by ub to get low 32 bits
+ */
+ uint64_t ah = ua >> 32; /* High 32 bits */
+ uint64_t al = ua & 0xFFFFFFFFULL; /* Low 32 bits */
+
+ /* Divide (ah << 32) / ub */
+ uint64_t ah_shifted = ah << 32;
+ uint64_t q_h = ah_shifted / ub;
+ uint64_t rem = ah_shifted % ub;
+
+ /* Divide ((rem << 32) | al) / ub */
+ uint64_t dividend_low = (rem << 32) | al;
+ uint64_t q_l = dividend_low / ub;
+
+ /* Combine quotients: result = (q_h << 32) | q_l */
+ uint64_t result = (q_h << 32) | q_l;
+
+ return neg ? -(int64_t) result : (int64_t) result;
+}
+
+#define twin_xfixed_mul(a, b) _twin_xfixed_mul_32bit(a, b)
+#define twin_xfixed_div(a, b) _twin_xfixed_div_32bit(a, b)
+#endif
/*
* 'double' is a no-no in any shipping code, but useful during
@@ -432,6 +523,9 @@ twin_time_t _twin_timeout_delay(void);
void _twin_run_work(void);
+/* Event dispatch - internal use only */
+bool twin_dispatch_once(twin_context_t *ctx);
+
void _twin_box_init(twin_box_t *box,
twin_box_t *parent,
twin_window_t *window,
@@ -514,6 +608,9 @@ typedef struct twin_backend {
bool (*poll)(twin_context_t *ctx);
+ /* Start the main loop with application initialization callback */
+ void (*start)(twin_context_t *ctx, void (*init_callback)(twin_context_t *));
+
/* Device cleanup when drawing is done */
void (*exit)(twin_context_t *ctx);
} twin_backend_t;
diff --git a/tools/perf.c b/tools/perf.c
index e045bb2c..cdfb3d18 100644
--- a/tools/perf.c
+++ b/tools/perf.c
@@ -10,10 +10,21 @@
#include
#include
#include
-#include
#include
+#include
#include
+/* getrusage() is POSIX-specific for memory profiling
+ * Not available on bare-metal or systems without full POSIX support
+ */
+#if defined(__unix__) || defined(__unix) || defined(unix) || \
+ (defined(__APPLE__) && defined(__MACH__)) || defined(__linux__) || \
+ defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \
+ defined(__HAIKU__)
+#define HAVE_GETRUSAGE 1
+#include
+#endif
+
#include "twin.h"
#define TEST_PIX_WIDTH 1200
@@ -293,9 +304,14 @@ static void run_large_tests(void)
/* Memory profiling mode */
-/* Get memory usage statistics */
+/* Get memory usage statistics
+ * Note: getrusage() requires POSIX support (Linux, macOS, etc.)
+ * For bare-metal or embedded systems, return zero
+ */
static void get_memory_usage(long *rss_kb, long *max_rss_kb)
{
+#ifdef HAVE_GETRUSAGE
+ /* POSIX systems with getrusage() support */
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
#ifdef __APPLE__
@@ -304,6 +320,11 @@ static void get_memory_usage(long *rss_kb, long *max_rss_kb)
*max_rss_kb = usage.ru_maxrss; /* Linux reports in KB */
#endif
*rss_kb = *max_rss_kb; /* Current RSS approximation */
+#else
+ /* Bare-metal or systems without getrusage() */
+ *rss_kb = 0;
+ *max_rss_kb = 0;
+#endif
}
/* Print memory test statistics */
@@ -500,16 +521,23 @@ static void run_memory_profiling(void)
int main(void)
{
+ /* Print header */
+ printf("Mado performance tester");
+#ifdef HAVE_GETRUSAGE
+ /* Full POSIX systems can provide hostname and timestamp */
time_t now;
char hostname[256];
- /* Print header */
time(&now);
if (gethostname(hostname, sizeof(hostname)) != 0)
strcpy(hostname, "localhost");
- printf("Mado performance tester on %s\n", hostname);
+ printf(" on %s\n", hostname);
printf("%s", ctime(&now));
+#else
+ /* Bare-metal systems: minimal header */
+ printf("\n");
+#endif
/* Measure sync time adjustment */
measure_sync_time();