The Low Level Programming Experience

Posted on 12th August 2018

If you have been following my GitHub this year (who am I kidding), you may have realised I took on a starkly different project - an emulator for an obsolete fictional computer, written in Standard C - a language which predates my birth by a fair margin! I'm talking about chipbox, my "low-level" project.

But why did I decide to be such a rebel? Well, I wanted to try something different. Up until chipbox, I have dealt with mainly very high-level technologies, things which abstracted my view of the actual hardware to a large degree and made me lazier than I would care to admit. Since I started programming with scripting languages like Javascript and Python, the computer did a lot of thinking for me. Which was fine. Until my curiosity got the better of me.

I decided to go all the way back to C, the grandfather of most programming languages in use today. Writing code in C was surprisingly easy given that it has fewer convenient features, and a type system which scripters would not have to concern themselves with. The most annoying part of C by far is the manual memory management. In higher level languages, you can make arrays of any size, with any type of variables, make them multidimensional, and even resize them after you made them. All that comes at a large cost in brain power in C. I missed the ease with which one could throw around memory like candy in Javascript. However, I also ended up realising just how inefficient my older code was, and how C forced me to think that memory was a scarce resource. Then, I also realised how unnecessary all those micro-optimisations were.

The nice part about C is you can compile it for basically anything. The bad part is you need to compile it for basically anything - beyond a simple "hello world", different platforms need different procedures. Thanks to using the awesome SDL2 library to abstract media input and output over many platforms, and CMake to make compiling on different platforms sane, the problem was largely solved. I also joined the club and found a random FindSDL2.cmake file because an official one does not exist.

After a few months of on and off work, I am pleased with chipbox. It does all the basics and a few fancy things such as resizing, "quirk" handling, and logging. Writing it in C, the "one true" low level programming language (ignoring assembly), was quite an eye opening experience and worthy challenge as it encouraged a somewhat different approach to what I am used to.

I leave you with the main "game loop" of my interpreter. It's a bit ugly, but it's also a work of art.

int run_chipbox(SDL_Renderer *renderer, byte *play_sound, byte file_data[], int size_to_read, struct chipbox_sdl_config *config) {
    SDL_Event e;
    struct chipbox_chip8_state state;
    int pixel_count;
    SDL_Rect pixel_rects[CHIPBOX_SCREEN_WIDTH_PIXELS * CHIPBOX_SCREEN_HEIGHT];
    unsigned long new_time;
    unsigned long current_time;
    unsigned long delta_time = 0;
    int running = 1;
    int i;
    int ticks_to_do;

    state = chipbox_init_state(config->tps);
    state.compat_mode = config->compat_mode;
    chipbox_cpu_load_program(&state, file_data, size_to_read);
    current_time = SDL_GetTicks();
    while (running) {
        new_time = SDL_GetTicks();
        delta_time += new_time - current_time;
        current_time = new_time;
        while(SDL_PollEvent(&e) != 0) {
            if (e.type == SDL_QUIT) {
                running = 0;
            }
        }
        chipbox_vm_update_input(&state);

        for (ticks_to_do = i = (delta_time * config->tps) / 1000; i > 0; i--) {
            if (!chipbox_vm_step(&state, config->min_log_level)) {
                running = 0;
                break;
            } else {
                *play_sound = handle_sound(state.ST);
            }
        }
        delta_time -= (ticks_to_do * 1000) / config->tps; /* account for left over time */

        chipbox_screen_to_sdl_rects(state.screen, pixel_rects, &pixel_count);
        chipbox_render(renderer, pixel_rects, pixel_count, config->scale);
    }

    return 0;
}