AstraOS-Tutorial: From nothing to window with colours

Do you want to learn how to set up AstraOS and write programs for it? Then you have come to the right place. This is the first ever tutorial or guide I am writing for AstraOS. It is still very early days so some things are slightly hacky but overall it shouldn’t be that difficult to follow, I hope. I am assuming you are running an arch based linux distribution and have some basic understanding of C. If you are using a non arch based linux distribution you will have to do some translation of the packages to be installed. I do know you can build everything on debian for instance, but some of the packages had slightly different names.

The first step is acquiring the toolchain used to compile all our AstraOS related software. As you may or may not know, AstraOS targets risc-v RV64-GV exclusively. This allows us to have many nice features that would otherwise be impossible. We start by cloning the risc-v toolchain with git. If you do not have git installed, install it first.

git clone https://github.com/riscv/riscv-gnu-toolchain.git

We also have to install the dependencies required by the toolchain. The following need to be installed on arch, other distros may vary.

sudo pacman -Syy autoconf automake curl python3 mpc mpfr gmp gawk \
base-devel bison flex texinfo gperf libtool patchutils bc zlib expat

Now we will install the risc-v toolchain to /opt/riscv. If you are nervous about running things as root, feel free to install it somewhere under your user. For the rest of you who don’t care about that but for some reason don’t like the fact that we are compiling with root permissions. I say, it’s easier this way for the common person. If you still care, you can modify the instructions to fit your liking. The following installs to /opt/riscv and touches no other files so if you want to get rid of it later you just delete /opt/riscv and the git repo.

cd riscv-gnu-toolchain && \
sudo mkdir /opt/riscv && \
export PATH=/opt/riscv/bin:$PATH && \
./configure --prefix=/opt/riscv && \
sudo make -j$(nproc) && \
cd ..

This will take quite a while, about 10 minutes on my machine. The size of the git repo after it’s downloaded everything is about 4 GB. This could be an excellent time to have some tea or go outside. If you don’t want the above command to use all the cores on your machine replace $(nproc) with the amount of threads you want used.

Now that we have the toolchain we can build AstraOS. Any time you want to be working with AstraOS you need to have /opt/riscv/bin in your PATH. If you are using the same terminal as you did in the above command then that is already the case. We start by cloning AstraOS.

git clone https://git.sr.ht/~samhsmith/AstraOS && \
cd AstraOS && \
git submodule update --init

Now, AstraOS’s awesomeness is enabled by it only supporting a specific set on not yet built hardware. So in the mean time we use a custom version of QEMU to emulate the future AstraOS capable box on your machine. To build QEMU we must first install the required dependencies.

sudo pacman -Sy ninja glusterfs libiscsi python \
python-sphinx spice-protocol xfsprogs

Now that we have installed the required dependencies on our arch based system we can build QEMU.

mkdir qemu/build && \
cd qemu/build && \
CFLAGS=-Wno-error ../configure --target-list=riscv64-softmmu && \
make -j$(nproc) || \
cd ../..

Now, at last, we can build and run AstraOS. Because we don’t live in the 80s and our computers can actually hold all of our source code in memory at once, we do not use a build system for AstraOS. If that shocks you, let me lay down the facts. Imagine you are trying to increment an integer as fast as possible. You have many cores on the system and you want to use them all. So you write a program that reads an int from a file on disk, increments it, then writes it to a new file. Then you run that program on every core at the same time. You see that all the cores are used and somehow you survive the cognitive dissonance enough to feel like you did a good job. You didn’t do a good job, you made it slower, yet you still get a promotion at the web company you are working for. I don’t control who gets a promotion at other companies, but here we don’t use build systems. The only build system you need is a script/program that runs the necessary steps to build your program.

Now that we have cleared that up. Execute the following commands to clean, build, and run AstraOS.

./build.sh clean && CFLAGS="-w -O3" ./build.sh && ./build.sh run

If nothing went wrong you should see a new window open with AstraOS running inside it. You should see mostly blue with a frametime indicator in the bottom right. If you click inside the window, the mouse is grabbed and you can move the mouse around inside AstraOS. There should be 2 items listed in the menu, super_cool_square and another_partition_with_nothing_in_it. As the latter suggests these are partitions on the virtual harddisk. You can change which one is currently selected with the up and down keys and attempt to launch the selected partition as a program with the enter key. If you launch another_partition_with_nothing_in_it you will see the message NOT ELF in the terminal indicating that there is no ELF file on the partition. If you launch super_cool_square however, you will see a new window pop up with a world-renowned spinning square. You can left-click and drag on the window to move it, right-click and drag to resize it, or you can press CTRL+F (CTRL+U on dvorak) to enter fullscreen. When in fullscreen the program receives input from the keyboard. You can therefore use the arrow keys to move the square around. When you don’t want to be in fullscreen anymore, simple press CTRL+F again. You can launch as many programs as you want, given that the virtual machines memory doesn’t run out. If any program crashes the whole OS faults. There is currently some stuttering due to QEMU not being built to deliver video frames continously, you get around this by constantly moving the mouse. Alpha software has sharp corners.

Creating a program

Now onto the exciting part, writing our own program. First we must add a new partition on the drive to store our program. Edit the file disk_dir/layout and add the line,

daves_colours 34 1

As you may guess this tells fsmake to create a partition called daves_colours with a size of 34 blocks (each block is always 4096 bytes in AstraOS). But if you attempt to run this partition in Astra you will find that it does not contain an ELF file. Let’s fix that. Let’s first try by copying the spinning square program into the partition.

