diff --git a/Makefile b/Makefile index 2890b42..3c2120c 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,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/README.md b/README.md index 99bcb50..0543230 100644 --- a/README.md +++ b/README.md @@ -60,30 +60,54 @@ benefiting the entire application stack. ## Build and Verify +### Prerequisites + `Mado` is built with a minimalist design in mind. However, its verification relies on certain third-party packages for full functionality and access to all -its features. To ensure proper operation, the development environment should -have the [SDL2 library](https://www.libsdl.org/), [libjpeg](https://www.ijg.org/), and [libpng](https://github.com/pnggroup/libpng) installed. +its features. We encourage the development environment to be installed with all optional +packages, including [libjpeg](https://www.ijg.org/), [libpng](https://github.com/pnggroup/libpng), +and the [SDL2 library](https://www.libsdl.org/). * macOS: `brew install sdl2 jpeg libpng` * Ubuntu Linux / Debian: `sudo apt install libsdl2-dev libjpeg-dev libpng-dev` -Configure via [Kconfiglib](https://pypi.org/project/kconfiglib/) +### Configuration + +Configure via [Kconfiglib](https://pypi.org/project/kconfiglib/), you should select either SDL +video output or the Linux framebuffer. ```shell $ make config ``` -Build the library and demo program. +### Build and execution + +Build the library and demo program: + ```shell $ make ``` -Run sample `Mado` program: +To run demo program with SDL backend: + ```shell $ ./demo-sdl ``` Once the window appears, you should be able to move the windows and interact with the widgets. +To run demo program with the Linux framebuffer backend: + +```shell +$ sudo ./demo-fbdev +``` + +Normal users don't have access to `/dev/fb0` so require `sudo`. Alternatively, you can add the user to the video group to avoid typing `sudo` every time: + +```shell +$ sudo usermod -a -G video $USERNAME +``` + +In addition, the framebuffer device can be assigned via the environment variable `FRAMEBUFFER`. + ## License `Mado` is available under a MIT-style license, permitting liberal commercial use. diff --git a/backend/fbdev.c b/backend/fbdev.c new file mode 100644 index 0000000..eb50cd1 --- /dev/null +++ b/backend/fbdev.c @@ -0,0 +1,280 @@ +/* + * 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 "linux_input.h" +#include "twin_backend.h" +#include "twin_private.h" + +#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 *dest = + (uint32_t *) ((uintptr_t) tx->fb_base + (off * sizeof(uint32_t))); + memcpy(dest, pixels, 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) { + log_error("Failed to get framebuffer information"); + 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) { + log_error("Failed to set framebuffer mode"); + return false; + } + + /* Read changable information of the framebuffer again */ + if (ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) < 0) { + log_error("Failed to get framebuffer information"); + return false; + } + + /* Check bits per pixel */ + if (tx->fb_var.bits_per_pixel != 32) { + log_error("Failed to set framebuffer bpp to 32"); + 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) { + log_error("Failed to mmap framebuffer"); + return false; + } + + return true; +} + +static int twin_vt_open(int vt_num) +{ + int fd; + + char vt_dev[30] = {0}; + snprintf(vt_dev, 30, "/dev/tty%d", vt_num); + + fd = open(vt_dev, O_RDWR); + if (fd < 0) { + log_error("Failed to open %s", vt_dev); + } + + return fd; +} + +static bool twin_vt_setup(twin_fbdev_t *tx) +{ + /* Open VT0 to inquire information */ + if ((tx->vt_fd = twin_vt_open(0)) < -1) { + log_error("Failed to open VT0"); + return false; + } + + /* Inquire for current VT number */ + struct vt_stat vt; + if (ioctl(tx->vt_fd, VT_GETSTATE, &vt) == -1) { + log_error("Failed to get VT number"); + return false; + } + tx->vt_num = vt.v_active; + + /* Open the VT */ + if ((tx->vt_fd = twin_vt_open(tx->vt_num)) < -1) { + return false; + } + + /* Set VT to graphics mode to inhibit command-line text */ + if (ioctl(tx->vt_fd, KDSETMODE, KD_GRAPHICS) < 0) { + log_error("Failed to set KD_GRAPHICS mode"); + return false; + } + + return true; +} + +twin_context_t *twin_fbdev_init(int width, int height) +{ + char *fbdev_path = getenv(FBDEV_NAME); + if (!fbdev_path) { + log_info("Environment variable $FRAMEBUFFER not set, use %s by default", + 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) { + log_error("Failed to open %s", 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)) { + log_error("Failed to apply configurations to the framebuffer device"); + 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) { + log_error("Failed to create Linux input system object"); + goto bail_screen; + } + + /* Setup file handler and work functions */ + twin_set_work(twin_fbdev_work, TWIN_WORK_REDISPLAY, ctx); + + return ctx; + +bail_screen: + twin_screen_destroy(ctx->screen); +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); + 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..a5c72cf --- /dev/null +++ b/backend/linux_input.c @@ -0,0 +1,187 @@ +/* + * 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 "linux_input.h" +#include "twin_private.h" + +#define EVDEV_CNT_MAX 32 +#define EVDEV_NAME_SIZE_MAX 50 + +typedef struct { + twin_screen_t *screen; + pthread_t evdev_thread; + int fd; + int btns; + int x, y; +} twin_linux_input_t; + +static int evdev_fd[EVDEV_CNT_MAX]; +static int evdev_cnt; + +static void check_mouse_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 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 */ + + twin_event_t tev; + + switch (ev->type) { + case EV_REL: + if (ev->code == REL_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); + } else if (ev->code == REL_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); + } + break; + case EV_ABS: + 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); + } 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); + } + break; + case EV_KEY: + if (ev->code == BTN_LEFT) { + tm->btns = ev->value > 0 ? 1 : 0; + tev.kind = tm->btns ? 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); + } + } +} + +static void *twin_linux_evdev_thread(void *arg) +{ + twin_linux_input_t *tm = arg; + + /* Open all event devices */ + char evdev_name[EVDEV_NAME_SIZE_MAX] = {0}; + for (int i = 0; i < EVDEV_CNT_MAX; i++) { + snprintf(evdev_name, EVDEV_NAME_SIZE_MAX, "/dev/input/event%d", i); + int fd = open(evdev_name, O_RDWR | O_NONBLOCK); + if (fd > 0) { + evdev_fd[evdev_cnt] = fd; + evdev_cnt++; + } + } + + /* Initialize pollfd array */ + struct pollfd pfds[EVDEV_CNT_MAX]; + for (int i = 0; i < evdev_cnt; i++) { + pfds[i].fd = evdev_fd[i]; + pfds[i].events = POLLIN; + } + + /* Event polling */ + struct input_event ev; + while (1) { + /* Wait until any event is available */ + if (poll(pfds, evdev_cnt, -1) <= 0) + continue; + + /* Iterate through all file descriptors */ + for (int i = 0; i < evdev_cnt; i++) { + /* Try reading events */ + ssize_t n = read(pfds[i].fd, &ev, sizeof(ev)); + if (n == sizeof(ev)) { + /* Handle events */ + twin_linux_input_events(&ev, tm); + } + } + } + + return NULL; +} + +static bool dummy(int file, twin_file_op_t ops, void *closure) +{ + return true; +} + +void *twin_linux_input_create(twin_screen_t *screen) +{ + /* 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; + +#if 1 + /* FIXME: Need to fix the unexpected termination of the program. + * Hooking a dummy function here is only a hack*/ + + /* Set file handler for reading input device file */ + twin_set_file(dummy, tm->fd, TWIN_READ, tm); +#endif + + /* Start event handling thread */ + if (pthread_create(&tm->evdev_thread, NULL, twin_linux_evdev_thread, tm)) { + log_error("Failed to create evdev thread"); + return NULL; + } + + 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 1d7966b..672fbf1 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"