Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/headless-test.yml
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions backend/headless-shm.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <twin.h>

/* 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_ */
Loading