Skip to content

Commit a50cc03

Browse files
committed
Add headless backend for testing and automation
This commit implements a headless graphics backend that renders to shared memory instead of physical display, enabling automated testing, CI/CD integration, and memory analysis without GUI dependencies. - POSIX shared memory architecture for IPC - Double buffering (front/back buffers) - Real-time FPS tracking and statistics - PNG screenshot capability with built-in encoder - Event queue with overflow detection (max 64 events) - Graceful shutdown command Use cases: - Automated GUI testing in CI/CD pipelines - Memory leak detection with Valgrind/ASan - Performance benchmarking without display overhead - Regression testing with pixel-perfect comparison
1 parent 6c49b0d commit a50cc03

File tree

9 files changed

+1435
-3
lines changed

9 files changed

+1435
-3
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
name: Headless Backend Tests
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
headless-test:
7+
runs-on: ubuntu-24.04
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
with:
12+
submodules: recursive
13+
14+
- name: Install dependencies
15+
run: |
16+
sudo apt-get update
17+
sudo apt-get install -y valgrind imagemagick python3
18+
19+
- name: Configure build
20+
run: |
21+
make defconfig
22+
python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig \
23+
BACKEND_HEADLESS=y \
24+
TOOLS=y \
25+
TOOL_HEADLESS_CTL=y \
26+
DEMO_MULTI=y
27+
28+
- name: Build with headless backend
29+
run: |
30+
make -j$(nproc)
31+
32+
- name: Verify build outputs
33+
run: |
34+
test -x ./demo-headless || (echo "demo-headless not built" && exit 1)
35+
test -x ./headless-ctl || (echo "headless-ctl not built" && exit 1)
36+
echo "Build outputs verified successfully"
37+
38+
- name: Run basic headless test
39+
run: |
40+
# Start demo in background
41+
timeout 30s ./demo-headless &
42+
DEMO_PID=$!
43+
echo "Started demo-headless with PID $DEMO_PID"
44+
45+
# Wait for backend to initialize
46+
sleep 3
47+
48+
# Test control tool commands
49+
./headless-ctl status
50+
./headless-ctl shot test_output.png
51+
52+
# Inject mouse events
53+
./headless-ctl mouse move 100 100
54+
./headless-ctl mouse down 100 100 1
55+
./headless-ctl mouse up 100 100 1
56+
57+
# Check status again after events
58+
./headless-ctl status
59+
60+
# Shutdown gracefully
61+
./headless-ctl shutdown || true
62+
63+
# Wait for process to exit
64+
wait $DEMO_PID || true
65+
echo "Basic test completed"
66+
67+
- name: Verify screenshot output
68+
run: |
69+
test -f test_output.png || (echo "Screenshot not created" && exit 1)
70+
file test_output.png | grep -q PNG || (echo "Screenshot is not a valid PNG" && exit 1)
71+
echo "Screenshot verification passed"
72+
73+
- name: Test deterministic rendering
74+
run: |
75+
# Run twice and compare outputs for determinism
76+
echo "First run..."
77+
timeout 10s ./demo-headless &
78+
PID1=$!
79+
sleep 3
80+
./headless-ctl shot run1.png
81+
./headless-ctl shutdown
82+
wait $PID1 || true
83+
84+
echo "Second run..."
85+
timeout 10s ./demo-headless &
86+
PID2=$!
87+
sleep 3
88+
./headless-ctl shot run2.png
89+
./headless-ctl shutdown
90+
wait $PID2 || true
91+
92+
# Compare images (allowing minor differences)
93+
DIFF=$(compare -metric AE run1.png run2.png null: 2>&1 || echo "0")
94+
echo "Pixel difference: $DIFF"
95+
96+
if [ "$DIFF" -gt "100" ]; then
97+
echo "ERROR: Renderings differ by more than 100 pixels"
98+
exit 1
99+
fi
100+
echo "Deterministic rendering test passed"
101+
102+
- name: Memory leak detection with Valgrind
103+
run: |
104+
# Run with Valgrind for leak detection
105+
timeout 15s valgrind \
106+
--leak-check=full \
107+
--show-leak-kinds=definite,possible \
108+
--errors-for-leak-kinds=definite \
109+
--error-exitcode=1 \
110+
--log-file=valgrind.log \
111+
./demo-headless &
112+
VALGRIND_PID=$!
113+
114+
sleep 5
115+
116+
# Perform some operations
117+
./headless-ctl mouse move 50 50 || true
118+
./headless-ctl shot valgrind_screenshot.png || true
119+
./headless-ctl shutdown || true
120+
121+
# Wait for Valgrind to finish
122+
wait $VALGRIND_PID || VALGRIND_EXIT=$?
123+
124+
# Display Valgrind summary
125+
echo "=== Valgrind Summary ==="
126+
grep -A 10 "LEAK SUMMARY" valgrind.log || true
127+
grep "ERROR SUMMARY" valgrind.log || true
128+
129+
# Check for definite leaks
130+
if grep -q "definitely lost: [1-9]" valgrind.log; then
131+
echo "ERROR: Memory leaks detected"
132+
cat valgrind.log
133+
exit 1
134+
fi
135+
echo "No definite memory leaks detected"

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ libtwin.a_cflags-y += $(shell pkg-config --cflags neatvnc aml pixman-1)
133133
TARGET_LIBS += $(shell pkg-config --libs neatvnc aml pixman-1)
134134
endif
135135

