Try   HackMD

Improve window system for RISC-V

阮祈翰

Main goal

Run Mado on rv32emu by using SDL system call and RV32IM ISA (with B extension)

Research Focus

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
$ 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
  1. To test if xpm starts
$ which xpm
/home/eric/.nvm/versions/node/v22.13.0/bin/xpm
$ xpm --version
0.20.5
  1. Install xPack GNU RISC-V Embedded GCC globally
$ xpm install @xpack-dev-tools/riscv-none-elf-gcc@latest --global --verbose
  1. PATH setup (Make sure to check the version manually in order to set the correct $PATH)
$ export PATH=$HOME/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/bin:$PATH
  1. Quick test
$ riscv-none-elf-gcc --version
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.
  1. Write a C code
//hello.c #include <stdio.h> int main(){ printf("hello world!!!\n"); return 0; }
  1. Compile and run on rv32emu
riscv-none-elf-gcc -march=rv32gc -mabi=ilp32 -o hello hello.c
$ build/rv32emu hello
hello world!!!
inferior exit code 0

Try using SDL in rv32emu

  • Write a C code
#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
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

Download Mado

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Analyzing Mado

  • When using the command make.
$ 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
$ 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 to implement window display.

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
#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
+#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
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
# 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
-#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

  • Set environment variables
$ PACKAGE_PREFIX=$HOME/.local/xPacks/@xpack-dev-tools/riscv-none-elf-gcc/14.2.0-2.1/.content/riscv-none-elf
# 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.

$ build/rv32emu demo-rv32emu

Screenshot from 2025-01-19 14-30-25

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