From 7824fa044ac7d2db13af5873ce2e90365153e0be Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 31: Software Drawing and Graphical Games Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- src/main/c/simple_audio.c | 40 ++++++++++++++ src/main/c/simple_audio.h | 16 ++++++ src/main/c/simple_graphics.c | 60 +++++++++++++++++++++ src/main/c/simple_graphics.h | 19 +++++++ src/main/c/simple_miniaudio.c | 123 ++++++++++++++++++++++++++++++++++++++++++ src/main/c/simple_minifb.c | 113 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 371 insertions(+) create mode 100644 src/main/c/simple_audio.c create mode 100644 src/main/c/simple_audio.h create mode 100644 src/main/c/simple_graphics.c create mode 100644 src/main/c/simple_graphics.h create mode 100644 src/main/c/simple_miniaudio.c create mode 100644 src/main/c/simple_minifb.c (limited to 'src/main/c') diff --git a/src/main/c/simple_audio.c b/src/main/c/simple_audio.c new file mode 100644 index 00000000..b8ed62de --- /dev/null +++ b/src/main/c/simple_audio.c @@ -0,0 +1,40 @@ +#include "simple_audio.h" +#include +#include +#include +#include + +static const struct simple_audio_api *api; +static void *library; + +int32_t simple_audio_open(void) { + const char *path = getenv("SIMPLE_MINIAUDIO"); + if (path && strcmp(path,"off")==0) return -1; + if (!api) { + if (!path || !*path) return -1; // Optional: games may run silently. + library = dlopen(path,RTLD_NOW | RTLD_LOCAL); + if (!library) { + fprintf(stderr,"Simple audio: cannot load backend '%s': %s\n",path,dlerror()); + return -1; + } + const struct simple_audio_api *(*get_api)(void) = + (const struct simple_audio_api *(*)(void))dlsym(library,"simple_miniaudio_api"); + const struct simple_audio_api *candidate = get_api ? get_api() : NULL; + if (!candidate || candidate->version!=SIMPLE_AUDIO_VERSION || !candidate->open || + !candidate->load || !candidate->play || !candidate->stop || + !candidate->playing || !candidate->close) { + fprintf(stderr,"Simple audio: incompatible backend; rebuild with make audio.\n"); + dlclose(library); + library=NULL; + return -1; + } + api=candidate; + atexit(api->close); + } + return api->open(); +} +int32_t simple_audio_load(const int16_t *samples, uint32_t count) { return api ? api->load(samples,count) : -1; } +int32_t simple_audio_play(int32_t sound) { return api ? api->play(sound) : -1; } +int32_t simple_audio_stop(void) { return api ? api->stop() : 0; } +int32_t simple_audio_playing(void) { return api ? api->playing() : 0; } +int32_t simple_audio_close(void) { if (api) api->close(); return 0; } diff --git a/src/main/c/simple_audio.h b/src/main/c/simple_audio.h new file mode 100644 index 00000000..77cce530 --- /dev/null +++ b/src/main/c/simple_audio.h @@ -0,0 +1,16 @@ +#ifndef SIMPLE_AUDIO_H +#define SIMPLE_AUDIO_H +#include + +// Main-thread API. Samples are copied; no Simple heap pointer is retained. +struct simple_audio_api { + int32_t version; + int32_t (*open)(void); + int32_t (*load)(const int16_t *, uint32_t); + int32_t (*play)(int32_t); + int32_t (*stop)(void); + int32_t (*playing)(void); + void (*close)(void); +}; +#define SIMPLE_AUDIO_VERSION 1 +#endif diff --git a/src/main/c/simple_graphics.c b/src/main/c/simple_graphics.c new file mode 100644 index 00000000..41073470 --- /dev/null +++ b/src/main/c/simple_graphics.c @@ -0,0 +1,60 @@ +#include "simple_graphics.h" +#include +#include +#include +#include + +// No MiniFB headers or link dependency in the default runtime. Cygwin's +// dlopen also loads native Windows DLLs; both use the x64 Windows C ABI. +static const struct simple_graphics_api *api; +static void *library; + +static int load_graphics(void) { + // Explicitly disabling graphics also makes headless tests deterministic. + const char *path = getenv("SIMPLE_MINIFB"); + if (path && strcmp(path, "off") == 0) { + fprintf(stderr, "Simple graphics: disabled (SIMPLE_MINIFB=off).\n"); + return -1; + } + if (api) return 0; + if (!path || !*path) { + fprintf(stderr, "Simple graphics: MiniFB backend not configured. " + "Run make graphics, then set SIMPLE_MINIFB to the backend library's full path.\n"); + return -1; + } + library = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!library) { + fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror()); + return -1; + } + const struct simple_graphics_api *(*get_api)(void) = + (const struct simple_graphics_api *(*)(void))dlsym(library, "simple_minifb_api"); + const struct simple_graphics_api *candidate = get_api ? get_api() : NULL; + if (!candidate || candidate->version != SIMPLE_GRAPHICS_VERSION || + !candidate->open || !candidate->present || !candidate->poll || + !candidate->mouse_x || !candidate->mouse_y || !candidate->button || + !candidate->key || !candidate->close) { + fprintf(stderr, "Simple graphics: incompatible MiniFB backend '%s'. Rebuild with make graphics.\n", path); + dlclose(library); + library = NULL; + return -1; + } + api = candidate; + // Keep the library loaded until process exit; its window callbacks may + // still be registered. Closing a window releases its native resources. + atexit(api->close); + return 0; +} + +int32_t simple_gfx_open(const char *title, uint32_t width, uint32_t height) { + return load_graphics() ? -1 : api->open(title, width, height); +} +int32_t simple_gfx_present(const uint32_t *pixels, uint32_t count) { + return api ? api->present(pixels, count) : -1; +} +int32_t simple_gfx_poll(int32_t wait) { return api ? api->poll(wait) : -1; } +int32_t simple_gfx_mouse_x(void) { return api ? api->mouse_x() : 0; } +int32_t simple_gfx_mouse_y(void) { return api ? api->mouse_y() : 0; } +int32_t simple_gfx_button(int32_t button) { return api ? api->button(button) : 0; } +int32_t simple_gfx_key(int32_t key) { return api ? api->key(key) : 0; } +int32_t simple_gfx_close(void) { if (api) api->close(); return 0; } diff --git a/src/main/c/simple_graphics.h b/src/main/c/simple_graphics.h new file mode 100644 index 00000000..35a0aa08 --- /dev/null +++ b/src/main/c/simple_graphics.h @@ -0,0 +1,19 @@ +#ifndef SIMPLE_GRAPHICS_H +#define SIMPLE_GRAPHICS_H +#include + +// Private, versioned boundary between the tiny runtime loader and MiniFB. +// All calls run on the main thread. Pixel memory is borrowed for present only. +struct simple_graphics_api { + int32_t version; + int32_t (*open)(const char *, uint32_t, uint32_t); + int32_t (*present)(const uint32_t *, uint32_t); + int32_t (*poll)(int32_t); + int32_t (*mouse_x)(void); + int32_t (*mouse_y)(void); + int32_t (*button)(int32_t); + int32_t (*key)(int32_t); + void (*close)(void); +}; +#define SIMPLE_GRAPHICS_VERSION 1 +#endif diff --git a/src/main/c/simple_miniaudio.c b/src/main/c/simple_miniaudio.c new file mode 100644 index 00000000..03dbadd7 --- /dev/null +++ b/src/main/c/simple_miniaudio.c @@ -0,0 +1,123 @@ +#include "simple_audio.h" +#define MA_NO_DECODING +#define MA_NO_ENCODING +#define MA_NO_RESOURCE_MANAGER +#define MA_NO_NODE_GRAPH +#define MA_NO_ENGINE +#define MINIAUDIO_IMPLEMENTATION +#include +#include +#include +#include +#include + +#define RATE 48000 +#define SLOTS 32 +static ma_device device; +static ma_mutex mutex; +static int opened, active; +static uint32_t cursor; +static struct { int16_t *pcm; uint32_t count; } sounds[SLOTS]; + +// Only native-owned immutable PCM is read on the audio thread. The lock covers +// a bounded copy, never allocation, synthesis, device calls, or Simple code. +static void output(ma_device *unused, void *out, const void *in, ma_uint32 frames) { + (void)unused; (void)in; + memset(out,0,frames*sizeof(int16_t)); + ma_mutex_lock(&mutex); + if (active) { + uint32_t count=sounds[active].count-cursor; + if (count>frames) count=frames; + memcpy(out,sounds[active].pcm+cursor,count*sizeof(int16_t)); + cursor+=count; + if (cursor==sounds[active].count) active=0; + } + ma_mutex_unlock(&mutex); +} +static int32_t load(const int16_t *pcm, uint32_t count) { + if (!opened || !pcm || !count || count>RATE*10) return -1; + int slot; + for (slot=4; slot=SLOTS || !sounds[sound].pcm) return -1; + ma_mutex_lock(&mutex); + active=sound; cursor=0; + ma_mutex_unlock(&mutex); + return 0; +} +static int32_t stop(void) { + if (opened) { ma_mutex_lock(&mutex); active=0; ma_mutex_unlock(&mutex); } + return 0; +} +static int32_t playing(void) { + if (!opened) return 0; + ma_mutex_lock(&mutex); + int result=active!=0; + ma_mutex_unlock(&mutex); + return result; +} +static void close_audio(void) { + if (!opened) return; + ma_device_uninit(&device); // Joins callback before freeing buffers/mutex. + ma_mutex_uninit(&mutex); + for (int i=1;i +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +// One fixed-size window for now. No Simple pointer survives a native call: +// MiniFB retains its last draw buffer for OS repaint events, so copy it here. +static struct mfb_window *window; +static uint32_t *frame; +static uint32_t width, height; + +// The C-only build never installs the upstream C++ callback wrappers. +void release_cpp_stub(struct mfb_window *unused) { (void)unused; } + +static void close_window(void) { + if (window) { + mfb_close(window); // Requests close. + mfb_update_events(window); // Actually destroys the window. + window = NULL; + } + free(frame); + frame = NULL; +} + +static int32_t open_window(const char *title, uint32_t w, uint32_t h) { + if (window || !w || !h || w > UINT32_MAX / 4 || + (uint64_t)w * h > UINT32_MAX || (uint64_t)w * h > SIZE_MAX / 4) { + fprintf(stderr, "Simple graphics: invalid dimensions or a window is already open.\n"); + return -1; + } + frame = calloc((size_t)w * h, sizeof(*frame)); + if (!frame) { + fprintf(stderr, "Simple graphics: cannot allocate the display buffer.\n"); + return -1; + } + window = mfb_open(title, w, h); + if (!window) { + fprintf(stderr, "Simple graphics: MiniFB could not open a window (is a graphical display available?).\n"); + close_window(); + return -1; + } + width = w; + height = h; + return 0; +} + +static int32_t status(mfb_update_state state) { + if (state == MFB_STATE_OK) return 0; + if (state == MFB_STATE_EXIT) { + window = NULL; // MiniFB has already destroyed it. + close_window(); + return 1; + } + fprintf(stderr, "Simple graphics: MiniFB update failed (%d).\n", state); + close_window(); + return -1; +} + +static int32_t present(const uint32_t *pixels, uint32_t count) { + if (!window) return 1; + if (!pixels || count != (uint64_t)width * height) { + fprintf(stderr, "Simple graphics: pixel array length must equal width * height.\n"); + return -1; + } + memcpy(frame, pixels, (size_t)count * sizeof(*frame)); + return status(mfb_update_ex(window, frame, width, height)); +} + +static int32_t poll_events(int32_t wait) { + if (!window) return 1; + if (wait) { + // mfb_wait_sync returns false for both destruction and errors that + // leave the window alive. Use a short OS wait, then the status-bearing + // event API, so we know whether the window still needs destruction. +#ifdef _WIN32 + Sleep(17); +#else + struct timespec delay = {0, 16666667}; + while (nanosleep(&delay, &delay) < 0) { + if (errno != EINTR) { + perror("Simple graphics: event wait"); + close_window(); + return -1; + } + } +#endif + } + return status(mfb_update_events(window)); +} +static int32_t mouse_x(void) { return window ? mfb_get_mouse_x(window) : 0; } +static int32_t mouse_y(void) { return window ? mfb_get_mouse_y(window) : 0; } +static int32_t button(int32_t b) { + return window && b >= 0 && b < 8 ? mfb_get_mouse_button_buffer(window)[b] != 0 : 0; +} +static int32_t key(int32_t k) { + return window && k >= 0 && k <= MFB_KB_KEY_LAST ? mfb_get_key_buffer(window)[k] != 0 : 0; +} + +static const struct simple_graphics_api api = { + SIMPLE_GRAPHICS_VERSION, open_window, present, poll_events, + mouse_x, mouse_y, button, key, close_window +}; +#ifdef _WIN32 +__declspec(dllexport) +#endif +const struct simple_graphics_api *simple_minifb_api(void) { return &api; } -- cgit v1.2.3