diff --git a/Makefile b/Makefile index b014a51..0468000 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,12 @@ libtwin.a_cflags-y += $(shell sdl2-config --cflags) TARGET_LIBS += $(shell sdl2-config --libs) endif +ifeq ($(CONFIG_BACKEND_FBDEV), y) +BACKEND = fbdev +libtwin.a_files-y += backend/fbdev.c +libtwin.a_files-y += backend/linux_input.c +endif + # Standalone application ifeq ($(CONFIG_DEMO_APPLICATIONS), y) diff --git a/backend/fbdev.c b/backend/fbdev.c new file mode 100644 index 0000000..997ba73 --- /dev/null +++ b/backend/fbdev.c @@ -0,0 +1,315 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2024 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "linux_input.h" +#include "twin_backend.h" + +#define VT_MAX 63 +#define FBDEV_NAME "FRAMEBUFFER" +#define FBDEV_DEFAULT "/dev/fb0" +#define SCREEN(x) ((twin_context_t *) x)->screen +#define PRIV(x) ((twin_fbdev_t *) ((twin_context_t *) x)->priv) + +typedef struct { + twin_screen_t *screen; + + /* Linux input system */ + void *input; + + /* Linux virtual terminal (VT) */ + int vt_fd; + int vt_num; + bool vt_active; + + /* Linux framebuffer */ + int fb_fd; + struct fb_var_screeninfo fb_var; + struct fb_fix_screeninfo fb_fix; + uint16_t cmap[3][256]; + uint8_t *fb_base; + size_t fb_len; +} twin_fbdev_t; + +static void _twin_fbdev_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_fbdev_t *tx = PRIV(closure); + + if (tx->fb_base == MAP_FAILED) + return; + + twin_coord_t width = right - left; + off_t off = top * screen->width + left; + uint32_t *src = pixels; + uint32_t *dest = + (uint32_t *) ((uintptr_t) tx->fb_base + (off * sizeof(uint32_t))); + memcpy(dest, src, width * sizeof(uint32_t)); +} + +static void twin_fbdev_get_screen_size(twin_fbdev_t *tx, + int *width, + int *height) +{ + struct fb_var_screeninfo info; + ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &info); + *width = info.xres; + *height = info.yres; +} + +static void twin_fbdev_damage(twin_screen_t *screen, twin_fbdev_t *tx) +{ + int width, height; + twin_fbdev_get_screen_size(tx, &width, &height); + twin_screen_damage(tx->screen, 0, 0, width, height); +} + +static bool twin_fbdev_work(void *closure) +{ + twin_screen_t *screen = SCREEN(closure); + + if (twin_screen_damaged(screen)) + twin_screen_update(screen); + return true; +} + +static bool twin_fbdev_apply_config(twin_fbdev_t *tx) +{ + /* Read changable information of the framebuffer */ + if (ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) == -1) { + printf("error : failed getting framebuffer information\n"); + return false; + } + + /* Set the virtual screen size to be the same as the physical screen */ + tx->fb_var.xres_virtual = tx->fb_var.xres; + tx->fb_var.yres_virtual = tx->fb_var.yres; + tx->fb_var.bits_per_pixel = 32; + if (ioctl(tx->fb_fd, FBIOPUT_VSCREENINFO, &tx->fb_var) < 0) { + printf("Failed to set framebuffer mode\n"); + return false; + } + + /* Read changable information of the framebuffer again */ + if (ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) < 0) { + printf("Failed to get framebuffer information\n"); + return false; + } + + /* Check bits per pixel */ + if (tx->fb_var.bits_per_pixel != 32) { + printf("Failed to set framebuffer bpp to 32\n"); + return false; + } + + /* Read unchangable information of the framebuffer */ + ioctl(tx->fb_fd, FBIOGET_FSCREENINFO, &tx->fb_fix); + + /* Align the framebuffer memory address with the page size */ + off_t pgsize = getpagesize(); + off_t start = (off_t) tx->fb_fix.smem_start & (pgsize - 1); + + /* Round up the framebuffer memory size to match the page size */ + tx->fb_len = start + (size_t) tx->fb_fix.smem_len + (pgsize - 1); + tx->fb_len &= ~(pgsize - 1); + + /* Map framebuffer device to the virtual memory */ + tx->fb_base = mmap(NULL, tx->fb_len, PROT_READ | PROT_WRITE, MAP_SHARED, + tx->fb_fd, 0); + if (tx->fb_base == MAP_FAILED) { + printf("Failed to mmap framebuffer.\n"); + return false; + } + + return true; +} + +static bool twin_find_free_vt(int *vt_num) +{ + char vt_path[30]; + int fd; + + /* Find a usable virtual terminal. Since VT1 is occupied by the GNU/Linux + * desktop environment and VT2 is unusable, the search begins from VT3. + */ + for (int i = 3; i <= VT_MAX; i++) { + snprintf(vt_path, sizeof(vt_path), "/dev/tty%d", i); + fd = open(vt_path, O_RDWR); + if (fd > 0) { + close(fd); + *vt_num = i; + return true; + } + } + + /* No free VT found */ + return false; +} + +static bool twin_vt_setup(twin_fbdev_t *tx) +{ + /* Open VT0 to inquire information */ + tx->vt_fd = open("/dev/tty0", O_RDWR); + if (tx->vt_fd == -1) { + printf("Failed to open /dev/tty0.\n"); + return false; + } + + /* Find next free virtual terminal */ + if (!twin_find_free_vt(&tx->vt_num)) { + printf("Failed to find a free virtual terminal.\n"); + return false; + } + close(tx->vt_fd); + + /* Open the found free virtual terminal */ + char vt_dev[30] = {0}; + snprintf(vt_dev, 30, "/dev/tty%d", tx->vt_num); + close(tx->vt_fd); + + tx->vt_fd = open(vt_dev, O_RDWR); + if (tx->vt_fd == -1) { + printf("Failed to open %s.\n", vt_dev); + return false; + } + + /* Switch to the free virtual terminal */ + if (ioctl(tx->vt_fd, VT_ACTIVATE, tx->vt_num) < 0) { + printf("Failed to activate virtual terminal %d.\n", tx->vt_num); + return false; + } + + /* This brings Mado to the activated virtual terminal */ + if (ioctl(tx->vt_fd, VT_WAITACTIVE, tx->vt_num) < 0) { + printf("Failed to wait virtual terminal %d to be activated.\n", + tx->vt_num); + return false; + } + + /* Disable virtual terminal output */ + /* TODO: Warning: There is currently no way to stop the program once + * KD_GRAPHICS mode is set. To prevent the system from rebooting, consider + * commenting out the following code. + */ + if (ioctl(tx->vt_fd, KDSETMODE, KD_GRAPHICS) < 0) { + printf("Failed to set KD_GRAPHICS mode.\n"); + return false; + } + + return true; +} + +twin_context_t *twin_fbdev_init(int width, int height) +{ + char *fbdev_path = getenv(FBDEV_NAME); + if (!fbdev_path) { + printf( + "Environment variable $FRAMEBUFFER not set, use %s by default.\n", + FBDEV_DEFAULT); + fbdev_path = FBDEV_DEFAULT; + } + + twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); + if (!ctx) + return NULL; + ctx->priv = calloc(1, sizeof(twin_fbdev_t)); + if (!ctx->priv) + return NULL; + + twin_fbdev_t *tx = ctx->priv; + + /* Open the framebuffer device */ + tx->fb_fd = open(fbdev_path, O_RDWR); + if (tx->fb_fd == -1) { + printf("Failed to open %s.\n", fbdev_path); + goto bail; + } + + /* Set up virtual terminal environment */ + if (!twin_vt_setup(tx)) { + goto bail_fb_fd; + } + + /* Apply configurations to the framebuffer device */ + if (!twin_fbdev_apply_config(tx)) { + printf("Failed to apply configurations to the framebuffer device.\n"); + goto bail_vt_fd; + } + + /* Create TWIN screen */ + ctx->screen = + twin_screen_create(width, height, NULL, _twin_fbdev_put_span, ctx); + + /* Create Linux input system object */ + tx->input = twin_linux_input_create(ctx->screen); + if (!tx->input) { + printf("Failed at creating Linux input system object.\n"); + goto bail_vt_fd; + } + + /* Setup file handler and work functions */ + twin_set_work(twin_fbdev_work, TWIN_WORK_REDISPLAY, ctx); + + return ctx; + +bail_vt_fd: + close(tx->vt_fd); +bail_fb_fd: + close(tx->fb_fd); +bail: + free(ctx->priv); + free(ctx); + return NULL; +} + +static void twin_fbdev_configure(twin_context_t *ctx) +{ + int width, height; + twin_fbdev_t *tx = ctx->priv; + twin_fbdev_get_screen_size(tx, &width, &height); + twin_screen_resize(ctx->screen, width, height); +} + +static void twin_fbdev_exit(twin_context_t *ctx) +{ + if (!ctx) + return; + + twin_fbdev_t *tx = PRIV(ctx); + /* TODO: KD_TEXT mode setting requires keyboard input handling + * or a dedicated button on the UI + */ + ioctl(tx->vt_fd, KDSETMODE, KD_TEXT); + munmap(tx->fb_base, tx->fb_len); + twin_linux_input_destroy(tx->input); + close(tx->vt_fd); + close(tx->fb_fd); + free(ctx->priv); + free(ctx); +} + +/* Register the Linux framebuffer backend */ + +const twin_backend_t g_twin_backend = { + .init = twin_fbdev_init, + .configure = twin_fbdev_configure, + .exit = twin_fbdev_exit, +}; diff --git a/backend/linux_input.c b/backend/linux_input.c new file mode 100644 index 0000000..276614c --- /dev/null +++ b/backend/linux_input.c @@ -0,0 +1,128 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2024 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include +#include +#include +#include +#include +#include + +#include "linux_input.h" + +#define MOUSE_EVENT_LEFT_BTN 0x1 +#define MOUSE_EVENT_RIGHT_BTN 0x2 +#define MOUSE_EVENT_DATA_SIZE 3 + +#define MOUSE_DEV_NAME "MOUSE" +#define MOUSE_DEV_DEFAULT "/dev/input/mice" + +typedef struct { + twin_screen_t *screen; + int fd; + char residual[2]; + int res_cnt; + int btns; + int x, y; +} twin_linux_input_t; + +static void _mouse_check_bounds(twin_linux_input_t *tm) +{ + if (tm->x < 0) + tm->x = 0; + if (tm->x > tm->screen->width) + tm->x = tm->screen->width; + if (tm->y < 0) + tm->y = 0; + if (tm->y > tm->screen->height) + tm->y = tm->screen->height; +} + +static bool twin_linux_input_events(int file, twin_file_op_t ops, void *closure) +{ + twin_linux_input_t *tm = closure; + char evts[34]; /* XXX: How big should we allocate for the event buffer? */ + int n = tm->res_cnt; + twin_event_t tev; + + /* Prepare input data for unpacking */ + if (n) + memcpy(evts, tm->residual, n); + n += read(file, evts + n, 32); /* XXX: Same as above */ + + char *ep; + for (ep = evts; n >= MOUSE_EVENT_DATA_SIZE; + n -= MOUSE_EVENT_DATA_SIZE, ep += MOUSE_EVENT_DATA_SIZE) { + /* Add incremental change to the cursor position */ + int dx = +ep[1]; + int dy = -ep[2]; + if (dx || dy) { + tm->x += dx, tm->y += dy; + _mouse_check_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); + } + + /* Handle left mouse button */ + int btn = ep[0] & MOUSE_EVENT_LEFT_BTN; + if (btn != tm->btns) { + tm->btns = btn; + tev.kind = btn ? TwinEventButtonDown : TwinEventButtonUp; + 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); + } + } + + /* Preserve left unpacked data */ + tm->res_cnt = n; + if (n) + memcpy(tm->residual, ep, n); + + return true; +} + +void *twin_linux_input_create(twin_screen_t *screen) +{ + /* Read mouse device file path from environment variable */ + char *mouse_dev_path = getenv(MOUSE_DEV_NAME); + if (!mouse_dev_path) + mouse_dev_path = MOUSE_DEV_DEFAULT; + + /* Create object for handling Linux input system */ + twin_linux_input_t *tm = calloc(1, sizeof(twin_linux_input_t)); + if (!tm) + return NULL; + + tm->screen = screen; + + /* Centering the cursor position */ + tm->x = screen->width / 2; + tm->y = screen->height / 2; + + /* Open the mouse device file */ + tm->fd = open(mouse_dev_path, O_RDONLY); + if (tm->fd < 0) { + free(tm); + return NULL; + } + + /* Set file handler for reading input device file */ + twin_set_file(twin_linux_input_events, tm->fd, TWIN_READ, tm); + + return tm; +} + +void twin_linux_input_destroy(void *_tm) +{ + twin_linux_input_t *tm = _tm; + close(tm->fd); + free(tm); +} diff --git a/backend/linux_input.h b/backend/linux_input.h new file mode 100644 index 0000000..93480a4 --- /dev/null +++ b/backend/linux_input.h @@ -0,0 +1,14 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2024 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#ifndef _LINUX_INPUT_H__ +#define _LINUX_INPUT_H__ + +void *twin_linux_input_create(twin_screen_t *screen); + +void twin_linux_input_destroy(void *tm); + +#endif diff --git a/configs/Kconfig b/configs/Kconfig index 3567484..5125d66 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -10,6 +10,7 @@ choice config BACKEND_FBDEV bool "Linux framebuffer support" + select CURSOR config BACKEND_SDL bool "SDL video output support"