summaryrefslogtreecommitdiff
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
parenta269fa7e8aea51b4fbdd2f2c6c24e84ef26f9ca4 (diff)
downloadSimple-f4bac750504ae2d21068a87b6a6bf4a733476993.tar.gz
Simple-f4bac750504ae2d21068a87b6a6bf4a733476993.zip
First cut graphics
-rw-r--r--chapter31/Makefile15
-rw-r--r--chapter31/README.md10
-rw-r--r--chapter31/docs/examples/Graphics.smp17
-rw-r--r--chapter31/docs/examples/README.md4
-rw-r--r--chapter31/docs/graphics.md109
-rw-r--r--chapter31/graphics.mk50
-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
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java69
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java16
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java2
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/TestC.java3
17 files changed, 534 insertions, 8 deletions
diff --git a/chapter31/Makefile b/chapter31/Makefile
index dc4e630b..9e4f5b89 100644
--- a/chapter31/Makefile
+++ b/chapter31/Makefile
@@ -155,6 +155,7 @@ RELEASE:= build/release
TEST_SYS:= $(OBJS)/lib_$(CPU_ABI)/sys.o
RELEASE_SYS:= $(RELEASE)/$(CPU_ABI)/sys.o
RELEASE_CRT:= $(RELEASE)/$(CPU_ABI)/simple_crt.o
+RELEASE_GRAPHICS:= $(RELEASE)/$(CPU_ABI)/simple_graphics.o
RELEASE_ARGS:= $(RELEASE)/$(CPU_ABI)/simple_args.o
C_CALL_CONV:= $(if $(filter win64,$(CALL_CONV)),ms_abi,sysv_abi)
GAME_DIR:= build/examples/$(CPU_ABI)
@@ -183,11 +184,23 @@ $(RELEASE_ARGS): src/main/smp/sys/rt/simple_args.smp $(CLZDIR)/main/.mtag $(simp
@echo "simping " $@
@$(JVM) com.seaofnodes.simple.Simple --cpu $(CPU_PORT) --abi $(CALL_CONV) --norun -o $@ $<
-$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) Makefile
+$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) $(RELEASE_GRAPHICS) Makefile
@echo "compiling " $@
@[ -d $(dir $@) ] || mkdir -p $(dir $@)
@gcc -c -O2 -DCALL_CONV=$(C_CALL_CONV) -o $@ $<
+$(RELEASE_GRAPHICS): src/main/c/simple_graphics.c src/main/c/simple_graphics.h Makefile
+ @echo "compiling " $@
+ @mkdir -p $(dir $@)
+ @gcc -c -O2 -Wall -Wextra -o $@ $<
+
+# Optional native graphics backend. Nothing in the default build depends on it.
+include graphics.mk
+
+.PHONY: tests_nographics
+tests_nographics:
+ @SIMPLE_MINIFB=off $(MAKE) tests
+
#######################################################
# Base launch line for JVM tests
JVM=nice java -ea -cp "$(CLZDIR)/main${SEP}${jars}${SEP}$(CLZDIR)/test"
diff --git a/chapter31/README.md b/chapter31/README.md
index 5cf7f996..011cb7ed 100644
--- a/chapter31/README.md
+++ b/chapter31/README.md
@@ -4,8 +4,14 @@ English | [日本語](README.ja.md)
[Previous: Chapter 30 — Text Formatting and Diagnostics](../chapter30/README.md)
-This chapter starts from the completed Chapter 30 compiler and standard library.
-Its next topic is open; the [roadmap](../docs/roadmap.md) lists the candidates.
+Small graphical games drive this chapter's language and library work, starting
+with a pixel window for a Simple-written 2-D graphics library. MiniFB supplies
+the optional native window/input backend; drawing stays in Simple.
+
+The default build and test suite need no graphics dependencies. See
+[optional graphics](docs/graphics.md) for building MiniFB, running the pixel
+example, and handling a missing backend. `make tests_nographics CTAGS=` runs
+all ordinary tests with graphics explicitly disabled.
The inherited language and library are described in [Chapter 30](../chapter30/README.md).
The [grammar and code map](docs/31-grammar.md),
diff --git a/chapter31/docs/examples/Graphics.smp b/chapter31/docs/examples/Graphics.smp
new file mode 100644
index 00000000..7ba5c6e7
--- /dev/null
+++ b/chapter31/docs/examples/Graphics.smp
@@ -0,0 +1,17 @@
+// Optional graphics smoke test: pixels are drawn entirely in Simple.
+// Pass any argument for a short, self-closing test; otherwise close or press Esc.
+val main = { u8[~][] args ->
+ if( sys.gfx.open("Simple pixels",320,240) != sys.gfx.OK ) return 1;
+ val pixels = new u32[320*240];
+ for( int y=0; y<240; y++ )
+ for( int x=0; x<320; x++ )
+ pixels[y*320+x] = (x*255/319 << 16) | (y*255/239 << 8) | 64;
+ int result = sys.gfx.present(pixels);
+ int frames = 0;
+ while( result == sys.gfx.OK ) {
+ result = sys.gfx.poll(true);
+ if( args# > 1 && ++frames == 5 ) break;
+ }
+ sys.gfx.close();
+ return result == sys.gfx.ERROR ? 1 : 0;
+};
diff --git a/chapter31/docs/examples/README.md b/chapter31/docs/examples/README.md
index 58f7817b..2f19f09c 100644
--- a/chapter31/docs/examples/README.md
+++ b/chapter31/docs/examples/README.md
@@ -1,7 +1,9 @@
# Runnable examples
These programs live beside the documentation so readers can find and run them.
-Their native regression tests still run as part of `make tests`.
+Their non-graphical native regression tests run as part of `make tests`.
+The optional [pixel-window example](Graphics.smp) uses MiniFB; see
+[graphics setup](../graphics.md). `make run-graphics` opens it explicitly.
From `chapter31`, build the tic-tac-toe executable (without starting it):
diff --git a/chapter31/docs/graphics.md b/chapter31/docs/graphics.md
new file mode 100644
index 00000000..7e1636f9
--- /dev/null
+++ b/chapter31/docs/graphics.md
@@ -0,0 +1,109 @@
+# Optional graphics
+
+Simple owns the pixel array and drawing code. [MiniFB](https://github.com/emoon/minifb)
+provides a window, input, and pixel presentation. The first interface is
+`sys.gfx`: one fixed-size window, operated on the main thread. The
+[Graphics example](examples/Graphics.smp) draws a color gradient in Simple.
+
+## Default: no graphics dependency
+
+From `chapter31`, the usual `make release`, `make tests`, and `make tictactoe`
+need no MiniFB, graphics headers, OpenGL, CMake, or running display. Nothing
+downloads MiniFB unless you explicitly request `make graphics`.
+
+`make tests_nographics CTAGS=` runs the entire ordinary suite with
+`SIMPLE_MINIFB=off`. This includes the missing-backend tests, which never open
+windows. It excludes the interactive `run-graphics` target.
+
+The runtime contains a small loader using the OS dynamic-library facilities.
+It does nothing until `sys.gfx.open`. Without a configured backend, `open`
+prints a diagnostic to stderr and returns `sys.gfx.ERROR` (-1). Programs can
+return a nonzero status or fall back to text; the library does not terminate
+the process. Non-graphics programs ignore even a broken graphics configuration.
+
+## Build the optional backend
+
+From `chapter31`:
+
+```sh
+make graphics graphics-example CTAGS=
+make run-graphics CTAGS=
+```
+
+`graphics` downloads the revision pinned in [graphics.mk](../graphics.mk) into
+ignored `build/deps/minifb` and builds only its C framebuffer backend. Windows
+uses the MinGW-w64 GCC compiler (`x86_64-w64-mingw32-gcc`) and GDI; the rest of
+Simple still uses the existing Cygwin GCC. Linux uses GCC and X11 (`libx11-dev`
+on Ubuntu), including XWayland on a Wayland desktop. OpenGL, the C++ callback
+wrappers, and upstream example programs are omitted. Native Wayland and macOS
+builds are not provided here yet.
+
+Outputs:
+
+| Platform | Backend | Example |
+| --- | --- | --- |
+| Windows | `build/release/x86_64_v2_win64/simple_minifb.dll` | `build/examples/x86_64_v2_win64/Graphics.exe` |
+| Linux | `build/release/x86_64_v2_SystemV/libsimple_minifb.so` | `build/examples/x86_64_v2_SystemV/Graphics` |
+
+`MiniFB-LICENSE` is copied alongside the backend; include it when distributing
+the library. MiniFB sources remain outside the tracked Simple sources.
+
+`graphics-example` alone builds the executable without requiring MiniFB. It
+also demonstrates the diagnostic and exit status 1 when graphics is absent.
+`run-graphics` builds both, sets the backend path, and opens a window. Close it
+or release Escape to exit. Passing any argument to the executable makes it
+close after five polling iterations, for a quick smoke test.
+
+To run directly from PowerShell:
+
+```powershell
+$env:SIMPLE_MINIFB = (Resolve-Path build/release/x86_64_v2_win64/simple_minifb.dll).Path
+.\build\examples\x86_64_v2_win64\Graphics.exe
+```
+
+From Cygwin bash:
+
+```sh
+export SIMPLE_MINIFB="$PWD/build/release/x86_64_v2_win64/simple_minifb.dll"
+./build/examples/x86_64_v2_win64/Graphics.exe
+```
+
+For WSL, from the repository root:
+
+```sh
+wsl -d Ubuntu-24.04 -- bash -lc 'cd chapter31 && make graphics graphics-example CLZDIR=build/classes-linux CTAGS='
+```
+
+Inside WSL, set `SIMPLE_MINIFB` to the full path of the Linux `.so`, then run
+the Linux example with a working X11 display. For an automated virtual display:
+
+```sh
+SIMPLE_MINIFB="$PWD/build/release/x86_64_v2_SystemV/libsimple_minifb.so" \
+ xvfb-run -a build/examples/x86_64_v2_SystemV/Graphics smoke
+```
+
+The backend path is explicit: a DLL installed elsewhere or a different working
+directory cannot silently select the wrong version. `SIMPLE_MINIFB=off` disables
+graphics; an absent, unloadable, or incompatible backend produces a diagnostic.
+A backend that loads but cannot create a window also reports an error cleanly.
+
+## Initial interface
+
+| Operation | Meaning |
+| --- | --- |
+| `open(title,width,height)` | Open one window; returns `OK` or `ERROR`. |
+| `present(pixels)` | Present a `u32[~]` of exactly `width*height` pixels, packed `0x00RRGGBB`. |
+| `poll(wait)` | Process input; `true` waits about 17ms first to avoid a busy loop. |
+| `mouseX()`, `mouseY()` | Current mouse position, origin at upper left. |
+| `button(LEFT)`, `key(code)` | Current pressed state; printable keys use uppercase ASCII. |
+| `close()` | Release native window and pixel storage; safe to repeat. |
+
+`present` and `poll` return `OK` (0), `CLOSED` (1, normal close/Escape), or
+`ERROR` (-1). Stop polling after either terminal result and call `close`.
+The initial input API samples held state; it is not a queue of every transition.
+
+MiniFB retains its last framebuffer for repaint events. The adapter copies the
+Simple array into native-owned storage before presenting; it never retains a
+Simple heap pointer. Framebuffer allocation, the copy, and window/input calls
+are the only native work here. Drawing primitives and the games can be built
+in Simple on top of this boundary.
diff --git a/chapter31/graphics.mk b/chapter31/graphics.mk
new file mode 100644
index 00000000..a42bc99e
--- /dev/null
+++ b/chapter31/graphics.mk
@@ -0,0 +1,50 @@
+# Optional MiniFB build, pinned independently of Simple's default build.
+# A C-only GDI/X11 subset: no OpenGL, C++ wrapper, examples, or CMake required.
+MINIFB_REV := f92be8e966f194004853c10196e704f4510f58d4
+MINIFB_SRC := build/deps/minifb
+MINIFB_STAMP := $(MINIFB_SRC)/.simple-$(MINIFB_REV)
+MINIFB_COMMON := MiniFB_common MiniFB_cpu_x86 MiniFB_internal MiniFB_stretch \
+ MiniFB_stretch_sse2 MiniFB_stretch_avx2 MiniFB_stretch_neon MiniFB_timer MiniFB_utf8
+ifeq ($(OS),Windows_NT)
+ GRAPHICS_CC := x86_64-w64-mingw32-gcc
+ GRAPHICS_LIB := $(RELEASE)/$(CPU_ABI)/simple_minifb.dll
+ MINIFB_PLATFORM := windows/WinMiniFB
+ MINIFB_LIBS := -lgdi32 -luser32 -lwinmm
+ MINIFB_FLAGS := -D_WIN32_WINNT=0x0601
+else
+ GRAPHICS_CC := gcc
+ GRAPHICS_LIB := $(RELEASE)/$(CPU_ABI)/libsimple_minifb.so
+ MINIFB_PLATFORM := x11/X11MiniFB x11/X11MiniFB_scale_funcs x11/X11MiniFB_text \
+ MiniFB_linux MiniFB_evdev_keys
+ MINIFB_LIBS := -lX11 -lm
+ MINIFB_FLAGS := -fPIC -D_POSIX_C_SOURCE=200809L
+endif
+MINIFB_SOURCES = $(addprefix $(MINIFB_SRC)/src/,$(addsuffix .c,$(MINIFB_COMMON) $(MINIFB_PLATFORM)))
+GRAPHICS_EXAMPLE := $(GAME_DIR)/Graphics$(if $(filter Windows_NT,$(OS)),.exe,)
+
+.PHONY: graphics graphics-example run-graphics
+graphics: $(GRAPHICS_LIB)
+
+$(MINIFB_STAMP): graphics.mk
+ @mkdir -p $(dir $(MINIFB_SRC))
+ @test -d $(MINIFB_SRC)/.git || git clone --no-checkout https://github.com/emoon/minifb.git $(MINIFB_SRC)
+ @git -C $(MINIFB_SRC) fetch --depth 1 origin $(MINIFB_REV)
+ @git -C $(MINIFB_SRC) checkout --detach $(MINIFB_REV)
+ @touch $@
+
+$(GRAPHICS_LIB): $(MINIFB_STAMP) src/main/c/simple_minifb.c src/main/c/simple_graphics.h graphics.mk
+ @echo "building " $@
+ @mkdir -p $(dir $@)
+ @$(GRAPHICS_CC) -shared -O2 -Wall -Wextra -Wno-unused-parameter -Wno-implicit-fallthrough $(MINIFB_FLAGS) \
+ -I$(MINIFB_SRC)/include -I$(MINIFB_SRC)/src \
+ src/main/c/simple_minifb.c $(MINIFB_SOURCES) $(MINIFB_LIBS) -o $@
+ @cp $(MINIFB_SRC)/LICENSE $(dir $@)MiniFB-LICENSE
+
+# This executable also builds without MiniFB, to exercise the missing-backend path.
+graphics-example: $(GRAPHICS_EXAMPLE)
+$(GRAPHICS_EXAMPLE): docs/examples/Graphics.smp $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) graphics.mk
+ @$(JVM) com.seaofnodes.simple.Simple --link --cpu $(CPU_PORT) --abi $(CALL_CONV) -L $(dir $(RELEASE_SYS)) -o $(GAME_DIR)/Graphics.o $<
+
+# An interactive target, never part of default/tests.
+run-graphics: graphics graphics-example
+ SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" $(GRAPHICS_EXAMPLE)
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(); };
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 67e7f982..44cdbada 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -475,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, text, char, collections, and array utilities.
+ // Sys includes the graphics interface; its native backend is optional.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/char", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
new file mode 100644
index 00000000..2e9c8dbc
--- /dev/null
+++ b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -0,0 +1,69 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// These tests never open a window and do not need MiniFB installed.
+public class GraphicsTest {
+ private static String graphics, text;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ graphics = TestC.compile(Files.readString(Path.of("docs/examples/Graphics.smp")),
+ "graphics_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ text = TestC.compile("val main={ -> sys.io.p(\"text still works\\n\"); return 0; };",
+ "graphics_text",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend, int exit) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(exe).redirectErrorStream(true);
+ if( backend == null ) pb.environment().remove("SIMPLE_MINIFB");
+ else pb.environment().put("SIMPLE_MINIFB",backend);
+ Process child = pb.start();
+ try {
+ assertTrue("native program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output = new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,exit,child.exitValue());
+ return output;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void missingConfiguration() throws Exception {
+ String output = run(graphics,null,1);
+ assertTrue(output,output.contains("MiniFB backend not configured"));
+ assertTrue(output,output.contains("make graphics"));
+ }
+
+ @Test public void disabledGraphics() throws Exception {
+ assertTrue(run(graphics,"off",1).contains("disabled"));
+ }
+
+ @Test public void missingLibrary() throws Exception {
+ String missing = Path.of("build","no-such-minifb-backend").toAbsolutePath().toString();
+ String output = run(graphics,missing,1);
+ assertTrue(output,output.contains("cannot load MiniFB backend"));
+ }
+
+ @Test public void textIgnoresGraphicsConfiguration() throws Exception {
+ assertEquals("text still works\n",run(text,"off",0));
+ assertEquals("text still works\n",run(text,"no-such-backend",0));
+ }
+
+ @Test public void incompatibleLibrary() throws Exception {
+ Path source = Path.of("build/objs/graphics_incompatible.c");
+ Path library = Path.of("build/objs/graphics_incompatible" +
+ (TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ static const struct simple_graphics_api api = {999};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(graphics,library.toAbsolutePath().toString(),1).contains("incompatible MiniFB backend"));
+ }
+}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index 1853aa48..1c278791 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -10,6 +10,22 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class SeparateCompilationTest {
+ @Test public void testImportedReadOnlyPrimitiveArray() throws Exception {
+ Path build=Path.of("build/objs/separate");
+ Files.createDirectories(build);
+ Path lib=Files.createTempDirectory(build,"pixels-");
+ new CodeGen(null,lib.toString(),null,"Pixels",
+ "val first={ u32[~] pixels -> pixels[0]; };",123,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"PixelClient","""
+ val pixels=new u32[2];
+ pixels[0]=4294967295;
+ return Pixels.first(pixels);
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4294967295",Eval2.eval(code,0));
+ }
+
@Test public void testImportedNativeExposure() throws Exception {
Path build=Path.of("build/objs/separate");
Files.createDirectories(build);
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
index f3491314..2aa2e3da 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -131,6 +131,8 @@ public class StreamIOTest extends TestC {
gcc.addAll(linkObjs(code));
gcc.add(runtimeObject("simple_crt"));
gcc.add(runtimeObject("simple_args"));
+ gcc.add(runtimeObject("simple_graphics"));
+ if( !OS.startsWith("Windows") ) gcc.add("-ldl");
gcc.addAll(new String[]{"-lm","-o",exe});
exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/TestC.java b/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
index dd7999c9..73795111 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
@@ -190,6 +190,9 @@ public abstract class TestC {
params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.addAll(objects);
+ // The loader has no MiniFB dependency and opens nothing until gfx.open.
+ params.add(runtimeObject("simple_graphics"));
+ if( !OS.startsWith("Windows") ) params.add("-ldl");
// Standalone Simple programs export `simple_main`; the small C
// runtime owns native main and calls the Simple argument converter
// before making the ABI-qualified entry call.