cp bin/square disk_dir/partitions/daves_colours

Nice, running the partition produces the same spinning square. Now all we have to do is produce an alternative ELF file to use instead. Because I have not implemented streams in Astra yet, we have to jump through some extra hoops. This will be ironed out in the future though.

To start you want create a new folder the program. Copy the userland folder and all it’s contents over from AstraOS into your new source tree. This folder contains the OS’s syscalls. The syscalls depend on definitions in common/, so copy them over too. Lastly you will want to copy over src/uart.h, src/printf.c and src/printf.h, these are the placerholder for streams. Your directory should now look like this.

my_program/
├── common
│   ├── maths.h
│   └── types.h
├── printf.c
├── printf.h
├── uart.h
└── userland
    ├── aos_syscalls.h
    └── aos_syscalls.s

Now create a file called my_program.c and put this code inside it.

//
// First we need to import some types
//
#include "common/types.h"

//
// Then we include the syscall definitions.
//
#include "userland/aos_syscalls.h"

//
// Here is the tomfoolery required because we haven't
// implemented streams yet. You only need to know that
// this provides printf.
//
#include "uart.h"
#include "printf.h"
void _putchar(char c)
{
    uart_write(&c, 1);
}

//
// Since we are posix uncompliant and are compiling as
// freestanding we have no main and must start our program
// in _start.
//
void _start()
{
    printf("welcome to my program!\n");
    while(1)
    {
        AOS_thread_awake_after_time(10000000);
        AOS_thread_sleep();
    }
    // It also means that we can't exit so we must
    // sleep in a loop.
}

Then we compile the program with the riscv64-unknown-elf-gcc compiler that should exist on your path. We compile with the flags contained in the always_to_be_used_compiler_flags file. Then we can copy over the program and run Astra.

riscv64-unknown-elf-gcc -o my_program printf.c userland/aos_syscalls.s my_program.c \
-fno-merge-constants -mcmodel=medany -static -ffreestanding -nostdlib \
-march=rv64g -mabi=lp64

cp my_program ../AstraOS/disk_dir/partitions/daves_colours

../AstraOS/build.sh clean && \
CFLAGS="-w -O3" ../AstraOS/build.sh && \
../AstraOS/build.sh run

Great! We now have a program that prints out “welcome to my program!”. You can even see that an empty window has been created for us. How would we go about drawing to that window? Well in AstraOS it’s actually pretty simple. We ask to acquire a frame, then we draw to it by writing to memory, then we commit that frame. And to be proper and orderly we can also sleep until there is a new frame for us to render.

#include "common/types.h"

#include "userland/aos_syscalls.h"

#include "uart.h"
#include "printf.h"
void _putchar(char c)
{
    uart_write(&c, 1);
}

void _start()
{
    printf("welcome to my program!\n");

    u64 surface_id = 0;

    while(1)
    {
        // The reason we pass the surface we want to awake on as an
        // array is because in an advance application with
        // many windows you might want to wait on several surfaces
        AOS_thread_awake_on_surface(&surface_id, 1);
        AOS_thread_sleep();

        // here there is a frame to be rendered!

        // How many pages are needed for the framebuffer?
        u64 fb_page_count = AOS_surface_acquire(surface_id, 0, 0);

        // Here we are being lazy and just putting the framebuffer at some rando
        // address. 39 bits of virtual address space allows us to be sloppy.
        AOS_Framebuffer* fb = 0x23242000; // we have to put 3 zeros to align to the page boundry

        // the acquire could fail
        if(AOS_surface_acquire(surface_id, fb, fb_page_count))
        {

            // Great! we have a framebuffer, now we just have to loop through
            // all the pixels and assign a colour to them
            for(u64 y = 0; y < fb->height; y++)
            for(u64 x = 0; x < fb->width; x++)
            {
                u64 pixel_index = x + y * fb->width;

                fb->data[pixel_index*4 + 0] = 0.2; // red
                fb->data[pixel_index*4 + 1] = 0.6; // green
                fb->data[pixel_index*4 + 2] = 1.0; // blue
                fb->data[pixel_index*4 + 3] = 1.0; // alpha
            }

            // now finally we must commit the frame
            AOS_surface_commit(surface_id);
        }
    }
}

To run this we again execute the following commands

riscv64-unknown-elf-gcc -o my_program printf.c userland/aos_syscalls.s my_program.c \
-fno-merge-constants -mcmodel=medany -static -ffreestanding -nostdlib \
-march=rv64g -mabi=lp64

cp my_program ../AstraOS/disk_dir/partitions/daves_colours

../AstraOS/build.sh clean && \
CFLAGS="-w -O3" ../AstraOS/build.sh && \
../AstraOS/build.sh run

Now you know how to write a programs for AstraOS. Simple programs yes, but it doesn’t get that much more complicated. You should be able to figure out how to get keyboard input from looking at the spinning square example program. It’s source code is in square_src/elf.c. If you have any questions please email me at sam.henning.smith@protonmail.com. If you want to discuss anything please feel free to do so on the discussion mailing list. If you want to here from me in the future, subscribe to the announcements mailing list, or just ask me to notify you.

“AstraOS-Tutorial: From nothing to window with colours” was written by Sam H Smith on the 17th of June 2021, 2021-06-17.

Back to the blog