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
16 changes: 16 additions & 0 deletions backend/fbdev.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ typedef struct {
uint16_t cmap[3][256];
uint8_t *fb_base;
size_t fb_len;

/* Initialization state */
bool first_run; /* Track first work queue execution for initial damage */
} twin_fbdev_t;

/* color conversion */
Expand Down Expand Up @@ -201,6 +204,16 @@ static bool twin_fbdev_work(void *closure)
twin_fbdev_t *tx = PRIV(closure);
twin_screen_t *screen = SCREEN(closure);

/* Mark entire screen as damaged on first run to ensure initial rendering.
* This is necessary because the screen content is not automatically
* rendered when the application starts. Using per-context state allows
* proper reinitialization if the backend is torn down and recreated.
*/
if (tx->first_run) {
tx->first_run = false;
twin_screen_damage(screen, 0, 0, screen->width, screen->height);
}

if (tx->vt_active && (tx->fb_base != MAP_FAILED)) {
/* Unmap the fbdev */
munmap(tx->fb_base, tx->fb_len);
Expand Down Expand Up @@ -240,6 +253,9 @@ twin_context_t *twin_fbdev_init(int width, int height)

twin_fbdev_t *tx = ctx->priv;

/* Initialize first_run flag for initial screen damage */
tx->first_run = true;

/* Open the framebuffer device */
tx->fb_fd = open(fbdev_path, O_RDWR);
if (tx->fb_fd == -1) {
Expand Down
216 changes: 194 additions & 22 deletions backend/linux_input.c
Original file line number Diff line number Diff line change
@@ -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.
*/

Expand All @@ -9,6 +9,7 @@
#include <linux/input.h>
#include <poll.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <twin.h>
Expand All @@ -27,6 +28,17 @@ struct evdev_info {
int fd;
};

/* Coordinate smoothing configuration */
/* Weight for smoothing (0=off, 1-7=strength) */
#ifndef TWIN_INPUT_SMOOTH_WEIGHT
#define TWIN_INPUT_SMOOTH_WEIGHT 3
#endif

/* Compile-time bounds check for smoothing weight */
#if TWIN_INPUT_SMOOTH_WEIGHT < 0 || TWIN_INPUT_SMOOTH_WEIGHT > 7
#error "TWIN_INPUT_SMOOTH_WEIGHT must be in range [0, 7]"
#endif

typedef struct {
twin_screen_t *screen;
pthread_t evdev_thread;
Expand All @@ -36,25 +48,69 @@ typedef struct {
int fd;
int btns;
int x, y;
int abs_x_min, abs_y_min; /* Minimum value for ABS_X/ABS_Y from device */
int abs_x_max, abs_y_max; /* Maximum value for ABS_X/ABS_Y from device */
bool abs_range_initialized; /* Whether ABS range has been queried */
#if TWIN_INPUT_SMOOTH_WEIGHT > 0
int smooth_x, smooth_y; /* Smoothed coordinates for ABS events */
bool smooth_initialized; /* Whether smoothing has been initialized */
#endif
} twin_linux_input_t;

static void check_mouse_bounds(twin_linux_input_t *tm)
{
/* Guard against zero-sized screens */
if (tm->screen->width == 0 || tm->screen->height == 0) {
tm->x = tm->y = 0;
return;
}

if (tm->x < 0)
tm->x = 0;
if (tm->x > tm->screen->width)
tm->x = tm->screen->width;
if (tm->x >= tm->screen->width)
tm->x = tm->screen->width - 1;
if (tm->y < 0)
tm->y = 0;
if (tm->y > tm->screen->height)
tm->y = tm->screen->height;
if (tm->y >= tm->screen->height)
tm->y = tm->screen->height - 1;
}

#if TWIN_INPUT_SMOOTH_WEIGHT > 0
/* Apply weighted moving average smoothing to reduce jitter
* Formula: smooth = (smooth * weight + raw) / (weight + 1)
* Higher weight = more smoothing but more latency
*/
static inline void smooth_abs_coords(twin_linux_input_t *tm,
int raw_x,
int raw_y)
{
if (!tm->smooth_initialized) {
/* Cold start: use raw values directly */
tm->smooth_x = raw_x;
tm->smooth_y = raw_y;
tm->smooth_initialized = true;
} else {
/* Weighted moving average with 64-bit intermediates to prevent overflow
*/
int64_t acc_x =
(int64_t) tm->smooth_x * TWIN_INPUT_SMOOTH_WEIGHT + raw_x;
int64_t acc_y =
(int64_t) tm->smooth_y * TWIN_INPUT_SMOOTH_WEIGHT + raw_y;
tm->smooth_x = (int) (acc_x / (TWIN_INPUT_SMOOTH_WEIGHT + 1));
tm->smooth_y = (int) (acc_y / (TWIN_INPUT_SMOOTH_WEIGHT + 1));
}

tm->x = tm->smooth_x;
tm->y = tm->smooth_y;
}
#endif

static void twin_linux_input_events(struct input_event *ev,
twin_linux_input_t *tm)
{
/* TODO: twin_screen_dispatch() should be protect by mutex_lock, but the
* counterpart piece of code with mutex is not yet sure */
* counterpart piece of code with mutex is not yet sure.
*/

twin_event_t tev;

Expand All @@ -79,22 +135,56 @@ static void twin_linux_input_events(struct input_event *ev,
}
break;
case EV_ABS:
/* Scale absolute coordinates to screen resolution.
* The range is dynamically queried from each device using EVIOCGABS.
* Formula: scaled = ((value - min) * (screen_size - 1)) / (max - min)
* This correctly maps device coordinates [min, max] to screen [0,
* size-1]
*/
if (ev->code == ABS_X) {
tm->x = ev->value;
check_mouse_bounds(tm);
tev.kind = TwinEventMotion;
tev.u.pointer.screen_x = tm->x;
tev.u.pointer.screen_y = tm->y;
tev.u.pointer.button = tm->btns;
twin_screen_dispatch(tm->screen, &tev);
int range = tm->abs_x_max - tm->abs_x_min;
if (range > 0) {
int raw_x = ((int64_t) (ev->value - tm->abs_x_min) *
(tm->screen->width - 1)) /
range;
#if TWIN_INPUT_SMOOTH_WEIGHT > 0
/* Apply smoothing to X axis, keep Y unchanged */
smooth_abs_coords(tm, raw_x, tm->y);
#else
tm->x = raw_x;
#endif
check_mouse_bounds(tm);
tev.kind = TwinEventMotion;
tev.u.pointer.screen_x = tm->x;
tev.u.pointer.screen_y = tm->y;
tev.u.pointer.button = tm->btns;
twin_screen_dispatch(tm->screen, &tev);
} else {
log_warn("Ignoring ABS_X event: invalid range [%d,%d]",
tm->abs_x_min, tm->abs_x_max);
}
} else if (ev->code == ABS_Y) {
tm->y = ev->value;
check_mouse_bounds(tm);
tev.kind = TwinEventMotion;
tev.u.pointer.screen_x = tm->x;
tev.u.pointer.screen_y = tm->y;
tev.u.pointer.button = tm->btns;
twin_screen_dispatch(tm->screen, &tev);
int range = tm->abs_y_max - tm->abs_y_min;
if (range > 0) {
int raw_y = ((int64_t) (ev->value - tm->abs_y_min) *
(tm->screen->height - 1)) /
range;
#if TWIN_INPUT_SMOOTH_WEIGHT > 0
/* Apply smoothing to Y axis, keep X unchanged */
smooth_abs_coords(tm, tm->x, raw_y);
#else
tm->y = raw_y;
#endif
check_mouse_bounds(tm);
tev.kind = TwinEventMotion;
tev.u.pointer.screen_x = tm->x;
tev.u.pointer.screen_y = tm->y;
tev.u.pointer.button = tm->btns;
twin_screen_dispatch(tm->screen, &tev);
} else {
log_warn("Ignoring ABS_Y event: invalid range [%d,%d]",
tm->abs_y_min, tm->abs_y_max);
}
}
break;
case EV_KEY:
Expand Down Expand Up @@ -169,6 +259,57 @@ static bool twin_linux_udev_update(struct udev_monitor *mon)
return false;
}

/* Query absolute axis information from an input device */
static void twin_linux_input_query_abs(int fd, twin_linux_input_t *tm)
{
struct input_absinfo abs_info;

/* Query ABS_X range (minimum and maximum) */
if (ioctl(fd, EVIOCGABS(ABS_X), &abs_info) == 0) {
/* Validate range: maximum must be greater than minimum */
if (abs_info.maximum > abs_info.minimum) {
int range = abs_info.maximum - abs_info.minimum;
int current_range = tm->abs_x_max - tm->abs_x_min;

/* Accept first device range unconditionally, or larger ranges from
* subsequent devices
*/
if (!tm->abs_range_initialized || range > current_range) {
log_info("ABS_X range updated: [%d,%d] (device fd=%d)",
abs_info.minimum, abs_info.maximum, fd);
tm->abs_x_min = abs_info.minimum;
tm->abs_x_max = abs_info.maximum;
tm->abs_range_initialized = true;
}
} else {
log_warn("Device fd=%d: ABS_X range invalid [%d,%d]", fd,
abs_info.minimum, abs_info.maximum);
}
}

/* Query ABS_Y range (minimum and maximum) */
if (ioctl(fd, EVIOCGABS(ABS_Y), &abs_info) == 0) {
/* Validate range: maximum must be greater than minimum */
if (abs_info.maximum > abs_info.minimum) {
int range = abs_info.maximum - abs_info.minimum;
int current_range = tm->abs_y_max - tm->abs_y_min;

/* Accept first device range unconditionally, or larger ranges from
* subsequent devices
*/
if (!tm->abs_range_initialized || range > current_range) {
log_info("ABS_Y range updated: [%d,%d] (device fd=%d)",
abs_info.minimum, abs_info.maximum, fd);
tm->abs_y_min = abs_info.minimum;
tm->abs_y_max = abs_info.maximum;
}
} else {
log_warn("Device fd=%d: ABS_Y range invalid [%d,%d]", fd,
abs_info.minimum, abs_info.maximum);
}
}
}

static void twin_linux_edev_open(struct pollfd *pfds, twin_linux_input_t *tm)
{
/* New event device list */
Expand Down Expand Up @@ -200,23 +341,39 @@ static void twin_linux_edev_open(struct pollfd *pfds, twin_linux_input_t *tm)

/* Open the file if it is not on the list */
int fd = open(evdev_name, O_RDWR | O_NONBLOCK);
if (fd > 0 && !opened) {
if (fd >= 0 && !opened) {
/* Query absolute axis info for newly opened devices */
twin_linux_input_query_abs(fd, tm);
evdevs[new_evdev_cnt].idx = i;
evdevs[new_evdev_cnt].fd = fd;
new_evdev_cnt++;
}
}

/* Close disconnected devices */
bool device_list_changed = false;
for (size_t i = 0; i < tm->evdev_cnt; i++) {
if (tm->evdevs[i].fd > 0)
if (tm->evdevs[i].fd > 0) {
close(tm->evdevs[i].fd);
device_list_changed = true;
}
}

/* Check if new devices were added */
if (new_evdev_cnt != tm->evdev_cnt)
device_list_changed = true;

/* Overwrite the evdev list */
memcpy(tm->evdevs, evdevs, sizeof(tm->evdevs));
tm->evdev_cnt = new_evdev_cnt;

#if TWIN_INPUT_SMOOTH_WEIGHT > 0
/* Reset smoothing state on device reconnection to prevent coordinate jumps
*/
if (device_list_changed && tm->smooth_initialized)
tm->smooth_initialized = false;
#endif

/* Initialize evdev poll file descriptors */
for (size_t i = tm->udev_cnt; i < tm->evdev_cnt + tm->udev_cnt; i++) {
pfds[i].fd = tm->evdevs[i - 1].fd;
Expand Down Expand Up @@ -293,10 +450,25 @@ void *twin_linux_input_create(twin_screen_t *screen)

tm->screen = screen;

/* Initialize ABS axis ranges to common touchscreen default.
* These will be updated to actual device values when devices are opened.
*/
tm->abs_x_min = tm->abs_y_min = 0;
tm->abs_x_max = tm->abs_y_max = 32767;
log_info(
"Input system initialized: screen=%dx%d, default ABS range=[%d,%d]",
screen->width, screen->height, tm->abs_x_min, tm->abs_x_max);

/* Centering the cursor position */
tm->x = screen->width / 2;
tm->y = screen->height / 2;

#if TWIN_INPUT_SMOOTH_WEIGHT > 0
/* Initialize smoothing from center position */
tm->smooth_x = tm->x, tm->smooth_y = tm->y;
tm->smooth_initialized = true;
#endif

/* Start event handling thread */
if (pthread_create(&tm->evdev_thread, NULL, twin_linux_evdev_thread, tm)) {
log_error("Failed to create evdev thread");
Expand Down