diff options
author | Raymaekers Luca <luca@spacehb.net> | 2025-06-22 08:41:24 +0200 |
---|---|---|
committer | Raymaekers Luca <luca@spacehb.net> | 2025-06-22 08:56:08 +0200 |
commit | dd24f66c90b220de500f2b3c6b0843c56de123b4 (patch) | |
tree | cba08a4b1866cd578357fa1e8348ad599713be0c | |
parent | 7427ae1688e5933625a3609bcfba7e32988971fb (diff) |
checkpoint
-rw-r--r-- | README.md | 10 | ||||
-rwxr-xr-x | build.sh | 15 | ||||
-rw-r--r-- | spall.h | 438 | ||||
-rw-r--r-- | todo.txt | 21 |
4 files changed, 481 insertions, 3 deletions
@@ -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 @@ -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 |