commit 973b00e231554a33e79b152c324301c13ce5414e
Author: MikoĊaj Lenczewski <mikolaj@lenczewski.org>
Date: Sun, 23 Feb 2025 15:25:06 +0000
Initial commit
Diffstat:
12 files changed, 1722 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,17 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+guidelines = 80, 120, 160
+
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
+
+[*.{md,txt}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+bin/
+dep/
+
+**/.*.swp
diff --git a/build.sh b/build.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+CC="${CC:-clang}"
+
+WARNINGS="-Wall -Wextra -Wpedantic ${WERROR:+-Werror} -Wno-unused-parameter"
+
+FLAGS="-DRTS_DEBUG" # -DRTS_LOGLEVEL=0"
+
+CFLAGS="-std=c11 -g -O0"
+CPPFLAGS="$FLAGS"
+LDFLAGS="-nostdlib"
+
+PLATFORM_CFLAGS="-std=c11 -g -O0"
+PLATFORM_CPPFLAGS="$FLAGS"
+PLATFORM_LDFLAGS="-Wl,--as-needed"
+
+PROTOCOLS=/usr/share/wayland-protocols
+DEPS="$(pkg-config --cflags --libs alsa xkbcommon wayland-client wayland-egl egl glesv2)"
+
+set -ex
+
+mkdir -p bin dep
+
+$CC -shared -o bin/rts.so rts/rts.c \
+ $WARNINGS $CFLAGS $CPPFLAGS $LDFLAGS
+
+wayland-scanner client-header $PROTOCOLS/stable/xdg-shell/xdg-shell.xml dep/xdg-shell.h
+wayland-scanner private-code $PROTOCOLS/stable/xdg-shell/xdg-shell.xml dep/xdg-shell.c
+
+$CC -o bin/linux_rts platform/linux.c dep/xdg-shell.c \
+ $WARNINGS $PLATFORM_CFLAGS $PLATFORM_CPPFLAGS -Idep $PLATFORM_LDFLAGS $DEPS
diff --git a/clean.sh b/clean.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+rm -rf bin dep
diff --git a/platform/linux.c b/platform/linux.c
@@ -0,0 +1,788 @@
+#define RTS_API_IMPL 1
+
+#include "linux.h"
+
+internal struct linux_state state;
+
+enum debug_level loglevel;
+
+int
+main(int argc, char **argv)
+{
+#ifdef RTS_LOGLEVEL
+ loglevel = RTS_LOGLEVEL;
+#else
+ loglevel = INFO; // WARN;
+#endif
+
+ memset(&state, 0, sizeof state);
+
+ load_platform_api(&state.platform_api);
+ load_renderer_api(&state.renderer_api);
+
+ char game_lib_path[PATH_MAX];
+ strcpy(game_lib_path, argv[0]);
+ strcpy(strrchr(game_lib_path, '/'), "/rts.so");
+
+ if (!load_game_api(game_lib_path, &state.rts_api)) {
+ dbglog(ERROR, "Failed to load game api: %s\n", game_lib_path);
+ exit(EXIT_FAILURE);
+ }
+
+ struct thread thread = {
+ .id = 0,
+ };
+
+ u64 total_memory = PLATFORM_MEMORY + GAME_MEMORY;
+ if (!init_memory(&state.memory, total_memory)) {
+ dbglog(ERROR, "Failed to initialise platform and game memory\n");
+ exit(EXIT_FAILURE);
+ }
+
+ struct arena platform_arena = {
+ .ptr = state.memory.ptr,
+ .cap = PLATFORM_MEMORY,
+ .len = 0,
+ };
+
+ struct memory game_memory = {
+ .ptr = (u8 *) state.memory.ptr + PLATFORM_MEMORY,
+ .len = GAME_MEMORY,
+ };
+
+ f32 target_render_rate = 60.0, target_physics_rate = 60.0;
+ u32 target_us_per_frame = USECS / target_render_rate;
+ u32 target_us_per_tick = USECS / target_physics_rate;
+
+ (void) target_us_per_tick;
+
+ u32 period_samples = RTS_AUDIO_SAMPLE_RATE / target_render_rate;
+ u32 buffer_samples = RTS_AUDIO_SAMPLE_RATE;
+ if (!init_audio(&platform_arena, &state.audio,
+ period_samples, buffer_samples)) {
+ dbglog(ERROR, "Failed to initialise platform audio\n");
+ exit(EXIT_FAILURE);
+ }
+
+ char const *title = "RTS Demo";
+ if (!init_video(&platform_arena, &state.video, title)) {
+ dbglog(ERROR, "Failed to initialise platform video\n");
+ exit(EXIT_FAILURE);
+ }
+
+ if (!state.rts_api.init(&thread, &game_memory, &state.platform_api)) {
+ dbglog(ERROR, "Failed to initialise rts game\n");
+ exit(EXIT_FAILURE);
+ }
+
+ state.running = 1;
+
+ f32 dt = 1 / target_render_rate;
+ while (state.running) {
+ struct timespec frame_start, frame_end;
+ clock_gettime(CLOCK_MONOTONIC, &frame_start);
+
+ struct timespec update_start, update_end;
+ clock_gettime(CLOCK_MONOTONIC, &update_start);
+
+ handle_pending_events(&state);
+
+#ifdef RTS_DEBUG
+ dbglog(INFO, "input: W/A/S/D: %d/%d/%d/%d , X/Y/Z: %03d/%03d/%03d\n",
+ state.video.input.keyboard.buttons[KEYBOARD_UP].was_pressed,
+ state.video.input.keyboard.buttons[KEYBOARD_LEFT].was_pressed,
+ state.video.input.keyboard.buttons[KEYBOARD_DOWN].was_pressed,
+ state.video.input.keyboard.buttons[KEYBOARD_RIGHT].was_pressed,
+ state.video.input.mouse.x,
+ state.video.input.mouse.y,
+ state.video.input.mouse.z);
+#endif
+
+ state.rts_api.update(&thread, &game_memory,
+ &state.platform_api,
+ &state.video.input,
+ state.video.width, state.video.height, dt);
+
+ clock_gettime(CLOCK_MONOTONIC, &update_end);
+
+ struct timespec sample_start, sample_end;
+ clock_gettime(CLOCK_MONOTONIC, &sample_start);
+
+ if (state.audio.enabled) {
+ update_expected_frames(&state.audio, dt);
+
+ state.rts_api.sample(&thread, &game_memory,
+ &state.platform_api,
+ &state.audio.audio_buffer,
+ dt);
+
+ play_audio(&state.audio);
+ } else {
+ reset_audio(&state.audio);
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &sample_end);
+
+ struct timespec render_start, render_end;
+ clock_gettime(CLOCK_MONOTONIC, &render_start);
+
+ state.rts_api.render(&thread, &game_memory,
+ &state.platform_api,
+ &state.renderer_api, state.video.renderer,
+ state.video.width, state.video.height, dt);
+
+ draw_frame(&state.video);
+
+ clock_gettime(CLOCK_MONOTONIC, &render_end);
+
+ clock_gettime(CLOCK_MONOTONIC, &frame_end);
+
+ u32 elapsed_us = linux_elapsed_ns(&frame_start, &frame_end) / 1000;
+ if (elapsed_us < target_us_per_frame) {
+ struct timespec delay = {
+ .tv_sec = 0,
+ .tv_nsec = (target_us_per_frame - elapsed_us) * 1000,
+ };
+
+ while (nanosleep(&delay, &delay) < 0);
+
+ clock_gettime(CLOCK_MONOTONIC, &frame_end);
+ elapsed_us = linux_elapsed_ns(&frame_start, &frame_end) / 1000;
+ } else {
+ /* TODO: missed frame */
+ }
+
+ dt = (f32) elapsed_us / USECS;
+
+#ifdef RTS_DEBUG
+ dbglog(INFO, "timings: "
+ "update: %" PRIu64 " ns, "
+ "sample: %" PRIu64 " ns, "
+ "render: %" PRIu64 " ns, "
+ "total: %" PRIu64 " ns, "
+ "\n",
+ linux_elapsed_ns(&update_start, &update_end),
+ linux_elapsed_ns(&sample_start, &sample_end),
+ linux_elapsed_ns(&render_start, &render_end),
+ linux_elapsed_ns(&frame_start, &frame_end));
+#endif
+ }
+
+ exit(EXIT_SUCCESS);
+}
+
+internal u64
+linux_elapsed_ns(struct timespec *restrict start, struct timespec *restrict end)
+{
+ return (end->tv_sec - start->tv_sec) * NSECS + (end->tv_nsec - start->tv_nsec);
+}
+
+#define LOADSYM(val, T, lib, sym) ((val) = (T *) dlsym(lib, sym))
+
+internal b32
+load_game_api(char const *path, struct rts_api *api)
+{
+ void *library = dlopen(path, RTLD_NOW);
+ if (!library)
+ return false;
+
+ LOADSYM(api->init, rts_api_init_t, library, "rts_init");
+ LOADSYM(api->update, rts_api_update_t, library, "rts_update");
+ LOADSYM(api->render, rts_api_render_t, library, "rts_render");
+ LOADSYM(api->sample, rts_api_sample_t, library, "rts_sample");
+ LOADSYM(api->free, rts_api_free_t, library, "rts_free");
+
+ return true;
+}
+
+internal b32
+init_memory(struct memory *memory, u64 capacity)
+{
+#ifdef RTS_DEBUG
+ void *base = (void *) (2 * TiB);
+#else
+ void *base = NULL;
+#endif
+
+ int prot = PROT_READ | PROT_WRITE;
+ int flags = MAP_PRIVATE | MAP_ANONYMOUS;
+
+#ifdef RTS_DEBUG
+ flags |= MAP_FIXED;
+#endif
+
+ memory->len = capacity;
+ memory->ptr = mmap(base, memory->len, prot, flags, -1, 0);
+ if (memory->ptr == MAP_FAILED)
+ return false;
+
+ madvise(memory->ptr, memory->len, MADV_HUGEPAGE);
+
+ return true;
+}
+
+internal void
+handle_pending_events(struct linux_state *state)
+{
+ wl_display_dispatch_pending(state->video.display);
+}
+
+internal b32
+init_audio(struct arena *arena, struct linux_audio *audio,
+ u32 period_samples, u32 buffer_samples)
+{
+ audio->enabled = false;
+
+ int err;
+ if ((err = snd_pcm_open(&audio->handle, "default",
+ SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0) {
+ dbglog(WARN, "snd_pcm_open: %s\n", snd_strerror(err));
+ return false;
+ }
+
+ snd_pcm_hw_params_t *hw_params = alloca(snd_pcm_hw_params_sizeof());
+ assert(hw_params);
+
+ snd_pcm_hw_params_any(audio->handle, hw_params);
+ snd_pcm_hw_params_set_access(audio->handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
+ snd_pcm_hw_params_set_format(audio->handle, hw_params, SND_PCM_FORMAT_S32_LE);
+ snd_pcm_hw_params_set_channels(audio->handle, hw_params, RTS_AUDIO_CHANNELS);
+ snd_pcm_hw_params_set_rate(audio->handle, hw_params, RTS_AUDIO_SAMPLE_RATE, 0);
+ snd_pcm_hw_params_set_period_size(audio->handle, hw_params, period_samples, 0);
+ snd_pcm_hw_params_set_buffer_size(audio->handle, hw_params, buffer_samples);
+
+ if ((err = snd_pcm_hw_params(audio->handle, hw_params)) < 0) {
+ dbglog(WARN, "and_pcm_hw_params: %s\n", snd_strerror(err));
+ return false;
+ }
+
+ int dir;
+ snd_pcm_hw_params_get_period_time(hw_params, &audio->period_us, &dir);
+ snd_pcm_hw_params_get_period_size(hw_params, &audio->period_frames, &dir);
+ snd_pcm_hw_params_get_buffer_size(hw_params, &audio->buffer_frames);
+
+ dbglog(INFO, "Audio period frames: %" PRIu64 ", %" PRIu32 " us\n",
+ audio->period_frames, audio->period_us);
+ dbglog(INFO, "Audio buffer frames: %" PRIu64 "\n", audio->buffer_frames);
+
+ audio->audio_buffer.buffer = ALLOC_ARRAY(arena, u8, RTS_AUDIO_FRAME_BYTES * audio->buffer_frames);
+ assert(audio->audio_buffer.buffer);
+
+ audio->audio_buffer.channels = RTS_AUDIO_CHANNELS;
+ audio->audio_buffer.sample_rate = RTS_AUDIO_SAMPLE_RATE;
+ audio->audio_buffer.sample_bits = RTS_AUDIO_SAMPLE_BITS;
+ audio->audio_buffer.samples = audio->buffer_frames;
+
+ audio->enabled = true;
+
+ return true;
+}
+
+internal void
+reset_audio(struct linux_audio *audio)
+{
+ if (snd_pcm_prepare(audio->handle) == 0)
+ audio->enabled = true;
+}
+
+internal void
+update_expected_frames(struct linux_audio *audio, f32 dt)
+{
+ snd_pcm_sframes_t total = audio->buffer_frames;
+ snd_pcm_sframes_t expected = RTS_AUDIO_SAMPLE_RATE * dt;
+
+ snd_pcm_sframes_t avail, delay;
+ if (snd_pcm_avail_delay(audio->handle, &avail, &delay) < 0) {
+ audio->enabled = false;
+ return;
+ }
+
+ dbglog(DEBUG, "total: %ld, expected: %ld, avail: %ld, pending: %ld\n",
+ total, expected, avail, delay);
+
+ audio->audio_buffer.samples = avail;
+}
+
+internal void
+play_audio(struct linux_audio *audio)
+{
+ s32 written_samples = snd_pcm_writei(audio->handle,
+ audio->audio_buffer.buffer,
+ audio->audio_buffer.samples);
+
+ if (written_samples < 0) {
+ written_samples = snd_pcm_recover(audio->handle, written_samples, 0);
+ } if ((u32) written_samples < audio->audio_buffer.samples) {
+ dbglog(DEBUG, "underrun! wrote %d/%" PRIu32 " samples\n",
+ written_samples, audio->audio_buffer.samples);
+ }
+
+ snd_pcm_sframes_t total = audio->buffer_frames, avail, delay;
+ snd_pcm_avail_delay(audio->handle, &avail, &delay);
+ dbglog(DEBUG, "total: %ld, written: %d, new avail: %ld, new pending: %ld\n",
+ total, written_samples, avail, delay);
+}
+
+internal void
+process_button_input(struct button_input *buttons, s32 id, b32 is_pressed)
+{
+ struct button_input *input = buttons + id;
+
+ if (input->was_pressed != is_pressed)
+ input->half_transition_count++;
+
+ input->was_pressed = is_pressed;
+}
+
+internal void
+process_button(struct input *input, u32 button, b32 is_pressed)
+{
+ switch (button) {
+ case BTN_LEFT:
+ process_button_input(input->mouse.buttons, MOUSE_L, is_pressed);
+ break;
+
+ case BTN_RIGHT:
+ process_button_input(input->mouse.buttons, MOUSE_R, is_pressed);
+ break;
+
+ case BTN_MIDDLE:
+ process_button_input(input->mouse.buttons, MOUSE_M, is_pressed);
+ break;
+
+ case BTN_X:
+ process_button_input(input->mouse.buttons, MOUSE_X, is_pressed);
+ break;
+
+ case BTN_Y:
+ process_button_input(input->mouse.buttons, MOUSE_Y, is_pressed);
+ break;
+ }
+}
+
+internal void
+wl_pointer_enter(void *data, struct wl_pointer *pointer, u32 serial,
+ struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y)
+{
+ struct linux_video *video = data;
+ video->input.mouse.x = wl_fixed_to_int(x);
+ video->input.mouse.y = wl_fixed_to_int(y);
+}
+
+internal void
+wl_pointer_leave(void *data, struct wl_pointer *pointer, u32 serial,
+ struct wl_surface *surface)
+{
+ struct linux_video *video = data;
+ video->input.mouse.x = video->input.mouse.y = video->input.mouse.z = 0;
+}
+
+internal void
+wl_pointer_motion(void *data, struct wl_pointer *pointer, u32 serial,
+ wl_fixed_t x, wl_fixed_t y)
+{
+ struct linux_video *video = data;
+ video->input.mouse.x = wl_fixed_to_int(x);
+ video->input.mouse.y = wl_fixed_to_int(y);
+}
+
+internal void
+wl_pointer_button(void *data, struct wl_pointer *pointer, u32 serial,
+ u32 time, u32 button, u32 state)
+{
+ struct linux_video *video = data;
+ process_button(&video->input, button, state);
+}
+
+internal void
+wl_pointer_axis(void *data, struct wl_pointer *pointer, u32 serial,
+ u32 axis, wl_fixed_t value)
+{
+ struct linux_video *video = data;
+ if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
+ video->input.mouse.z = wl_fixed_to_int(value);
+}
+
+internal const struct wl_pointer_listener wl_pointer_listener = {
+ .enter = wl_pointer_enter,
+ .leave = wl_pointer_leave,
+ .motion = wl_pointer_motion,
+ .button = wl_pointer_button,
+ .axis = wl_pointer_axis,
+};
+
+internal void
+process_keysym(struct input *input, xkb_keysym_t keysym, b32 is_pressed)
+{
+ switch (keysym) {
+ case XKB_KEY_w:
+ case XKB_KEY_Up:
+ process_button_input(input->keyboard.buttons, KEYBOARD_UP, is_pressed);
+ break;
+
+ case XKB_KEY_a:
+ case XKB_KEY_Left:
+ process_button_input(input->keyboard.buttons, KEYBOARD_LEFT, is_pressed);
+ break;
+
+ case XKB_KEY_s:
+ case XKB_KEY_Down:
+ process_button_input(input->keyboard.buttons, KEYBOARD_DOWN, is_pressed);
+ break;
+
+ case XKB_KEY_d:
+ case XKB_KEY_Right:
+ process_button_input(input->keyboard.buttons, KEYBOARD_RIGHT, is_pressed);
+ break;
+
+ case XKB_KEY_q:
+ process_button_input(input->keyboard.buttons, KEYBOARD_Q, is_pressed);
+ break;
+
+ case XKB_KEY_e:
+ process_button_input(input->keyboard.buttons, KEYBOARD_E, is_pressed);
+ break;
+
+ case XKB_KEY_r:
+ process_button_input(input->keyboard.buttons, KEYBOARD_R, is_pressed);
+ break;
+
+ case XKB_KEY_t:
+ process_button_input(input->keyboard.buttons, KEYBOARD_T, is_pressed);
+ break;
+
+ case XKB_KEY_i:
+ process_button_input(input->keyboard.buttons, KEYBOARD_I, is_pressed);
+ break;
+
+ case XKB_KEY_j:
+ process_button_input(input->keyboard.buttons, KEYBOARD_J, is_pressed);
+ break;
+
+ case XKB_KEY_k:
+ process_button_input(input->keyboard.buttons, KEYBOARD_K, is_pressed);
+ break;
+
+ case XKB_KEY_l:
+ process_button_input(input->keyboard.buttons, KEYBOARD_L, is_pressed);
+ break;
+
+ case XKB_KEY_space:
+ process_button_input(input->keyboard.buttons, KEYBOARD_SPACE, is_pressed);
+ break;
+
+ case XKB_KEY_Escape:
+ process_button_input(input->keyboard.buttons, KEYBOARD_ESCAPE, is_pressed);
+ break;
+ }
+}
+
+internal void
+wl_keyboard_keymap(void *data ,struct wl_keyboard *keyboard, u32 format,
+ int fd, u32 size)
+{
+ assert(format = WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1);
+
+ struct linux_video *video = data;
+
+ char *mapped = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ assert(mapped != MAP_FAILED);
+
+ xkb_keymap_unref(video->xkb_keymap);
+ video->xkb_keymap = xkb_keymap_new_from_string(video->xkb_context, mapped,
+ XKB_KEYMAP_FORMAT_TEXT_V1,
+ XKB_KEYMAP_COMPILE_NO_FLAGS);
+
+ munmap(mapped, size);
+ close(fd);
+
+ xkb_state_unref(video->xkb_state);
+ video->xkb_state = xkb_state_new(video->xkb_keymap);
+}
+
+internal void
+wl_keyboard_enter(void *data, struct wl_keyboard *keyboard, u32 serial,
+ struct wl_surface *surface, struct wl_array *keys)
+{
+ struct linux_video *video = data;
+
+ u32 *key;
+ wl_array_for_each(key, keys) {
+ xkb_keysym_t sym = xkb_state_key_get_one_sym(video->xkb_state, *key + 8);
+ process_keysym(&video->input, sym, 1);
+ }
+}
+
+internal void
+wl_keyboard_leave(void *data, struct wl_keyboard *keyboard, u32 serial,
+ struct wl_surface *surface)
+{
+}
+
+internal void
+wl_keyboard_key(void *data, struct wl_keyboard *keyboard, u32 serial,
+ u32 time, u32 key, u32 state)
+{
+ struct linux_video *video = data;
+ xkb_keysym_t sym = xkb_state_key_get_one_sym(video->xkb_state, key + 8);
+ process_keysym(&video->input, sym, state);
+}
+
+internal void
+wl_keyboard_modifiers(void *data, struct wl_keyboard *keyboard, u32 serial,
+ u32 depressed, u32 latched, u32 locked, u32 group)
+{
+ struct linux_video *video = data;
+ xkb_state_update_mask(video->xkb_state, depressed, latched, locked, 0, 0, group);
+}
+
+internal const struct wl_keyboard_listener wl_keyboard_listener = {
+ .keymap = wl_keyboard_keymap,
+ .enter = wl_keyboard_enter,
+ .leave = wl_keyboard_leave,
+ .key = wl_keyboard_key,
+ .modifiers = wl_keyboard_modifiers,
+};
+
+internal void
+wl_seat_capabilities(void *data, struct wl_seat *seat, u32 caps)
+{
+ struct linux_video *video = data;
+
+ b32 has_pointer = TESTBITS(caps, WL_SEAT_CAPABILITY_POINTER);
+ if (has_pointer && !video->pointer) {
+ video->pointer = wl_seat_get_pointer(seat);
+ wl_pointer_add_listener(video->pointer, &wl_pointer_listener, video);
+ } else if (!has_pointer && video->pointer) {
+ wl_pointer_release(video->pointer);
+ video->pointer = NULL;
+ }
+
+ b32 has_keyboard = TESTBITS(caps, WL_SEAT_CAPABILITY_KEYBOARD);
+ if (has_keyboard && !video->keyboard) {
+ video->keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener(video->keyboard, &wl_keyboard_listener, video);
+ } else if (!has_keyboard && video->keyboard) {
+ wl_keyboard_release(video->keyboard);
+ video->keyboard = NULL;
+ }
+}
+
+internal void
+wl_seat_name(void *data, struct wl_seat *seat, char const *name)
+{
+}
+
+internal const struct wl_seat_listener wl_seat_listener = {
+ .capabilities = wl_seat_capabilities,
+ .name = wl_seat_name,
+};
+
+internal void
+xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm_base, u32 serial)
+{
+ xdg_wm_base_pong(wm_base, serial);
+}
+
+internal const struct xdg_wm_base_listener xdg_wm_base_listener = {
+ .ping = xdg_wm_base_handle_ping,
+};
+
+internal void
+xdg_surface_handle_configure(void *data, struct xdg_surface *xdg_surface, u32 serial)
+{
+ xdg_surface_ack_configure(xdg_surface, serial);
+}
+
+internal const struct xdg_surface_listener xdg_surface_listener = {
+ .configure = xdg_surface_handle_configure,
+};
+
+internal void
+xdg_toplevel_handle_configure(void *data, struct xdg_toplevel *toplevel, s32 width, s32 height, struct wl_array *states)
+{
+ struct linux_video *video = data;
+ video->width = width ? width : RTS_VIDEO_HRES;
+ video->height = height ? height : RTS_VIDEO_HRES;
+ wl_egl_window_resize(video->egl_window, video->width, video->height, 0, 0);
+}
+
+internal void
+xdg_toplevel_handle_close(void *data, struct xdg_toplevel *toplevel)
+{
+ state.running = false;
+}
+
+internal void
+xdg_toplevel_handle_configure_bounds(void *data, struct xdg_toplevel *toplevel, s32 width, s32 height)
+{
+ struct linux_video *video = data;
+ video->width = width ? width : RTS_VIDEO_HRES;
+ video->height = height ? height : RTS_VIDEO_HRES;
+ wl_egl_window_resize(video->egl_window, video->width, video->height, 0, 0);
+}
+
+internal void
+xdg_toplevel_handle_wm_capabilities(void *data, struct xdg_toplevel *toplevel, struct wl_array *caps)
+{
+ // TODO: handle me, do we care about any special capabilities?
+}
+
+internal const struct xdg_toplevel_listener xdg_toplevel_listener = {
+ .configure = xdg_toplevel_handle_configure,
+ .close = xdg_toplevel_handle_close,
+ .configure_bounds = xdg_toplevel_handle_configure_bounds,
+ .wm_capabilities = xdg_toplevel_handle_wm_capabilities,
+};
+
+internal void
+wl_registry_handle_global(void *data, struct wl_registry *registry, u32 name, char const *interface, u32 version)
+{
+ struct linux_video *video = data;
+
+ dbglog(DEBUG, "wl: interface: %u, name: %s, version: %u\n", name, interface, version);
+
+ if (strcmp(interface, wl_compositor_interface.name) == 0) {
+ video->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 6);
+ } else if (strcmp(interface, wl_seat_interface.name) == 0) {
+ video->seat = wl_registry_bind(registry, name, &wl_seat_interface, 3);
+ wl_seat_add_listener(video->seat, &wl_seat_listener, data);
+ } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
+ video->wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 6);
+ xdg_wm_base_add_listener(video->wm_base, &xdg_wm_base_listener, data);
+ }
+}
+
+internal void
+wl_registry_handle_global_remove(void *data, struct wl_registry *registry, u32 name)
+{
+}
+
+internal const struct wl_registry_listener wl_registry_listener = {
+ .global = wl_registry_handle_global,
+ .global_remove = wl_registry_handle_global_remove,
+};
+
+internal b32
+init_video(struct arena *arena, struct linux_video *video, char const *title)
+{
+ video->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+
+ if (!(video->display = wl_display_connect(NULL))) {
+ dbglog(WARN, "wl_display_connect: could not connect\n");
+ return false;
+ }
+
+ video->registry = wl_display_get_registry(video->display);
+ wl_registry_add_listener(video->registry, &wl_registry_listener, video);
+ wl_display_roundtrip(video->display);
+
+ assert(video->compositor);
+ assert(video->seat);
+ assert(video->wm_base);
+
+ video->surface = wl_compositor_create_surface(video->compositor);
+ video->egl_window = wl_egl_window_create(video->surface, RTS_VIDEO_HRES, RTS_VIDEO_VRES);
+
+ video->xdg_surface = xdg_wm_base_get_xdg_surface(video->wm_base, video->surface);
+ xdg_surface_add_listener(video->xdg_surface, &xdg_surface_listener, NULL);
+
+ video->xdg_toplevel = xdg_surface_get_toplevel(video->xdg_surface);
+ xdg_toplevel_add_listener(video->xdg_toplevel, &xdg_toplevel_listener, video);
+
+ xdg_toplevel_set_app_id(video->xdg_toplevel, title);
+ xdg_toplevel_set_title(video->xdg_toplevel, title);
+
+ wl_surface_commit(video->surface);
+ wl_display_roundtrip(video->display);
+
+ video->egl_display = eglGetDisplay(video->display);
+
+ EGLint major, minor;
+ eglInitialize(video->egl_display, &major, &minor);
+
+ dbglog(INFO, "EGL version: %d.%d\n", major, minor);
+
+ eglBindAPI(EGL_OPENGL_API);
+
+ EGLint attrs[] = {
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_ALPHA_SIZE, 8,
+ EGL_NONE,
+ };
+
+ EGLConfig config;
+ EGLint num_config;
+ eglChooseConfig(video->egl_display, attrs, &config, 1, &num_config);
+
+ video->egl_context = eglCreateContext(video->egl_display, config, EGL_NO_CONTEXT, NULL);
+
+ video->egl_surface = eglCreateWindowSurface(video->egl_display, config, video->egl_window, NULL);
+ if (video->egl_surface == EGL_NO_SURFACE) {
+ dbglog(WARN, "Failed to create surface!\n");
+ return false;
+ }
+
+ eglMakeCurrent(video->egl_display, video->egl_surface, video->egl_surface, video->egl_context);
+ eglSwapInterval(video->egl_display, 0); // make eglSwapBuffers non-blocking, at cost of frame tearing
+
+ if (!(video->renderer = create_renderer(arena, video->width, video->height))) {
+ dbglog(DEBUG, "Failed to initialise egl renderer\n");
+ return false;
+ }
+
+ return true;
+}
+
+internal void
+draw_frame(struct linux_video *video)
+{
+ eglSwapBuffers(video->egl_display, video->egl_surface);
+}
+
+/* platform api
+ * ===========================================================================
+ */
+
+#ifdef RTS_DEBUG
+
+DEBUG_PLATFORM_READ_FILE(DEBUG_platform_read_file)
+{
+ return (struct DEBUG_platform_read_file_result) {0};
+}
+
+DEBUG_PLATFORM_WRITE_FILE(DEBUG_platform_write_file)
+{
+ return false;
+}
+
+DEBUG_PLATFORM_FREE_FILE(DEBUG_platform_free_file)
+{
+}
+
+#endif /* RTS_DEBUG */
+
+PLATFORM_PRINTF(platform_printf)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ size_t res = vprintf(fmt, ap);
+ va_end(ap);
+
+ return res;
+}
+
+internal void
+load_platform_api(struct platform_api *api)
+{
+#ifdef RTS_DEBUG
+ api->DEBUG_read_file = DEBUG_platform_read_file;
+ api->DEBUG_write_file = DEBUG_platform_write_file;
+ api->DEBUG_free_file = DEBUG_platform_free_file;
+#endif
+ api->printf = platform_printf;
+}
+
+#include "renderer.c"
diff --git a/platform/linux.h b/platform/linux.h
@@ -0,0 +1,169 @@
+#ifndef PLATFORM_LINUX_H
+#define PLATFORM_LINUX_H
+
+#define _XOPEN_SOURCE 700
+#define _GNU_SOURCE 1
+#define _DEFAULT_SOURCE 1
+
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/mman.h>
+
+#include <dlfcn.h>
+
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <alsa/asoundlib.h>
+
+#include <xkbcommon/xkbcommon.h>
+#include <linux/input-event-codes.h>
+
+#include <wayland-client.h>
+#include "xdg-shell.h"
+
+#include <wayland-egl.h>
+#include <EGL/egl.h>
+
+#include "../rts/api.h"
+#include "renderer.h"
+
+enum debug_level {
+ ERROR,
+ WARN,
+ INFO,
+ DEBUG,
+};
+
+extern enum debug_level loglevel;
+
+static inline void
+dbglog(enum debug_level level, char const *fmt, ...)
+{
+ if (level > loglevel)
+ return;
+
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+#define PLATFORM_MEMORY (64 * MiB)
+#define GAME_MEMORY (64 * MiB)
+
+struct linux_thread {
+ u32 id;
+};
+
+#if 0
+#define RTS_AUDIO_CHANNELS 2
+#define RTS_AUDIO_SAMPLE_RATE 48000
+#define RTS_AUDIO_SAMPLE_BITS 32
+
+#define RTS_AUDIO_FRAME_BYTES \
+ (RTS_AUDIO_CHANNELS * (RTS_AUDIO_SAMPLE_BITS / 8))
+
+struct audio {
+ void *buffer;
+ u32 channels;
+ u32 sample_rate;
+ u32 sample_bits;
+ u32 expected_frames;
+};
+#endif
+
+struct linux_audio {
+ snd_pcm_t *handle;
+
+ u32 period_us;
+ snd_pcm_uframes_t period_frames;
+ snd_pcm_uframes_t buffer_frames;
+
+ struct audio audio_buffer;
+
+ b32 enabled;
+};
+
+struct linux_video {
+ struct wl_display *display;
+ struct wl_registry *registry;
+
+ struct wl_compositor *compositor;
+ struct wl_seat *seat;
+ struct xdg_wm_base *wm_base;
+
+ struct wl_pointer *pointer;
+ struct wl_keyboard *keyboard;
+
+ struct xkb_context *xkb_context;
+ struct xkb_keymap *xkb_keymap;
+ struct xkb_state *xkb_state;
+
+ struct input input;
+
+ struct wl_surface *surface;
+ struct xdg_surface *xdg_surface;
+ struct xdg_toplevel *xdg_toplevel;
+
+ struct wl_egl_window *egl_window;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+
+ struct renderer *renderer;
+
+ u32 width, height;
+};
+
+struct linux_state {
+ struct memory memory;
+
+ struct linux_audio audio;
+ struct linux_video video;
+
+ struct platform_api platform_api;
+ struct renderer_api renderer_api;
+ struct rts_api rts_api;
+
+ b32 running;
+};
+
+internal u64
+linux_elapsed_ns(struct timespec *restrict start, struct timespec *restrict end);
+
+internal b32
+load_game_api(char const *path, struct rts_api *api);
+
+internal void
+load_platform_api(struct platform_api *api);
+
+internal b32
+init_memory(struct memory *memory, u64 capacity);
+
+internal void
+handle_pending_events(struct linux_state *state);
+
+internal b32
+init_audio(struct arena *arena, struct linux_audio *audio,
+ u32 period_samples, u32 buffer_samples);
+
+internal void
+reset_audio(struct linux_audio *audio);
+
+internal void
+update_expected_frames(struct linux_audio *audio, f32 dt);
+
+internal void
+play_audio(struct linux_audio *audio);
+
+internal b32
+init_video(struct arena *arena, struct linux_video *video, char const *title);
+
+internal void
+draw_frame(struct linux_video *video);
+
+#endif /* PLATFORM_LINUX_H */
diff --git a/platform/renderer.c b/platform/renderer.c
@@ -0,0 +1,50 @@
+#include "renderer.h"
+
+struct renderer {
+ u32 width, height;
+};
+
+internal struct renderer *
+create_renderer(struct arena *arena, u32 width, u32 height)
+{
+ struct renderer *renderer = ALLOC_SIZED(arena, struct renderer);
+ assert(renderer);
+
+ renderer->width = width;
+ renderer->height = height;
+
+ return renderer;
+}
+
+RENDERER_BEGIN_FRAME(renderer_begin_frame)
+{
+ return NULL;
+}
+
+RENDERER_BEGIN_RENDER_PASS(renderer_begin_render_pass)
+{
+}
+
+RENDERER_CLEAR_SCREEN(renderer_clear_screen)
+{
+ glClearColor(colour->r, colour->g, colour->b, colour->a);
+ glClear(GL_COLOR_BUFFER_BIT);
+}
+
+RENDERER_END_RENDER_PASS(renderer_end_render_pass)
+{
+}
+
+RENDERER_END_FRAME(renderer_end_frame)
+{
+}
+
+internal void
+load_renderer_api(struct renderer_api *api)
+{
+ api->begin_frame = renderer_begin_frame;
+ api->begin_render_pass = renderer_begin_render_pass;
+ api->clear_screen = renderer_clear_screen;
+ api->end_render_pass = renderer_end_render_pass;
+ api->end_frame = renderer_end_frame;
+}
diff --git a/platform/renderer.h b/platform/renderer.h
@@ -0,0 +1,14 @@
+#ifndef PLATFORM_RENDERER_H
+#define PLATFORM_RENDERER_H
+
+#include "../rts/api.h"
+
+#include <GLES/gl.h>
+
+internal struct renderer *
+create_renderer(struct arena *arena, u32 width, u32 height);
+
+internal void
+load_renderer_api(struct renderer_api *api);
+
+#endif /* PLATFORM_RENDERER_H */
diff --git a/rts/api.h b/rts/api.h
@@ -0,0 +1,457 @@
+#ifndef RTS_API_H
+#define RTS_API_H
+
+#include <stdalign.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* type definitions
+ * ===========================================================================
+ */
+
+typedef int32_t b32;
+
+#define true 1
+#define false 0
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64;
+
+typedef float f32;
+typedef double f64;
+
+typedef unsigned char c8;
+
+#define KiB 1024ULL
+#define MiB (1024 * KiB)
+#define GiB (1024 * MiB)
+#define TiB (1024 * GiB)
+
+#define MSECS (1000ULL)
+#define USECS (1000 * MSECS)
+#define NSECS (1000 * USECS)
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define CLAMP(v, min, max) MIN(MAX(v, min), max)
+
+#define LERP(v0, v1, t) (((1 - (t)) * (v0)) + ((t) * (v1)))
+
+#define TESTBITS(v, mask) (((v) & (mask)) == (mask))
+
+#define IS_POW2(v) (((v) & ((v) - 1)) == 0)
+#define IS_ALIGNED(v, align) (((v) & ~((align) - 1)) == 0)
+
+#define ALIGN_PREV(v, align) ((v) & ~((align) - 1))
+#define ALIGN_NEXT(v, align) ALIGN_PREV((v) + (align) - 1, (align))
+
+#define TO_PARENT(ptr, T, member) ((T *) ((uintptr_t) ptr - offsetof(T, member)))
+
+#ifdef STARFIELD_DEBUG
+# define ASSERT(cond) (!(cond) && *((volatile u8 *) 0) = 0)
+#else
+# define ASSERT(cond)
+#endif
+
+#define internal static
+#define func_local static
+
+/* utility definitions
+ * ===========================================================================
+ */
+
+struct str {
+ c8 *ptr;
+ u64 len;
+};
+
+inline c8 *
+strnchr(c8 *src, c8 *end, c8 marker)
+{
+ while (src < end && *src != marker)
+ src++;
+
+ if (src == end)
+ return NULL;
+
+ return src;
+}
+
+inline c8 *
+strnstr(c8 *src, c8 *end, c8 *marker)
+{
+ while (src < end) {
+ c8 *ptr = src;
+ c8 *cur = marker;
+
+ while (ptr < end && *cur && *cur == *ptr) {
+ ptr++;
+ cur++;
+ }
+
+ if (!cur)
+ goto found;
+
+ if (ptr == end)
+ goto not_found;
+
+ src = ptr;
+ }
+
+not_found:
+ return NULL;
+
+found:
+ return src;
+}
+
+struct arena {
+ u8 *ptr;
+ u64 cap, len;
+};
+
+inline void
+arena_reset(struct arena *arena)
+{
+ arena->len = 0;
+}
+
+inline void *
+arena_alloc(struct arena *arena, size_t size, size_t align)
+{
+ u64 aligned_len = ALIGN_NEXT(arena->len, align);
+ if (aligned_len + size > arena->cap)
+ return NULL;
+
+ void *ptr = (void *) (uintptr_t) (arena->ptr + aligned_len);
+ arena->len = aligned_len + size;
+ return ptr;
+}
+
+#define ALLOC_ARRAY(arena, T, n) \
+ arena_alloc((arena), sizeof(T) * (n), alignof(T))
+
+#define ALLOC_SIZED(arena, T) ALLOC_ARRAY((arena), T, 1)
+
+struct list_node {
+ struct list_node *prev, *next;
+};
+
+#define FROM_NODE(node, T, member) TO_PARENT(node, T, member)
+
+#define LIST_NODE_ITER(node) \
+ for (struct list_node *it = (node); it; it = it->next)
+
+#define LIST_NODE_RITER(node) \
+ for (struct list_node *it = (node); it; it = it->prev)
+
+struct list {
+ struct list_node *head, *tail;
+};
+
+#define LIST_ITER(list) \
+ LIST_NODE_ITER((list)->head)
+
+#define LIST_RITER(list) \
+ LIST_NODE_RITER((list)->tail)
+
+inline void
+list_push_head(struct list *restrict list, struct list_node *restrict node)
+{
+ if (!list->tail)
+ list->tail = node;
+
+ if (list->head)
+ list->head->prev = node;
+
+ node->next = list->head;
+ list->head = node;
+}
+
+inline void
+list_push_tail(struct list *restrict list, struct list_node *restrict node)
+{
+ if (!list->head)
+ list->head = node;
+
+ if (list->tail)
+ list->tail->next = node;
+
+ node->prev = list->tail;
+ list->tail = node;
+}
+
+inline struct list_node *
+list_pop_head(struct list *list)
+{
+ if (!list->head)
+ return NULL;
+
+ struct list_node *node = list->head;
+ list->head = node->next;
+ return node;
+}
+
+inline struct list_node *
+list_pop_tail(struct list *list)
+{
+ if (!list->tail)
+ return NULL;
+
+ struct list_node *node = list->tail;
+ list->tail = node->prev;
+ return node;
+}
+
+/* platform api definitions
+ * ===========================================================================
+ */
+
+#ifdef RTS_DEBUG
+
+struct DEBUG_platform_read_file_result {
+ void *ptr;
+ u64 len;
+};
+
+#define DEBUG_PLATFORM_READ_FILE(name) \
+ struct DEBUG_platform_read_file_result name(c8 const *path)
+
+#define DEBUG_PLATFORM_WRITE_FILE(name) \
+ b32 name(c8 const *path, void *buf, u64 len)
+
+#define DEBUG_PLATFORM_FREE_FILE(name) \
+ void name(void *ptr)
+
+typedef DEBUG_PLATFORM_READ_FILE(DEBUG_platform_api_read_file_t);
+typedef DEBUG_PLATFORM_WRITE_FILE(DEBUG_platform_api_write_file_t);
+typedef DEBUG_PLATFORM_FREE_FILE(DEBUG_platform_api_free_file_t);
+
+#endif
+
+#define PLATFORM_PRINTF(name) \
+ size_t name(char const *fmt, ...)
+
+typedef PLATFORM_PRINTF(platform_api_printf_t);
+
+struct platform_api {
+#ifdef RTS_DEBUG
+ DEBUG_platform_api_read_file_t *DEBUG_read_file;
+ DEBUG_platform_api_write_file_t *DEBUG_write_file;
+ DEBUG_platform_api_free_file_t *DEBUG_free_file;
+#endif
+ platform_api_printf_t *printf;
+};
+
+/* renderer api definitions
+ * ===========================================================================
+ */
+
+struct renderer;
+struct render_frame;
+
+struct colour {
+ f32 r, g, b, a;
+};
+
+#define RENDERER_BEGIN_FRAME(name) \
+ struct render_frame *name(struct renderer *renderer, u32 width, u32 height)
+
+#define RENDERER_BEGIN_RENDER_PASS(name) \
+ void name(struct renderer *renderer, struct render_frame *frame)
+
+#define RENDERER_CLEAR_SCREEN(name) \
+ void name(struct renderer *renderer, struct render_frame *frame, struct colour *colour)
+
+#define RENDERER_END_RENDER_PASS(name) \
+ void name(struct renderer *renderer, struct render_frame *frame)
+
+#define RENDERER_END_FRAME(name) \
+ void name(struct renderer *renderer, struct render_frame *frame)
+
+typedef RENDERER_BEGIN_FRAME(renderer_api_begin_frame_t);
+typedef RENDERER_BEGIN_RENDER_PASS(renderer_api_begin_render_pass_t);
+typedef RENDERER_CLEAR_SCREEN(renderer_api_clear_screen_t);
+typedef RENDERER_END_RENDER_PASS(renderer_api_end_render_pass_t);
+typedef RENDERER_END_FRAME(renderer_api_end_frame_t);
+
+struct renderer_api {
+ renderer_api_begin_frame_t *begin_frame;
+ renderer_api_begin_render_pass_t *begin_render_pass;
+ renderer_api_clear_screen_t *clear_screen;
+ renderer_api_end_render_pass_t *end_render_pass;
+ renderer_api_end_frame_t *end_frame;
+};
+
+/* rts api definitions
+ * ===========================================================================
+ */
+
+struct thread {
+ u32 id;
+};
+
+struct memory {
+ void *ptr;
+ u64 len;
+};
+
+struct button_input {
+ u32 half_transition_count;
+ b32 was_pressed;
+};
+
+enum mouse_button {
+ MOUSE_L,
+ MOUSE_R,
+ MOUSE_M,
+
+ MOUSE_X,
+ MOUSE_Y,
+
+ _MOUSE_BUTTON_COUNT,
+};
+
+struct mouse_input {
+ s16 x, y, z;
+ struct button_input buttons[_MOUSE_BUTTON_COUNT];
+};
+
+enum keyboard_button {
+ KEYBOARD_ESCAPE,
+
+ KEYBOARD_UP,
+ KEYBOARD_LEFT,
+ KEYBOARD_DOWN,
+ KEYBOARD_RIGHT,
+
+ KEYBOARD_Q,
+ KEYBOARD_E,
+ KEYBOARD_R,
+ KEYBOARD_T,
+
+ KEYBOARD_I,
+ KEYBOARD_J,
+ KEYBOARD_K,
+ KEYBOARD_L,
+
+ KEYBOARD_SPACE,
+ KEYBOARD_SHIFT,
+ KEYBOARD_CTRL,
+
+ _KEYBOARD_BUTTON_COUNT,
+};
+
+struct keyboard_input {
+ struct button_input buttons[_KEYBOARD_BUTTON_COUNT];
+};
+
+struct input {
+ struct mouse_input mouse;
+ struct keyboard_input keyboard;
+};
+
+#define RTS_VIDEO_HRES 1024 // 1920
+#define RTS_VIDEO_VRES 576 // 1080
+
+#define RTS_VIDEO_BYTES_PER_PIXEL 4
+
+#define RTS_AUDIO_CHANNELS 2
+#define RTS_AUDIO_SAMPLE_RATE 48000
+#define RTS_AUDIO_SAMPLE_BITS 32
+
+#define RTS_AUDIO_FRAME_BYTES \
+ (RTS_AUDIO_CHANNELS * (RTS_AUDIO_SAMPLE_BITS / 8))
+
+struct audio {
+ void *buffer;
+ u32 channels;
+ u32 sample_rate;
+ u32 sample_bits;
+ u32 samples;
+};
+
+#define RTS_INIT(name) \
+ b32 name(struct thread *thread, \
+ struct memory *memory, \
+ struct platform_api *platform)
+
+#define RTS_FREE(name) \
+ void name(struct thread *thread, \
+ struct memory *memory, \
+ struct platform_api *platform)
+
+#define RTS_UPDATE(name) \
+ void name(struct thread *thread, \
+ struct memory *memory, \
+ struct platform_api *platform, \
+ struct input *input, \
+ u32 width, u32 height, f32 dt)
+
+#define RTS_SAMPLE(name) \
+ void name(struct thread *thread, \
+ struct memory *memory, \
+ struct platform_api *platform, \
+ struct audio *audio, \
+ f32 dt)
+
+#define RTS_RENDER(name) \
+ void name(struct thread *thread, \
+ struct memory *memory, \
+ struct platform_api *platform, \
+ struct renderer_api *renderer_api, \
+ struct renderer *renderer, \
+ u32 width, u32 height, f32 dt)
+
+typedef RTS_INIT(rts_api_init_t);
+typedef RTS_FREE(rts_api_free_t);
+typedef RTS_UPDATE(rts_api_update_t);
+typedef RTS_SAMPLE(rts_api_sample_t);
+typedef RTS_RENDER(rts_api_render_t);
+
+struct rts_api {
+ rts_api_init_t *init;
+ rts_api_free_t *free;
+ rts_api_update_t *update;
+ rts_api_sample_t *sample;
+ rts_api_render_t *render;
+};
+
+#endif /* RTS_API_H */
+
+#ifdef RTS_API_IMPL
+
+extern inline c8 *
+strnchr(c8 *src, c8 *end, c8 marker);
+
+extern inline c8 *
+strnstr(c8 *src, c8 *end, c8 *marker);
+
+extern inline void
+arena_reset(struct arena *arena);
+
+extern inline void *
+arena_alloc(struct arena *arena, size_t size, size_t align);
+
+extern inline void
+list_push_head(struct list *list, struct list_node *node);
+
+extern inline void
+list_push_tail(struct list *list, struct list_node *node);
+
+extern inline struct list_node *
+list_pop_head(struct list *list);
+
+extern inline struct list_node *
+list_pop_tail(struct list *list);
+
+#endif
diff --git a/rts/rts.c b/rts/rts.c
@@ -0,0 +1,179 @@
+#include "rts.h"
+
+static inline void *
+memset(void *dst, unsigned char val, size_t len)
+{
+ unsigned char *lhs = dst;
+ while (len--) *lhs++ = val;
+ return dst;
+}
+
+#if 0
+
+static inline void *
+memcpy(void *dst, void *src, size_t len)
+{
+ unsigned char *lhs = dst, *rhs = src;
+ while (len--) *lhs++ = *rhs++;
+ return dst;
+}
+
+#endif
+
+struct sound {
+ s32 hz;
+ s32 volume;
+ u32 samples;
+};
+
+struct mixer_sound {
+ struct sound sound;
+ u32 ctr;
+
+ struct list_node list_node;
+};
+
+struct mixer {
+ struct list sounds;
+};
+
+struct rts_state {
+ s32 tone_hz;
+ s32 tone_volume;
+ f32 tone_t;
+
+ struct mixer_sound sound_a, sound_b;
+
+ struct mixer mixer;
+};
+
+#define TONE_HZ_MIN 64
+#define TONE_HZ_MAX 1024
+
+#define TONE_VOLUME_MIN (INT32_MAX / 16)
+#define TONE_VOLUME_MAX (INT32_MAX / 8)
+
+RTS_INIT(rts_init)
+{
+ struct rts_state *state = memory->ptr;
+
+ state->tone_hz = 0;
+ state->tone_volume = 0;
+ state->tone_t = 0;
+
+ state->sound_a.sound.hz = 256;
+ state->sound_a.sound.volume = INT32_MAX / 16;
+ state->sound_a.sound.samples= RTS_AUDIO_SAMPLE_RATE;
+ state->sound_a.ctr = 0;
+ state->sound_a.list_node.prev = state->sound_a.list_node.next = NULL;
+
+ state->sound_b.sound.hz = 1024;
+ state->sound_b.sound.volume = INT32_MAX / 24;
+ state->sound_b.sound.samples= RTS_AUDIO_SAMPLE_RATE / 2;
+ state->sound_b.ctr = 0;
+ state->sound_b.list_node.prev = state->sound_b.list_node.next = NULL;
+
+ state->mixer.sounds.head = state->mixer.sounds.tail = NULL;
+
+ return true;
+}
+
+RTS_FREE(rts_free)
+{
+}
+
+RTS_UPDATE(rts_update)
+{
+ struct rts_state *state = memory->ptr;
+
+ if (input->keyboard.buttons[KEYBOARD_Q].was_pressed)
+ list_push_tail(&state->mixer.sounds, &state->sound_a.list_node);
+
+ if (input->keyboard.buttons[KEYBOARD_E].was_pressed)
+ list_push_tail(&state->mixer.sounds, &state->sound_b.list_node);
+
+ if (input->keyboard.buttons[KEYBOARD_R].was_pressed)
+ state->mixer.sounds.head = state->mixer.sounds.tail = NULL;
+}
+
+RTS_SAMPLE(rts_sample)
+{
+ struct rts_state *state = memory->ptr;
+
+ const f32 PI = 3.1415926535f;
+
+#if 1
+ memset(audio->buffer, 0, audio->samples * RTS_AUDIO_FRAME_BYTES);
+
+ struct list new_sound_list;
+ memset(&new_sound_list, 0, sizeof new_sound_list);
+
+ size_t active_sounds = 0;
+ LIST_ITER(&state->mixer.sounds) {
+ struct mixer_sound *sound_info = FROM_NODE(it, struct mixer_sound, list_node);
+
+ u32 available_samples = MIN(audio->samples, sound_info->sound.samples);
+ u32 wave_period = audio->sample_rate / sound_info->sound.hz;
+
+ f64 t = 0, hz = sound_info->sound.hz;
+ s32 *ptr = audio->buffer;
+
+ platform->printf("sound %zu: samples: %u/%u\n",
+ active_sounds,
+ available_samples, sound_info->sound.samples);
+
+#if 0
+ for (size_t i = 0; i < available_samples; i++) {
+ f64 v = 0;
+ __asm__ volatile ("fsin" : "=t" (v) : "0" (hz));
+
+ s32 pcm = v * sound_info->sound.volume;
+ for (u32 j = 0; j < audio->channels; j++)
+ *ptr++ += pcm;
+
+ t += 2 * PI / wave_period;
+ if (t >= 2 * PI)
+ t -= 2 * PI;
+ }
+#endif
+
+ sound_info->ctr += available_samples;
+ if (sound_info->ctr < sound_info->sound.samples) {
+ active_sounds++;
+ } else {
+ // TODO: mark sound as inactive
+ }
+ }
+
+ platform->printf("active sounds: %zu\n", active_sounds);
+
+ state->mixer.sounds = new_sound_list;
+#else
+ s32 *sample = audio->buffer;
+ for (u32 i = 0; i < audio->samples; i++) {
+ f64 v = 0;
+ __asm__ volatile ("fsin" : "=t" (v) : "0" (state->tone_t));
+
+ int32_t sample_value = (int32_t) (v * state->tone_volume);
+ for (u32 j = 0; j < audio->channels; j++)
+ *sample++ = sample_value;
+
+ state->tone_t += 2 * PI / wave_period;
+ if (state->tone_t >= 2 * PI)
+ state->tone_t -= 2 * PI;
+ }
+#endif
+}
+
+RTS_RENDER(rts_render)
+{
+ struct render_frame *frame = renderer_api->begin_frame(renderer, width, height);
+ ASSERT(frame);
+
+ struct colour colour = { 0.0f, 0.0f, 0.0f, 1.0f, };
+ renderer_api->clear_screen(renderer, frame, &colour);
+
+ renderer_api->end_frame(renderer, frame);
+}
+
+#include "utils.c"
diff --git a/rts/rts.h b/rts/rts.h
@@ -0,0 +1,6 @@
+#ifndef RTS_H
+#define RTS_H
+
+#include "api.h"
+
+#endif /* RTS_H */
diff --git a/rts/utils.c b/rts/utils.c
@@ -0,0 +1,2 @@
+#define RTS_API_IMPL
+#include "api.h"