136+
ifeq ($(CONFIG_BACKEND_HEADLESS), y)
137+
BACKEND = headless
138+
libtwin.a_files-y += backend/headless.c
139+
endif
140+
136141
# Performance tester
137142

138143
ifeq ($(CONFIG_PERF_TEST), y)
@@ -171,6 +176,12 @@ font-edit_cflags-y := \
171176
font-edit_ldflags-y := \
172177
$(shell pkg-config --libs cairo) \
173178
$(shell sdl2-config --libs)
179+
180+
# Headless control tool
181+
target-$(CONFIG_TOOL_HEADLESS_CTL) += headless-ctl
182+
headless-ctl_files-y = tools/headless-ctl.c
183+
headless-ctl_includes-y := include
184+
headless-ctl_ldflags-y := # -lrt
174185
endif
175186

176187
# Build system integration

backend/headless-shm.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Twin - A Tiny Window System
3+
* Copyright (c) 2025 National Cheng Kung University, Taiwan
4+
* All rights reserved.
5+
*/
6+
7+
#ifndef _TWIN_HEADLESS_SHM_H_
8+
#define _TWIN_HEADLESS_SHM_H_
9+
10+
#include <stdint.h>
11+
#include <twin.h>
12+
13+
/* Shared memory layout definitions - shared between backend and control tool */
14+
15+
#define TWIN_HEADLESS_SHM_NAME "/mado-headless-shm"
16+
#define TWIN_HEADLESS_MAGIC 0x5457494E /* "TWIN" */
17+
#define TWIN_HEADLESS_VERSION 1
18+
#define TWIN_HEADLESS_MAX_EVENTS 64
19+
20+
typedef enum {
21+
TWIN_HEADLESS_CMD_NONE = 0,
22+
TWIN_HEADLESS_CMD_SCREENSHOT,
23+
TWIN_HEADLESS_CMD_GET_STATE,
24+
TWIN_HEADLESS_CMD_INJECT_MOUSE,
25+
TWIN_HEADLESS_CMD_SHUTDOWN,
26+
} twin_headless_cmd_t;
27+
28+
typedef struct {
29+
twin_event_kind_t kind;
30+
union {
31+
struct {
32+
twin_coord_t x, y;
33+
twin_count_t button;
34+
} pointer;
35+
} u;
36+
} twin_headless_event_t;
37+
38+
typedef struct {
39+
uint32_t magic;
40+
uint32_t version;
41+
42+
/* Screen info */
43+
twin_coord_t width, height;
44+
45+
/* Event processing statistics */
46+
uint64_t last_activity_timestamp; /* Unix timestamp in microseconds */
47+
uint32_t events_processed_total; /* Total events processed since start */
48+
uint32_t events_processed_second; /* Events processed in current second */
49+
uint64_t events_second_timestamp; /* Timestamp for current second period */
50+
uint32_t commands_processed_total; /* Total commands from control tool */
51+
uint32_t mouse_events_total; /* Total mouse events processed */
52+
53+
/* Rendering statistics */
54+
uint32_t frames_per_second; /* Current FPS */
55+
uint32_t frames_rendered_total; /* Total frames rendered since start */
56+
57+
/* Command interface */
58+
twin_headless_cmd_t command;
59+
uint32_t command_seq;
60+
char command_data[256];
61+
62+
/* Response */
63+
uint32_t response_seq;
64+
int32_t response_status;
65+
char response_data[256];
66+
67+
/* Event injection */
68+
uint32_t event_write_idx, event_read_idx;
69+
twin_headless_event_t events[TWIN_HEADLESS_MAX_EVENTS];
70+
71+
/* Framebuffer follows this header */
72+
/* twin_argb32_t framebuffer[width * height]; */
73+
} twin_headless_shm_t;
74+
75+
#endif /* _TWIN_HEADLESS_SHM_H_ */

0 commit comments

Comments
 (0)