# Improve window system for RISC-V
> 阮祈翰
## Main goal
Run [Mado](https://github.com/sysprog21/mado) on [rv32emu](https://github.com/sysprog21/rv32emu) by using [SDL](https://www.libsdl.org/) system call and RV32IM ISA (with B extension)
### Research Focus
- Learn how to use [SDL](https://www.libsdl.org/) system call in [rv32emu](https://github.com/sysprog21/rv32emu)
- Learn how to transplant [Mado](https://github.com/sysprog21/mado) onto [rv32emu](https://github.com/sysprog21/rv32emu)
## Step by step
### Download xPack GNU RISC-V Embedded GCC
- A standalone cross-platform (Windows, macOS, GNU/Linux) binary distribution of GNU RISC-V Embedded GCC, intended for reproducible builds.
1. Install [xpm](https://xpack.github.io/xpm/docs/install/)
```shell
$ wget -q -O - https://raw.githubusercontent.com/xpack/assets/master/scripts/install-nvm-node-npm-xpm.sh | ${SHELL}
$ export NVM_DIR="$HOME/.nvm"
$ source "$NVM_DIR/nvm.sh"
$ npm install --location=global xpm@latest
```
2. To test if [xpm](https://xpack.github.io/xpm/docs/install/) starts
```shell
$ which xpm
/home/eric/.nvm/versions/node/v22.13.0/bin/xpm
$ xpm --version
0.20.5
```
3. Install [xPack GNU RISC-V Embedded GCC](https://xpack-dev-tools.github.io/riscv-none-elf-gcc-xpack/) globally
```shell
$ xpm install @xpack-dev-tools/riscv-none-elf-gcc@latest --global --verbose
```
4. PATH setup (Make sure to check the version manually in order to set the correct $PATH)
```shell
$ export PATH=$HOME/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/bin:$PATH
```
5. Quick test
```shell
$ riscv-none-elf-gcc --version
```
```shell
riscv-none-elf-gcc (xPack GNU RISC-V Embedded GCC x86_64) 14.2.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
6. Write a C code
```c=
//hello.c
#include <stdio.h>
int main(){
printf("hello world!!!\n");
return 0;
}
```
7. Compile and run on rv32emu
```shell
riscv-none-elf-gcc -march=rv32gc -mabi=ilp32 -o hello hello.c
$ build/rv32emu hello
```
```shell
hello world!!!
inferior exit code 0
```
### Try using SDL in rv32emu
- Write a C code
```c=
#include <stdio.h>
int main(){
int syscall_num = 0xbeef;
int width = 1000;
int height = 600;
static int buffer[1000 * 600];
int i = 0;
for(;i < 1000 * 300;i++){
buffer[i] = 0xFF0000;
}
for(;i < 1000 * 600;i++){
buffer[i] = 0xFBFF00;
}
register int* a0 asm("a0") = &buffer[0];
register int a1 asm("a1") = width;
register int a2 asm("a2") = height;
while(1) {
register int a7 asm("a7") = syscall_num;
asm volatile ("scall");
}
return 0;
}
```
- Run in rv32emu

### Download Mado
1. Download needed package then make
```shell
$ sudo apt install libjpeg-dev libpng-dev
$ sudo apt install libpixman-1-dev
$ sudo apt install libsdl2-dev
$ make config
$ make
```
2. Run demo program with SDL backend
```shell
$ ./demo-sdl
```

### Analyzing Mado
- When using the command `make`.
```shell
$ make
DEP .libtwin.a/src/work.c.d
DEP .libtwin.a/src/window.c.d
DEP .libtwin.a/src/widget.c.d
...
CC .demo-sdl/apps/main.c.o
LINK .demo-sdl/demo-sdl
CP demo-sdl
```
1. The files in `app` and `src` will implement `Dependency Generation (DEP)` and `Compile (CC)` then write into `.libapps.a/apps` and `.libtwin.a/src` respectively.
2. `AR` stands for "Archive tool". It collects all the `.o` file to `.a` file which become a static library. In this case, we collect files in `.libapps.a/apps` to `.libapps.a/libapps.a` and files in `.libtwin.a/src` to `.libtwin.a/libtwin.a`
3. `RANLIB` stands for "Random Library Index Generator." It creates an index table for `.a` static libraries. This index table contains the symbol information of all `.o` files within the library, allowing for quick location of required functions or variables during the linking process.
4. implement `DEP` and `CC` on `app/main.c` and `LINK` the following files : `.demo-sdl/apps/main.c.o` `libapps.a` `libtwin.a` in order to create executable file `.demo-sdl/demo-sdl`.
- Check file description
```shell
$ file demo-sdl
demo-sdl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f554f2df4c8710331ec9b5cb8bfb511e763d58cd, for GNU/Linux 3.2.0, not stripped
```
This tell us `demo-sdl` is 64-bit executable file and is built in `x86-64` ISA. The final goal is to turn it to 32-bit which built in `riscv-none-elf-gcc` and run on `rv32emu`.
Since `riscv-none-elf-gcc` is not able to access third-party packages like `libsdl2-dev` or `libjpeg-dev`. I have to write a new graphic backend `rv32emu.c` which call the system calls in [rv32emu](https://github.com/sysprog21/rv32emu) to implement window display.
- Check `mado/mk/toolchain.mk`, changed the compiler to prebuild [xPack GNU RISC-V Embedded GCC](https://xpack-dev-tools.github.io/riscv-none-elf-gcc-xpack/).
```diff
CC_IS_CLANG :=
CC_IS_GCC :=
# FIXME: Cross-compilation using Clang is not supported.
ifdef CROSS_COMPILE
CC := $(CROSS_COMPILE)gcc
endif
+ CC = riscv-none-elf-gcc
+ CFLAGS += -std=gnu99
+ CFLAGS += -march=rv32im
+ CFLAGS += -mabi=ilp32
override CC := $(shell which $(CC))
ifndef CC
$(error "Valid C compiler not found.")
endif
```
### Implement new graphic backend `rv32emu.c`
- Code for rv32emu.c
```c
#ifndef MADO
#define MADO
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
struct mado {
const char *title;
const int width, height;
uint32_t *buf;
int keys[256]; /* keys are mostly ASCII, but arrows are 17..20 */
int mod; /* mod is 4 bits mask, ctrl=1, shift=2, alt=4, meta=8 */
int x, y;
int mouse;
void *event_queue;
uint32_t event_count;
size_t event_queue_start;
};
#ifndef MADO_API
#define MADO_API extern
#endif
MADO_API int mado_open(struct mado *m);
MADO_API int mado_loop(struct mado *m);
MADO_API void mado_close(struct mado *m);
MADO_API void mado_sleep(int64_t ms);
MADO_API int64_t mado_time(void);
#define mado_pixel(m, x, y) ((m)->buf[((y) * (m)->width) + (x)])
#ifndef MADO_HEADER
#define RV_QUEUE_CAPACITY 128
enum {
RV_KEYCODE_RETURN = 0x0000000D,
RV_KEYCODE_UP = 0x40000052,
RV_KEYCODE_DOWN = 0x40000051,
RV_KEYCODE_RIGHT = 0x4000004F,
RV_KEYCODE_LEFT = 0x40000050,
RV_KEYCODE_LCTRL = 0x400000E0,
RV_KEYCODE_RCTRL = 0x400000E4,
RV_KEYCODE_LSHIFT = 0x400000E1,
RV_KEYCODE_RSHIFT = 0x400000E5,
RV_KEYCODE_LALT = 0x400000E2,
RV_KEYCODE_RALT = 0x400000E6,
RV_KEYCODE_LMETA = 0x400000E3,
RV_KEYCODE_RMETA = 0x400000E7,
};
//KEY_EVENT
typedef struct {
uint32_t keycode;
uint8_t state;
uint16_t mod;
} rv_key_rv_event_t;
//MOUSE_MOTION_EVENT
typedef struct {
int32_t x, y, xrel, yrel;
} rv_mouse_motion_t;
enum {
RV_MOUSE_BUTTON_LEFT = 1,
};
//MOUSE_BUTTON_EVENT
typedef struct {
uint8_t button;
uint8_t state;
} rv_mouse_button_t;
enum {
RV_KEY_EVENT = 0,
RV_MOUSE_MOTION_EVENT = 1,
RV_MOUSE_BUTTON_EVENT = 2,
RV_QUIT_EVENT = 3,
};
typedef struct {
uint32_t type;
union {
rv_key_rv_event_t key_event;
union {
rv_mouse_motion_t motion;
rv_mouse_button_t button;
} mouse;
};
} rv_event_t;
enum {
RV_RELATIVE_MODE_SUBMISSION = 0,
RV_WINDOW_TITLE_SUBMISSION = 1,
};
typedef struct {
uint8_t enabled;
} rv_mouse_rv_submission_t;
typedef struct {
uint32_t title;
uint32_t size;
} rv_title_rv_submission_t;
typedef struct {
uint32_t type;
union {
rv_mouse_rv_submission_t mouse;
rv_title_rv_submission_t title;
};
} rv_submission_t;
MADO_API int mado_open(struct mado *m)
{
m->event_queue = malloc((sizeof(rv_event_t) + sizeof(rv_submission_t)) *
RV_QUEUE_CAPACITY);
m->event_count = 0;
register int a0 __asm("a0") = (int) m->event_queue;
register int a1 __asm("a1") = RV_QUEUE_CAPACITY;
register int a2 __asm("a2") = (int) &m->event_count;
register int a7 __asm("a7") = 0xc0de;
__asm volatile("scall" : : "r"(a0), "r"(a1), "r"(a2), "r"(a7) : "memory");
m->event_queue_start = 0;
rv_submission_t submission = {
.type = RV_WINDOW_TITLE_SUBMISSION,
.title.title = (uint32_t) m->title,
.title.size = strlen(m->title),
};
rv_submission_t *submission_queue =
(rv_submission_t *) ((rv_event_t *) m->event_queue + RV_QUEUE_CAPACITY);
submission_queue[0] = submission;
a0 = 1;
a7 = 0xfeed;
__asm volatile("scall" : : "r"(a0), "r"(a7) : "memory");
return 0;
}
MADO_API void mado_close(struct mado *m)
{
free(m->event_queue);
}
MADO_API int mado_loop(struct mado *m)
{
for (; m->event_count > 0; m->event_count--) {
rv_event_t event =
((rv_event_t *) m->event_queue)[m->event_queue_start];
switch (event.type) {
case RV_KEY_EVENT: {
uint32_t keycode = event.key_event.keycode;
uint8_t state = event.key_event.state;
if (keycode == RV_KEYCODE_RETURN)
m->keys[10] = state;
else if (keycode < 128)
m->keys[keycode] = state;
if (keycode == RV_KEYCODE_UP)
m->keys[17] = state;
if (keycode == RV_KEYCODE_DOWN)
m->keys[18] = state;
if (keycode == RV_KEYCODE_RIGHT)
m->keys[19] = state;
if (keycode == RV_KEYCODE_LEFT)
m->keys[20] = state;
if (keycode == RV_KEYCODE_LCTRL || keycode == RV_KEYCODE_RCTRL)
m->mod = state ? m->mod | 1 : m->mod & 0b1110;
if (keycode == RV_KEYCODE_LSHIFT || keycode == RV_KEYCODE_RSHIFT)
m->mod = state ? m->mod | 2 : m->mod & 0b1101;
if (keycode == RV_KEYCODE_LALT || keycode == RV_KEYCODE_RALT)
m->mod = state ? m->mod | 4 : m->mod & 0b1011;
if (keycode == RV_KEYCODE_LMETA || keycode == RV_KEYCODE_RMETA)
m->mod = state ? m->mod | 8 : m->mod & 0b0111;
break;
}
case RV_MOUSE_MOTION_EVENT:
m->x = event.mouse.motion.x;
m->y = event.mouse.motion.y;
break;
case RV_MOUSE_BUTTON_EVENT:
if (event.mouse.button.button == RV_MOUSE_BUTTON_LEFT)
m->mouse = event.mouse.button.state;
break;
case RV_QUIT_EVENT:
return -1;
}
m->event_queue_start =
(m->event_queue_start + 1) & (RV_QUEUE_CAPACITY - 1);
}
register int a0 __asm("a0") = (uintptr_t) m->buf;
register int a1 __asm("a1") = m->width;
register int a2 __asm("a2") = m->height;
register int a7 __asm("a7") = 0xbeef;
__asm volatile("scall" : : "r"(a0), "r"(a1), "r"(a2), "r"(a7) : "memory");
return 0;
}
#undef RV_QUEUE_CAPACITY
MADO_API int64_t mado_time(void)
{
return (int64_t) clock() / (CLOCKS_PER_SEC / 1000.0f);
}
MADO_API void mado_sleep(int64_t ms)
{
int64_t start = mado_time();
while (mado_time() - start < ms)
;
}
#ifdef __cplusplus
class Mado
{
struct mado m;
int64_t now;
public:
Mado(const int w, const int h, const char *title)
: m{.title = title, .width = w, .height = h}
{
this->m.buf = new uint32_t[w * h];
this->now = mado_time();
mado_open(&this->m);
}
~Mado()
{
mado_close(&this->m);
delete[] this->m.buf;
}
bool loop(const int fps)
{
int64_t t = mado_time();
if (t - this->now < 1000 / fps) {
mado_sleep(t - now);
}
this->now = t;
return mado_loop(&this->m) == 0;
}
inline uint32_t &px(const int x, const int y)
{
return mado_pixel(&this->m, x, y);
}
bool key(int c) { return c >= 0 && c < 128 ? this->m.keys[c] : false; }
int x() { return this->m.x; }
int y() { return this->m.y; }
int mouse() { return this->m.mouse; }
int mod() { return this->m.mod; }
};
#endif /* __cplusplus */
#endif /* !MADO_HEADER */
#endif /* MADO_H */
```
- Code for main.c
```diff
+#include "../backend/rv32emu.c"
int main(void)
{
+ enum { W = 640, H = 480 };
+ uint32_t buf[W * H];
+ struct mado m = {.title = "mado", .width = W, .height = H, .buf = buf};
+ mado_open(&m);
+ uint32_t t = 0;
+ while(mado_loop(&m) == 0){
+ t++;
+ for (int i = 0; i < 320; i++) {
+ for (int j = 0; j < 240; j++)
+ mado_pixel(&m, i, j) = i ^ j ^ t; /* Munching squares */
+ }
+ mado_sleep(100);
+ }
+ mado_close(&m);
+ return 0;
- tx = twin_create(WIDTH, HEIGHT);
- if (!tx)
- return 1;
//scroll to bottom
- twin_dispatch(tx);
```
- Modify `mado/configs/Kconfig`
```diff
choice
prompt "Backend Selection"
default BACKEND_SDL
+ config BACKEND_rv32emu
+ bool "rv32emu output support"
config BACKEND_FBDEV
bool "Linux framebuffer support"
select CURSOR
config BACKEND_SDL
bool "SDL video output support"
config BACKEND_VNC
bool "VNC server output support"
endchoice
```
- Modify `Makefile`
```diff
# Graphical backends
BACKEND := none
+ifeq ($(CONFIG_BACKEND_rv32emu), y)
+BACKEND = rv32emu
+libtwin.a_files-y += backend/rv32emu.c
+libtwin.a_cflags-y += -I$(HOME)/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/riscv-none-elf/include
+endif
ifeq ($(CONFIG_BACKEND_SDL), y)
BACKEND = sdl
libtwin.a_files-y += backend/sdl.c
libtwin.a_cflags-y += $(shell sdl2-config --cflags)
TARGET_LIBS += $(shell sdl2-config --libs)
endif
```
- Modify `mado/src/draw.c`
```diff
-#if defined(__APPLE__)
#include <machine/endian.h>
-#else
-#include <endian.h>
-#endif
```
### Add RISCV version packages
Third-party packages like `libjpeg` and `libpng` are prepared for x86-64 projects.
So I have to compile those packages to riscv version using the cross-compiler [xPack GNU RISC-V Embedded GCC](https://xpack-dev-tools.github.io/riscv-none-elf-gcc-xpack/)
- Set environment variables
```shell
$ PACKAGE_PREFIX=$HOME/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/riscv-none-elf
```
```shell
# build libjpeg and libpng
$ sudo tar xzf jpegsrc.v9f $PACKAGE_PREFIX
$ cd jpeg-9f
$ ./configure –host=riscv-none-elf-gcc –prefix=$PACKAGE_PREFIX
$ make
$ sudo make install
$ sudo tar xzf libpng-1.5.12 $PACKAGE_PREFIX
$ cd libpng-1.5.12
$ ./configure –host=riscv-none-elf-gcc –prefix=$PACKAGE_PREFIX
$ make
$ sudo make install
```
### Run `demo-rv32emu`
Compare to original demo file `demo-sdl`, I took away the jpeg, png and gif file from the makefile, just barely implement system call for display window.
```shell
$ build/rv32emu demo-rv32emu
```

## Conclusion
I implement cross-compile on mado from `x86-64` to `riscv-none-elf-gcc` and run on rv32emu, but I can only show an empty window without those pictures like the `demo-sdl`.
Although I didn't finish my goal, I spend a lot time to do the research and actually learn knowledge about cross-compile and how to write Makefile. So it's still a great experience I think.
## Reference
- https://github.com/sysprog21/mado
- https://github.com/sysprog21/rv32emu
- https://github.com/sysprog21/rv32emu/blob/master/tests/fenster.h
- https://mortoray.com/android-ndk-cross-compile-setup-libpng-and-freetype/
- https://makefiletutorial.com/