Skip to content

Implement VNC backend #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 4, 2024
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
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ libtwin.a_files-y += backend/fbdev.c
libtwin.a_files-y += backend/linux_input.c
endif

ifeq ($(CONFIG_BACKEND_VNC), y)
BACKEND = vnc
libtwin.a_files-y += backend/vnc.c
libtwin.a_files-y += src/cursor.c
libtwin.a_cflags-y += $(shell pkg-config --cflags neatvnc aml pixman-1)
Copy link
Collaborator

@Bennctu Bennctu Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #55, it also uses pixman features. Therefore, whoever merges first, another person needs to pay attention to the adjustment of pixman patch in the Makefile.

TARGET_LIBS += $(shell pkg-config --libs neatvnc aml pixman-1)
endif

# Standalone application

ifeq ($(CONFIG_DEMO_APPLICATIONS), y)
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ 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`

Please note that the VNC backend is only tested on GNU/Linux, and the prebuilt [neatvnc](https://github.com/any1/neatvnc) package might be outdated. To ensure you have the latest versions, you can build the dependent packages from source by running the script:
```bash
$ tools/build-neatvnc.sh
```

### Configuration

Configure via [Kconfiglib](https://pypi.org/project/kconfiglib/), you should select either SDL
Expand Down Expand Up @@ -108,6 +113,14 @@ $ sudo usermod -a -G video $USERNAME

In addition, the framebuffer device can be assigned via the environment variable `FRAMEBUFFER`.

To run demo program with the neat-vnc backend:

```shell
$ ./demo-vnc
```

It would launch the vnc server. You could use any VNC client to connect with given IP address(default is "127.0.0.1") and port (default is 5900), you could assign the IP address via the environment variable `MADO_VNC_HOST` and the port via `MADO_VNC_PORT`

## License

`Mado` is available under a MIT-style license, permitting liberal commercial use.
Expand Down
294 changes: 294 additions & 0 deletions backend/vnc.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
Twin - A Tiny Window System
Copyright (c) 2024 National Cheng Kung University, Taiwan
All rights reserved.
*/
#define AML_UNSTABLE_API 1
#include <aml.h>
#include <assert.h>
#include <neatvnc.h>
#include <pixman.h>
#include <stdlib.h>
#include <string.h>
#include <twin.h>

#include "twin_backend.h"
#include "twin_private.h"

#define SCREEN(x) ((twin_context_t *) x)->screen
#define PRIV(x) ((twin_vnc_t *) ((twin_context_t *) x)->priv)
#define MADO_VNC_HOST "MADO_VNC_HOST"
#define MADO_VNC_PORT "MADO_VNC_PORT"
#define MADO_VNC_HOST_DEFAULT "127.0.0.1"
#define MADO_VNC_PORT_DEFAULT "5900"

#ifndef DRM_FORMAT_ARGB8888
#define fourcc_code(a, b, c, d) \
((uint32_t) (a) | ((uint32_t) (b) << 8) | ((uint32_t) (c) << 16) | \
((uint32_t) (d) << 24))
#define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4')
#endif

typedef struct {
twin_screen_t *screen;
struct aml *aml;
struct aml_handler *aml_handler;
struct nvnc *server;
struct nvnc_display *display;
struct nvnc_fb *current_fb;
struct pixman_region16 damage_region;
uint32_t *framebuffer;
int width;
int height;
} twin_vnc_t;

typedef struct {
uint16_t px, py;
enum nvnc_button_mask prev_button;
} twin_peer_t;

#define CURSOR_WIDTH 14
#define CURSOR_HEIGHT 20

static void _twin_vnc_put_begin(twin_coord_t left,
twin_coord_t top,
twin_coord_t right,
twin_coord_t bottom,
void *closure)
{
(void) left;
(void) top;
(void) right;
(void) bottom;
twin_vnc_t *tx = PRIV(closure);
pixman_region_init_rect(&tx->damage_region, 0, 0, tx->width, tx->height);
}

static void _twin_vnc_put_span(twin_coord_t left,
twin_coord_t top,
twin_coord_t right,
twin_argb32_t *pixels,
void *closure)
{
twin_vnc_t *tx = PRIV(closure);
uint32_t *fb_pixels = tx->framebuffer + top * tx->width + left;
size_t span_width = right - left;

memcpy(fb_pixels, pixels, span_width * sizeof(*fb_pixels));

pixman_region_init_rect(&tx->damage_region, left, top, span_width, 1);

if (pixman_region_not_empty(&tx->damage_region)) {
nvnc_display_feed_buffer(tx->display, tx->current_fb,
&tx->damage_region);
pixman_region_clear(&tx->damage_region);
}
aml_poll(tx->aml, 0);
aml_dispatch(tx->aml);
}

static void twin_vnc_get_screen_size(twin_vnc_t *tx, int *width, int *height)
{
*width = nvnc_fb_get_width(tx->current_fb);
*height = nvnc_fb_get_height(tx->current_fb);
}

static bool _twin_vnc_work(void *closure)
{
twin_screen_t *screen = SCREEN(closure);

if (twin_screen_damaged(screen))
twin_screen_update(screen);
return true;
}

static void _twin_vnc_new_client(struct nvnc_client *client)
{
twin_peer_t *peer = malloc(sizeof(twin_peer_t));
nvnc_set_userdata(client, peer, NULL);
}

static bool _twin_vnc_read_events(int fd, twin_file_op_t op, void *closure)
{
(void) fd;
(void) op;
(void) closure;
return true;
}

static void _twin_vnc_pointer_event(struct nvnc_client *client,
uint16_t x,
uint16_t y,
enum nvnc_button_mask button)
{
twin_peer_t *peer = nvnc_get_userdata(client);
twin_event_t tev;
if ((button & NVNC_BUTTON_LEFT) &&
!(peer->prev_button & NVNC_BUTTON_LEFT)) {
tev.u.pointer.screen_x = x;
tev.u.pointer.screen_y = y;
tev.kind = TwinEventButtonDown;
tev.u.pointer.button = 1;
} else if (!(button & NVNC_BUTTON_LEFT) &&
(peer->prev_button & NVNC_BUTTON_LEFT)) {
tev.u.pointer.screen_x = x;
tev.u.pointer.screen_y = y;
tev.kind = TwinEventButtonUp;
tev.u.pointer.button = 1;
}
if ((peer->px != x || peer->py != y)) {
peer->px = x;
peer->py = y;
tev.u.pointer.screen_x = x;
tev.u.pointer.screen_y = y;
tev.u.pointer.button = 0;
tev.kind = TwinEventMotion;
}
peer->prev_button = button;
struct nvnc *server = nvnc_client_get_server(client);
twin_vnc_t *tx = nvnc_get_userdata(server);
twin_screen_dispatch(tx->screen, &tev);
}

static struct nvnc_fb *_twin_vnc_create_cursor()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should have a strong reason to create cursor here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since neatvnc provides a cursor feature, we could use it to render the cursor directly in the VNC framebuffer which makes smoother cursor movement.

{
struct nvnc_fb *fb = nvnc_fb_new(CURSOR_WIDTH, CURSOR_HEIGHT,
DRM_FORMAT_ARGB8888, CURSOR_WIDTH);
uint32_t *pixels = nvnc_fb_get_addr(fb);
for (int i = 0; i < CURSOR_WIDTH * CURSOR_HEIGHT; i++) {
uint32_t a = _twin_cursor_default[i * 4];
uint32_t r = _twin_cursor_default[i * 4 + 1];
uint32_t g = _twin_cursor_default[i * 4 + 2];
uint32_t b = _twin_cursor_default[i * 4 + 3];
pixels[i] = (a << 24) | (r << 16) | (g << 8) | b;
}
return fb;
}

twin_context_t *twin_vnc_init(int width, int height)
{
twin_context_t *ctx = calloc(1, sizeof(twin_context_t));
if (!ctx)
return NULL;
ctx->priv = calloc(1, sizeof(twin_vnc_t));
if (!ctx->priv) {
free(ctx);
return NULL;
}

twin_vnc_t *tx = ctx->priv;
tx->width = width;
tx->height = height;

tx->aml = aml_new();
if (!tx->aml) {
log_error("Failed to create aml");
goto bail_priv;
}
aml_set_default(tx->aml);
char *vnc_host = getenv(MADO_VNC_HOST);
if (!vnc_host) {
log_info(
"Environment variable $MADO_VNC_HOST not set, use %s by default",
MADO_VNC_HOST_DEFAULT);
vnc_host = MADO_VNC_HOST_DEFAULT;
}
char *vnc_port = getenv(MADO_VNC_PORT);
if (!vnc_port) {
log_info(
"Environment variable $MADO_VNC_PORT not set, use %s by default",
MADO_VNC_PORT_DEFAULT);
vnc_port = MADO_VNC_PORT_DEFAULT;
}
log_info("NeatVNC server IP %s PORT %s", vnc_host, vnc_port);
tx->server = nvnc_open(vnc_host, atoi(vnc_port));
if (!tx->server) {
log_error("Failed to open neatvnc server");
goto bail_aml;
}

tx->display = nvnc_display_new(0, 0);
if (!tx->display) {
log_error("Failed to create neatvnc display");
goto bail_server;
}

nvnc_add_display(tx->server, tx->display);
nvnc_set_name(tx->server, "Twin VNC Backend");
nvnc_set_pointer_fn(tx->server, _twin_vnc_pointer_event);
nvnc_set_new_client_fn(tx->server, _twin_vnc_new_client);
nvnc_set_userdata(tx->server, tx, NULL);
struct nvnc_fb *cursor = _twin_vnc_create_cursor();
nvnc_set_cursor(tx->server, cursor, CURSOR_WIDTH, CURSOR_HEIGHT, 0, 0,
true);
nvnc_fb_unref(cursor);

ctx->screen = twin_screen_create(width, height, _twin_vnc_put_begin,
_twin_vnc_put_span, ctx);
if (!ctx->screen)
goto bail_display;

tx->framebuffer = calloc(width * height, sizeof(uint32_t));
if (!tx->framebuffer) {
log_error("Failed to allocate framebuffer");
goto bail_screen;
}

tx->current_fb = nvnc_fb_from_buffer(tx->framebuffer, width, height,
DRM_FORMAT_ARGB8888, width);
if (!tx->current_fb) {
log_error("Failed to init VNC framebuffer");
goto bail_framebuffer;
}
int aml_fd = aml_get_fd(tx->aml);
twin_set_file(_twin_vnc_read_events, aml_fd, TWIN_READ, tx);

twin_set_work(_twin_vnc_work, TWIN_WORK_REDISPLAY, ctx);
tx->screen = ctx->screen;

return ctx;

bail_framebuffer:
free(tx->framebuffer);
bail_screen:
twin_screen_destroy(ctx->screen);
bail_display:
nvnc_display_unref(tx->display);
bail_server:
nvnc_close(tx->server);
bail_aml:
aml_unref(tx->aml);
bail_priv:
free(ctx->priv);
free(ctx);
return NULL;
}

static void twin_vnc_configure(twin_context_t *ctx)
{
int width, height;
twin_vnc_t *tx = ctx->priv;
twin_vnc_get_screen_size(tx, &width, &height);
twin_screen_resize(ctx->screen, width, height);
}

static void twin_vnc_exit(twin_context_t *ctx)
{
if (!ctx)
return;

twin_vnc_t *tx = PRIV(ctx);

nvnc_display_unref(tx->display);
nvnc_close(tx->server);
aml_unref(tx->aml);

free(ctx->priv);
free(ctx);
}

const twin_backend_t g_twin_backend = {
.init = twin_vnc_init,
.configure = twin_vnc_configure,
.exit = twin_vnc_exit,
};
3 changes: 3 additions & 0 deletions configs/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ config BACKEND_FBDEV
config BACKEND_SDL
bool "SDL video output support"

config BACKEND_VNC
bool "VNC server output support"
endchoice

menu "Features"
Expand All @@ -39,6 +41,7 @@ comment "Logging is disabled"
config CURSOR
bool "Manipulate cursor"
default n
depends on !BACKEND_VNC

endmenu

Expand Down
2 changes: 2 additions & 0 deletions include/twin_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ static inline int twin_clz(uint32_t v)
}
#endif

extern const uint8_t _twin_cursor_default[];

/* Pattern Matching for C macros.
* https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,-tips,-and-idioms
*/
Expand Down
4 changes: 3 additions & 1 deletion src/cursor.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <stddef.h>
#include <twin.h>

static const uint8_t _twin_cursor_default[] = {
#include "twin_private.h"

const uint8_t _twin_cursor_default[] = {
0x19, 0x19, 0x19, 0xb8, 0x1e, 0x1e, 0x1e, 0xc8, 0x00, 0x00, 0x00, 0x13,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
Expand Down
25 changes: 25 additions & 0 deletions tools/build-neatvnc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

# Update and install dependencies
sudo apt update
sudo apt install -y meson ninja-build libpixman-1-dev zlib1g-dev libdrm-dev pkg-config

TOP_DIR=$(pwd)
# Clone and build aml
git clone https://github.com/any1/aml && pushd aml
meson -Dprefix=$TOP_DIR/_static --default-library=static build
ninja -C build install
popd

export PKG_CONFIG_PATH=$TOP_DIR/_static/lib/$(uname -m)-linux-gnu/pkgconfig

# Clone and build NeatVNC
git clone https://github.com/any1/neatvnc --depth=1 -b v0.8.1 && pushd neatvnc
meson -Dprefix=$TOP_DIR/_static --default-library=static build
ninja -C build install
popd

# Prompt for PKG_CONFIG_PATH
echo "Now, statically-linked libraries of both aml and neatvnc were installed, and you can set PKG_CONFIG_PATH properly for the Mado build system to detect and facilitate."
echo "For example, you can run:"
echo " export PKG_CONFIG_PATH=\$(pwd)/_static/lib/\$(uname -m)-linux-gnu/pkgconfig/"