diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml new file mode 100644 index 00000000..c4137e68 --- /dev/null +++ b/.github/workflows/headless-test.yml @@ -0,0 +1,135 @@ +name: Headless Backend Tests + +on: [push, pull_request] + +jobs: + headless-test: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y valgrind imagemagick python3 + + - name: Configure build + run: | + make defconfig + python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig \ + BACKEND_HEADLESS=y \ + TOOLS=y \ + TOOL_HEADLESS_CTL=y \ + DEMO_MULTI=y + + - name: Build with headless backend + run: | + make -j$(nproc) + + - name: Verify build outputs + run: | + test -x ./demo-headless || (echo "demo-headless not built" && exit 1) + test -x ./headless-ctl || (echo "headless-ctl not built" && exit 1) + echo "Build outputs verified successfully" + + - name: Run basic headless test + run: | + # Start demo in background + timeout 30s ./demo-headless & + DEMO_PID=$! + echo "Started demo-headless with PID $DEMO_PID" + + # Wait for backend to initialize + sleep 3 + + # Test control tool commands + ./headless-ctl status + ./headless-ctl shot test_output.png + + # Inject mouse events + ./headless-ctl mouse move 100 100 + ./headless-ctl mouse down 100 100 1 + ./headless-ctl mouse up 100 100 1 + + # Check status again after events + ./headless-ctl status + + # Shutdown gracefully + ./headless-ctl shutdown || true + + # Wait for process to exit + wait $DEMO_PID || true + echo "Basic test completed" + + - name: Verify screenshot output + run: | + test -f test_output.png || (echo "Screenshot not created" && exit 1) + file test_output.png | grep -q PNG || (echo "Screenshot is not a valid PNG" && exit 1) + echo "Screenshot verification passed" + + - name: Test deterministic rendering + run: | + # Run twice and compare outputs for determinism + echo "First run..." + timeout 10s ./demo-headless & + PID1=$! + sleep 3 + ./headless-ctl shot run1.png + ./headless-ctl shutdown + wait $PID1 || true + + echo "Second run..." + timeout 10s ./demo-headless & + PID2=$! + sleep 3 + ./headless-ctl shot run2.png + ./headless-ctl shutdown + wait $PID2 || true + + # Compare images (allowing minor differences) + DIFF=$(compare -metric AE run1.png run2.png null: 2>&1 || echo "0") + echo "Pixel difference: $DIFF" + + if [ "$DIFF" -gt "100" ]; then + echo "ERROR: Renderings differ by more than 100 pixels" + exit 1 + fi + echo "Deterministic rendering test passed" + + - name: Memory leak detection with Valgrind + run: | + # Run with Valgrind for leak detection + timeout 15s valgrind \ + --leak-check=full \ + --show-leak-kinds=definite,possible \ + --errors-for-leak-kinds=definite \ + --error-exitcode=1 \ + --log-file=valgrind.log \ + ./demo-headless & + VALGRIND_PID=$! + + sleep 5 + + # Perform some operations + ./headless-ctl mouse move 50 50 || true + ./headless-ctl shot valgrind_screenshot.png || true + ./headless-ctl shutdown || true + + # Wait for Valgrind to finish + wait $VALGRIND_PID || VALGRIND_EXIT=$? + + # Display Valgrind summary + echo "=== Valgrind Summary ===" + grep -A 10 "LEAK SUMMARY" valgrind.log || true + grep "ERROR SUMMARY" valgrind.log || true + + # Check for definite leaks + if grep -q "definitely lost: [1-9]" valgrind.log; then + echo "ERROR: Memory leaks detected" + cat valgrind.log + exit 1 + fi + echo "No definite memory leaks detected" diff --git a/Makefile b/Makefile index 658cd7bf..4667b75f 100644 --- a/Makefile +++ b/Makefile @@ -133,6 +133,11 @@ libtwin.a_cflags-y += $(shell pkg-config --cflags neatvnc aml pixman-1) TARGET_LIBS += $(shell pkg-config --libs neatvnc aml pixman-1) endif +ifeq ($(CONFIG_BACKEND_HEADLESS), y) +BACKEND = headless +libtwin.a_files-y += backend/headless.c +endif + # Performance tester ifeq ($(CONFIG_PERF_TEST), y) @@ -171,6 +176,12 @@ font-edit_cflags-y := \ font-edit_ldflags-y := \ $(shell pkg-config --libs cairo) \ $(shell sdl2-config --libs) + +# Headless control tool +target-$(CONFIG_TOOL_HEADLESS_CTL) += headless-ctl +headless-ctl_files-y = tools/headless-ctl.c +headless-ctl_includes-y := include +headless-ctl_ldflags-y := # -lrt endif # Build system integration diff --git a/backend/headless-shm.h b/backend/headless-shm.h new file mode 100644 index 00000000..e7d6dc3d --- /dev/null +++ b/backend/headless-shm.h @@ -0,0 +1,75 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2025 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#ifndef _TWIN_HEADLESS_SHM_H_ +#define _TWIN_HEADLESS_SHM_H_ + +#include +#include + +/* Shared memory layout definitions - shared between backend and control tool */ + +#define TWIN_HEADLESS_SHM_NAME "/mado-headless-shm" +#define TWIN_HEADLESS_MAGIC 0x5457494E /* "TWIN" */ +#define TWIN_HEADLESS_VERSION 1 +#define TWIN_HEADLESS_MAX_EVENTS 64 + +typedef enum { + TWIN_HEADLESS_CMD_NONE = 0, + TWIN_HEADLESS_CMD_SCREENSHOT, + TWIN_HEADLESS_CMD_GET_STATE, + TWIN_HEADLESS_CMD_INJECT_MOUSE, + TWIN_HEADLESS_CMD_SHUTDOWN, +} twin_headless_cmd_t; + +typedef struct { + twin_event_kind_t kind; + union { + struct { + twin_coord_t x, y; + twin_count_t button; + } pointer; + } u; +} twin_headless_event_t; + +typedef struct { + uint32_t magic; + uint32_t version; + + /* Screen info */ + twin_coord_t width, height; + + /* Event processing statistics */ + uint64_t last_activity_timestamp; /* Unix timestamp in microseconds */ + uint32_t events_processed_total; /* Total events processed since start */ + uint32_t events_processed_second; /* Events processed in current second */ + uint64_t events_second_timestamp; /* Timestamp for current second period */ + uint32_t commands_processed_total; /* Total commands from control tool */ + uint32_t mouse_events_total; /* Total mouse events processed */ + + /* Rendering statistics */ + uint32_t frames_per_second; /* Current FPS */ + uint32_t frames_rendered_total; /* Total frames rendered since start */ + + /* Command interface */ + twin_headless_cmd_t command; + uint32_t command_seq; + char command_data[256]; + + /* Response */ + uint32_t response_seq; + int32_t response_status; + char response_data[256]; + + /* Event injection */ + uint32_t event_write_idx, event_read_idx; + twin_headless_event_t events[TWIN_HEADLESS_MAX_EVENTS]; + + /* Framebuffer follows this header */ + /* twin_argb32_t framebuffer[width * height]; */ +} twin_headless_shm_t; + +#endif /* _TWIN_HEADLESS_SHM_H_ */ diff --git a/backend/headless.c b/backend/headless.c new file mode 100644 index 00000000..12d0e7c8 --- /dev/null +++ b/backend/headless.c @@ -0,0 +1,370 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2025 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "headless-shm.h" +#include "twin_private.h" + +typedef struct { + twin_headless_shm_t *shm; + twin_argb32_t *framebuffer; /* Points to current back buffer */ + twin_argb32_t *front_buffer; /* Buffer visible to control tool */ + twin_argb32_t *back_buffer; /* Buffer for rendering */ + int shm_fd; + size_t shm_size; + uint32_t last_cmd_seq; + uint64_t last_frame_time; + uint32_t frame_count; + bool running; +} twin_headless_t; + +#define SCREEN(x) ((twin_context_t *) x)->screen +#define PRIV(x) ((twin_headless_t *) ((twin_context_t *) x)->priv) + +/* Get current timestamp in microseconds */ +static uint64_t _twin_headless_get_timestamp(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (uint64_t) tv.tv_sec * 1000000 + tv.tv_usec; +} + +/* Update event statistics in shared memory */ +static void _twin_headless_track_event(twin_headless_t *tx, + const char *event_type) +{ + if (!tx->shm) + return; + + uint64_t now = _twin_headless_get_timestamp(); + tx->shm->last_activity_timestamp = now; + tx->shm->events_processed_total++; + + /* Update per-second counter */ + uint64_t elapsed = now - tx->shm->events_second_timestamp; + if (elapsed >= 1000000) { /* 1 second in microseconds */ + tx->shm->events_second_timestamp = now; + tx->shm->events_processed_second = 0; + } + tx->shm->events_processed_second++; + + /* Track specific event types */ + if (strstr(event_type, "mouse")) + tx->shm->mouse_events_total++; +} + +static void _twin_headless_put_begin(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_coord_t bottom, + void *closure) +{ + (void) left; + (void) top; + (void) right; + (void) bottom; + (void) closure; +} + +static void _twin_headless_put_span(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure) +{ + twin_screen_t *screen = SCREEN(closure); + twin_headless_t *tx = PRIV(closure); + + if (!tx->framebuffer) + return; + + twin_coord_t width = screen->width; + for (twin_coord_t x = left; x < right; x++) { + tx->framebuffer[top * width + x] = *pixels++; + } +} + +static void _twin_headless_process_commands(twin_context_t *ctx) +{ + twin_headless_t *tx = ctx->priv; + twin_headless_shm_t *shm = tx->shm; + + if (!shm || shm->command_seq == tx->last_cmd_seq) + return; + + tx->last_cmd_seq = shm->command_seq; + + /* Track command processing */ + _twin_headless_track_event(tx, "command"); + tx->shm->commands_processed_total++; + + switch (shm->command) { + case TWIN_HEADLESS_CMD_SCREENSHOT: + /* Screenshot functionality moved to control tool */ + shm->response_status = 0; + strcpy(shm->response_data, "Screenshot data available in framebuffer"); + break; + + case TWIN_HEADLESS_CMD_GET_STATE: + shm->response_status = 0; + snprintf(shm->response_data, sizeof(shm->response_data), + "events=%u frames=%u fps=%u running=%d", + shm->events_processed_total, shm->frames_rendered_total, + shm->frames_per_second, tx->running); + break; + + case TWIN_HEADLESS_CMD_SHUTDOWN: + tx->running = false; + shm->response_status = 0; + strcpy(shm->response_data, "Shutting down"); + break; + + default: + shm->response_status = -1; + strcpy(shm->response_data, "Unknown command"); + break; + } + + shm->response_seq = shm->command_seq; +} + +static void _twin_headless_inject_events(twin_context_t *ctx) +{ + twin_headless_t *tx = ctx->priv; + twin_headless_shm_t *shm = tx->shm; + twin_screen_t *screen = ctx->screen; + + if (!shm) + return; + + while (shm->event_read_idx != shm->event_write_idx) { + uint32_t idx = shm->event_read_idx % TWIN_HEADLESS_MAX_EVENTS; + twin_headless_event_t *evt = &shm->events[idx]; + twin_event_t twin_evt; + + memset(&twin_evt, 0, sizeof(twin_evt)); + twin_evt.kind = evt->kind; + + switch (evt->kind) { + case TwinEventButtonDown: + case TwinEventButtonUp: + case TwinEventMotion: + twin_evt.u.pointer.screen_x = evt->u.pointer.x; + twin_evt.u.pointer.screen_y = evt->u.pointer.y; + twin_evt.u.pointer.button = evt->u.pointer.button; + _twin_headless_track_event(tx, "mouse"); + break; + + default: + _twin_headless_track_event(tx, "other"); + break; + } + + twin_screen_dispatch(screen, &twin_evt); + shm->event_read_idx++; + } +} + +static void twin_headless_update_damage(void *closure) +{ + twin_context_t *ctx = closure; + twin_screen_t *screen = ctx->screen; + twin_headless_t *tx = ctx->priv; + + if (!twin_screen_damaged(screen)) + return; + + twin_screen_update(screen); + + /* Flip buffers - copy back buffer to front buffer */ + size_t buffer_size = screen->width * screen->height * sizeof(twin_argb32_t); + memcpy(tx->front_buffer, tx->back_buffer, buffer_size); + + /* Update frame statistics */ + uint64_t now = _twin_headless_get_timestamp(); + tx->frame_count++; + + /* Calculate FPS every second */ + uint64_t elapsed = now - tx->last_frame_time; + if (elapsed >= 1000000) { /* 1 second in microseconds */ + if (tx->shm) { + tx->shm->frames_per_second = + (uint32_t) ((double) tx->frame_count * 1000000.0 / elapsed); + tx->shm->frames_rendered_total += tx->frame_count; + } + tx->last_frame_time = now; + tx->frame_count = 0; + } +} + +static void twin_headless_configure(twin_context_t *ctx) +{ + twin_screen_t *screen = ctx->screen; + + screen->put_begin = _twin_headless_put_begin; + screen->put_span = _twin_headless_put_span; +} + +static bool twin_headless_poll(twin_context_t *ctx) +{ + twin_headless_t *tx = ctx->priv; + + if (!tx->running) + return false; + + /* Process commands from control tool */ + _twin_headless_process_commands(ctx); + + /* Inject events from control tool */ + _twin_headless_inject_events(ctx); + + /* Screen update is handled by damage callback, no need to call here */ + + /* Update heartbeat timestamp periodically to show backend is alive */ + if (tx->shm) { + uint64_t now = _twin_headless_get_timestamp(); + uint64_t elapsed = now - tx->shm->last_activity_timestamp; + + /* Update heartbeat every 500ms when idle to show backend is alive */ + if (elapsed >= 500000) /* 500ms in microseconds */ + tx->shm->last_activity_timestamp = now; + } + + return tx->running; +} + +static void twin_headless_exit(twin_context_t *ctx) +{ + if (!ctx) + return; + + twin_headless_t *tx = ctx->priv; + if (tx) { + if (tx->shm) + munmap(tx->shm, tx->shm_size); + if (tx->shm_fd >= 0) { + close(tx->shm_fd); + shm_unlink(TWIN_HEADLESS_SHM_NAME); + } + free(tx); + } + free(ctx); +} + +twin_context_t *twin_headless_init(int width, int height) +{ + twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); + if (!ctx) + return NULL; + + ctx->priv = calloc(1, sizeof(twin_headless_t)); + if (!ctx->priv) { + free(ctx); + return NULL; + } + + twin_headless_t *tx = ctx->priv; + tx->running = true; + tx->shm_fd = -1; + + /* Calculate shared memory size (header + 2 buffers for double buffering) */ + size_t header_size = sizeof(twin_headless_shm_t); + size_t fb_size = width * height * sizeof(twin_argb32_t); + tx->shm_size = header_size + (fb_size * 2); /* Two buffers */ + + /* Create shared memory */ + tx->shm_fd = shm_open(TWIN_HEADLESS_SHM_NAME, O_CREAT | O_RDWR, 0666); + if (tx->shm_fd < 0) { + fprintf(stderr, "Failed to create shared memory: %s\n", + strerror(errno)); + goto error; + } + + /* Ensure size is page-aligned for macOS */ + size_t page_size = getpagesize(); + tx->shm_size = ((tx->shm_size + page_size - 1) / page_size) * page_size; + + if (ftruncate(tx->shm_fd, tx->shm_size) < 0) { + fprintf(stderr, "Failed to resize shared memory to %zu bytes: %s\n", + tx->shm_size, strerror(errno)); + goto error; + } + + /* Map shared memory */ + tx->shm = mmap(NULL, tx->shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, + tx->shm_fd, 0); + if (tx->shm == MAP_FAILED) { + fprintf(stderr, "Failed to map shared memory: %s\n", strerror(errno)); + goto error; + } + + /* Initialize shared memory header */ + memset(tx->shm, 0, tx->shm_size); + tx->shm->magic = TWIN_HEADLESS_MAGIC; + tx->shm->version = TWIN_HEADLESS_VERSION; + tx->shm->width = width; + tx->shm->height = height; + + /* Initialize event tracking fields */ + uint64_t now = _twin_headless_get_timestamp(); + tx->shm->last_activity_timestamp = now; + tx->shm->events_second_timestamp = now; + tx->shm->events_processed_total = 0; + tx->shm->events_processed_second = 0; + tx->shm->commands_processed_total = 0; + tx->shm->mouse_events_total = 0; + tx->shm->frames_per_second = 0; + tx->shm->frames_rendered_total = 0; + + /* Initialize frame tracking */ + tx->last_frame_time = now; + tx->frame_count = 0; + + /* Set framebuffer pointers */ + tx->front_buffer = (twin_argb32_t *) ((char *) tx->shm + header_size); + tx->back_buffer = tx->front_buffer + (width * height); + tx->framebuffer = tx->back_buffer; /* Start rendering to back buffer */ + + /* Create screen */ + ctx->screen = twin_screen_create(width, height, _twin_headless_put_begin, + _twin_headless_put_span, ctx); + if (!ctx->screen) + goto error; + + twin_screen_register_damaged(ctx->screen, twin_headless_update_damage, ctx); + + return ctx; + +error: + if (tx->shm && tx->shm != MAP_FAILED) + munmap(tx->shm, tx->shm_size); + if (tx->shm_fd >= 0) { + close(tx->shm_fd); + shm_unlink(TWIN_HEADLESS_SHM_NAME); + } + free(ctx->priv); + free(ctx); + return NULL; +} + +/* Register the headless backend */ +const twin_backend_t g_twin_backend = { + .init = twin_headless_init, + .configure = twin_headless_configure, + .poll = twin_headless_poll, + .exit = twin_headless_exit, +}; diff --git a/configs/Kconfig b/configs/Kconfig index 298147a7..36cc35a2 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -6,16 +6,16 @@ config CONFIGURED # Dependency detection using Kconfiglib shell function config HAVE_SDL2 - def_bool $(shell,pkg-config --exists sdl2 && echo y) + def_bool $(shell,pkg-config --exists sdl2 && echo y || echo n) config HAVE_PIXMAN - def_bool $(shell,pkg-config --exists pixman-1 && echo y) + def_bool $(shell,pkg-config --exists pixman-1 && echo y || echo n) config HAVE_LIBPNG - def_bool $(shell,pkg-config --exists libpng && echo y) + def_bool $(shell,pkg-config --exists libpng && echo y || echo n) config HAVE_LIBJPEG - def_bool $(shell,pkg-config --exists libjpeg && echo y) + def_bool $(shell,pkg-config --exists libjpeg && echo y || echo n) choice prompt "Backend Selection" @@ -31,6 +31,9 @@ config BACKEND_SDL config BACKEND_VNC bool "VNC server output support" + +config BACKEND_HEADLESS + bool "Headless backend" endchoice choice @@ -174,11 +177,16 @@ config TOOLS config TOOL_FONTEDIT bool "Build scalable font editor" default y - depends on TOOLS + depends on TOOLS && HAVE_SDL2 config PERF_TEST bool "Build performance tester" default y depends on TOOLS +config TOOL_HEADLESS_CTL + bool "Build headless backend control tool" + default y + depends on TOOLS && BACKEND_HEADLESS + endmenu diff --git a/docs/backends.md b/docs/backends.md new file mode 100644 index 00000000..c4ee863d --- /dev/null +++ b/docs/backends.md @@ -0,0 +1,294 @@ +# Graphics Backends for Mado + +Mado supports multiple graphics backends to accommodate different deployment scenarios and requirements. Each backend provides the same API to applications, ensuring code portability across different display systems. + +All backends maintain the same application interface, so switching between them requires only a recompilation, not code changes. + +## Available Backends + +### SDL Backend +The SDL (Simple DirectMedia Layer) backend provides cross-platform graphics output with hardware acceleration support. + +**Features:** +- Cross-platform (Windows, macOS, Linux, etc.) +- Hardware-accelerated rendering when available +- Full keyboard and mouse input support +- Windowed and fullscreen modes +- Audio support (if needed) + +**Build:** +```shell +make BACKEND=sdl +# or +make config # Select "SDL video output support" +``` + +**Use Cases:** +- Desktop applications +- Cross-platform development +- Games and multimedia applications + +### Linux Framebuffer (fbdev) +Direct framebuffer access for embedded Linux systems without X11/Wayland. + +**Features:** +- Direct hardware access +- Minimal dependencies +- Built-in cursor support +- Linux input subsystem integration +- Virtual terminal switching support + +**Build:** +```shell +make BACKEND=fbdev +# or +make config # Select "Linux framebuffer support" +``` + +**Use Cases:** +- Embedded Linux systems +- Kiosk applications +- Boot splash screens +- Lightweight desktop environments + +### VNC Backend +Provides remote display capabilities through the VNC (Virtual Network Computing) protocol. + +**Features:** +- Remote access over network +- Multiple client support +- Platform-independent clients +- Built-in authentication +- Compression support + +**Build:** +```shell +make BACKEND=vnc +# or +make config # Select "VNC server output support" +``` + +**Use Cases:** +- Remote desktop applications +- Server-side rendering +- Thin client deployments +- Remote administration tools + +### Headless Backend +Renders to a memory buffer without any display output, ideal for testing and automation. + +**Features:** +- No display dependencies +- Shared memory architecture +- Screenshot capability +- Event injection for testing +- Memory debugging friendly + +**Build:** +```shell +# Using setconfig.py (recommended) +make defconfig +python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig \ + BACKEND_HEADLESS=y \ + TOOLS=y \ + TOOL_HEADLESS_CTL=y +make + +# Alternative: Interactive configuration +make config # Select "Headless backend" and "Headless control tool" +make +``` + +**Use Cases:** +- Automated testing +- CI/CD pipelines +- Memory analysis +- Screenshot generation +- Performance benchmarking + +## Backend Selection + +Backends are selected at compile time through the Kconfig-based build system. + +### Configuration Methods + +Mado uses [Kconfiglib](https://github.com/sysprog21/Kconfiglib) for configuration management, providing several methods to configure backends: + +**Method 1: Interactive Configuration** (best for exploration) +```shell +make config # Terminal-based menu +# or +make menuconfig # Alternative terminal interface +make +``` + +**Method 2: Using `setconfig.py`** (best for scripting) +```shell +make defconfig +python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig \ + BACKEND_SDL=y # or BACKEND_FBDEV=y, BACKEND_VNC=y, etc. +make +``` + +Advantages: +- Single command to set multiple options +- Automatically handles dependencies +- No need to manually edit `.config` +- Symbol names don't require `CONFIG_` prefix + +**Method 3: Configuration Fragments** (best for CI/CD) +```shell +# Create a fragment file (e.g., configs/mybackend.fragment) +# Contents: CONFIG_BACKEND_SDL=y +# CONFIG_TOOLS=y + +make defconfig +python3 tools/kconfig/examples/merge_config.py configs/Kconfig .config \ + configs/defconfig configs/mybackend.fragment +make +``` + +Advantages: +- Reusable configuration snippets +- Version controllable +- Easy to share and document +- Handles dependency conflicts automatically + +## Common Backend Interface + +All backends implement the same core interface: +- Screen initialization and configuration +- Pixel buffer management +- Event handling (mouse, keyboard) +- Screen updates and synchronization +- Cleanup and resource management + +This ensures applications written for Mado work seamlessly across all backends without modification. + +## Headless Backend Details + +The headless backend deserves special attention as it enables unique testing and automation capabilities. + +### Architecture + +The headless backend uses POSIX shared memory for inter-process communication: +- Server: The Mado application with headless backend creates and manages the shared memory +- Client: The `headless-ctl` tool connects to the shared memory to control and monitor the backend + +### Headless Control Tool (headless-ctl) + +The `headless-ctl` tool provides command-line access to the headless backend: + +#### Commands + +- `status`: Get backend status with detailed statistics + - Screen dimensions + - Rendering performance (FPS, frame count) + - Event statistics + - Last activity timestamp +- `shot FILE.png`: Save current framebuffer to FILE.png + - Supports PNG format with minimal dependencies + - No external image libraries required +- `shutdown`: Gracefully shutdown the backend +- `mouse TYPE X Y [BUTTON]`: Inject mouse events + - TYPE: move, down, up + - X, Y: Screen coordinates (validated against screen size) + - BUTTON: Button number (optional, default 0) +- `monitor`: Live monitoring of backend activity + - Real-time FPS display + - Frame rendering statistics + - Event processing counts + - Backend health status + +#### Examples + +```shell +# Check backend status +$ ./headless-ctl status +events=42 frames=1250 fps=60 running=1 +Screen: 640x480 +Rendering: 60 FPS, 1250 frames total +Events: 42 total (0/sec, 12 mouse) +Commands: 1 +Last Activity: 14:23:45.123456 +Status: Active (12.3ms ago) + +# Take a screenshot +$ ./headless-ctl shot screenshot.png +Screenshot data available in framebuffer +Screenshot saved to screenshot.png + +# Simulate mouse click at (100, 200) +./headless-ctl mouse move 100 200 +./headless-ctl mouse down 100 200 1 +./headless-ctl mouse up 100 200 1 + +# Monitor backend activity in real-time +$ ./headless-ctl monitor +Monitoring backend activity (Ctrl+C to stop)... +Time Events Frames FPS Status +--------------- ------ ------ ---- -------- +14:23:45.123456 42 1250 60 ACTIVE +14:23:46.234567 42 1310 60 IDLE +14:23:47.345678 45 1370 60 ACTIVE +``` + +### Memory Debugging + +The headless backend is ideal for memory analysis: + +```shell +# Run with Valgrind +valgrind --leak-check=full --show-leak-kinds=all \ + --track-origins=yes ./demo-headless & + +# In another terminal, interact with the backend +./headless-ctl mouse move 100 100 +./headless-ctl shot test.png +./headless-ctl shutdown + +# Run with AddressSanitizer +make defconfig +echo "CONFIG_BACKEND_HEADLESS=y" >> .config +python3 tools/kconfig/genconfig.py configs/Kconfig +make CFLAGS="-fsanitize=address -g" +./demo-headless +``` + +### Limitations + +- Single shared memory segment (one backend instance at a time) +- No hardware acceleration (software rendering only) +- PNG output format uses uncompressed DEFLATE for simplicity +- Maximum 64 queued events in circular buffer +- Memory usage doubled due to double buffering (front + back buffers) +- Coordinate validation enforced - events outside screen bounds are rejected +- Command timeout set to 3 seconds for control tool operations + +## Backend-Specific Configuration + +Each backend may have specific configuration options: + +### SDL Backend +- Window size and position +- Fullscreen mode toggle +- Hardware acceleration preferences +- VSync settings + +### Framebuffer Backend +- Device path (default: `/dev/fb0`) +- Input device configuration +- Cursor settings +- Color depth adaptation + +### VNC Backend +- Port number (default: 5900) +- Authentication methods +- Compression levels +- Client connection limits + +### Headless Backend +- Buffer dimensions +- Shared memory path +- Event queue size +- Screenshot triggers diff --git a/src/draw-common.c b/src/draw-common.c index eb4d9bf5..859eeeaa 100644 --- a/src/draw-common.c +++ b/src/draw-common.c @@ -184,7 +184,7 @@ void twin_shadow_border(twin_pixmap_t *shadow, * Create a shadow with a diagonal shape, extending from the * top-left to the bottom-right. */ - clip = min((y - y_start), offset_x); + clip = min((twin_coord_t) (y - y_start), offset_x); twin_cover(shadow, color, right_edge, y, clip); } else { /* Calculate the range of the corner. */ @@ -203,7 +203,8 @@ void twin_shadow_border(twin_pixmap_t *shadow, * Handle the case where the vertical shadow offset is larger than * the horizontal shadow offset. */ - corner_offset = min(right_span - right_edge, offset); + corner_offset = + min((twin_coord_t) (right_span - right_edge), offset); for (twin_coord_t i = 0; i < corner_offset; i++) { /* The corner's pixels are symmetrical to the diagonal. */ twin_cover(shadow, color, right_edge + i, y, 1); diff --git a/tools/font-edit/font-edit.c b/tools/font-edit/font-edit.c index 3987a575..a0121070 100644 --- a/tools/font-edit/font-edit.c +++ b/tools/font-edit/font-edit.c @@ -46,7 +46,8 @@ static int offsets[1024]; */ static bool exit_window = false; -static int init(int argc, char **argv) +static int init(int argc __attribute__((unused)), + char **argv __attribute__((unused))) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { printf("Failed to initialize SDL video. Reason: %s\n", SDL_GetError()); diff --git a/tools/headless-ctl.c b/tools/headless-ctl.c new file mode 100644 index 00000000..71a08ec9 --- /dev/null +++ b/tools/headless-ctl.c @@ -0,0 +1,537 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2025 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../backend/headless-shm.h" + +/* Get current timestamp in microseconds */ +static uint64_t get_timestamp(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (uint64_t) tv.tv_sec * 1000000 + tv.tv_usec; +} + +/* Format timestamp for display */ +static void format_timestamp(uint64_t timestamp, char *buffer, size_t size) +{ + time_t sec = timestamp / 1000000; + int usec = timestamp % 1000000; + struct tm *tm = localtime(&sec); + snprintf(buffer, size, "%02d:%02d:%02d.%06d", tm->tm_hour, tm->tm_min, + tm->tm_sec, usec); +} + +/* Calculate time difference in milliseconds */ +static double time_diff_ms(uint64_t start, uint64_t end) +{ + return (double) (end - start) / 1000.0; +} + +/* Writes PNG files without external dependencies */ + +/* Portable byte swap for 32-bit values */ +#if defined(__GNUC__) || defined(__clang__) +#define BSWAP32(x) __builtin_bswap32(x) +#else +static inline uint32_t bswap32(uint32_t x) +{ + return ((x & 0x000000ff) << 24) | ((x & 0x0000ff00) << 8) | + ((x & 0x00ff0000) >> 8) | ((x & 0xff000000) >> 24); +} +#define BSWAP32(x) bswap32(x) +#endif + +/* CRC32 table for PNG chunk verification */ +static const uint32_t crc32_table[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, + 0x4db26158, 0x5005713c, 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, + 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, +}; + +/* Calculate CRC32 checksum */ +static uint32_t crc32(uint32_t crc, const uint8_t *buf, size_t len) +{ + crc = ~crc; + for (size_t i = 0; i < len; i++) { + crc ^= buf[i]; + crc = (crc >> 4) ^ crc32_table[crc & 0x0f]; + crc = (crc >> 4) ^ crc32_table[crc & 0x0f]; + } + return ~crc; +} + +/* Write PNG file with minimal dependencies */ +static int save_png(const char *filename, + const twin_argb32_t *pixels, + int width, + int height) +{ + FILE *fp = fopen(filename, "wb"); + if (!fp) + return -1; + + /* PNG magic bytes */ + static const uint8_t png_sig[8] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + }; + fwrite(png_sig, 1, 8, fp); + +/* Helper macros for writing PNG chunks */ +#define PUT_U32(u) \ + do { \ + uint8_t b[4]; \ + b[0] = (u) >> 24; \ + b[1] = (u) >> 16; \ + b[2] = (u) >> 8; \ + b[3] = (u); \ + fwrite(b, 1, 4, fp); \ + } while (0) + +#define PUT_BYTES(buf, len) fwrite(buf, 1, len, fp) + + /* Write IHDR chunk */ + uint8_t ihdr[13]; + uint32_t *p32 = (uint32_t *) ihdr; + p32[0] = BSWAP32(width); + p32[1] = BSWAP32(height); + ihdr[8] = 8; /* bit depth */ + ihdr[9] = 6; /* color type: RGBA */ + ihdr[10] = 0; /* compression */ + ihdr[11] = 0; /* filter */ + ihdr[12] = 0; /* interlace */ + + PUT_U32(13); /* chunk length */ + PUT_BYTES("IHDR", 4); + PUT_BYTES(ihdr, 13); + uint32_t crc = crc32(0, (uint8_t *) "IHDR", 4); + crc = crc32(crc, ihdr, 13); + PUT_U32(crc); + + /* Write IDAT chunk */ + size_t raw_size = height * (1 + width * 4); /* filter byte + RGBA per row */ + size_t max_deflate_size = + raw_size + ((raw_size + 7) >> 3) + ((raw_size + 63) >> 6) + 11; + + uint8_t *idat = malloc(max_deflate_size); + if (!idat) { + fclose(fp); + return -1; + } + + /* Simple uncompressed DEFLATE block */ + size_t idat_size = 0; + idat[idat_size++] = 0x78; /* ZLIB header */ + idat[idat_size++] = 0x01; + + /* Write uncompressed blocks */ + uint8_t *raw_data = malloc(raw_size); + if (!raw_data) { + free(idat); + fclose(fp); + return -1; + } + + /* Convert ARGB to RGBA with filter bytes */ + size_t raw_pos = 0; + for (int y = 0; y < height; y++) { + raw_data[raw_pos++] = 0; /* filter type: none */ + for (int x = 0; x < width; x++) { + twin_argb32_t pixel = pixels[y * width + x]; + raw_data[raw_pos++] = (pixel >> 16) & 0xff; /* R */ + raw_data[raw_pos++] = (pixel >> 8) & 0xff; /* G */ + raw_data[raw_pos++] = pixel & 0xff; /* B */ + raw_data[raw_pos++] = (pixel >> 24) & 0xff; /* A */ + } + } + + /* Write as uncompressed DEFLATE blocks */ + size_t pos = 0; + while (pos < raw_size) { + size_t chunk = raw_size - pos; + if (chunk > 65535) + chunk = 65535; + + /* final block flag */ + idat[idat_size++] = (pos + chunk >= raw_size) ? 1 : 0; + idat[idat_size++] = chunk & 0xff; + idat[idat_size++] = (chunk >> 8) & 0xff; + idat[idat_size++] = ~chunk & 0xff; + idat[idat_size++] = (~chunk >> 8) & 0xff; + + memcpy(idat + idat_size, raw_data + pos, chunk); + idat_size += chunk; + pos += chunk; + } + + /* ADLER32 checksum */ + uint32_t adler = 1; + for (size_t i = 0; i < raw_size; i++) { + adler = (adler + raw_data[i]) % 65521 + + ((((adler >> 16) + raw_data[i]) % 65521) << 16); + } + idat[idat_size++] = (adler >> 24) & 0xff; + idat[idat_size++] = (adler >> 16) & 0xff; + idat[idat_size++] = (adler >> 8) & 0xff; + idat[idat_size++] = adler & 0xff; + + PUT_U32(idat_size); + PUT_BYTES("IDAT", 4); + PUT_BYTES(idat, idat_size); + crc = crc32(0, (uint8_t *) "IDAT", 4); + crc = crc32(crc, idat, idat_size); + PUT_U32(crc); + + /* Write IEND chunk */ + PUT_U32(0); + PUT_BYTES("IEND", 4); + PUT_U32(crc32(0, (uint8_t *) "IEND", 4)); + +#undef PUT_U32 +#undef PUT_BYTES + + free(raw_data); + free(idat); + fclose(fp); + return 0; +} + +static twin_headless_shm_t *connect_to_backend(int *fd_out, size_t *size_out) +{ + int fd = shm_open(TWIN_HEADLESS_SHM_NAME, O_RDWR, 0666); + if (fd < 0) { + fprintf(stderr, "Failed to open shared memory: %s\n", strerror(errno)); + fprintf(stderr, "Is the headless backend running?\n"); + return NULL; + } + + /* Get size */ + struct stat st; + if (fstat(fd, &st) < 0) { + fprintf(stderr, "Failed to get shared memory size: %s\n", + strerror(errno)); + close(fd); + return NULL; + } + + /* Map shared memory */ + void *addr = + mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) { + fprintf(stderr, "Failed to map shared memory: %s\n", strerror(errno)); + close(fd); + return NULL; + } + + twin_headless_shm_t *shm = (twin_headless_shm_t *) addr; + + /* Verify magic and version */ + if (shm->magic != TWIN_HEADLESS_MAGIC) { + fprintf(stderr, "Invalid shared memory magic\n"); + munmap(addr, st.st_size); + close(fd); + return NULL; + } + + if (shm->version != TWIN_HEADLESS_VERSION) { + fprintf(stderr, "Version mismatch (expected %d, got %d)\n", + TWIN_HEADLESS_VERSION, shm->version); + munmap(addr, st.st_size); + close(fd); + return NULL; + } + + *fd_out = fd; + *size_out = st.st_size; + return shm; +} + +static int send_command(twin_headless_shm_t *shm, + twin_headless_cmd_t cmd, + const char *data) +{ + static uint32_t seq = 0; + + shm->command = cmd; + if (data) + strncpy(shm->command_data, data, sizeof(shm->command_data) - 1); + else + shm->command_data[0] = '\0'; + + shm->command_seq = ++seq; + + /* Wait for response (with timeout) */ + int timeout = 30; /* 3 seconds total */ + while (shm->response_seq != seq && timeout > 0) { + usleep(100000); /* 100ms */ + timeout--; + } + + if (timeout == 0) { + fprintf(stderr, "Command timeout (backend may be unresponsive)\n"); + return -1; + } + + if (shm->response_status != 0) { + fprintf(stderr, "Command failed: %s\n", shm->response_data); + return -1; + } + + printf("%s\n", shm->response_data); + return 0; +} + +static int inject_mouse_event(twin_headless_shm_t *shm, + const char *type, + int x, + int y, + int button) +{ + /* Validate coordinates */ + if (x < 0 || y < 0 || x >= shm->width || y >= shm->height) { + fprintf(stderr, "Invalid coordinates (%d, %d) - screen size is %dx%d\n", + x, y, shm->width, shm->height); + return -1; + } + + /* Check for event queue overflow */ + uint32_t pending = shm->event_write_idx - shm->event_read_idx; + if (pending >= TWIN_HEADLESS_MAX_EVENTS) { + fprintf(stderr, + "Event queue full (%u pending) - backend may be stalled\n", + pending); + return -1; + } + + uint32_t idx = shm->event_write_idx % TWIN_HEADLESS_MAX_EVENTS; + twin_headless_event_t *evt = &shm->events[idx]; + + if (strcmp(type, "move") == 0) { + evt->kind = TwinEventMotion; + } else if (strcmp(type, "down") == 0) { + evt->kind = TwinEventButtonDown; + } else if (strcmp(type, "up") == 0) { + evt->kind = TwinEventButtonUp; + } else { + fprintf(stderr, "Unknown mouse event type: %s (use: move/down/up)\n", + type); + return -1; + } + + evt->u.pointer.x = x; + evt->u.pointer.y = y; + evt->u.pointer.button = button; + + shm->event_write_idx++; + return 0; +} + + +static void print_usage(const char *prog) +{ + printf("Usage: %s [options] command\n", prog); + printf("\nCommands:\n"); + printf(" status Get backend status\n"); + printf(" shot FILE Save screenshot to FILE.png\n"); + printf(" shutdown Shutdown the backend\n"); + printf(" mouse TYPE X Y [B] Inject mouse event (TYPE: move/down/up)\n"); + printf(" monitor Monitor backend activity\n"); + printf("\nOptions:\n"); + printf(" -h, --help Show this help\n"); + printf("\nExamples:\n"); + printf(" %s status\n", prog); + printf(" %s shot output.png\n", prog); + printf(" %s mouse move 100 200\n", prog); + printf(" %s mouse down 100 200 1\n", prog); +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + print_usage(argv[0]); + return 1; + } + + /* Parse options */ + int opt; + static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0}, + }; + + while ((opt = getopt_long(argc, argv, "h", long_options, NULL)) != -1) { + switch (opt) { + case 'h': + print_usage(argv[0]); + return 0; + default: + print_usage(argv[0]); + return 1; + } + } + + if (optind >= argc) { + print_usage(argv[0]); + return 1; + } + + const char *command = argv[optind]; + + /* Connect to backend */ + int shm_fd; + size_t shm_size; + twin_headless_shm_t *shm = connect_to_backend(&shm_fd, &shm_size); + if (!shm) + return 1; + + int ret = 0; + + /* Support aliases for shot command */ + if (strcmp(command, "s") == 0 || strcmp(command, "screenshot") == 0) { + command = "shot"; + } + + if (strcmp(command, "status") == 0) { + ret = send_command(shm, TWIN_HEADLESS_CMD_GET_STATE, NULL); + if (ret == 0) { + uint64_t now = get_timestamp(); + char last_activity_str[32]; + format_timestamp(shm->last_activity_timestamp, last_activity_str, + sizeof(last_activity_str)); + + printf("Screen: %dx%d\n", shm->width, shm->height); + printf("Rendering: %u FPS, %u frames total\n", + shm->frames_per_second, shm->frames_rendered_total); + printf("Events: %u total (%u/sec, %u mouse)\n", + shm->events_processed_total, shm->events_processed_second, + shm->mouse_events_total); + printf("Commands: %u\n", shm->commands_processed_total); + + /* Event queue status */ + uint32_t pending = shm->event_write_idx - shm->event_read_idx; + if (pending > 0) { + printf("Event Queue: %u pending\n", pending); + } + + printf("Last Activity: %s\n", last_activity_str); + + double age_ms = time_diff_ms(shm->last_activity_timestamp, now); + if (age_ms < 1000) { + printf("Status: Active (%.1fms ago)\n", age_ms); + } else if (age_ms < 5000) { + printf("Status: Idle (%.1fs ago)\n", age_ms / 1000.0); + } else { + printf("Status: Stale (%.1fs ago)\n", age_ms / 1000.0); + } + } + } else if (strcmp(command, "shot") == 0) { + if (optind + 1 >= argc) { + fprintf(stderr, "Missing filename argument\n"); + ret = 1; + } else { + const char *filename = argv[optind + 1]; + + /* Get front buffer pointer (first buffer after header) */ + size_t header_size = sizeof(twin_headless_shm_t); + twin_argb32_t *fb = (twin_argb32_t *) ((char *) shm + header_size); + + /* Save as PNG */ + if (save_png(filename, fb, shm->width, shm->height) < 0) { + fprintf(stderr, "Failed to save screenshot: %s\n", + strerror(errno)); + ret = 1; + } else { + printf("Screenshot saved to %s\n", filename); + } + } + } else if (strcmp(command, "shutdown") == 0) { + ret = send_command(shm, TWIN_HEADLESS_CMD_SHUTDOWN, NULL); + } else if (strcmp(command, "mouse") == 0) { + if (optind + 3 >= argc) { + fprintf(stderr, "Usage: mouse TYPE X Y [BUTTON]\n"); + ret = 1; + } else { + const char *type = argv[optind + 1]; + int x = atoi(argv[optind + 2]); + int y = atoi(argv[optind + 3]); + int button = (optind + 4 < argc) ? atoi(argv[optind + 4]) : 0; + + ret = inject_mouse_event(shm, type, x, y, button); + if (ret == 0) + printf("Mouse event injected\n"); + } + } else if (strcmp(command, "monitor") == 0) { + printf("Monitoring backend activity (Ctrl+C to stop)...\n"); + printf("Time Events Frames FPS Status\n"); + printf("--------------- ------ ------ ---- --------\n"); + + uint32_t last_events = shm->events_processed_total; + uint32_t last_frames = shm->frames_rendered_total; + uint64_t last_timestamp = shm->last_activity_timestamp; + uint64_t last_print_time = get_timestamp(); + + while (1) { + uint64_t now = get_timestamp(); + bool events_updated = (shm->events_processed_total != last_events); + bool frames_updated = (shm->frames_rendered_total != last_frames); + bool timestamp_updated = + (shm->last_activity_timestamp != last_timestamp); + + /* Print update when something changes or every second */ + uint64_t since_print = now - last_print_time; + if (events_updated || frames_updated || timestamp_updated || + since_print >= 1000000) { + char time_str[20]; + format_timestamp(shm->last_activity_timestamp, time_str, + sizeof(time_str)); + + double age_ms = time_diff_ms(shm->last_activity_timestamp, now); + const char *status; + if (age_ms < 100) + status = "ACTIVE"; + else if (age_ms < 1000) + status = "IDLE"; + else + status = "STALE"; + + printf("%-15s %6u %6u %4u %s\n", time_str, + shm->events_processed_total, shm->frames_rendered_total, + shm->frames_per_second, status); + + last_events = shm->events_processed_total; + last_frames = shm->frames_rendered_total; + last_timestamp = shm->last_activity_timestamp; + last_print_time = now; + } + + usleep(100000); /* 100ms */ + } + } else { + fprintf(stderr, "Unknown command: %s\n", command); + print_usage(argv[0]); + ret = 1; + } + + /* Cleanup */ + munmap(shm, shm_size); + close(shm_fd); + + return ret; +}