summaryrefslogtreecommitdiff
path: root/chapter31/src/main
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-11 11:49:04 -0800
committerCliff Click <cliffc@acm.org>2026-11-12 13:48:30 -0800
commitf4bac750504ae2d21068a87b6a6bf4a733476993 (patch)
tree644741a31d045d17f9456f07078a2fca2bb21412 /chapter31/src/main
parenta269fa7e8aea51b4fbdd2f2c6c24e84ef26f9ca4 (diff)
downloadSimple-f4bac750504ae2d21068a87b6a6bf4a733476993.tar.gz
Simple-f4bac750504ae2d21068a87b6a6bf4a733476993.zip
First cut graphics
Diffstat (limited to 'chapter31/src/main')
-rw-r--r--chapter31/src/main/c/simple_graphics.c60
-rw-r--r--chapter31/src/main/c/simple_graphics.h19
-rw-r--r--chapter31/src/main/c/simple_minifb.c113
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java12
-rw-r--r--chapter31/src/main/smp/sys.smp1
-rw-r--r--chapter31/src/main/smp/sys/gfx.smp38
6 files changed, 241 insertions, 2 deletions
diff --git a/chapter31/src/main/c/simple_graphics.c b/chapter31/src/main/c/simple_graphics.c
new file mode 100644
index 00000000..41073470
--- /dev/null
+++ b/chapter31/src/main/c/simple_graphics.c
@@ -0,0 +1,60 @@
+#include "simple_graphics.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+
+// 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/chapter31/src/main/c/simple_graphics.h b/chapter31/src/main/c/simple_graphics.h
new file mode 100644
index 00000000..35a0aa08
--- /dev/null
+++ b/chapter31/src/main/c/simple_graphics.h
@@ -0,0 +1,19 @@
+#ifndef SIMPLE_GRAPHICS_H
+#define SIMPLE_GRAPHICS_H
+#include <stdint.h>
+
+// 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/chapter31/src/main/c/simple_minifb.c b/chapter31/src/main/c/simple_minifb.c
new file mode 100644
index 00000000..34663e2e
--- /dev/null
+++ b/chapter31/src/main/c/simple_minifb.c
@@ -0,0 +1,113 @@
+#include "simple_graphics.h"
+#include <MiniFB.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <errno.h>
+#include <time.h>
+#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; }
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
index ae4ecb74..79bebe1f 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
@@ -417,7 +417,7 @@ public class Serialize {
assert elf._clz._name==strs[1/*skip null ptr*/];
for( Type type : types )
if( type instanceof TypeStruct ts )
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
for( Type type : types ) {
if( !(type instanceof TypeStruct ts) || !TypeStruct.startsClzPrefix(ts._name) )
continue;
@@ -429,7 +429,7 @@ public class Serialize {
elf._pubTypes = new TypeStruct[npubTypes];
for( int i=0; i<npubTypes; i++ ) {
TypeStruct ts = elf._pubTypes[i] = (TypeStruct)types[pubTypeIds[i]];
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
}
elf._functions = new FunctionSummary[bais.packed4()];
for( int i = 0; i<elf._functions.length; i++ ) {
@@ -444,6 +444,14 @@ public class Serialize {
return elf._types = new TypeRead(types,strs,fileAliases,fileFidxs,fileRpcs);
}
+ private static void installType(TypeStruct ts) {
+ // TYPES names the mutable array declaration. Read-only arrays in
+ // imported signatures keep their own types, but must not replace that
+ // declaration: closeOver looks up its mutable variant by name.
+ if( ts.isAry() ) ts = ts.replace(ts.field("[]").makeFrom(false));
+ Parser.TYPES.put(ts._name,ts);
+ }
+
static void readAll( CodeGen code, ElfReader elf, GlobalBits aliases, GlobalBits fidxs, GlobalBits rpcs ) {
BAOS bais = elf._bais;
TypeRead tread = readTypes(code, elf, aliases, fidxs, rpcs);
diff --git a/chapter31/src/main/smp/sys.smp b/chapter31/src/main/smp/sys.smp
index 1201315b..deaafa0b 100644
--- a/chapter31/src/main/smp/sys.smp
+++ b/chapter31/src/main/smp/sys.smp
@@ -7,6 +7,7 @@ val aryi64 = aryi64; // Variable sized array of i64
val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
val io = io; // IO library
+val gfx = gfx; // Optional pixel window (MiniFB)
val text = text; // Typed text conversions
val libc = libc; // libc FFI bindings
val rt = rt; // Run Time support
diff --git a/chapter31/src/main/smp/sys/gfx.smp b/chapter31/src/main/smp/sys/gfx.smp
new file mode 100644
index 00000000..1f0e3ca9
--- /dev/null
+++ b/chapter31/src/main/smp/sys/gfx.smp
@@ -0,0 +1,38 @@
+// Minimal pixel-window interface; drawing belongs in Simple.
+// One fixed-size window, main thread only. Pixel format is 0x00RRGGBB.
+// open/present/poll return OK, CLOSED (normal user close/Esc), or ERROR.
+int OK = 0;
+int CLOSED = 1;
+int ERROR = -1;
+int LEFT = 1;
+int ESCAPE = 256;
+
+{ i64 u32 u32 -> i32 } simple_gfx_open = "C";
+{ i64 u32 -> i32 } simple_gfx_present = "C";
+{ i32 -> i32 } simple_gfx_poll = "C";
+{ i32 } simple_gfx_mouse_x = "C";
+{ i32 } simple_gfx_mouse_y = "C";
+{ i32 -> i32 } simple_gfx_button = "C";
+{ i32 -> i32 } simple_gfx_key = "C";
+{ i32 } simple_gfx_close = "C";
+
+// Failure prints a diagnostic to stderr and returns ERROR; callers decide
+// whether to exit or fall back to text. No native library is loaded until here.
+val open = { u8[~] title, u32 width, u32 height ->
+ i64 ptr = aryu8.str0(title);
+ simple_gfx_open(ptr,width,height);
+};
+// Array length must be width*height. The native layer copies for OS repaints;
+// it never keeps a pointer into the Simple heap after this call returns.
+val present = { u32[~] pixels ->
+ i64 ptr = pixels;
+ simple_gfx_present(ptr,pixels#);
+};
+// wait=true waits about 17ms before polling, avoiding a busy loop.
+val poll = { bool wait -> simple_gfx_poll(wait); };
+val mouseX = { -> simple_gfx_mouse_x(); };
+val mouseY = { -> simple_gfx_mouse_y(); };
+val button = { int button -> simple_gfx_button(button); };
+// Printable keys use uppercase ASCII; special keys use MiniFB's key codes.
+val key = { int key -> simple_gfx_key(key); };
+val close = { -> simple_gfx_close(); };