Skip to content

Commit b27cb98

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 b27cb98

File tree

9 files changed

+1437
-3
lines changed

9 files changed

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