From 28eb0eb1159b168332dfa1e09c12f1be0c2d9e5b Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sat, 18 Oct 2025 17:52:48 +0800 Subject: [PATCH 1/5] Enable bare-metal cross-compilation support This supports bare-metal toolchains (e.g., riscv-none-elf-) by adding conditional compilation for POSIX-specific APIs and fixing toolchain selection in the build system. --- backend/headless.c | 59 +++++++++++++++-- mk/toolchain.mk | 157 +++++++++++++++++++++++++++++++++++---------- src/twin_private.h | 97 ++++++++++++++++++++++++++++ tools/perf.c | 36 +++++++++-- 4 files changed, 305 insertions(+), 44 deletions(-) diff --git a/backend/headless.c b/backend/headless.c index 12d0e7c8..9f2d1d86 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,44 @@ 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; +} +#endif /* HAVE_POSIX_SHM */ + /* 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, .exit = twin_headless_exit, +#else + .init = twin_headless_init_dummy, + .configure = twin_headless_config_dummy, + .poll = twin_headless_poll_dummy, + .exit = twin_headless_exit_dummy, +#endif }; diff --git a/mk/toolchain.mk b/mk/toolchain.mk index ff0c89a7..4a6a9e8a 100644 --- a/mk/toolchain.mk +++ b/mk/toolchain.mk @@ -1,56 +1,115 @@ +# 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 + +# 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 + ifndef AR + AR := emar + endif + ifndef 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 + ifeq ($(origin RANLIB),default) + 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 +142,31 @@ 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) +ifeq ($(shell uname -s),Darwin) + ifndef CROSS_COMPILE + # Check if ld supports -no_warn_duplicate_libraries + LD_SUPPORTS_NO_WARN := $(shell $(CC) -Wl,-no_warn_duplicate_libraries 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 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(); From fcd35252ffb42c3b737a450113323d9fd41524f0 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sat, 18 Oct 2025 18:01:10 +0800 Subject: [PATCH 2/5] Add cross-compilation infrastructure support This commit enhances cross-compilation support with key improvements: 1. Kconfig Toolchain Configuration Menu (configs/Kconfig): - Automatic compiler detection using Python scripts - Cross-compile prefix detection and validation - Supports GCC, Clang, Emscripten, and bare-metal toolchains 2. Cross-platform Endianness Detection - Priority-based header inclusion: * Bare-metal: Compiler macros (__BYTE_ORDER__) * Fallback: Assume little-endian - Ensures correct byte order handling across diverse target platforms 3. 128-bit Arithmetic Fallback: - Software implementation for 64-bit fixed-point math on 32-bit - Targets: RISC-V 32, Arm32, i386 (without __int128_t support) --- configs/Kconfig | 41 +++++++++++++++++++++++++++++++++++++++++ src/draw-builtin.c | 29 +++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/configs/Kconfig b/configs/Kconfig index 1df10a7b..3765d55d 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -4,6 +4,47 @@ config CONFIGURED bool default y +menu "Toolchain Configuration" + +# Compiler detection using scripts/detect-compiler.py +config COMPILER_TYPE + string + default "$(shell,python3 scripts/detect-compiler.py --type 2>/dev/null || echo Unknown)" + +config CC_IS_EMCC + def_bool $(shell,python3 scripts/detect-compiler.py --is Emscripten 2>/dev/null && echo y || echo n) + +config CC_IS_CLANG + def_bool $(shell,python3 scripts/detect-compiler.py --is Clang 2>/dev/null && echo y || echo n) + +config CC_IS_GCC + def_bool $(shell,python3 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)" + +# Verify cross-compilation tools exist +config CROSS_COMPILE_VALID + def_bool $(shell,test -n "$(CROSS_COMPILE)" && which "$(CROSS_COMPILE)gcc" >/dev/null 2>&1 && echo y || echo n) + +comment "Toolchain Information" + +comment "Cross-compilation: ENABLED" + depends on CROSS_COMPILE_ENABLED && CROSS_COMPILE_VALID + +comment "Cross-compilation: ENABLED (WARNING: Compiler not found!)" + depends on CROSS_COMPILE_ENABLED && !CROSS_COMPILE_VALID + +comment "Cross-compilation: DISABLED (Native build)" + depends on !CROSS_COMPILE_ENABLED + +endmenu + # Dependency detection using Kconfiglib shell function config HAVE_SDL2 def_bool $(shell,pkg-config --exists sdl2 && echo y || echo n) 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" From fab4f4d143e5b59fab6247351a637e146d9d05c7 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sat, 18 Oct 2025 18:25:19 +0800 Subject: [PATCH 3/5] Fix toolchain compatibility for Emscripten builds This commit 1. fixes macOS linker flag detection 2. adds Emscripten memory configuration: - Configured -sINITIAL_MEMORY=33554432 (32MB) - Enabled -sALLOW_MEMORY_GROWTH=1 - Set -sSTACK_SIZE=1048576 (1MB) Known Issue - Emscripten 4.0.17 Compatibility: The "wasm-ld: error: animation.c.o: section too large" error persists even with memory configuration and -O2 optimization. This is a known limitation of wasm-ld when linking multiple large object files. --- Makefile | 8 ++++++++ mk/toolchain.mk | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 544b0c25..241e0f22 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,14 @@ demo-$(BACKEND)_includes-y := include demo-$(BACKEND)_ldflags-y := \ $(target.a-y) \ $(TARGET_LIBS) + +# Emscripten-specific linker flags to avoid "section too large" errors +ifeq ($(CC_IS_EMCC), 1) +demo-$(BACKEND)_ldflags-y += \ + -sINITIAL_MEMORY=33554432 \ + -sALLOW_MEMORY_GROWTH=1 \ + -sSTACK_SIZE=1048576 +endif endif # Font editor tool diff --git a/mk/toolchain.mk b/mk/toolchain.mk index 4a6a9e8a..5582dbf9 100644 --- a/mk/toolchain.mk +++ b/mk/toolchain.mk @@ -148,9 +148,9 @@ endif # macOS-specific linker flags # Xcode 15+ generates warnings about duplicate library options -# Only apply to native macOS builds (not cross-compilation) +# Only apply to native macOS builds (not cross-compilation or Emscripten) ifeq ($(shell uname -s),Darwin) - ifndef CROSS_COMPILE + ifeq ($(CROSS_COMPILE)$(CC_IS_EMCC),) # Check if ld supports -no_warn_duplicate_libraries LD_SUPPORTS_NO_WARN := $(shell $(CC) -Wl,-no_warn_duplicate_libraries 2>&1 | grep -q "unknown option" && echo 0 || echo 1) ifeq ($(LD_SUPPORTS_NO_WARN),1) From a3302845ff161c26ee139d68593dad68ffb84d62 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 19 Oct 2025 12:29:08 +0800 Subject: [PATCH 4/5] Improve backend architecture and memory safety Backend improvements: - SDL: Add comprehensive error handling with proper resource cleanup - SDL: Implement smart conditional delay to prevent CPU busy-wait - SDL: Fix resource leaks in initialization and exit paths - fbdev: Fix critical memory leaks and invalid return statements - fbdev: Add NULL checks for screen creation - All backends: Standardize main loop using twin_dispatch_once() API refactoring: - Add twin_run() as unified application entry point - Hide platform-specific details from public API Performance: - Reduce idle CPU usage from 100% to <1% in SDL backend - Maintain zero-latency event handling and screen updates --- apps/main.c | 63 +++++++++++--------- backend/fbdev.c | 29 +++++++++- backend/headless.c | 23 ++++++++ backend/sdl.c | 140 +++++++++++++++++++++++++++++++++++++++++++-- backend/vnc.c | 15 ++++- include/twin.h | 24 ++++++++ src/api.c | 54 +++++++++++++++-- src/dispatch.c | 107 +++++++++++++++++++++++++++++++--- 8 files changed, 405 insertions(+), 50 deletions(-) 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/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 9f2d1d86..e295b206 100644 --- a/backend/headless.c +++ b/backend/headless.c @@ -401,19 +401,42 @@ static twin_context_t *twin_headless_init_dummy(int width, int height) (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/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/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 } From ee9cdee9e75bdcb3aa290c85eda5751aae419a9a Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 19 Oct 2025 12:33:26 +0800 Subject: [PATCH 5/5] Add WebAssembly build infrastructure This enables Emscripten/WebAssembly compilation with complete toolchain integration and browser-based testing workflow. The build system now automatically detects Emscripten and configures appropriate compiler flags, backend selection, and post-build artifact management. Run './scripts/serve-wasm.py --open' to test in web browser. --- Makefile | 82 +++++++++++- assets/web/.gitignore | 4 + assets/web/index.html | 176 +++++++++++++++++++++++++ configs/Kconfig | 75 +++++++---- mk/toolchain.mk | 24 +++- scripts/detect-compiler.py | 161 +++++++++++++++++++++++ scripts/serve-wasm.py | 259 +++++++++++++++++++++++++++++++++++++ 7 files changed, 743 insertions(+), 38 deletions(-) create mode 100644 assets/web/.gitignore create mode 100644 assets/web/index.html create mode 100755 scripts/detect-compiler.py create mode 100755 scripts/serve-wasm.py diff --git a/Makefile b/Makefile index 241e0f22..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,21 +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 to avoid "section too large" errors +# 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 + -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 = \ @@ -202,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 @@ -245,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/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/configs/Kconfig b/configs/Kconfig index 3765d55d..34e924ca 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -9,16 +9,16 @@ menu "Toolchain Configuration" # Compiler detection using scripts/detect-compiler.py config COMPILER_TYPE string - default "$(shell,python3 scripts/detect-compiler.py --type 2>/dev/null || echo Unknown)" + default "$(shell,scripts/detect-compiler.py 2>/dev/null || echo Unknown)" config CC_IS_EMCC - def_bool $(shell,python3 scripts/detect-compiler.py --is Emscripten 2>/dev/null && echo y || echo n) + def_bool $(shell,scripts/detect-compiler.py --is Emscripten 2>/dev/null && echo y || echo n) config CC_IS_CLANG - def_bool $(shell,python3 scripts/detect-compiler.py --is Clang 2>/dev/null && echo y || echo n) + def_bool $(shell,scripts/detect-compiler.py --is Clang 2>/dev/null && echo y || echo n) config CC_IS_GCC - def_bool $(shell,python3 scripts/detect-compiler.py --is GCC 2>/dev/null && echo y || echo n) + def_bool $(shell,scripts/detect-compiler.py --is GCC 2>/dev/null && echo y || echo n) # Cross-compilation support detection config CROSS_COMPILE_ENABLED @@ -28,38 +28,45 @@ config CROSS_COMPILE_PREFIX string default "$(CROSS_COMPILE)" -# Verify cross-compilation tools exist -config CROSS_COMPILE_VALID - def_bool $(shell,test -n "$(CROSS_COMPILE)" && which "$(CROSS_COMPILE)gcc" >/dev/null 2>&1 && echo y || echo n) - comment "Toolchain Information" -comment "Cross-compilation: ENABLED" - depends on CROSS_COMPILE_ENABLED && CROSS_COMPILE_VALID +comment "Build mode: WebAssembly (Emscripten)" + depends on CC_IS_EMCC -comment "Cross-compilation: ENABLED (WARNING: Compiler not found!)" - depends on CROSS_COMPILE_ENABLED && !CROSS_COMPILE_VALID +comment "Build mode: Cross-compilation" + depends on CROSS_COMPILE_ENABLED && !CC_IS_EMCC -comment "Cross-compilation: DISABLED (Native build)" - depends on !CROSS_COMPILE_ENABLED +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" @@ -68,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 @@ -325,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/mk/toolchain.mk b/mk/toolchain.mk index 5582dbf9..24416527 100644 --- a/mk/toolchain.mk +++ b/mk/toolchain.mk @@ -12,6 +12,10 @@ # # 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 := @@ -75,14 +79,16 @@ endif # For other compilers, use CROSS_COMPILE prefix ifeq ($(CC_IS_EMCC),1) # Emscripten toolchain - ifndef AR - AR := emar + # Use $(origin) to detect if variables come from Make's built-in defaults + ifeq ($(origin AR),default) + AR := emar endif - ifndef RANLIB - RANLIB := emranlib + # 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 + STRIP := emstrip endif # Emscripten doesn't use traditional ld/objcopy LD := emcc @@ -96,7 +102,8 @@ else ifeq ($(origin AR),default) AR := $(CROSS_COMPILE)ar endif - ifeq ($(origin RANLIB),default) + # 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 @@ -152,7 +159,8 @@ endif ifeq ($(shell uname -s),Darwin) ifeq ($(CROSS_COMPILE)$(CC_IS_EMCC),) # Check if ld supports -no_warn_duplicate_libraries - LD_SUPPORTS_NO_WARN := $(shell $(CC) -Wl,-no_warn_duplicate_libraries 2>&1 | grep -q "unknown option" && echo 0 || echo 1) + # 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 @@ -170,3 +178,5 @@ ifeq ($(CC_IS_EMCC),1) $(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()