aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaymaekers Luca <luca@spacehb.net>2025-06-22 08:41:24 +0200
committerRaymaekers Luca <luca@spacehb.net>2025-06-22 08:56:08 +0200
commitdd24f66c90b220de500f2b3c6b0843c56de123b4 (patch)
treecba08a4b1866cd578357fa1e8348ad599713be0c
parent7427ae1688e5933625a3609bcfba7e32988971fb (diff)
checkpoint
-rw-r--r--README.md10
-rwxr-xr-xbuild.sh15
-rw-r--r--spall.h438
-rw-r--r--todo.txt21
4 files changed, 481 insertions, 3 deletions
diff --git a/README.md b/README.md
index 1145d0b..076a629 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,19 @@
## Overview
To make sure I internalize the Handmade Hero series, I am porting the game over to Linux.
+## Usage
+Create a `handmade.cpp` and implement `handmade_platform.h`. It will then get compiled into a
+shared object by the build script.
+
## Build
-Run the build script.
+Run the build script. This will create a `build` directory in the parent directory.
```sh
-./code/build.sh
+./build.sh
```
## Running
```sh
-./build/linux_handmade
+../build/linux_handmade
```
# Resources
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..724f053
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+ThisDir="$(dirname "$(readlink -f "$0")")"
+cd "$ThisDir"
+
+mkdir ../build > /dev/null 2>&1
+
+CompilerFlags="-ggdb -DHANDMADE_PROFILING -DHANDMADE_INTERNAL -DHANDMADE_SLOW"
+WarningFlags="-Wall -Wextra -Wno-unused-but-set-variable -Wno-unused-variable -Wno-write-strings -Wno-unused-parameter -Wno-unused-function"
+
+printf 'handmade.cpp\n'
+g++ $CompilerFlags $WarningFlags -shared -o ../build/handmade.so handmade.cpp
+
+printf 'linux_handmade.cpp\n'
+g++ $CompilerFlags $WarningFlags -o ../build/linux_handmade linux_handmade.cpp -lasound -lm -lX11 -lXfixes
diff --git a/spall.h b/spall.h
new file mode 100644
index 0000000..934cd65
--- /dev/null
+++ b/spall.h
@@ -0,0 +1,438 @@
+// SPDX-FileCopyrightText: © 2023 Phillip Trudeau-Tavara <pmttavara@protonmail.com>
+// SPDX-License-Identifier: MIT
+
+/*
+
+TODO: Optional Helper APIs:
+
+ - Compression API: would require a mutexed lockable context (yuck...)
+ - Either using a ZIP library, a name cache + TIDPID cache, or both (but ZIP is likely more than enough!!!)
+ - begin()/end() writes compressed chunks to a caller-determined destination
+ - The destination can be the buffered-writing API or a custom user destination
+ - Ultimately need to take a lock with some granularity... can that be the caller's responsibility?
+
+ - Counter Event: should allow tracking arbitrary named values with a single event, for memory and frame profiling
+
+ - Ring-buffer API
+ spall_ring_init
+ spall_ring_emit_begin
+ spall_ring_emit_end
+ spall_ring_flush
+*/
+
+#ifndef SPALL_H
+#define SPALL_H
+
+#if !defined(_MSC_VER) || defined(__clang__)
+#define SPALL_NOINSTRUMENT __attribute__((no_instrument_function))
+#define SPALL_FORCEINLINE __attribute__((always_inline))
+#else
+#define _CRT_SECURE_NO_WARNINGS
+#define SPALL_NOINSTRUMENT // Can't noinstrument on MSVC!
+#define SPALL_FORCEINLINE __forceinline
+#endif
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+
+#define SPALL_FN static inline SPALL_NOINSTRUMENT
+
+#define SPALL_MIN(a, b) (((a) < (b)) ? (a) : (b))
+
+#pragma pack(push, 1)
+
+typedef struct SpallHeader {
+ uint64_t magic_header; // = 0x0BADF00D
+ uint64_t version; // = 1
+ double timestamp_unit;
+ uint64_t must_be_0;
+} SpallHeader;
+
+enum {
+ SpallEventType_Invalid = 0,
+ SpallEventType_Custom_Data = 1, // Basic readers can skip this.
+ SpallEventType_StreamOver = 2,
+
+ SpallEventType_Begin = 3,
+ SpallEventType_End = 4,
+ SpallEventType_Instant = 5,
+
+ SpallEventType_Overwrite_Timestamp = 6, // Retroactively change timestamp units - useful for incrementally improving RDTSC frequency.
+ SpallEventType_Pad_Skip = 7,
+};
+
+typedef struct SpallBeginEvent {
+ uint8_t type; // = SpallEventType_Begin
+ uint8_t category;
+
+ uint32_t pid;
+ uint32_t tid;
+ double when;
+
+ uint8_t name_length;
+ uint8_t args_length;
+} SpallBeginEvent;
+
+typedef struct SpallBeginEventMax {
+ SpallBeginEvent event;
+ char name_bytes[255];
+ char args_bytes[255];
+} SpallBeginEventMax;
+
+typedef struct SpallEndEvent {
+ uint8_t type; // = SpallEventType_End
+ uint32_t pid;
+ uint32_t tid;
+ double when;
+} SpallEndEvent;
+
+typedef struct SpallPadSkipEvent {
+ uint8_t type; // = SpallEventType_Pad_Skip
+ uint32_t size;
+} SpallPadSkipEvent;
+
+#pragma pack(pop)
+
+typedef struct SpallProfile SpallProfile;
+
+// Important!: If you define your own callbacks, mark them SPALL_NOINSTRUMENT!
+typedef bool (*SpallWriteCallback)(SpallProfile *self, const void *data, size_t length);
+typedef bool (*SpallFlushCallback)(SpallProfile *self);
+typedef void (*SpallCloseCallback)(SpallProfile *self);
+
+struct SpallProfile {
+ double timestamp_unit;
+ bool is_json;
+ SpallWriteCallback write;
+ SpallFlushCallback flush;
+ SpallCloseCallback close;
+ void *data;
+};
+
+// Important!: If you are writing Begin/End events, then do NOT write
+// events for the same PID + TID pair on different buffers!!!
+typedef struct SpallBuffer {
+ void *data;
+ size_t length;
+
+ // Internal data - don't assign this
+ size_t head;
+ SpallProfile *ctx;
+} SpallBuffer;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(SPALL_BUFFER_PROFILING) && !defined(SPALL_BUFFER_PROFILING_GET_TIME)
+#error "You must #define SPALL_BUFFER_PROFILING_GET_TIME() to profile buffer flushes."
+#endif
+
+ SPALL_FN SPALL_FORCEINLINE void spall__buffer_profile(SpallProfile *ctx, SpallBuffer *wb, double spall_time_begin, double spall_time_end, const char *name, int name_len);
+#ifdef SPALL_BUFFER_PROFILING
+#define SPALL_BUFFER_PROFILE_BEGIN() double spall_time_begin = (SPALL_BUFFER_PROFILING_GET_TIME())
+ // Don't call this with anything other than a string literal
+#define SPALL_BUFFER_PROFILE_END(name) spall__buffer_profile(ctx, wb, spall_time_begin, (SPALL_BUFFER_PROFILING_GET_TIME()), "" name "", sizeof("" name "") - 1)
+#else
+#define SPALL_BUFFER_PROFILE_BEGIN()
+#define SPALL_BUFFER_PROFILE_END(name)
+#endif
+
+ SPALL_FN SPALL_FORCEINLINE bool spall__file_write(SpallProfile *ctx, const void *p, size_t n) {
+ if (!ctx->data) return false;
+#ifdef SPALL_DEBUG
+ if (feof((FILE *)ctx->data)) return false;
+ if (ferror((FILE *)ctx->data)) return false;
+#endif
+
+ if (fwrite(p, n, 1, (FILE *)ctx->data) != 1) return false;
+ return true;
+ }
+ SPALL_FN bool spall__file_flush(SpallProfile *ctx) {
+ if (!ctx->data) return false;
+ if (fflush((FILE *)ctx->data)) return false;
+ return true;
+ }
+ SPALL_FN void spall__file_close(SpallProfile *ctx) {
+ if (!ctx->data) return;
+
+ if (ctx->is_json) {
+#ifdef SPALL_DEBUG
+ if (!feof((FILE *)ctx->data) && !ferror((FILE *)ctx->data))
+#endif
+ {
+ fseek((FILE *)ctx->data, -2, SEEK_CUR); // seek back to overwrite trailing comma
+ fwrite("\n]}\n", sizeof("\n]}\n") - 1, 1, (FILE *)ctx->data);
+ }
+ }
+ fflush((FILE *)ctx->data);
+ fclose((FILE *)ctx->data);
+ ctx->data = NULL;
+ }
+
+ SPALL_FN SPALL_FORCEINLINE bool spall__buffer_flush(SpallProfile *ctx, SpallBuffer *wb) {
+ // precon: wb
+ // precon: wb->data
+ // precon: wb->head <= wb->length
+ // precon: !ctx || ctx->write
+#ifdef SPALL_DEBUG
+ if (wb->ctx != ctx) return false; // Buffer must be bound to this context (or to NULL)
+#endif
+
+ if (wb->head && ctx) {
+ SPALL_BUFFER_PROFILE_BEGIN();
+ if (!ctx->write) return false;
+ if (ctx->write == spall__file_write) {
+ if (!spall__file_write(ctx, wb->data, wb->head)) return false;
+ } else {
+ if (!ctx->write(ctx, wb->data, wb->head)) return false;
+ }
+ SPALL_BUFFER_PROFILE_END("Buffer Flush");
+ }
+ wb->head = 0;
+ return true;
+ }
+
+ SPALL_FN SPALL_FORCEINLINE bool spall__buffer_write(SpallProfile *ctx, SpallBuffer *wb, void *p, size_t n) {
+ // precon: !wb || wb->head < wb->length
+ // precon: !ctx || ctx->write
+ if (!wb) return ctx->write && ctx->write(ctx, p, n);
+#ifdef SPALL_DEBUG
+ if (wb->ctx != ctx) return false; // Buffer must be bound to this context (or to NULL)
+#endif
+ if (wb->head + n > wb->length && !spall__buffer_flush(ctx, wb)) return false;
+ if (n > wb->length) {
+ SPALL_BUFFER_PROFILE_BEGIN();
+ if (!ctx->write || !ctx->write(ctx, p, n)) return false;
+ SPALL_BUFFER_PROFILE_END("Unbuffered Write");
+ return true;
+ }
+ memcpy((char *)wb->data + wb->head, p, n);
+ wb->head += n;
+ return true;
+ }
+
+ SPALL_FN bool spall_buffer_flush(SpallProfile *ctx, SpallBuffer *wb) {
+#ifdef SPALL_DEBUG
+ if (!wb) return false;
+ if (!wb->data) return false;
+#endif
+
+ if (!spall__buffer_flush(ctx, wb)) return false;
+ return true;
+ }
+
+ SPALL_FN bool spall_buffer_init(SpallProfile *ctx, SpallBuffer *wb) {
+ if (!spall_buffer_flush(NULL, wb)) return false;
+ wb->ctx = ctx;
+ return true;
+ }
+ SPALL_FN bool spall_buffer_quit(SpallProfile *ctx, SpallBuffer *wb) {
+ if (!spall_buffer_flush(ctx, wb)) return false;
+ wb->ctx = NULL;
+ return true;
+ }
+
+ SPALL_FN bool spall_buffer_abort(SpallBuffer *wb) {
+ if (!wb) return false;
+ wb->ctx = NULL;
+ if (!spall__buffer_flush(NULL, wb)) return false;
+ return true;
+ }
+
+ SPALL_FN size_t spall_build_header(void *buffer, size_t rem_size, double timestamp_unit) {
+ size_t header_size = sizeof(SpallHeader);
+ if (header_size > rem_size) {
+ return 0;
+ }
+
+ SpallHeader *header = (SpallHeader *)buffer;
+ header->magic_header = 0x0BADF00D;
+ header->version = 1;
+ header->timestamp_unit = timestamp_unit;
+ header->must_be_0 = 0;
+ return header_size;
+ }
+ SPALL_FN SPALL_FORCEINLINE size_t spall_build_begin(void *buffer, size_t rem_size, const char *name, signed long name_len, const char *args, signed long args_len, double when, uint32_t tid, uint32_t pid) {
+ SpallBeginEventMax *ev = (SpallBeginEventMax *)buffer;
+ uint8_t trunc_name_len = (uint8_t)SPALL_MIN(name_len, 255); // will be interpreted as truncated in the app (?)
+ uint8_t trunc_args_len = (uint8_t)SPALL_MIN(args_len, 255); // will be interpreted as truncated in the app (?)
+
+ size_t ev_size = sizeof(SpallBeginEvent) + trunc_name_len + trunc_args_len;
+ if (ev_size > rem_size) {
+ return 0;
+ }
+
+ ev->event.type = SpallEventType_Begin;
+ ev->event.category = 0;
+ ev->event.pid = pid;
+ ev->event.tid = tid;
+ ev->event.when = when;
+ ev->event.name_length = trunc_name_len;
+ ev->event.args_length = trunc_args_len;
+ memcpy(ev->name_bytes, name, trunc_name_len);
+ memcpy(ev->name_bytes + trunc_name_len, args, trunc_args_len);
+
+ return ev_size;
+ }
+ SPALL_FN SPALL_FORCEINLINE size_t spall_build_end(void *buffer, size_t rem_size, double when, uint32_t tid, uint32_t pid) {
+ size_t ev_size = sizeof(SpallEndEvent);
+ if (ev_size > rem_size) {
+ return 0;
+ }
+
+ SpallEndEvent *ev = (SpallEndEvent *)buffer;
+ ev->type = SpallEventType_End;
+ ev->pid = pid;
+ ev->tid = tid;
+ ev->when = when;
+
+ return ev_size;
+ }
+
+ SPALL_FN void spall_quit(SpallProfile *ctx) {
+ if (!ctx) return;
+ if (ctx->close) ctx->close(ctx);
+
+ memset(ctx, 0, sizeof(*ctx));
+ }
+
+ SPALL_FN SpallProfile spall_init_callbacks(double timestamp_unit,
+ SpallWriteCallback write,
+ SpallFlushCallback flush,
+ SpallCloseCallback close,
+ void *userdata,
+ bool is_json) {
+ SpallProfile ctx;
+ memset(&ctx, 0, sizeof(ctx));
+ if (timestamp_unit < 0) return ctx;
+ ctx.timestamp_unit = timestamp_unit;
+ ctx.is_json = is_json;
+ ctx.data = userdata;
+ ctx.write = write;
+ ctx.flush = flush;
+ ctx.close = close;
+
+ if (ctx.is_json) {
+ if (!ctx.write(&ctx, "{\"traceEvents\":[\n", sizeof("{\"traceEvents\":[\n") - 1)) { spall_quit(&ctx); return ctx; }
+ } else {
+ SpallHeader header;
+ size_t len = spall_build_header(&header, sizeof(header), timestamp_unit);
+ if (!ctx.write(&ctx, &header, len)) { spall_quit(&ctx); return ctx; }
+ }
+
+ return ctx;
+ }
+
+ SPALL_FN SpallProfile spall_init_file_ex(const char *filename, double timestamp_unit, bool is_json) {
+ SpallProfile ctx;
+ memset(&ctx, 0, sizeof(ctx));
+ if (!filename) return ctx;
+ ctx.data = fopen(filename, "wb"); // TODO: handle utf8 and long paths on windows
+ if (ctx.data) { // basically freopen() but we don't want to force users to lug along another macro define
+ fclose((FILE *)ctx.data);
+ ctx.data = fopen(filename, "ab");
+ }
+ if (!ctx.data) { spall_quit(&ctx); return ctx; }
+ ctx = spall_init_callbacks(timestamp_unit, spall__file_write, spall__file_flush, spall__file_close, ctx.data, is_json);
+ return ctx;
+ }
+
+ SPALL_FN SpallProfile spall_init_file (const char* filename, double timestamp_unit) { return spall_init_file_ex(filename, timestamp_unit, false); }
+ SPALL_FN SpallProfile spall_init_file_json(const char* filename, double timestamp_unit) { return spall_init_file_ex(filename, timestamp_unit, true); }
+
+ SPALL_FN bool spall_flush(SpallProfile *ctx) {
+#ifdef SPALL_DEBUG
+ if (!ctx) return false;
+#endif
+
+ if (!ctx->flush || !ctx->flush(ctx)) return false;
+ return true;
+ }
+
+ SPALL_FN SPALL_FORCEINLINE bool spall_buffer_begin_args(SpallProfile *ctx, SpallBuffer *wb, const char *name, signed long name_len, const char *args, signed long args_len, double when, uint32_t tid, uint32_t pid) {
+#ifdef SPALL_DEBUG
+ if (!ctx) return false;
+ if (!name) return false;
+ if (name_len <= 0) return false;
+ if (!wb) return false;
+#endif
+
+ if (ctx->is_json) {
+ char buf[1024];
+ int buf_len = snprintf(buf, sizeof(buf),
+ "{\"ph\":\"B\",\"ts\":%f,\"pid\":%u,\"tid\":%u,\"name\":\"%.*s\",\"args\":\"%.*s\"},\n",
+ when * ctx->timestamp_unit, pid, tid, (int)(uint8_t)name_len, name, (int)(uint8_t)args_len, args);
+ if (buf_len <= 0) return false;
+ if (buf_len >= (int)sizeof(buf)) return false;
+ if (!spall__buffer_write(ctx, wb, buf, buf_len)) return false;
+ } else {
+ if ((wb->head + sizeof(SpallBeginEventMax)) > wb->length) {
+ if (!spall__buffer_flush(ctx, wb)) {
+ return false;
+ }
+ }
+
+ wb->head += spall_build_begin((char *)wb->data + wb->head, wb->length - wb->head, name, name_len, args, args_len, when, tid, pid);
+ }
+
+ return true;
+ }
+
+ SPALL_FN SPALL_FORCEINLINE bool spall_buffer_begin_ex(SpallProfile *ctx, SpallBuffer *wb, const char *name, signed long name_len, double when, uint32_t tid, uint32_t pid) {
+ return spall_buffer_begin_args(ctx, wb, name, name_len, "", 0, when, tid, pid);
+ }
+
+ SPALL_FN bool spall_buffer_begin(SpallProfile *ctx, SpallBuffer *wb, const char *name, signed long name_len, double when) {
+ return spall_buffer_begin_args(ctx, wb, name, name_len, "", 0, when, 0, 0);
+ }
+
+ SPALL_FN SPALL_FORCEINLINE bool spall_buffer_end_ex(SpallProfile *ctx, SpallBuffer *wb, double when, uint32_t tid, uint32_t pid) {
+#ifdef SPALL_DEBUG
+ if (!ctx) return false;
+ if (!wb) return false;
+#endif
+
+ if (ctx->is_json) {
+ char buf[512];
+ int buf_len = snprintf(buf, sizeof(buf),
+ "{\"ph\":\"E\",\"ts\":%f,\"pid\":%u,\"tid\":%u},\n",
+ when * ctx->timestamp_unit, pid, tid);
+ if (buf_len <= 0) return false;
+ if (buf_len >= (int)sizeof(buf)) return false;
+ if (!spall__buffer_write(ctx, wb, buf, buf_len)) return false;
+ } else {
+ if ((wb->head + sizeof(SpallEndEvent)) > wb->length) {
+ if (!spall__buffer_flush(ctx, wb)) {
+ return false;
+ }
+ }
+
+ wb->head += spall_build_end((char *)wb->data + wb->head, wb->length - wb->head, when, tid, pid);
+ }
+
+ return true;
+ }
+
+ SPALL_FN bool spall_buffer_end(SpallProfile *ctx, SpallBuffer *wb, double when) { return spall_buffer_end_ex(ctx, wb, when, 0, 0); }
+
+ SPALL_FN SPALL_FORCEINLINE void spall__buffer_profile(SpallProfile *ctx, SpallBuffer *wb, double spall_time_begin, double spall_time_end, const char *name, int name_len) {
+ // precon: ctx
+ // precon: ctx->write
+ char temp_buffer_data[2048];
+ SpallBuffer temp_buffer = {};
+ temp_buffer.length = sizeof(temp_buffer_data);
+ temp_buffer.data = temp_buffer_data;
+
+ if (!spall_buffer_begin_ex(ctx, &temp_buffer, name, name_len, spall_time_begin, (uint32_t)(uintptr_t)wb->data, 4222222222)) return;
+ if (!spall_buffer_end_ex(ctx, &temp_buffer, spall_time_end, (uint32_t)(uintptr_t)wb->data, 4222222222)) return;
+ if (ctx->write) ctx->write(ctx, temp_buffer_data, temp_buffer.head);
+ }
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // SPALL_H
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 0000000..45f4a2e
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,21 @@
+- Pause program while the window is not focused?
+- Debug markers for sound (delay, avail, pointers, timestamps)
+- Input: Cross frame values, half transition count and average.j
+- Sound: Lagfree playback + recovery after live reload
+- Replay: use mremap(3) and memory backed file for faster writing
+- Replay: Multiple slots
+- x11: fullscreen
+- x11: hide decorations on other DEs.
+x Input: Detect new controllers / disconnects.
+x Hide cursor
+x Replay
+x Sound (Play sinewave)
+x Hot reload
+x Gamepad Input
+x Get Refresh rate
+x Keyboard Input
+x Timing
+x File IO
+x input: IsConnected bug: if checking on the property the inputs will be played multiple times.
+- Load libasound.so at runtime
+- statically link alsalib and xlib \ No newline at end of file