diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-11 11:49:04 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-12 13:48:30 -0800 |
| commit | f4bac750504ae2d21068a87b6a6bf4a733476993 (patch) | |
| tree | 644741a31d045d17f9456f07078a2fca2bb21412 | |
| parent | a269fa7e8aea51b4fbdd2f2c6c24e84ef26f9ca4 (diff) | |
| download | Simple-f4bac750504ae2d21068a87b6a6bf4a733476993.tar.gz Simple-f4bac750504ae2d21068a87b6a6bf4a733476993.zip | |
First cut graphics
| -rw-r--r-- | chapter31/Makefile | 15 | ||||
| -rw-r--r-- | chapter31/README.md | 10 | ||||
| -rw-r--r-- | chapter31/docs/examples/Graphics.smp | 17 | ||||
| -rw-r--r-- | chapter31/docs/examples/README.md | 4 | ||||
| -rw-r--r-- | chapter31/docs/graphics.md | 109 | ||||
| -rw-r--r-- | chapter31/graphics.mk | 50 | ||||
| -rw-r--r-- | chapter31/src/main/c/simple_graphics.c | 60 | ||||
| -rw-r--r-- | chapter31/src/main/c/simple_graphics.h | 19 | ||||
| -rw-r--r-- | chapter31/src/main/c/simple_minifb.c | 113 | ||||
| -rw-r--r-- | chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java | 12 | ||||
| -rw-r--r-- | chapter31/src/main/smp/sys.smp | 1 | ||||
| -rw-r--r-- | chapter31/src/main/smp/sys/gfx.smp | 38 | ||||
| -rw-r--r-- | chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java | 4 | ||||
| -rw-r--r-- | chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java | 69 | ||||
| -rw-r--r-- | chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java | 16 | ||||
| -rw-r--r-- | chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java | 2 | ||||
| -rw-r--r-- | chapter31/src/test/java/com/seaofnodes/simple/TestC.java | 3 |
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. |
