Browse Source

feat: SDL3 bindings for GNU Modula-2, starter set

Thin DEFINITION MODULE FOR "C" bindings of SDL 3.4.16 (init,
video, version, timer, error, events, renderer starter subset)
plus SDL3Utils helpers, 5 smoke tests, 4 examples and 7 ports of
upstream renderer/demo showcases. Verified with gm2 16.0.1;
tests/showcases pass headless via SDL_VIDEODRIVER=dummy.
Eric Streit 2 weeks ago
commit
ed80b57626

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+build/
+SDL-release-3.4.16/
+*.o
+*.ppu
+*.s

+ 68 - 0
Makefile

@@ -0,0 +1,68 @@
+# m2SDL3 — GNU Modula-2 bindings for SDL3
+GM2      ?= gm2
+GM2FLAGS ?=
+
+SRC_DIR := src
+LIB_DIR := lib
+BLD     := build
+OBJDIR  := $(BLD)/objs
+OBJS    := $(OBJDIR)/SDL3Rect.o $(OBJDIR)/SDL3Utils.o $(OBJDIR)/SDL3Key.o
+
+# SDL3 sources shipped alongside (read-only, see README): headers in
+# SDL-release-3.4.16/include, prebuilt shared lib in .../build.
+SDL_SRC := SDL-release-3.4.16
+SDL_BLD := $(SDL_SRC)/build
+SDL_INC := -I$(SDL_SRC)/include
+SDL_LIB := -L$(SDL_BLD) -lSDL3 -Wl,-rpath,$(abspath $(SDL_BLD))
+
+INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR) $(SDL_INC)
+
+TESTS    := test_init test_version test_rect test_window test_key
+EXAMPLES := hello_window version event_loop renderer_demo
+SHOWCASES := showcase_clear showcase_rectangles showcase_primitives showcase_snake showcase_lines showcase_points showcase_debug_text
+
+.PHONY: all tests examples showcases clean check
+
+all: tests examples showcases
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(BLD)/tests:
+	mkdir -p $(BLD)/tests
+
+$(BLD)/examples:
+	mkdir -p $(BLD)/examples
+
+$(OBJDIR)/SDL3Rect.o: $(SRC_DIR)/SDL3Rect.mod $(SRC_DIR)/SDL3Rect.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/SDL3Utils.o: $(LIB_DIR)/SDL3Utils.mod $(LIB_DIR)/SDL3Utils.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/SDL3Key.o: $(SRC_DIR)/SDL3Key.mod $(SRC_DIR)/SDL3Key.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) $(GM2FLAGS) -c $< -o $@
+
+tests: $(BLD)/tests $(OBJS) $(addprefix $(BLD)/tests/,$(TESTS))
+
+$(BLD)/tests/%: tests/%.mod $(OBJS) | $(BLD)/tests
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $< $(OBJS) -o $@ $(SDL_LIB)
+
+examples: $(BLD)/examples $(OBJS) $(addprefix $(BLD)/examples/,$(EXAMPLES))
+
+$(BLD)/examples/%: examples/%.mod $(OBJS) | $(BLD)/examples
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $< $(OBJS) -o $@ $(SDL_LIB)
+
+$(BLD)/showcases:
+	mkdir -p $(BLD)/showcases
+
+showcases: $(BLD)/showcases $(OBJS) $(addprefix $(BLD)/showcases/,$(SHOWCASES))
+
+$(BLD)/showcases/%: showcases/%.mod $(OBJS) | $(BLD)/showcases
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $< $(OBJS) -o $@ $(SDL_LIB)
+
+check: tests
+	./tests/run_tests.sh
+
+clean:
+	rm -rf $(BLD)

+ 120 - 0
README.md

@@ -0,0 +1,120 @@
+# m2SDL3 — SDL3 bindings for GNU Modula-2
+
+Thin `DEFINITION MODULE FOR "C"` bindings of SDL 3.4 for `gm2`
+(GCC 16 / GNU Modula-2), covering init, video, version,
+timer, error, events and the 2D renderer starter subset.
+
+Upstream sources live in `SDL-release-3.4.16/` (read-only reference,
+never modified by this binding; per its `AGENTS.md` no AI-generated
+code is contributed upstream). All binding code below is original
+work that only *uses* SDL headers at compile time and links the
+prebuilt `SDL-release-3.4.16/build/libSDL3.so`.
+
+## Status
+
+Starter set, verified with gm2 + SDL 3.4.16 on Linux:
+
+| Module | C header | Contents |
+|---|---|---|
+| `SDL3` | `<SDL3/SDL_init.h>`, `<SDL3/SDL_error.h>`, `<SDL3/SDL_timer.h>` | init/quit (`BOOLEAN` returns), `GetError`, `Delay`/`DelayNS`, 64-bit ticks, performance counter |
+| `SDL3Version` | `<SDL3/SDL_version.h>` | `SDL_GetVersion(): INTEGER` versionnum, `GetRevision` |
+| `SDL3Video` | `<SDL3/SDL_video.h>` | window create/destroy, title, size/position, show/hide, flags (`LONGCARD`) |
+| `SDL3Events` | `<SDL3/SDL_events.h>` | `PumpEvents`, `PollEvent`, `WaitEvent`, `WaitEventTimeout`, `PushEvent`, `HasEvent(s)` (`BOOLEAN`), quit/key/mouse + window-close constants, 128-byte buffer |
+| `SDL3Rect` | `<SDL3/SDL_rect.h>` | pure-M2 `SDLPoint`/`SDLFPoint`/`SDLRect`/`SDLFRect` + helpers (`SHORTREAL` = C float) |
+| `SDL3Render` | `<SDL3/SDL_render.h>` | window+renderer creation, draw color (+float), logical presentation, clear/present, points/lines/rects (float `SDLFRect`), render scale, output size, debug text |
+| `SDL3Key` | `<SDL3/SDL_keycode.h>` | pure-M2 keycode constants (arrows, Q/R, Escape...) |
+| `SDL3Utils` (`lib/`) | — | `CStrToM2()` + `GetErrorString()`, version decode, `DrainEvents()` quit pump (QUIT + WINDOW_CLOSE_REQUESTED), `PollM2Event()` decoded poll (keycode at offset 28) |
+
+## Layout
+
+```text
+m2SDL3/
+  src/        FOR "C" .def bindings (+ pure-M2 SDL3Rect/SDL3Key)
+  lib/        pure-M2 helpers built on the bindings (SDL3Utils)
+  tests/      gm2 smoke tests + run_tests.sh
+  examples/   hello_window, version, event_loop, renderer_demo
+  showcases/  clear, primitives, lines, points, rectangles, debug_text, snake
+  build/      generated binaries (git-ignored)
+  SDL-release-3.4.16/  upstream SDL reference + prebuilt lib (untouched)
+```
+
+## Prerequisites
+
+- `gm2` (tested: 16.0.1 experimental, `~/bin/Modula2/Gm2/bin/gm2`)
+- SDL 3.4.16 already built at `SDL-release-3.4.16/build/libSDL3.so`
+  (headers at `SDL-release-3.4.16/include`)
+
+```sh
+export PATH=$HOME/bin/Modula2/Gm2/bin:$PATH
+```
+
+## Build & test
+
+```sh
+make            # tests + examples + showcases -> build/
+./tests/run_tests.sh
+SDL_VIDEODRIVER=dummy ./tests/run_tests.sh   # headless
+./build/examples/hello_window
+./build/examples/version
+./build/examples/event_loop
+./build/examples/renderer_demo
+./build/showcases/showcase_clear   # or rectangles / primitives / snake
+```
+
+`make check` is `make tests` + the runner script.
+
+## Usage
+
+```modula2
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDLInitVideo;
+FROM SDL3Video IMPORT SDL_CreateWindow, SDL_DestroyWindow, WindowHidden;
+```
+
+Compile your program with the binding search path and the C library:
+
+```sh
+gm2 -Isrc -Ilib \
+  -ISDL-release-3.4.16/include \
+  myprog.mod src/SDL3Rect.mod lib/SDL3Utils.mod src/SDL3Key.mod \
+  -o myprog -LSDL-release-3.4.16/build -lSDL3 \
+  -Wl,-rpath,$PWD/SDL-release-3.4.16/build
+```
+
+Notes:
+
+- SDL3 `bool` <-> `BOOLEAN` (see gm2 "Elementary data
+  types" node); init/video/event/render calls return `[ BOOLEAN ]`
+  (ignorable): `IF NOT SDL_Init(...) THEN` or `SDL_RenderClear(ren);`.
+- `CARDINAL <-> Uint32`, `LONGCARD <-> Uint64`, `INTEGER <-> int`
+  (window flags are 64-bit: pass `VAL(LONGCARD, WindowResizable)`).
+- `SHORTREAL <-> float` (`SDL_FRect`/`SDL_FPoint` fields, draw
+  coordinates, `SetRenderDrawColorFloat`); `REAL <-> double`.
+- Opaque `SDL_Window *` / `SDL_Renderer *` / `char *` travel as
+  `ADDRESS` (`NIL` == failure / default driver).
+- `ARRAY OF CHAR` parameters map onto `char *`: string literals and
+  `CHAR` arrays can be passed directly (see gm2 "Interface to C" node).
+- `VAR` record parameters map onto `T *`: `SDL_GetWindowSize(win,w,h)`,
+  `SDL_RenderFillRect(ren, rect)`.
+- `SDL_GetTicks()` is now `LONGCARD` milliseconds; `SDL_GetVersion()`
+  returns an `INTEGER` versionnum (`VersionMajor/Minor/Micro` decode it).
+- Closing a window yields `EventWindowCloseRequested` (0210H) as well
+  as `EventQuit`; `DrainEvents`/`PollM2Event` treat both as `evQuit`.
+
+## SDL2 -> SDL3 porting cheatsheet
+
+| SDL2 binding | SDL3 binding |
+|---|---|
+| `SDL_Init(): INTEGER`, `rc # 0` | `SDL_Init(): [ BOOLEAN ]`, `NOT ...` / `= FALSE` |
+| `CreateWindow(t,x,y,w,h,flags: CARDINAL)` | `CreateWindow(t,w,h,flags: LONGCARD)` |
+| `GetTicks(): CARDINAL` + `GetTicks64()` | `GetTicks(): LONGCARD` (ms), `GetTicksNS()` (ns) |
+| `GetVersion(VAR v)` record | `GetVersion(): INTEGER` + `VersionMajor/Minor/Micro` |
+| `RenderDrawPoint/Line/Rect` (int) | `RenderPoint/Line/Rect` (`SHORTREAL`/`SDLFRect`) |
+| `RenderSetLogicalSize(w,h)` | `SetRenderLogicalPresentation(w,h,mode)` |
+| `EventBufferBytes = 56`, sym at 20 | `EventBufferBytes = 128`, key at 28, close-request = quit |
+
+## Roadmap
+
+1. Texture/surface subset (`SDL_Texture`, `SDL_Surface` load/blit).
+2. Audio stream subset (`SDL_AudioStream`, device open).
+3. Gamepad/joystick subset.
+4. Full `SDL_Event` variant record (keyboard/mouse/window typed).

+ 68 - 0
docs/summary-v0.1.0.md

@@ -0,0 +1,68 @@
+# m2SDL3 summary — v0.1.0 (2026-09-24)
+
+SDL3 bindings for GNU Modula-2 (`gm2` 16.0.1, SDL 3.4.16, Linux).
+All builds verified with `make`; tests and showcases also pass
+headless via `SDL_VIDEODRIVER=dummy`.
+
+Upstream SDL sources in `SDL-release-3.4.16/` are a read-only
+reference (prebuilt `build/libSDL3.so` used for linking) and are
+*not* committed — per its `AGENTS.md` no AI-generated code is
+contributed upstream. The binding is original work that only uses
+SDL headers at compile time.
+
+## v0.1.0 — starter set + renderer + showcases
+
+- `src/SDL3.def` — init/quit, `GetError`, delay, 64-bit ticks,
+  perf counter. SDL3 returns C `bool`, hence `BOOLEAN` results;
+  `SDL_INIT_TIMER`/`EVERYTHING` gone, `GAMEPAD` renamed, `CAMERA` new
+- `src/SDL3Version.def` — `SDL_GetVersion(): INTEGER` versionnum
+  (`major*1000000+minor*1000+micro`), `GetRevision`
+- `src/SDL3Video.def` — opaque `WindowHandle`,
+  `CreateWindow(title, w, h, flags: LONGCARD)` (no x/y),
+  title/size/position/show/hide (`[ BOOLEAN ]`), 64-bit flag constants
+- `src/SDL3Events.def` — opaque 128-byte event pump (`PumpEvents`,
+  `PollEvent`, `WaitEvent`, `WaitEventTimeout`, `PushEvent`,
+  `HasEvent(s)`), quit/key/mouse ids plus `EventWindowCloseRequested`
+- `src/SDL3Rect.def/.mod` — pure-M2 `SDLPoint`/`SDLFPoint`/`SDLRect`/
+  `SDLFRect` + helpers (`SHORTREAL` = C float, verified vs C caller)
+- `src/SDL3Render.def` — `<SDL_render.h>` subset: `CreateRenderer`
+  (name or `NIL`), `CreateWindowAndRenderer` (title form, `BOOLEAN`),
+  draw color (+float), logical presentation, clear/present,
+  points/lines/rects in single (`VAR`) and array (`ADR` + count)
+  forms, render scale, output size, debug text
+- `src/SDL3Key.def/.mod` — pure-M2 keycode constants (unchanged values)
+- `lib/SDL3Utils.def/.mod` — `CStrToM2`, `GetErrorString`,
+  `VersionMajor/Minor/Micro`, `PollM2Event` (keycode at offset 28,
+  verified with `offsetof`; `QUIT` + `WINDOW_CLOSE_REQUESTED` -> quit),
+  `DrainEvents`
+- `tests/` — init, version, rect, window, key + `run_tests.sh` (5/5 green)
+- `examples/` — `hello_window`, `version`, `event_loop`, `renderer_demo`
+- `showcases/` — ports of SDL `examples/renderer/` + `examples/demo/`
+  (public domain): `showcase_clear` (01), `showcase_primitives` (02),
+  `showcase_lines` (03), `showcase_points` (04),
+  `showcase_rectangles` (05), `showcase_debug_text` (18),
+  `showcase_snake` (demo 01); SDL3 is native here, adaptations noted
+  in each header (callback flattening, `rand`/`randf`, ASCII text,
+  counter instead of varargs `RenderDebugTextFormat`)
+- `Makefile` — links the local `SDL-release-3.4.16/build/libSDL3.so`
+  with `rpath`; `make check` runs the test suite
+
+## GM2 FFI rules learned (encoded in code)
+
+1. `DEFINITION MODULE FOR "C"` + `EXPORT UNQUALIFIED`, link `-lSDL3`.
+2. No `*)` inside `(* *)` comments — C pointer spellings need a space.
+3. `BOOLEAN` <-> C `bool`, `CARDINAL` <-> Uint32, `LONGCARD` <-> Uint64,
+   `INTEGER` <-> int, `SHORTREAL` <-> float (`REAL` is double).
+4. Ignorable C-`bool` results need `[ BOOLEAN ]` to call as statements.
+5. Opaque pointers = `ADDRESS`, `ARRAY OF CHAR` params = `char *`,
+   `VAR` record params = `T *`, arrays = `ADR` + count.
+6. Same-size casts only: `SHORTREAL(real-expr)` fails, use
+   `VAL(SHORTREAL, ...)`.
+7. Pure-M2 helpers compile with `gm2 -c` to `.o`, then link.
+8. Window-close buttons yield `WINDOW_CLOSE_REQUESTED`, not `QUIT` —
+   handle both (done in `DrainEvents`/`PollM2Event`).
+
+## Next
+
+Texture/surface subset, audio streams, gamepad/joystick, typed
+`SDL_Event` variant record, then `infinite-monkeys` and further demos.

+ 44 - 0
examples/event_loop.mod

@@ -0,0 +1,44 @@
+MODULE event_loop ;
+
+(*
+   m2SDL3 example: event loop until window close or 5 seconds.
+   Quit arrives as EventQuit or EventWindowCloseRequested; both
+   decode to evQuit via SDL3Utils.PollM2Event, so DrainEvents is
+   enough for a close-button loop.
+*)
+
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo, SDLInitEvents;
+FROM SDL3Video IMPORT SDL_CreateWindow, SDL_DestroyWindow;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM libc IMPORT printf;
+FROM SYSTEM IMPORT ADDRESS;
+
+VAR
+   win: ADDRESS;
+   start, now: LONGCARD;
+   quit: BOOLEAN;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo + SDLInitEvents) THEN
+      printf("SDL_Init failed\n");
+      HALT(1)
+   END;
+   win := SDL_CreateWindow("Close me, or wait 5s", 640, 480, 0);
+   IF win = NIL THEN
+      printf("CreateWindow failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+      now := SDL_GetTicks();
+      IF now - start > 5000 THEN quit := TRUE END;
+      SDL_Delay(16)
+   END;
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("bye\n")
+END event_loop.

+ 35 - 0
examples/hello_window.mod

@@ -0,0 +1,35 @@
+MODULE hello_window ;
+
+(*
+   m2SDL3 example: open a window for ~2 seconds, then quit.
+   Build:  make examples   or see Makefile.
+   Run:    ./build/examples/hello_window
+   SDL3: CreateWindow takes (title, w, h, flags) - no x/y - and
+   init returns BOOLEAN.
+*)
+
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay, SDLInitVideo;
+FROM SDL3Video IMPORT SDL_CreateWindow, SDL_DestroyWindow, WindowHidden;
+FROM SYSTEM IMPORT ADDRESS;
+FROM libc IMPORT printf;
+
+VAR win: ADDRESS;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("SDL_Init failed\n");
+      HALT(1)
+   END;
+   win := SDL_CreateWindow("Hello from GNU Modula-2 + SDL3",
+                           640, 480, 0);
+   IF win = NIL THEN
+      printf("CreateWindow failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   printf("window open for 2s...\n");
+   SDL_Delay(2000);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("bye\n")
+END hello_window.

+ 73 - 0
examples/renderer_demo.mod

@@ -0,0 +1,73 @@
+MODULE renderer_demo ;
+
+(*
+   m2SDL3 example: renderer basics - clear, colored rects, present.
+   Uses SDL_CreateWindowAndRenderer (title, w, h, flags) and the
+   float SDLFRect records; colors are Uint8 CARDINAL8 values.
+   Run: ./build/examples/renderer_demo (auto-quits after 3 s).
+*)
+
+FROM SYSTEM IMPORT CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay, SDL_GetTicks,
+   SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, WindowResizable, SDL_DestroyWindow;
+FROM SDL3Render IMPORT RendererHandle, SDL_CreateWindowAndRenderer,
+   SDL_DestroyRenderer, SDL_SetRenderDrawColor,
+   SDL_RenderClear, SDL_RenderPresent, SDL_RenderFillRect,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox;
+FROM SDL3Rect IMPORT SDLFRect, MakeFRect;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM libc IMPORT printf;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   r: SDLFRect;
+   quit: BOOLEAN;
+   start: LONGCARD;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("renderer_demo: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 renderer demo",
+                                      640, 480,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("renderer_demo: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   IF NOT SDL_SetRenderLogicalPresentation(ren, 640, 480,
+                                           LogicalLetterbox) THEN
+      printf("renderer_demo: logical presentation failed\n")
+   END;
+
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 20), VAL(CARDINAL8, 20),
+                             VAL(CARDINAL8, 40), VAL(CARDINAL8, 255));
+      SDL_RenderClear(ren);
+      r := MakeFRect(VAL(SHORTREAL, 100.0), VAL(SHORTREAL, 100.0),
+                     VAL(SHORTREAL, 200.0), VAL(SHORTREAL, 150.0));
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 200),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderFillRect(ren, r);
+      r := MakeFRect(VAL(SHORTREAL, 350.0), VAL(SHORTREAL, 200.0),
+                     VAL(SHORTREAL, 150.0), VAL(SHORTREAL, 100.0));
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 200), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderFillRect(ren, r);
+      SDL_RenderPresent(ren);
+      IF SDL_GetTicks() - start > 3000 THEN quit := TRUE END;
+      SDL_Delay(16)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("renderer_demo: bye\n")
+END renderer_demo.

+ 16 - 0
examples/version.mod

@@ -0,0 +1,16 @@
+MODULE version ;
+
+(* m2SDL3 example: print linked SDL version + revision. *)
+
+FROM SDL3Version IMPORT SDL_GetVersion, SDL_GetRevision;
+FROM SDL3Utils IMPORT VersionMajor, VersionMinor, VersionMicro;
+FROM libc IMPORT printf;
+
+VAR v: INTEGER;
+
+BEGIN
+   v := SDL_GetVersion();
+   printf("SDL version %d.%d.%d", VersionMajor(v),
+          VersionMinor(v), VersionMicro(v));
+   printf(" revision %s\n", SDL_GetRevision())
+END version.

+ 40 - 0
lib/SDL3Utils.def

@@ -0,0 +1,40 @@
+DEFINITION MODULE SDL3Utils ;
+
+(*
+   m2SDL3 - small pure-Modula-2 helpers over the FOR "C" bindings.
+   CStrToM2 copies NUL-terminated C strings (ADDRESS) into
+   Modula-2 CHAR arrays (NUL-terminated, truncated to fit).
+   PollM2Event decodes one queued SDL_Event into a small record:
+   SDL3 puts the Uint32 type at offset 0 of every event; key events
+   (SDL_KeyboardEvent) carry the Uint32 keycode at offset 28 (see
+   <SDL3/SDL_events.h>, verified with offsetof). Both QUIT (0100H)
+   and WINDOW_CLOSE_REQUESTED (0210H) map to evQuit, so a plain
+   "close window" button press terminates DrainEvents loops.
+   VersionMajor/Minor/Micro decode the int returned by
+   SDL3Version.SDL_GetVersion (major*1000000+minor*1000+micro).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   EventKind, M2Event,
+   CStrToM2, GetErrorString, PollM2Event, DrainEvents,
+   VersionMajor, VersionMinor, VersionMicro;
+
+TYPE
+   EventKind = (evQuit, evKey, evOther);
+   M2Event = RECORD
+      kind: EventKind;
+      sym: INTEGER;      (* valid when kind = evKey *)
+      pressed: BOOLEAN;  (* valid when kind = evKey *)
+   END;
+
+PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
+PROCEDURE GetErrorString (VAR buf: ARRAY OF CHAR);
+PROCEDURE PollM2Event (VAR ev: M2Event) : BOOLEAN;
+PROCEDURE DrainEvents (VAR quit: BOOLEAN);
+PROCEDURE VersionMajor (v: INTEGER) : INTEGER;
+PROCEDURE VersionMinor (v: INTEGER) : INTEGER;
+PROCEDURE VersionMicro (v: INTEGER) : INTEGER;
+
+END SDL3Utils.

+ 84 - 0
lib/SDL3Utils.mod

@@ -0,0 +1,84 @@
+IMPLEMENTATION MODULE SDL3Utils ;
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SDL3 IMPORT SDL_GetError;
+FROM SDL3Events IMPORT SDL_PollEvent,
+   EventQuit, EventWindowCloseRequested,
+   EventKeyDown, EventKeyUp, EventBufferBytes;
+FROM libc IMPORT strlen, strncpy;
+
+PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
+VAR n: CARDINAL;
+BEGIN
+   IF HIGH(dst) < 0 THEN RETURN END;
+   IF src = NIL THEN dst[0] := 0C; RETURN END;
+   (* strlen returns CSIZE_T; VAL converts without range check. *)
+   n := VAL(CARDINAL, strlen(src));
+   IF n > HIGH(dst) THEN n := HIGH(dst) END;
+   strncpy(ADR(dst), src, n);
+   IF n <= HIGH(dst) THEN dst[n] := 0C END
+END CStrToM2;
+
+PROCEDURE GetErrorString (VAR buf: ARRAY OF CHAR);
+BEGIN
+   CStrToM2(SDL_GetError(), buf)
+END GetErrorString;
+
+(* Little-endian 32-bit word at byte offset off. Hosts only. *)
+PROCEDURE LE32 (VAR buf: ARRAY OF CHAR; off: CARDINAL) : CARDINAL;
+BEGIN
+   RETURN VAL(CARDINAL, ORD(buf[off]))
+        + VAL(CARDINAL, ORD(buf[off+1])) * 256
+        + VAL(CARDINAL, ORD(buf[off+2])) * 65536
+        + VAL(CARDINAL, ORD(buf[off+3])) * 16777216
+END LE32;
+
+PROCEDURE PollM2Event (VAR ev: M2Event) : BOOLEAN;
+VAR
+   buf: ARRAY [0..EventBufferBytes-1] OF CHAR;
+   t: CARDINAL;
+BEGIN
+   IF NOT SDL_PollEvent(ADR(buf)) THEN RETURN FALSE END;
+   t := LE32(buf, 0);
+   ev.sym := 0;
+   ev.pressed := FALSE;
+   IF t = EventKeyDown THEN
+      ev.kind := evKey;
+      ev.pressed := TRUE;
+      ev.sym := VAL(INTEGER, LE32(buf, 28))
+   ELSIF t = EventKeyUp THEN
+      ev.kind := evKey;
+      ev.pressed := FALSE;
+      ev.sym := VAL(INTEGER, LE32(buf, 28))
+   ELSIF (t = EventQuit) OR (t = EventWindowCloseRequested) THEN
+      ev.kind := evQuit
+   ELSE
+      ev.kind := evOther
+   END;
+   RETURN TRUE
+END PollM2Event;
+
+PROCEDURE DrainEvents (VAR quit: BOOLEAN);
+VAR ev: M2Event;
+BEGIN
+   WHILE PollM2Event(ev) DO
+      IF ev.kind = evQuit THEN quit := TRUE END
+   END
+END DrainEvents;
+
+PROCEDURE VersionMajor (v: INTEGER) : INTEGER;
+BEGIN
+   RETURN v DIV 1000000
+END VersionMajor;
+
+PROCEDURE VersionMinor (v: INTEGER) : INTEGER;
+BEGIN
+   RETURN (v DIV 1000) MOD 1000
+END VersionMinor;
+
+PROCEDURE VersionMicro (v: INTEGER) : INTEGER;
+BEGIN
+   RETURN v MOD 1000
+END VersionMicro;
+
+END SDL3Utils.

+ 21 - 0
showcases/README.md

@@ -0,0 +1,21 @@
+# m2SDL3 showcases
+
+Ports of SDL upstream renderer examples plus the snake demo,
+flattened from the `SDL_AppInit/Iterate/...` callback style into
+plain main loops (auto-quit timers so they terminate unattended):
+
+| Showcase | Upstream source | What it shows |
+|---|---|---|
+| `showcase_clear` | `examples/renderer/01-clear` | fading clear via `SetRenderDrawColorFloat` + `sin` |
+| `showcase_primitives` | `examples/renderer/02-primitives` | fill rect, 500 points, outline, X (`SDLFPoint` arrays) |
+| `showcase_lines` | `examples/renderer/03-lines` | house segments, green polyline batch, 360-line circle (`RenderLines`, `cos`/`sin`) |
+| `showcase_points` | `examples/renderer/04-points` | 500 drifting points in one `RenderPoints` batch |
+| `showcase_rectangles` | `examples/renderer/05-rectangles` | animated outlines, fill, bar chart (`SDLFRect` arrays) |
+| `showcase_debug_text` | `examples/renderer/18-debug-text` | bitmap debug text, colors, 4x scale, live seconds counter |
+| `showcase_snake` | `examples/demo/01-snake` | playable Snake (arrows, R, Q/Esc), 30 s cap |
+
+Run headless with `SDL_VIDEODRIVER=dummy`, e.g.:
+
+```sh
+SDL_VIDEODRIVER=dummy ./build/showcases/showcase_clear
+```

+ 74 - 0
showcases/showcase_clear.mod

@@ -0,0 +1,74 @@
+MODULE showcase_clear ;
+
+(*
+   m2SDL3 showcase, ported from SDL upstream
+   examples/renderer/01-clear/clear.c (public domain).
+
+   Clears the window to a smoothly fading color every frame; the
+   sine-wave trick picks the color from the elapsed time.
+
+   SDL3 native form (no SDL2 adaptation needed here):
+   - SDL_CreateWindowAndRenderer(title, w, h, flags, win^, ren^).
+   - SDL_SetRenderDrawColorFloat takes 0.0..1.0 SHORTREAL values.
+   - Callback structure flattened into a plain main loop with
+     DrainEvents; auto-quit after 8 s so it terminates unattended.
+*)
+
+FROM SYSTEM IMPORT CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColorFloat, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM MathLib0 IMPORT sin;
+FROM libc IMPORT printf;
+
+CONST FrameMs = 16; MaxMs = 8000;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   quit: BOOLEAN;
+   start: LONGCARD;
+   t: REAL;
+   r, g, b: SHORTREAL;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("showcase_clear: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 clear", 640, 480,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_clear: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, 640, 480, LogicalLetterbox);
+
+   printf("showcase_clear: fading, close window or wait 8s\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+      (* seconds since start, as REAL (double). *)
+      t := LFLOAT(SDL_GetTicks()) / 1000.0;
+      r := VAL(SHORTREAL, 0.5 + 0.5 * sin(t));
+      g := VAL(SHORTREAL, 0.5 + 0.5 * sin(t + 2.0944));
+      b := VAL(SHORTREAL, 0.5 + 0.5 * sin(t + 4.1888));
+      SDL_SetRenderDrawColorFloat(ren, r, g, b, VAL(SHORTREAL, 1.0));
+      SDL_RenderClear(ren);
+      SDL_RenderPresent(ren);
+      IF SDL_GetTicks() - start > MaxMs THEN quit := TRUE END;
+      SDL_Delay(FrameMs)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_clear: bye\n")
+END showcase_clear.

+ 146 - 0
showcases/showcase_debug_text.mod

@@ -0,0 +1,146 @@
+MODULE showcase_debug_text ;
+
+(*
+   m2SDL3 showcase, ported from SDL upstream
+   examples/renderer/18-debug-text/debug-text.c (public domain).
+
+   Renders built-in bitmap debug text: plain lines, a colored line,
+   a 4x-scaled line and a live "running for N seconds" counter.
+
+   Porting notes
+   - SDL_RenderDebugTextFormat (C varargs) has no Modula-2 mapping,
+     so the counter line is composed into a CHAR buffer with the
+     local AppendCard helper instead.
+   - The upstream emoji line is ASCII-only in SDL anyway; the emoji
+     bytes are replaced by plain words here (CHAR literals stay
+     8-bit clean this way).
+   - Callback structure flattened into a main loop; auto-quit
+     after 8 s so the showcase also terminates unattended.
+*)
+
+FROM SYSTEM IMPORT CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox,
+   SDL_SetRenderScale, SDL_RenderDebugText;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM libc IMPORT printf;
+
+CONST FrameMs = 16; MaxMs = 8000;
+CONST WinW = 640; WinH = 480;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   quit: BOOLEAN;
+   start: LONGCARD;
+   secs: LONGCARD;
+   pos: CARDINAL;
+   msg: ARRAY [0..127] OF CHAR;
+
+(* Append decimal n at buf[pos]; RETURN new pos (buf stays NUL-ended). *)
+PROCEDURE AppendCard (VAR buf: ARRAY OF CHAR; pos: CARDINAL;
+                      n: LONGCARD) : CARDINAL;
+VAR digits: ARRAY [0..23] OF CHAR; nd, k: CARDINAL;
+BEGIN
+   nd := 0;
+   IF n = 0 THEN digits[0] := "0"; nd := 1
+   ELSE
+      WHILE n > 0 DO
+         digits[nd] := CHR(ORD("0") + VAL(CARDINAL, n MOD 10));
+         INC(nd);
+         n := n DIV 10
+      END
+   END;
+   k := nd;
+   WHILE k > 0 DO
+      DEC(k);
+      IF pos <= HIGH(buf) THEN buf[pos] := digits[k]; INC(pos) END
+   END;
+   IF pos <= HIGH(buf) THEN buf[pos] := 0C END;
+   RETURN pos
+END AppendCard;
+
+PROCEDURE AppendStr (VAR buf: ARRAY OF CHAR; pos: CARDINAL;
+                     s: ARRAY OF CHAR) : CARDINAL;
+VAR k: CARDINAL;
+BEGIN
+   k := 0;
+   WHILE (k <= HIGH(s)) AND (s[k] # 0C) DO
+      IF pos <= HIGH(buf) THEN buf[pos] := s[k]; INC(pos) END;
+      INC(k)
+   END;
+   IF pos <= HIGH(buf) THEN buf[pos] := 0C END;
+   RETURN pos
+END AppendStr;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("showcase_debug_text: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 debug text",
+                                      WinW, WinH,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_debug_text: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, WinW, WinH, LogicalLetterbox);
+
+   printf("showcase_debug_text: close window or wait 8s\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderClear(ren);
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+      SDL_RenderDebugText(ren, VAL(SHORTREAL, 272.0),
+                          VAL(SHORTREAL, 100.0), "Hello world!");
+      SDL_RenderDebugText(ren, VAL(SHORTREAL, 224.0),
+                          VAL(SHORTREAL, 150.0),
+                          "This is some debug text.");
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 51), VAL(CARDINAL8, 102),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+      SDL_RenderDebugText(ren, VAL(SHORTREAL, 184.0),
+                          VAL(SHORTREAL, 200.0),
+                          "You can do it in different colors.");
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+
+      SDL_SetRenderScale(ren, VAL(SHORTREAL, 4.0), VAL(SHORTREAL, 4.0));
+      SDL_RenderDebugText(ren, VAL(SHORTREAL, 14.0),
+                          VAL(SHORTREAL, 65.0), "It can be scaled.");
+      SDL_SetRenderScale(ren, VAL(SHORTREAL, 1.0), VAL(SHORTREAL, 1.0));
+      SDL_RenderDebugText(ren, VAL(SHORTREAL, 64.0),
+                          VAL(SHORTREAL, 350.0),
+                          "This only does ASCII chars.");
+
+      secs := (SDL_GetTicks() - start) DIV 1000;
+      pos := AppendStr(msg, 0, "(This program has been running for ");
+      pos := AppendCard(msg, pos, secs);
+      pos := AppendStr(msg, pos, " seconds.)");
+      SDL_RenderDebugText(ren, VAL(SHORTREAL, 136.0),
+                          VAL(SHORTREAL, 400.0), msg);
+
+      SDL_RenderPresent(ren);
+      IF SDL_GetTicks() - start > MaxMs THEN quit := TRUE END;
+      SDL_Delay(FrameMs)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_debug_text: bye\n")
+END showcase_debug_text.

+ 125 - 0
showcases/showcase_lines.mod

@@ -0,0 +1,125 @@
+MODULE showcase_lines ;
+
+(*
+   m2SDL3 showcase, ported from SDL upstream
+   examples/renderer/03-lines/lines.c (public domain).
+
+   Draws brown house-like line segments, a green polyline batch
+   and a 360-line multicolor circle every frame.
+
+   Porting notes
+   - SDL_RenderLine/RenderLines take SHORTREAL coordinates;
+     REAL math (cos/sin) converts via VAL(SHORTREAL, ...).
+   - SDL_rand(256) becomes libc rand() MOD 256.
+   - Callback structure flattened into a main loop; auto-quit
+     after 8 s so the showcase also terminates unattended.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox,
+   SDL_RenderLine, SDL_RenderLines;
+FROM SDL3Rect IMPORT SDLFPoint, MakeFPoint;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM MathLib0 IMPORT cos, sin;
+FROM libc IMPORT printf, rand, srand;
+
+CONST FrameMs = 16; MaxMs = 8000;
+CONST WinW = 640; WinH = 480;
+CONST Pi = 3.141592653589793;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   linePts: ARRAY [0..8] OF SDLFPoint;
+   quit: BOOLEAN;
+   start: LONGCARD;
+   i: INTEGER;
+   ang: REAL;
+   cx, cy, ex, ey: SHORTREAL;
+
+PROCEDURE F (v: REAL) : SHORTREAL;
+BEGIN
+   RETURN VAL(SHORTREAL, v)
+END F;
+
+PROCEDURE Col () : CARDINAL8;
+BEGIN
+   RETURN VAL(CARDINAL8, rand() MOD 256)
+END Col;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("showcase_lines: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 lines",
+                                      WinW, WinH,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_lines: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, WinW, WinH, LogicalLetterbox);
+   srand(VAL(INTEGER, SDL_GetTicks()));
+
+   linePts[0] := MakeFPoint(F(100.0), F(354.0));
+   linePts[1] := MakeFPoint(F(220.0), F(230.0));
+   linePts[2] := MakeFPoint(F(140.0), F(230.0));
+   linePts[3] := MakeFPoint(F(320.0), F(100.0));
+   linePts[4] := MakeFPoint(F(500.0), F(230.0));
+   linePts[5] := MakeFPoint(F(420.0), F(230.0));
+   linePts[6] := MakeFPoint(F(540.0), F(354.0));
+   linePts[7] := MakeFPoint(F(400.0), F(354.0));
+   linePts[8] := MakeFPoint(F(100.0), F(354.0));
+
+   printf("showcase_lines: close window or wait 8s\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 100), VAL(CARDINAL8, 100),
+                             VAL(CARDINAL8, 100), VAL(CARDINAL8, 255));
+      SDL_RenderClear(ren);
+
+      (* brown house segments, one call per line. *)
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 127), VAL(CARDINAL8, 49),
+                             VAL(CARDINAL8, 32), VAL(CARDINAL8, 255));
+      SDL_RenderLine(ren, F(240.0), F(450.0), F(400.0), F(450.0));
+      SDL_RenderLine(ren, F(240.0), F(356.0), F(400.0), F(356.0));
+      SDL_RenderLine(ren, F(240.0), F(356.0), F(240.0), F(450.0));
+      SDL_RenderLine(ren, F(400.0), F(356.0), F(400.0), F(450.0));
+
+      (* green polyline batch. *)
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderLines(ren, ADR(linePts), 9);
+
+      (* multicolor circle around (320, 80), radius 30. *)
+      cx := F(320.0); cy := F(80.0);
+      FOR i := 0 TO 359 DO
+         ang := VAL(REAL, i) * (Pi / 180.0);
+         ex := F(320.0 + cos(ang) * 30.0);
+         ey := F(80.0 + sin(ang) * 30.0);
+         SDL_SetRenderDrawColor(ren, Col(), Col(), Col(),
+                                VAL(CARDINAL8, 255));
+         SDL_RenderLine(ren, cx, cy, ex, ey)
+      END;
+
+      SDL_RenderPresent(ren);
+      IF SDL_GetTicks() - start > MaxMs THEN quit := TRUE END;
+      SDL_Delay(FrameMs)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_lines: bye\n")
+END showcase_lines.

+ 124 - 0
showcases/showcase_points.mod

@@ -0,0 +1,124 @@
+MODULE showcase_points ;
+
+(*
+   m2SDL3 showcase, ported from SDL upstream
+   examples/renderer/04-points/points.c (public domain).
+
+   500 white points drift down-right at 30..60 px/s; points that
+   leave the screen respawn at the top or left edge.
+
+   Porting notes
+   - SDL_randf() becomes (rand() MOD 10000) / 10000.0 with libc rand.
+   - Positions/speeds live in parallel arrays so the whole set goes
+     to SDL_RenderPoints in one call, as in the C version.
+   - Callback structure flattened into a main loop; auto-quit
+     after 8 s so the showcase also terminates unattended.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox,
+   SDL_RenderPoints;
+FROM SDL3Rect IMPORT SDLFPoint, MakeFPoint;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM libc IMPORT printf, rand, srand;
+
+CONST FrameMs = 16; MaxMs = 8000;
+CONST WinW = 640; WinH = 480; NPoints = 500;
+CONST MinSpeed = 30.0; MaxSpeed = 60.0;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   points: ARRAY [0..NPoints-1] OF SDLFPoint;
+   speeds: ARRAY [0..NPoints-1] OF REAL;
+   quit: BOOLEAN;
+   start, last, now: LONGCARD;
+   elapsed: REAL;
+   i: INTEGER;
+
+(* Random REAL in [0.0, 1.0). *)
+PROCEDURE RandF () : REAL;
+BEGIN
+   RETURN VAL(REAL, rand() MOD 10000) / 10000.0
+END RandF;
+
+PROCEDURE RandSpeed () : REAL;
+BEGIN
+   RETURN MinSpeed + RandF() * (MaxSpeed - MinSpeed)
+END RandSpeed;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("showcase_points: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 points",
+                                      WinW, WinH,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_points: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, WinW, WinH, LogicalLetterbox);
+
+   srand(VAL(INTEGER, SDL_GetTicks()));
+   FOR i := 0 TO NPoints - 1 DO
+      points[i] := MakeFPoint(VAL(SHORTREAL, RandF() * VAL(REAL, WinW)),
+                              VAL(SHORTREAL, RandF() * VAL(REAL, WinH)));
+      speeds[i] := RandSpeed()
+   END;
+
+   printf("showcase_points: close window or wait 8s\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   last := start;
+   WHILE NOT quit DO
+      DrainEvents(quit);
+      now := SDL_GetTicks();
+      elapsed := VAL(REAL, now - last) / 1000.0;
+      last := now;
+
+      FOR i := 0 TO NPoints - 1 DO
+         points[i].x := VAL(SHORTREAL,
+                            VAL(REAL, points[i].x)
+                            + elapsed * speeds[i]);
+         points[i].y := VAL(SHORTREAL,
+                            VAL(REAL, points[i].y)
+                            + elapsed * speeds[i]);
+         IF (VAL(REAL, points[i].x) >= VAL(REAL, WinW))
+            OR (VAL(REAL, points[i].y) >= VAL(REAL, WinH)) THEN
+            IF rand() MOD 2 = 0 THEN
+               points[i].x := VAL(SHORTREAL, RandF() * VAL(REAL, WinW));
+               points[i].y := VAL(SHORTREAL, 0.0)
+            ELSE
+               points[i].x := VAL(SHORTREAL, 0.0);
+               points[i].y := VAL(SHORTREAL, RandF() * VAL(REAL, WinH))
+            END;
+            speeds[i] := RandSpeed()
+         END
+      END;
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderClear(ren);
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+      SDL_RenderPoints(ren, ADR(points), NPoints);
+      SDL_RenderPresent(ren);
+
+      IF now - start > MaxMs THEN quit := TRUE END;
+      SDL_Delay(FrameMs)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_points: bye\n")
+END showcase_points.

+ 109 - 0
showcases/showcase_primitives.mod

@@ -0,0 +1,109 @@
+MODULE showcase_primitives ;
+
+(*
+   m2SDL3 showcase, ported from SDL upstream
+   examples/renderer/02-primitives/primitives.c (public domain).
+
+   Draws a filled rectangle, 500 random points, an inset outline
+   and a yellow X across the canvas every frame.
+
+   SDL3 native form:
+   - SDL_RenderRect / SDL_RenderPoints / SDL_RenderFillRect take
+     SDL_FRect / SDL_FPoint records with SHORTREAL fields.
+   - Point array passes as ADR(points) + count (see SDL3Render).
+   - libc rand seeded from SDL_GetTicks replaces SDL_randf.
+   - Callback structure flattened into a main loop; auto-quit
+     after 8 s so the showcase also terminates unattended.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox,
+   SDL_RenderLine, SDL_RenderRect, SDL_RenderPoints,
+   SDL_RenderFillRect;
+FROM SDL3Rect IMPORT SDLFRect, SDLFPoint, MakeFRect, MakeFPoint;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM libc IMPORT printf, rand, srand;
+
+CONST FrameMs = 16; MaxMs = 8000;
+CONST WinW = 640; WinH = 480; NPoints = 500;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   points: ARRAY [0..NPoints-1] OF SDLFPoint;
+   rect: SDLFRect;
+   quit: BOOLEAN;
+   start: LONGCARD;
+   i: INTEGER;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("showcase_primitives: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 primitives",
+                                      WinW, WinH,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_primitives: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, WinW, WinH, LogicalLetterbox);
+
+   (* scatter the points inside the blue rectangle, as in the C code. *)
+   srand(VAL(INTEGER, SDL_GetTicks()));
+   FOR i := 0 TO NPoints - 1 DO
+      points[i] := MakeFPoint(VAL(SHORTREAL, VAL(REAL, rand() MOD 440 + 100)),
+                              VAL(SHORTREAL, VAL(REAL, rand() MOD 280 + 100)))
+   END;
+
+   printf("showcase_primitives: close window or wait 8s\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 33), VAL(CARDINAL8, 33),
+                             VAL(CARDINAL8, 33), VAL(CARDINAL8, 255));
+      SDL_RenderClear(ren);
+
+      rect := MakeFRect(VAL(SHORTREAL, 100.0), VAL(SHORTREAL, 100.0),
+                        VAL(SHORTREAL, 440.0), VAL(SHORTREAL, 280.0));
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+      SDL_RenderFillRect(ren, rect);
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderPoints(ren, ADR(points), NPoints);
+
+      rect := MakeFRect(VAL(SHORTREAL, 130.0), VAL(SHORTREAL, 130.0),
+                        VAL(SHORTREAL, 380.0), VAL(SHORTREAL, 220.0));
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderRect(ren, rect);
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderLine(ren, VAL(SHORTREAL, 0.0), VAL(SHORTREAL, 0.0),
+                     VAL(SHORTREAL, VAL(REAL, WinW)), VAL(SHORTREAL, VAL(REAL, WinH)));
+      SDL_RenderLine(ren, VAL(SHORTREAL, 0.0), VAL(SHORTREAL, VAL(REAL, WinH)),
+                     VAL(SHORTREAL, VAL(REAL, WinW)), VAL(SHORTREAL, 0.0));
+
+      SDL_RenderPresent(ren);
+      IF SDL_GetTicks() - start > MaxMs THEN quit := TRUE END;
+      SDL_Delay(FrameMs)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_primitives: bye\n")
+END showcase_primitives.

+ 126 - 0
showcases/showcase_rectangles.mod

@@ -0,0 +1,126 @@
+MODULE showcase_rectangles ;
+
+(*
+   m2SDL3 showcase, ported from SDL upstream
+   examples/renderer/05-rectangles/rectangles.c (public domain).
+
+   Draws animated rectangle outlines, a filled rectangle and a
+   bar chart every frame; sizes breathe in and out over 2 seconds.
+
+   SDL3 native form:
+   - SDL_RenderRect/Rects/FillRect/FillRects take SDL_FRect
+     (SHORTREAL fields); integer sizes are converted with
+     VAL(SHORTREAL, VAL(REAL, ...)).
+   - Rect arrays pass as ADR(rects) + count (see SDL3Render).
+   - Callback structure flattened into a main loop; auto-quit
+     after 8 s so the showcase also terminates unattended.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox,
+   SDL_RenderRect, SDL_RenderRects,
+   SDL_RenderFillRect, SDL_RenderFillRects;
+FROM SDL3Rect IMPORT SDLFRect, MakeFRect;
+FROM SDL3Utils IMPORT DrainEvents;
+FROM libc IMPORT printf;
+
+CONST FrameMs = 16; MaxMs = 8000;
+CONST WinW = 640; WinH = 480;
+
+VAR
+   win: WindowHandle;
+   ren: RendererHandle;
+   rects: ARRAY [0..15] OF SDLFRect;
+   quit: BOOLEAN;
+   start: LONGCARD;
+   now: LONGCARD;
+   dir, sm, i, size, w, h: INTEGER;
+
+PROCEDURE F (v: INTEGER) : SHORTREAL;
+BEGIN
+   RETURN VAL(SHORTREAL, VAL(REAL, v))
+END F;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("showcase_rectangles: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 rectangles",
+                                      WinW, WinH,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_rectangles: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, WinW, WinH, LogicalLetterbox);
+
+   printf("showcase_rectangles: close window or wait 8s\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   WHILE NOT quit DO
+      DrainEvents(quit);
+      now := SDL_GetTicks();
+      (* breathe: +1 for one second, -1 for the next. *)
+      IF VAL(INTEGER, now MOD 2000) >= 1000 THEN dir := 1 ELSE dir := -1 END;
+      (* scale in permille, -500..500. *)
+      sm := (VAL(INTEGER, now MOD 1000) - 500) * dir;
+
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderClear(ren);
+
+      (* single outlined square, top left. *)
+      rects[0] := MakeFRect(F(100), F(100),
+                            F(100 + 100 * sm DIV 500),
+                            F(100 + 100 * sm DIV 500));
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderRect(ren, rects[0]);
+
+      (* three centered outlines at once. *)
+      FOR i := 0 TO 2 DO
+         size := (i + 1) * 50;
+         w := size + size * sm DIV 500;
+         rects[i] := MakeFRect(F((WinW - w) DIV 2), F((WinH - w) DIV 2),
+                               F(w), F(w))
+      END;
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+      SDL_RenderRects(ren, ADR(rects), 3);
+
+      (* one filled rectangle. *)
+      rects[0] := MakeFRect(F(400), F(50),
+                            F(100 + 100 * sm DIV 500),
+                            F(50 + 50 * sm DIV 500));
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 0),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+      SDL_RenderFillRect(ren, rects[0]);
+
+      (* white bar chart along the bottom. *)
+      FOR i := 0 TO 15 DO
+         w := WinW DIV 16;
+         h := i * 8;
+         rects[i] := MakeFRect(F(i * w), F(WinH - h), F(w), F(h))
+      END;
+      SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 255),
+                             VAL(CARDINAL8, 255), VAL(CARDINAL8, 255));
+      SDL_RenderFillRects(ren, ADR(rects), 16);
+
+      SDL_RenderPresent(ren);
+      IF SDL_GetTicks() - start > MaxMs THEN quit := TRUE END;
+      SDL_Delay(FrameMs)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_rectangles: bye\n")
+END showcase_rectangles.

+ 260 - 0
showcases/showcase_snake.mod

@@ -0,0 +1,260 @@
+MODULE showcase_snake ;
+
+(*
+   m2SDL3 demo, ported from SDL upstream examples/demo/01-snake
+   (public domain): the classic Snake game on a 24x18 board.
+
+   Arrows steer, R restarts, Q or Escape quits. The board packs
+   each cell into 3 bits (7 = 162 bytes); collisions restart.
+
+   SDL3 native notes
+   - SDL_FRect (SHORTREAL) rects via MakeFRect; Uint64 ms ticks
+     come from SDL_GetTicks (64 bit in SDL3).
+   - Keyboard arrives decoded via SDL3Utils.PollM2Event (type at
+     offset 0, keycode at offset 28); keycodes in SDL3Key.
+   - Window close buttons decode as evQuit too.
+   - Callback structure flattened into a main loop with a 30 s cap
+     so the demo also terminates unattended.
+*)
+
+FROM SYSTEM IMPORT CARDINAL8;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks, SDLInitVideo, SDLInitEvents;
+FROM SDL3Video IMPORT WindowHandle, SDL_DestroyWindow, WindowResizable;
+FROM SDL3Render IMPORT RendererHandle,
+   SDL_CreateWindowAndRenderer, SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_RenderClear, SDL_RenderPresent,
+   SDL_SetRenderLogicalPresentation, LogicalLetterbox,
+   SDL_RenderFillRect;
+FROM SDL3Rect IMPORT SDLFRect, MakeFRect;
+FROM SDL3Utils IMPORT PollM2Event, M2Event, evQuit, evKey;
+FROM SDL3Key IMPORT KeyEscape, KeyQ, KeyR,
+   KeyRight, KeyLeft, KeyDown, KeyUp;
+FROM libc IMPORT printf, rand, srand;
+
+CONST StepMs = 125; BlockPx = 24; GW = 24; GH = 18; MaxMs = 30000;
+CONST WinW = GW * BlockPx; WinH = GH * BlockPx;
+CONST CellNothing = 0; CellRight = 1; CellUp = 2;
+CONST CellLeft = 3; CellDown = 4; CellFood = 5;
+CONST DirRight = 0; DirUp = 1; DirLeft = 2; DirDown = 3;
+
+VAR
+   cells: ARRAY [0..161] OF CARDINAL8;
+   pow2: ARRAY [0..7] OF CARDINAL;
+   headX, headY, tailX, tailY, nextDir, inhibit: INTEGER;
+   occupied: CARDINAL;
+   win: WindowHandle;
+   ren: RendererHandle;
+
+PROCEDURE CellAt (x, y: INTEGER) : CARDINAL;
+VAR shift, byte, adj, range: CARDINAL;
+BEGIN
+   shift := VAL(CARDINAL, x + y * GW) * 3;
+   byte := shift DIV 8;
+   adj := shift MOD 8;
+   range := VAL(CARDINAL, cells[byte])
+          + VAL(CARDINAL, cells[byte+1]) * 256;
+   RETURN range DIV pow2[adj] MOD 8
+END CellAt;
+
+PROCEDURE PutCell (x, y: INTEGER; ct: CARDINAL);
+VAR shift, byte, adj, range, p: CARDINAL;
+BEGIN
+   shift := VAL(CARDINAL, x + y * GW) * 3;
+   byte := shift DIV 8;
+   adj := shift MOD 8;
+   range := VAL(CARDINAL, cells[byte])
+          + VAL(CARDINAL, cells[byte+1]) * 256;
+   p := pow2[adj];
+   range := range DIV (p * 8) * (p * 8) + range MOD p + ct * p;
+   cells[byte] := VAL(CARDINAL8, range MOD 256);
+   cells[byte+1] := VAL(CARDINAL8, range DIV 256)
+END PutCell;
+
+PROCEDURE NewFood;
+VAR x, y: INTEGER;
+BEGIN
+   LOOP
+      x := rand() MOD GW;
+      y := rand() MOD GH;
+      IF CellAt(x, y) = CellNothing THEN
+         PutCell(x, y, CellFood);
+         EXIT
+      END
+   END
+END NewFood;
+
+PROCEDURE SnakeInit;
+VAR i: INTEGER;
+BEGIN
+   FOR i := 0 TO 161 DO cells[i] := VAL(CARDINAL8, 0) END;
+   headX := GW DIV 2; tailX := headX;
+   headY := GH DIV 2; tailY := headY;
+   nextDir := DirRight;
+   inhibit := 4;
+   occupied := 4; DEC(occupied);
+   PutCell(tailX, tailY, CellRight);
+   FOR i := 0 TO 3 DO NewFood; INC(occupied) END
+END SnakeInit;
+
+PROCEDURE SnakeRedir (dir: INTEGER);
+VAR ct: CARDINAL;
+BEGIN
+   ct := CellAt(headX, headY);
+   IF ((dir = DirRight) AND (ct # CellLeft))
+      OR ((dir = DirUp) AND (ct # CellDown))
+      OR ((dir = DirLeft) AND (ct # CellRight))
+      OR ((dir = DirDown) AND (ct # CellUp)) THEN
+      nextDir := dir
+   END
+END SnakeRedir;
+
+PROCEDURE Wrap (VAR v: INTEGER; max: INTEGER);
+BEGIN
+   IF v < 0 THEN v := max - 1
+   ELSIF v > max - 1 THEN v := 0 END
+END Wrap;
+
+PROCEDURE SnakeStep;
+VAR dirCell, ct: CARDINAL; px, py: INTEGER;
+BEGIN
+   dirCell := VAL(CARDINAL, nextDir + 1);
+   DEC(inhibit);
+   IF inhibit = 0 THEN
+      INC(inhibit);
+      ct := CellAt(tailX, tailY);
+      PutCell(tailX, tailY, CellNothing);
+      IF ct = CellRight THEN INC(tailX)
+      ELSIF ct = CellUp THEN DEC(tailY)
+      ELSIF ct = CellLeft THEN DEC(tailX)
+      ELSIF ct = CellDown THEN INC(tailY) END;
+      Wrap(tailX, GW);
+      Wrap(tailY, GH)
+   END;
+   px := headX; py := headY;
+   IF nextDir = DirRight THEN INC(headX)
+   ELSIF nextDir = DirUp THEN DEC(headY)
+   ELSIF nextDir = DirLeft THEN DEC(headX)
+   ELSIF nextDir = DirDown THEN INC(headY) END;
+   Wrap(headX, GW);
+   Wrap(headY, GH);
+   ct := CellAt(headX, headY);
+   IF (ct # CellNothing) AND (ct # CellFood) THEN
+      SnakeInit;
+      RETURN
+   END;
+   PutCell(px, py, dirCell);
+   PutCell(headX, headY, dirCell);
+   IF ct = CellFood THEN
+      IF occupied = VAL(CARDINAL, GW * GH) THEN
+         SnakeInit;
+         RETURN
+      END;
+      NewFood;
+      INC(inhibit);
+      INC(occupied)
+   END
+END SnakeStep;
+
+PROCEDURE HandleSym (sym: INTEGER; VAR quit: BOOLEAN);
+BEGIN
+   IF sym = KeyEscape THEN quit := TRUE
+   ELSIF sym = KeyQ THEN quit := TRUE
+   ELSIF sym = KeyR THEN SnakeInit
+   ELSIF sym = KeyRight THEN SnakeRedir(DirRight)
+   ELSIF sym = KeyUp THEN SnakeRedir(DirUp)
+   ELSIF sym = KeyLeft THEN SnakeRedir(DirLeft)
+   ELSIF sym = KeyDown THEN SnakeRedir(DirDown) END
+END HandleSym;
+
+PROCEDURE DrawBoard;
+VAR gx, gy: INTEGER; ct: CARDINAL; r: SDLFRect;
+BEGIN
+   SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0), VAL(CARDINAL8, 0),
+                          VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+   SDL_RenderClear(ren);
+   FOR gy := 0 TO GH - 1 DO
+      FOR gx := 0 TO GW - 1 DO
+         ct := CellAt(gx, gy);
+         IF ct # CellNothing THEN
+            r := MakeFRect(VAL(SHORTREAL, VAL(REAL, gx * BlockPx)),
+                           VAL(SHORTREAL, VAL(REAL, gy * BlockPx)),
+                           VAL(SHORTREAL, VAL(REAL, BlockPx)),
+                           VAL(SHORTREAL, VAL(REAL, BlockPx)));
+            IF ct = CellFood THEN
+               SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 80),
+                                      VAL(CARDINAL8, 80),
+                                      VAL(CARDINAL8, 255),
+                                      VAL(CARDINAL8, 255))
+            ELSE
+               SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 0),
+                                      VAL(CARDINAL8, 128),
+                                      VAL(CARDINAL8, 0),
+                                      VAL(CARDINAL8, 255))
+            END;
+            SDL_RenderFillRect(ren, r)
+         END
+      END
+   END;
+   r := MakeFRect(VAL(SHORTREAL, VAL(REAL, headX * BlockPx)),
+                  VAL(SHORTREAL, VAL(REAL, headY * BlockPx)),
+                  VAL(SHORTREAL, VAL(REAL, BlockPx)),
+                  VAL(SHORTREAL, VAL(REAL, BlockPx)));
+   SDL_SetRenderDrawColor(ren, VAL(CARDINAL8, 255), VAL(CARDINAL8, 255),
+                          VAL(CARDINAL8, 0), VAL(CARDINAL8, 255));
+   SDL_RenderFillRect(ren, r);
+   SDL_RenderPresent(ren)
+END DrawBoard;
+
+VAR
+   ev: M2Event;
+   quit: BOOLEAN;
+   start, now, lastStep: LONGCARD;
+   i: INTEGER;
+
+BEGIN
+   FOR i := 0 TO 7 DO
+      IF i = 0 THEN pow2[0] := 1 ELSE pow2[i] := pow2[i-1] * 2 END
+   END;
+   srand(VAL(INTEGER, SDL_GetTicks()));
+   IF NOT SDL_Init(SDLInitVideo + SDLInitEvents) THEN
+      printf("showcase_snake: SDL_Init failed\n");
+      HALT(1)
+   END;
+   IF NOT SDL_CreateWindowAndRenderer("m2SDL3 snake",
+                                      WinW, WinH,
+                                      VAL(LONGCARD, WindowResizable),
+                                      win, ren) THEN
+      printf("showcase_snake: CreateWindowAndRenderer failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_SetRenderLogicalPresentation(ren, WinW, WinH, LogicalLetterbox);
+
+   SnakeInit;
+   printf("showcase_snake: arrows steer, R restarts, Q quits\n");
+   quit := FALSE;
+   start := SDL_GetTicks();
+   lastStep := start;
+   WHILE NOT quit DO
+      WHILE PollM2Event(ev) DO
+         IF ev.kind = evQuit THEN quit := TRUE
+         ELSIF (ev.kind = evKey) AND ev.pressed THEN
+            HandleSym(ev.sym, quit)
+         END
+      END;
+      now := SDL_GetTicks();
+      WHILE now - lastStep >= VAL(LONGCARD, StepMs) DO
+         SnakeStep;
+         lastStep := lastStep + VAL(LONGCARD, StepMs)
+      END;
+      DrawBoard;
+      IF now - start > VAL(LONGCARD, MaxMs) THEN quit := TRUE END;
+      SDL_Delay(16)
+   END;
+
+   SDL_DestroyRenderer(ren);
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("showcase_snake: bye\n")
+END showcase_snake.

+ 104 - 0
src/SDL3.def

@@ -0,0 +1,104 @@
+DEFINITION MODULE FOR "C" SDL3 ;
+
+(*
+   m2SDL3 - core SDL3 subsystem control, error handling and timing.
+   GNU Modula-2 binding, mapped onto <SDL3/SDL_init.h>,
+   <SDL3/SDL_error.h> and <SDL3/SDL_timer.h>.
+
+   SDL3 differences from SDL2 (see README.md)
+   ------------------------------------------
+   * Init/timer/error functions return C bool, hence BOOLEAN here
+     (gm2 BOOLEAN <-> C bool; see "Elementary data types" node).
+     Check success with "IF NOT SDL_Init(...) THEN" or "= FALSE".
+   * SDL_INIT_TIMER and SDL_INIT_EVERYTHING are gone in SDL3;
+     SDL_INIT_GAMECONTROLLER is renamed SDL_INIT_GAMEPAD and
+     SDL_INIT_CAMERA is new. SDLInitEverything below is a
+     convenience OR of all SDL3 flags, not an SDL macro.
+   * SDL_GetTicks now returns Uint64 milliseconds (LONGCARD);
+     the 32-bit SDL_GetTicks / 64-bit SDL_GetTicks64 split is gone.
+     SDL_GetTicksNS / SDL_DelayNS are new.
+
+   Mapping notes
+   -------------
+   * CARDINAL  <-> Uint32 / SDL_InitFlags
+   * LONGCARD  <-> Uint64
+   * INTEGER   <-> int
+   * ADDRESS   <-> opaque pointers (const char * from SDL_GetError)
+   * ARRAY OF CHAR parameters map onto (char * ) - pass string literals
+     or CHAR arrays directly (GM2 "Interface to C" node).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   (* --- init flags (SDL_INIT_* from SDL_init.h) --- *)
+   SDLInitAudio, SDLInitVideo,
+   SDLInitJoystick, SDLInitHaptic, SDLInitGamepad,
+   SDLInitEvents, SDLInitSensor, SDLInitCamera,
+   SDLInitEverything,
+
+   (* --- core --- *)
+   SDL_Init, SDL_InitSubSystem, SDL_QuitSubSystem,
+   SDL_WasInit, SDL_Quit,
+
+   (* --- errors --- *)
+   SDL_GetError, SDL_ClearError,
+
+   (* --- timing --- *)
+   SDL_Delay, SDL_DelayNS, SDL_GetTicks, SDL_GetTicksNS,
+   SDL_GetPerformanceCounter, SDL_GetPerformanceFrequency;
+
+
+CONST
+   SDLInitAudio    = 000000010H;  (* 0x00000010 *)
+   SDLInitVideo    = 000000020H;  (* 0x00000020 *)
+   SDLInitJoystick = 000000200H;  (* 0x00000200 *)
+   SDLInitHaptic   = 000001000H;  (* 0x00001000 *)
+   SDLInitGamepad  = 000002000H;  (* 0x00002000, was GAMECONTROLLER *)
+   SDLInitEvents   = 000004000H;  (* 0x00004000 *)
+   SDLInitSensor   = 000008000H;  (* 0x00008000 *)
+   SDLInitCamera   = 000010000H;  (* 0x00010000, new in SDL3 *)
+   (* Convenience OR of all flags above; not an SDL macro. *)
+   SDLInitEverything = 00001F230H;
+
+
+(* bool SDL_Init(SDL_InitFlags flags) *)
+PROCEDURE SDL_Init (flags: CARDINAL) : [ BOOLEAN ];
+
+(* bool SDL_InitSubSystem(SDL_InitFlags flags) *)
+PROCEDURE SDL_InitSubSystem (flags: CARDINAL) : [ BOOLEAN ];
+
+(* void SDL_QuitSubSystem(SDL_InitFlags flags) *)
+PROCEDURE SDL_QuitSubSystem (flags: CARDINAL);
+
+(* SDL_InitFlags SDL_WasInit(SDL_InitFlags flags) *)
+PROCEDURE SDL_WasInit (flags: CARDINAL) : CARDINAL;
+
+(* void SDL_Quit(void) *)
+PROCEDURE SDL_Quit ();
+
+(* const char *SDL_GetError(void) *)
+PROCEDURE SDL_GetError () : ADDRESS;
+
+(* void SDL_ClearError(void) *)
+PROCEDURE SDL_ClearError ();
+
+(* void SDL_Delay(Uint32 ms) *)
+PROCEDURE SDL_Delay (ms: CARDINAL);
+
+(* void SDL_DelayNS(Uint64 ns) *)
+PROCEDURE SDL_DelayNS (ns: LONGCARD);
+
+(* Uint64 SDL_GetTicks(void) - milliseconds *)
+PROCEDURE SDL_GetTicks () : LONGCARD;
+
+(* Uint64 SDL_GetTicksNS(void) - nanoseconds *)
+PROCEDURE SDL_GetTicksNS () : LONGCARD;
+
+(* Uint64 SDL_GetPerformanceCounter(void) *)
+PROCEDURE SDL_GetPerformanceCounter () : LONGCARD;
+
+(* Uint64 SDL_GetPerformanceFrequency(void) *)
+PROCEDURE SDL_GetPerformanceFrequency () : LONGCARD;
+
+END SDL3.

+ 72 - 0
src/SDL3Events.def

@@ -0,0 +1,72 @@
+DEFINITION MODULE FOR "C" SDL3Events ;
+
+(*
+   m2SDL3 - event pump, minimal starter subset (<SDL3/SDL_events.h>).
+
+   SDL3 differences from SDL2
+   --------------------------
+   * Event type names gain the SDL_EVENT_ infix, but the values used
+     here are unchanged: QUIT = 0100H, KEY_DOWN = 0300H,
+     KEY_UP = 0301H, MOUSE_BUTTON_DOWN = 0401H, UP = 0402H.
+   * Window close buttons now produce SDL_EVENT_WINDOW_CLOSE_REQUESTED
+     (0210H); SDL3Utils treats it like QUIT. Always handle both.
+   * SDL_Event grows from 56 to 128 bytes; EventBufferBytes tracks it.
+   * Poll/Wait/Push/HasEvent(s) return C bool, hence BOOLEAN here.
+     SDL_PollEvent returns TRUE while events remain (loop on it);
+     SDL_HasEvent(s) report presence without removing.
+
+   Layout note: the Uint32 type sits at offset 0 of every event.
+   Key events (SDL_KeyboardEvent) carry the SDL_Keycode at byte
+   offset 28 and the bool down flag at byte 36; SDL3Utils.PollM2Event
+   decodes exactly those two fields (verified with offsetof).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   EventQuit, EventTerminating,
+   EventWindowCloseRequested,
+   EventKeyDown, EventKeyUp,
+   EventMouseButtonDown, EventMouseButtonUp,
+   EventBufferBytes,
+   SDL_PumpEvents, SDL_PollEvent, SDL_WaitEvent,
+   SDL_WaitEventTimeout, SDL_PushEvent,
+   SDL_HasEvent, SDL_HasEvents;
+
+
+CONST
+   EventQuit                 = 100H;
+   EventTerminating          = 101H;
+   EventWindowCloseRequested = 210H;
+   EventKeyDown              = 300H;
+   EventKeyUp                = 301H;
+   EventMouseButtonDown      = 401H;
+   EventMouseButtonUp        = 402H;
+
+   (* Size of SDL_Event in SDL 3.4: 128 bytes. *)
+   EventBufferBytes = 128;
+
+
+(* void SDL_PumpEvents(void) *)
+PROCEDURE SDL_PumpEvents ();
+
+(* bool SDL_PollEvent(SDL_Event *event) - pass ADR(buf) *)
+PROCEDURE SDL_PollEvent (ev: ADDRESS) : [ BOOLEAN ];
+
+(* bool SDL_WaitEvent(SDL_Event *event) *)
+PROCEDURE SDL_WaitEvent (ev: ADDRESS) : [ BOOLEAN ];
+
+(* bool SDL_WaitEventTimeout(SDL_Event *event, Sint32 timeoutMS) *)
+PROCEDURE SDL_WaitEventTimeout (ev: ADDRESS;
+                                timeoutMS: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_PushEvent(SDL_Event *event) - pass ADR(buf) *)
+PROCEDURE SDL_PushEvent (ev: ADDRESS) : [ BOOLEAN ];
+
+(* bool SDL_HasEvent(Uint32 type) *)
+PROCEDURE SDL_HasEvent (type: CARDINAL) : [ BOOLEAN ];
+
+(* bool SDL_HasEvents(Uint32 minType, Uint32 maxType) *)
+PROCEDURE SDL_HasEvents (minType, maxType: CARDINAL) : [ BOOLEAN ];
+
+END SDL3Events.

+ 28 - 0
src/SDL3Key.def

@@ -0,0 +1,28 @@
+DEFINITION MODULE SDL3Key ;
+
+(*
+   m2SDL3 - keycode constants (SDL_Keycode from <SDL3/SDL_keycode.h>).
+
+   Pure constants, no C functions: printable keys equal their ASCII
+   code, arrows are SCANCODE_TO_KEYCODE values (bit 30 set), e.g.
+   KeyRight = 4000004FH = 1073741903. Values are unchanged from SDL2.
+*)
+
+EXPORT UNQUALIFIED
+   KeyEscape, KeyReturn, KeySpace, KeyQ, KeyR,
+   KeyRight, KeyLeft, KeyDown, KeyUp;
+
+
+CONST
+   KeyEscape = 27;
+   KeyReturn = 13;
+   KeySpace  = 32;
+   KeyQ      = 113;   (* "q" *)
+   KeyR      = 114;   (* "r" *)
+
+   KeyRight = 4000004FH;
+   KeyLeft  = 40000050H;
+   KeyDown  = 40000051H;
+   KeyUp    = 40000052H;
+
+END SDL3Key.

+ 5 - 0
src/SDL3Key.mod

@@ -0,0 +1,5 @@
+IMPLEMENTATION MODULE SDL3Key ;
+
+(* Constants only; nothing to initialize. *)
+
+END SDL3Key.

+ 44 - 0
src/SDL3Rect.def

@@ -0,0 +1,44 @@
+DEFINITION MODULE SDL3Rect ;
+
+(*
+   m2SDL3 - pure Modula-2 geometry records matching <SDL3/SDL_rect.h>.
+
+   C layout (integers are int <-> INTEGER, floats are float):
+     typedef struct SDL_Point  { int x, y; } SDL_Point;
+     typedef struct SDL_FPoint { float x, y; } SDL_FPoint;
+     typedef struct SDL_Rect   { int x, y, w, h; } SDL_Rect;
+     typedef struct SDL_FRect  { float x, y, w, h; } SDL_FRect;
+
+   gm2 REAL is C double, so the float fields must be SHORTREAL
+   (C float); this was verified against a C caller. Integer records
+   stay INTEGER and are layout-compatible with the C structs.
+   Pass single rects as VAR parameters (GM2 maps them onto T * );
+   pass arrays as ADR(rects) plus a count (see SDL3Render).
+*)
+
+EXPORT UNQUALIFIED
+   SDLPoint, SDLFPoint, SDLRect, SDLFRect,
+   MakePoint, MakeFPoint, MakeRect, MakeFRect,
+   PointEquals, FPointEquals, RectEquals, FRectEquals,
+   RectEmpty, FRectEmpty;
+
+
+TYPE
+   SDLPoint  = RECORD x, y: INTEGER END;
+   SDLFPoint = RECORD x, y: SHORTREAL END;
+   SDLRect   = RECORD x, y, w, h: INTEGER END;
+   SDLFRect  = RECORD x, y, w, h: SHORTREAL END;
+
+
+PROCEDURE MakePoint (x, y: INTEGER) : SDLPoint;
+PROCEDURE MakeFPoint (x, y: SHORTREAL) : SDLFPoint;
+PROCEDURE MakeRect (x, y, w, h: INTEGER) : SDLRect;
+PROCEDURE MakeFRect (x, y, w, h: SHORTREAL) : SDLFRect;
+PROCEDURE PointEquals (a, b: SDLPoint) : BOOLEAN;
+PROCEDURE FPointEquals (a, b: SDLFPoint) : BOOLEAN;
+PROCEDURE RectEquals (a, b: SDLRect) : BOOLEAN;
+PROCEDURE FRectEquals (a, b: SDLFRect) : BOOLEAN;
+PROCEDURE RectEmpty (VAR r: SDLRect) : BOOLEAN;
+PROCEDURE FRectEmpty (VAR r: SDLFRect) : BOOLEAN;
+
+END SDL3Rect.

+ 57 - 0
src/SDL3Rect.mod

@@ -0,0 +1,57 @@
+IMPLEMENTATION MODULE SDL3Rect ;
+
+PROCEDURE MakePoint (x, y: INTEGER) : SDLPoint;
+VAR p: SDLPoint;
+BEGIN
+   p.x := x; p.y := y; RETURN p
+END MakePoint;
+
+PROCEDURE MakeFPoint (x, y: SHORTREAL) : SDLFPoint;
+VAR p: SDLFPoint;
+BEGIN
+   p.x := x; p.y := y; RETURN p
+END MakeFPoint;
+
+PROCEDURE MakeRect (x, y, w, h: INTEGER) : SDLRect;
+VAR r: SDLRect;
+BEGIN
+   r.x := x; r.y := y; r.w := w; r.h := h; RETURN r
+END MakeRect;
+
+PROCEDURE MakeFRect (x, y, w, h: SHORTREAL) : SDLFRect;
+VAR r: SDLFRect;
+BEGIN
+   r.x := x; r.y := y; r.w := w; r.h := h; RETURN r
+END MakeFRect;
+
+PROCEDURE PointEquals (a, b: SDLPoint) : BOOLEAN;
+BEGIN
+   RETURN (a.x = b.x) AND (a.y = b.y)
+END PointEquals;
+
+PROCEDURE FPointEquals (a, b: SDLFPoint) : BOOLEAN;
+BEGIN
+   RETURN (a.x = b.x) AND (a.y = b.y)
+END FPointEquals;
+
+PROCEDURE RectEquals (a, b: SDLRect) : BOOLEAN;
+BEGIN
+   RETURN (a.x = b.x) AND (a.y = b.y) AND (a.w = b.w) AND (a.h = b.h)
+END RectEquals;
+
+PROCEDURE FRectEquals (a, b: SDLFRect) : BOOLEAN;
+BEGIN
+   RETURN (a.x = b.x) AND (a.y = b.y) AND (a.w = b.w) AND (a.h = b.h)
+END FRectEquals;
+
+PROCEDURE RectEmpty (VAR r: SDLRect) : BOOLEAN;
+BEGIN
+   RETURN (r.w <= 0) OR (r.h <= 0)
+END RectEmpty;
+
+PROCEDURE FRectEmpty (VAR r: SDLFRect) : BOOLEAN;
+BEGIN
+   RETURN (r.w <= SHORTREAL(0.0)) OR (r.h <= SHORTREAL(0.0))
+END FRectEmpty;
+
+END SDL3Rect.

+ 165 - 0
src/SDL3Render.def

@@ -0,0 +1,165 @@
+DEFINITION MODULE FOR "C" SDL3Render ;
+
+(*
+   m2SDL3 - 2D renderer binding (<SDL3/SDL_render.h>), starter subset.
+
+   SDL3 differences from SDL2
+   --------------------------
+   * SDL_CreateRenderer(window, name): the index/flags pair is gone;
+     pass NIL (default driver) or the ADDRESS of a NUL-terminated
+     name such as "software" / "opengl". There is no software /
+     accelerated / vsync flag word anymore.
+   * SDL_CreateWindowAndRenderer(title, w, h, window_flags, win^,
+     ren^) now takes a title and returns bool (BOOLEAN).
+   * Draw coordinates are float: RenderPoint/Line take SHORTREAL
+     (C float), rects are SDL_FRect (SHORTREAL fields, see SDL3Rect).
+     The old integer DrawPoint/DrawLine/DrawRect spellings are gone;
+     RenderRect/Rects/FillRect/FillRects are the only names.
+   * Every call returns bool (BOOLEAN) except Destroy; check
+     "= FALSE" or "NOT ..." on failure.
+   * Logical size becomes SDL_SetRenderLogicalPresentation with a
+     mode (Disabled/Stretch/Letterbox/Overscan/IntegerScale).
+
+   Opaque handle: SDL_Renderer * travels as ADDRESS (NIL == failure).
+   Single rects pass as VAR SDLFRect; arrays pass as ADDRESS of the
+   first element plus a count, e.g. ADR(rects) with
+   rects: ARRAY OF SDLFRect.
+   Color components are Uint8 in C, hence SYSTEM.CARDINAL8 here.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, CARDINAL8;
+FROM SDL3Rect IMPORT SDLFRect, SDLFPoint;
+FROM SDL3Video IMPORT WindowHandle;
+
+EXPORT UNQUALIFIED
+   RendererHandle,
+   LogicalDisabled, LogicalStretch, LogicalLetterbox,
+   LogicalOverscan, LogicalIntegerScale,
+
+   SDL_CreateRenderer, SDL_CreateWindowAndRenderer,
+   SDL_DestroyRenderer,
+   SDL_SetRenderDrawColor, SDL_GetRenderDrawColor,
+   SDL_SetRenderDrawColorFloat, SDL_GetRenderDrawColorFloat,
+   SDL_SetRenderLogicalPresentation,
+   SDL_RenderClear, SDL_RenderPresent,
+   SDL_RenderPoint, SDL_RenderPoints,
+   SDL_RenderLine, SDL_RenderLines,
+   SDL_RenderRect, SDL_RenderRects,
+   SDL_RenderFillRect, SDL_RenderFillRects,
+   SDL_SetRenderScale, SDL_GetCurrentRenderOutputSize,
+   DebugTextFontCharSize, SDL_RenderDebugText;
+
+
+TYPE
+   RendererHandle = ADDRESS;
+
+CONST
+   LogicalDisabled     = 0;
+   LogicalStretch      = 1;
+   LogicalLetterbox    = 2;
+   LogicalOverscan     = 3;
+   LogicalIntegerScale = 4;
+
+   (* Pixels per SDL_RenderDebugText character cell (8x8). *)
+   DebugTextFontCharSize = 8;
+
+
+(* SDL_Renderer *SDL_CreateRenderer(SDL_Window *win, const char *name)
+   - pass NIL for the default driver, or ADR("software"). *)
+PROCEDURE SDL_CreateRenderer (win: WindowHandle;
+                              name: ADDRESS) : RendererHandle;
+
+(* bool SDL_CreateWindowAndRenderer(const char *title, int w, int h,
+      SDL_WindowFlags flags, SDL_Window **win, SDL_Renderer **ren) *)
+PROCEDURE SDL_CreateWindowAndRenderer (title: ARRAY OF CHAR;
+                                       w, h: INTEGER; flags: LONGCARD;
+                                       VAR win: WindowHandle;
+                                       VAR ren: RendererHandle) : [ BOOLEAN ];
+
+(* void SDL_DestroyRenderer(SDL_Renderer *ren) *)
+PROCEDURE SDL_DestroyRenderer (ren: RendererHandle);
+
+(* bool SDL_SetRenderDrawColor(SDL_Renderer *ren,
+                               Uint8 r, Uint8 g, Uint8 b, Uint8 a) *)
+PROCEDURE SDL_SetRenderDrawColor (ren: RendererHandle;
+                                  r, g, b, a: CARDINAL8) : [ BOOLEAN ];
+
+(* bool SDL_GetRenderDrawColor(SDL_Renderer *ren,
+                               Uint8 *r, Uint8 *g, Uint8 *b, Uint8 *a) *)
+PROCEDURE SDL_GetRenderDrawColor (ren: RendererHandle;
+                                  VAR r, g, b, a: CARDINAL8) : [ BOOLEAN ];
+
+(* bool SDL_SetRenderDrawColorFloat(SDL_Renderer *ren,
+                                    float r, float g, float b, float a) *)
+PROCEDURE SDL_SetRenderDrawColorFloat (ren: RendererHandle;
+                                       r, g, b, a: SHORTREAL) : [ BOOLEAN ];
+
+(* bool SDL_GetRenderDrawColorFloat(SDL_Renderer *ren,
+                                    float *r, float *g, float *b, float *a) *)
+PROCEDURE SDL_GetRenderDrawColorFloat (ren: RendererHandle;
+                                       VAR r, g, b, a: SHORTREAL) : [ BOOLEAN ];
+
+(* bool SDL_SetRenderLogicalPresentation(SDL_Renderer *ren, int w, int h,
+                                          SDL_RendererLogicalPresentation mode) *)
+PROCEDURE SDL_SetRenderLogicalPresentation (ren: RendererHandle;
+                                            w, h, mode: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_RenderClear(SDL_Renderer *ren) *)
+PROCEDURE SDL_RenderClear (ren: RendererHandle) : [ BOOLEAN ];
+
+(* bool SDL_RenderPresent(SDL_Renderer *ren) *)
+PROCEDURE SDL_RenderPresent (ren: RendererHandle) : [ BOOLEAN ];
+
+(* bool SDL_RenderPoint(SDL_Renderer *ren, float x, float y) *)
+PROCEDURE SDL_RenderPoint (ren: RendererHandle;
+                           x, y: SHORTREAL) : [ BOOLEAN ];
+
+(* bool SDL_RenderPoints(SDL_Renderer *ren, const SDL_FPoint *pts,
+                         int count) *)
+PROCEDURE SDL_RenderPoints (ren: RendererHandle; pts: ADDRESS;
+                            count: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_RenderLine(SDL_Renderer *ren,
+                       float x1, float y1, float x2, float y2) *)
+PROCEDURE SDL_RenderLine (ren: RendererHandle;
+                          x1, y1, x2, y2: SHORTREAL) : [ BOOLEAN ];
+
+(* bool SDL_RenderLines(SDL_Renderer *ren, const SDL_FPoint *pts,
+                        int count) *)
+PROCEDURE SDL_RenderLines (ren: RendererHandle; pts: ADDRESS;
+                           count: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_RenderRect(SDL_Renderer *ren, const SDL_FRect *rect) *)
+PROCEDURE SDL_RenderRect (ren: RendererHandle;
+                          VAR rect: SDLFRect) : [ BOOLEAN ];
+
+(* bool SDL_RenderRects(SDL_Renderer *ren, const SDL_FRect *rects,
+                        int count) *)
+PROCEDURE SDL_RenderRects (ren: RendererHandle; rects: ADDRESS;
+                           count: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_RenderFillRect(SDL_Renderer *ren, const SDL_FRect *rect) *)
+PROCEDURE SDL_RenderFillRect (ren: RendererHandle;
+                              VAR rect: SDLFRect) : [ BOOLEAN ];
+
+(* bool SDL_RenderFillRects(SDL_Renderer *ren, const SDL_FRect *rects,
+                            int count) *)
+PROCEDURE SDL_RenderFillRects (ren: RendererHandle; rects: ADDRESS;
+                               count: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_SetRenderScale(SDL_Renderer *ren, float sx, float sy) *)
+PROCEDURE SDL_SetRenderScale (ren: RendererHandle;
+                              sx, sy: SHORTREAL) : [ BOOLEAN ];
+
+(* bool SDL_GetCurrentRenderOutputSize(SDL_Renderer *ren, int *w, int *h) *)
+PROCEDURE SDL_GetCurrentRenderOutputSize (ren: RendererHandle;
+                                          VAR w, h: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_RenderDebugText(SDL_Renderer *ren, float x, float y,
+                            const char *str) - ASCII only; str passes as
+   ARRAY OF CHAR like window titles. *)
+PROCEDURE SDL_RenderDebugText (ren: RendererHandle;
+                               x, y: SHORTREAL;
+                               str: ARRAY OF CHAR) : [ BOOLEAN ];
+
+END SDL3Render.

+ 37 - 0
src/SDL3Version.def

@@ -0,0 +1,37 @@
+DEFINITION MODULE FOR "C" SDL3Version ;
+
+(*
+   m2SDL3 - SDL version query (<SDL3/SDL_version.h>).
+
+   SDL3 differences from SDL2
+   --------------------------
+   * SDL_GetVersion(void) now returns the linked version as an int
+     version number (major*1000000 + minor*1000 + micro, see
+     SDL_VERSIONNUM) instead of filling an SDL_version record.
+     Decode it with SDL3Utils.VersionMajor/Minor/Micro, or with
+     "v DIV 1000000", "(v DIV 1000) MOD 1000", "v MOD 1000".
+   * SDL_GetRevision() is unchanged (const char *, NIL-able).
+
+   Compiled-against constants mirror SDL_MAJOR/MINOR/MICRO_VERSION.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   SDLCompiledMajor, SDLCompiledMinor, SDLCompiledMicro,
+   SDL_GetVersion, SDL_GetRevision;
+
+
+CONST
+   SDLCompiledMajor = 3;
+   SDLCompiledMinor = 4;
+   SDLCompiledMicro = 16;
+
+
+(* int SDL_GetVersion(void) - e.g. 3004016 for 3.4.16 *)
+PROCEDURE SDL_GetVersion () : INTEGER;
+
+(* const char *SDL_GetRevision(void) *)
+PROCEDURE SDL_GetRevision () : ADDRESS;
+
+END SDL3Version.

+ 124 - 0
src/SDL3Video.def

@@ -0,0 +1,124 @@
+DEFINITION MODULE FOR "C" SDL3Video ;
+
+(*
+   m2SDL3 - window management (<SDL3/SDL_video.h>), starter subset.
+
+   SDL3 differences from SDL2
+   --------------------------
+   * SDL_CreateWindow(title, w, h, flags): no x/y position; windows
+     are shown unless SDL_WINDOW_HIDDEN is set. Use
+     SDL_SetWindowPosition for placement (x/y still accept the
+     WINDOWPOS_CENTERED / UNDEFINED magic values via properties).
+   * Window flags are SDL_WindowFlags (Uint64); the parameter here
+     is LONGCARD. The values below all fit 32 bits but must be
+     passed where LONGCARD is expected.
+   * Most functions return bool now (BOOLEAN): SetTitle, SetSize,
+     GetSize, Show/Hide, SetPosition, GetPosition.
+   * SDL_CreateWindowAndRenderer lives in SDL3Render (it needs the
+     renderer type and would be circular here).
+
+   Opaque handle: SDL_Window * travels as ADDRESS (NIL == failure).
+   Title strings pass as ARRAY OF CHAR (GM2 maps them onto char * ).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   WindowHandle,
+   WindowPosCentered, WindowPosUndefined,
+   WindowFullscreen, WindowOpenGL, WindowOccluded, WindowHidden,
+   WindowBorderless, WindowResizable, WindowMinimized, WindowMaximized,
+   WindowMouseGrabbed, WindowInputFocus, WindowMouseFocus,
+   WindowExternal, WindowModal, WindowHighPixelDensity,
+   WindowMouseCapture, WindowMouseRelativeMode, WindowAlwaysOnTop,
+   WindowUtility, WindowTooltip, WindowPopupMenu, WindowKeyboardGrabbed,
+   WindowVulkan, WindowMetal, WindowTransparent, WindowNotFocusable,
+
+   SDL_CreateWindow, SDL_DestroyWindow,
+   SDL_GetWindowTitle, SDL_SetWindowTitle,
+   SDL_GetWindowSize, SDL_SetWindowSize, SDL_GetWindowSizeInPixels,
+   SDL_GetWindowPosition, SDL_SetWindowPosition,
+   SDL_GetWindowFlags,
+   SDL_ShowWindow, SDL_HideWindow;
+
+
+TYPE
+   WindowHandle = ADDRESS;
+
+CONST
+   WindowPosCentered  = 2FFF0000H;
+   WindowPosUndefined = 1FFF0000H;
+
+   WindowFullscreen       = 000000001H;
+   WindowOpenGL           = 000000002H;
+   WindowOccluded         = 000000004H;
+   WindowHidden           = 000000008H;
+   WindowBorderless       = 000000010H;
+   WindowResizable        = 000000020H;
+   WindowMinimized        = 000000040H;
+   WindowMaximized        = 000000080H;
+   WindowMouseGrabbed     = 000000100H;
+   WindowInputFocus       = 000000200H;
+   WindowMouseFocus       = 000000400H;
+   WindowExternal         = 000000800H;
+   WindowModal            = 000001000H;
+   WindowHighPixelDensity = 000002000H;
+   WindowMouseCapture     = 000004000H;
+   WindowMouseRelativeMode= 000008000H;
+   WindowAlwaysOnTop      = 000010000H;
+   WindowUtility          = 000020000H;
+   WindowTooltip          = 000040000H;
+   WindowPopupMenu        = 000080000H;
+   WindowKeyboardGrabbed  = 000100000H;
+   WindowVulkan           = 010000000H;
+   WindowMetal            = 020000000H;
+   WindowTransparent      = 040000000H;
+   WindowNotFocusable     = 080000000H;
+
+
+(* SDL_Window *SDL_CreateWindow(const char *title, int w, int h,
+                                SDL_WindowFlags flags) *)
+PROCEDURE SDL_CreateWindow (title: ARRAY OF CHAR;
+                            w, h: INTEGER;
+                            flags: LONGCARD) : WindowHandle;
+
+(* void SDL_DestroyWindow(SDL_Window *window) *)
+PROCEDURE SDL_DestroyWindow (win: WindowHandle);
+
+(* const char *SDL_GetWindowTitle(SDL_Window *window) *)
+PROCEDURE SDL_GetWindowTitle (win: WindowHandle) : ADDRESS;
+
+(* bool SDL_SetWindowTitle(SDL_Window *window, const char *title) *)
+PROCEDURE SDL_SetWindowTitle (win: WindowHandle;
+                              title: ARRAY OF CHAR) : [ BOOLEAN ];
+
+(* bool SDL_GetWindowSize(SDL_Window *window, int *w, int *h) *)
+PROCEDURE SDL_GetWindowSize (win: WindowHandle;
+                             VAR w, h: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_SetWindowSize(SDL_Window *window, int w, int h) *)
+PROCEDURE SDL_SetWindowSize (win: WindowHandle;
+                             w, h: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_GetWindowSizeInPixels(SDL_Window *window, int *w, int *h) *)
+PROCEDURE SDL_GetWindowSizeInPixels (win: WindowHandle;
+                                     VAR w, h: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_GetWindowPosition(SDL_Window *window, int *x, int *y) *)
+PROCEDURE SDL_GetWindowPosition (win: WindowHandle;
+                                 VAR x, y: INTEGER) : [ BOOLEAN ];
+
+(* bool SDL_SetWindowPosition(SDL_Window *window, int x, int y) *)
+PROCEDURE SDL_SetWindowPosition (win: WindowHandle;
+                                 x, y: INTEGER) : [ BOOLEAN ];
+
+(* SDL_WindowFlags SDL_GetWindowFlags(SDL_Window *window) *)
+PROCEDURE SDL_GetWindowFlags (win: WindowHandle) : LONGCARD;
+
+(* bool SDL_ShowWindow(SDL_Window *window) *)
+PROCEDURE SDL_ShowWindow (win: WindowHandle) : [ BOOLEAN ];
+
+(* bool SDL_HideWindow(SDL_Window *window) *)
+PROCEDURE SDL_HideWindow (win: WindowHandle) : [ BOOLEAN ];
+
+END SDL3Video.

+ 40 - 0
tests/run_tests.sh

@@ -0,0 +1,40 @@
+#!/bin/sh
+# m2SDL3 test runner - builds each tests/*.mod with gm2 and runs it.
+# Usage: ./run_tests.sh  (SDL_VIDEODRIVER=dummy also works headless)
+set -e
+CDIR=$(dirname "$0")
+ROOT=$(cd "$CDIR/.." && pwd)
+BIN="$ROOT/build/tests"
+OBJ="$ROOT/build/objs"
+GM2="${GM2:-gm2}"
+
+SDL_SRC="$ROOT/SDL-release-3.4.16"
+SDL_BLD="$SDL_SRC/build"
+SDL_INC="-I$SDL_SRC/include"
+SDL_LIB="-L$SDL_BLD -lSDL3 -Wl,-rpath,$SDL_BLD"
+
+mkdir -p "$BIN" "$OBJ"
+# pure-Modula-2 helpers must be compiled with -c first (no main),
+# then linked as .o files (passing the .mod directly would add
+# extra mains and fail to link).
+# shellcheck disable=SC2086
+$GM2 -I"$ROOT/src" -I"$ROOT/lib" $SDL_INC $GM2FLAGS -c "$ROOT/src/SDL3Rect.mod" -o "$OBJ/SDL3Rect.o"
+# shellcheck disable=SC2086
+$GM2 -I"$ROOT/src" -I"$ROOT/lib" $SDL_INC $GM2FLAGS -c "$ROOT/lib/SDL3Utils.mod" -o "$OBJ/SDL3Utils.o"
+# shellcheck disable=SC2086
+$GM2 -I"$ROOT/src" -I"$ROOT/lib" $SDL_INC $GM2FLAGS -c "$ROOT/src/SDL3Key.mod" -o "$OBJ/SDL3Key.o"
+pass=0; fail=0
+for t in test_init test_version test_rect test_window test_key; do
+  echo "== $t =="
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" $SDL_INC $GM2FLAGS "$ROOT/tests/$t.mod" \
+       "$OBJ/SDL3Rect.o" "$OBJ/SDL3Utils.o" "$OBJ/SDL3Key.o" \
+       -o "$BIN/$t" $SDL_LIB
+  if [ "$t" = "test_window" ] && [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
+    SDL_VIDEODRIVER=dummy "$BIN/$t"
+  else
+    "$BIN/$t"
+  fi
+  pass=$((pass+1))
+done
+echo "OK: $pass passed, $fail failed"

+ 27 - 0
tests/test_init.mod

@@ -0,0 +1,27 @@
+MODULE test_init ;
+
+(*
+   m2SDL3 smoke test: SDL_Init(SDLInitVideo) / SDL_Quit round-trip
+   plus SDL_WasInit mask check. Exit code 0 = pass, 1 = fail.
+   SDL3 returns BOOLEAN (not int) from init calls.
+*)
+
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDL_WasInit, SDLInitVideo;
+FROM libc IMPORT printf;
+
+VAR mask: CARDINAL;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("FAIL test_init: SDL_Init returned FALSE\n");
+      HALT(1)
+   END;
+   mask := SDL_WasInit(SDLInitVideo);
+   IF mask = 0 THEN
+      printf("FAIL test_init: SDL_WasInit mask is 0\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   SDL_Quit;
+   printf("PASS test_init (wasInit=%u)\n", mask)
+END test_init.

+ 78 - 0
tests/test_key.mod

@@ -0,0 +1,78 @@
+MODULE test_key ;
+
+(*
+   m2SDL3 test: push synthetic SDL_Event buffers through
+   SDL_PushEvent and check PollM2Event decodes type 0300H
+   (key down, keycode at offset 28) and 0100H (quit).
+   SDL3 events are 128 bytes; pushes return BOOLEAN.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDLInitEvents;
+FROM SDL3Events IMPORT SDL_PushEvent, SDL_PollEvent,
+   EventKeyDown, EventQuit, EventWindowCloseRequested, EventBufferBytes;
+FROM SDL3Utils IMPORT PollM2Event, M2Event, evKey, evQuit;
+FROM libc IMPORT printf;
+
+VAR
+   buf: ARRAY [0..EventBufferBytes-1] OF CHAR;
+   ev: M2Event;
+
+PROCEDURE ZeroBuf;
+VAR j: INTEGER;
+BEGIN
+   FOR j := 0 TO EventBufferBytes - 1 DO buf[j] := 0C END
+END ZeroBuf;
+
+PROCEDURE Put32 (off: CARDINAL; v: CARDINAL);
+BEGIN
+   buf[off] := CHR(v MOD 256);
+   buf[off+1] := CHR(v DIV 256 MOD 256);
+   buf[off+2] := CHR(v DIV 65536 MOD 256);
+   buf[off+3] := CHR(v DIV 16777216 MOD 256)
+END Put32;
+
+PROCEDURE Fail (msg: ARRAY OF CHAR);
+BEGIN
+   printf("FAIL test_key: %s\n", msg);
+   HALT(1)
+END Fail;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitEvents) THEN
+      printf("FAIL test_key: SDL_Init events\n");
+      HALT(1)
+   END;
+
+   (* key down, keycode "q" = 113 at offset 28. *)
+   ZeroBuf;
+   Put32(0, EventKeyDown);
+   Put32(28, 113);
+   IF NOT SDL_PushEvent(ADR(buf)) THEN Fail("push key") END;
+
+   (* window close request must also decode as quit. *)
+   ZeroBuf;
+   Put32(0, EventWindowCloseRequested);
+   IF NOT SDL_PushEvent(ADR(buf)) THEN Fail("push close") END;
+
+   (* quit. *)
+   ZeroBuf;
+   Put32(0, EventQuit);
+   IF NOT SDL_PushEvent(ADR(buf)) THEN Fail("push quit") END;
+
+   IF NOT PollM2Event(ev) THEN Fail("poll 1 empty") END;
+   IF ORD(ev.kind) # ORD(evKey) THEN Fail("ev1 not key") END;
+   IF NOT ev.pressed THEN Fail("ev1 not pressed") END;
+   IF ev.sym # 113 THEN Fail("ev1 sym") END;
+
+   IF NOT PollM2Event(ev) THEN Fail("poll 2 empty") END;
+   IF ORD(ev.kind) # ORD(evQuit) THEN Fail("ev2 not quit") END;
+
+   IF NOT PollM2Event(ev) THEN Fail("poll 3 empty") END;
+   IF ORD(ev.kind) # ORD(evQuit) THEN Fail("ev3 not quit") END;
+
+   IF PollM2Event(ev) THEN Fail("queue not drained") END;
+
+   SDL_Quit;
+   printf("PASS test_key\n")
+END test_key.

+ 39 - 0
tests/test_rect.mod

@@ -0,0 +1,39 @@
+MODULE test_rect ;
+
+(* m2SDL3 unit test: pure-Modula-2 SDL3Rect helpers, no SDL needed. *)
+
+FROM SDL3Rect IMPORT SDLRect, SDLFRect,
+   MakePoint, MakeFPoint, MakeRect, MakeFRect,
+   PointEquals, FPointEquals, RectEquals, FRectEquals,
+   RectEmpty, FRectEmpty;
+FROM libc IMPORT printf;
+
+VAR r: SDLRect; fr: SDLFRect;
+
+BEGIN
+   IF NOT PointEquals(MakePoint(1, 2), MakePoint(1, 2)) THEN
+      printf("FAIL test_rect: PointEquals\n"); HALT(1) END;
+   IF PointEquals(MakePoint(1, 2), MakePoint(3, 4)) THEN
+      printf("FAIL test_rect: PointEquals negative\n"); HALT(1) END;
+   IF NOT RectEquals(MakeRect(0, 0, 10, 20), MakeRect(0, 0, 10, 20)) THEN
+      printf("FAIL test_rect: RectEquals\n"); HALT(1) END;
+   r := MakeRect(0, 0, 10, 20);
+   IF RectEmpty(r) THEN
+      printf("FAIL test_rect: RectEmpty false positive\n"); HALT(1) END;
+   r := MakeRect(0, 0, 0, 20);
+   IF NOT RectEmpty(r) THEN
+      printf("FAIL test_rect: RectEmpty zero width\n"); HALT(1) END;
+   IF NOT FPointEquals(MakeFPoint(VAL(SHORTREAL, 1.5), VAL(SHORTREAL, 2.5)),
+                        MakeFPoint(VAL(SHORTREAL, 1.5), VAL(SHORTREAL, 2.5))) THEN
+      printf("FAIL test_rect: FPointEquals\n"); HALT(1) END;
+   IF NOT FRectEquals(MakeFRect(VAL(SHORTREAL, 0.0), VAL(SHORTREAL, 0.0),
+                                VAL(SHORTREAL, 10.0), VAL(SHORTREAL, 20.0)),
+                       MakeFRect(VAL(SHORTREAL, 0.0), VAL(SHORTREAL, 0.0),
+                                VAL(SHORTREAL, 10.0), VAL(SHORTREAL, 20.0))) THEN
+      printf("FAIL test_rect: FRectEquals\n"); HALT(1) END;
+   fr := MakeFRect(VAL(SHORTREAL, 0.0), VAL(SHORTREAL, 0.0),
+                   VAL(SHORTREAL, 0.0), VAL(SHORTREAL, 5.0));
+   IF NOT FRectEmpty(fr) THEN
+      printf("FAIL test_rect: FRectEmpty zero width\n"); HALT(1) END;
+   printf("PASS test_rect\n")
+END test_rect.

+ 32 - 0
tests/test_version.mod

@@ -0,0 +1,32 @@
+MODULE test_version ;
+
+(*
+   m2SDL3 smoke test: SDL_GetVersion returns 3.x.y as an int
+   version number (major*1000000+minor*1000+micro); the
+   SDL3Utils decode helpers must agree with DIV/MOD arithmetic.
+*)
+
+FROM SDL3Version IMPORT SDL_GetVersion, SDL_GetRevision,
+   SDLCompiledMajor, SDLCompiledMinor, SDLCompiledMicro;
+FROM SDL3Utils IMPORT VersionMajor, VersionMinor, VersionMicro;
+FROM libc IMPORT printf;
+
+VAR v: INTEGER;
+
+BEGIN
+   v := SDL_GetVersion();
+   printf("version %d.%d.%d (%d)\n",
+          VersionMajor(v), VersionMinor(v), VersionMicro(v), v);
+   printf("revision %s\n", SDL_GetRevision());
+   IF VersionMajor(v) # 3 THEN
+      printf("FAIL test_version: expected major 3\n");
+      HALT(1)
+   END;
+   IF (VersionMajor(v) # SDLCompiledMajor)
+      OR (VersionMinor(v) # SDLCompiledMinor)
+      OR (VersionMicro(v) # SDLCompiledMicro) THEN
+      printf("FAIL test_version: linked != compiled headers\n");
+      HALT(1)
+   END;
+   printf("PASS test_version\n")
+END test_version.

+ 72 - 0
tests/test_window.mod

@@ -0,0 +1,72 @@
+MODULE test_window ;
+
+(*
+   m2SDL3 smoke test: hidden window create -> title round-trip ->
+   size query -> destroy. Uses a hidden window so it also passes
+   headless (SDL_VIDEODRIVER=dummy).
+   SDL3: CreateWindow has no x/y; most calls return BOOLEAN.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SDL3 IMPORT SDL_Init, SDL_Quit, SDLInitVideo;
+FROM SDL3Video IMPORT SDL_CreateWindow, SDL_DestroyWindow,
+   SDL_GetWindowTitle, SDL_SetWindowTitle, SDL_GetWindowSize,
+   WindowHidden;
+FROM SDL3Events IMPORT SDL_PumpEvents;
+FROM SDL3Utils IMPORT CStrToM2;
+FROM StrLib IMPORT StrEqual;
+FROM libc IMPORT printf;
+
+VAR
+   win: ADDRESS;
+   title: ADDRESS;
+   titleBuf: ARRAY [0..63] OF CHAR;
+   w, h: INTEGER;
+
+BEGIN
+   IF NOT SDL_Init(SDLInitVideo) THEN
+      printf("FAIL test_window: SDL_Init video failed\n");
+      HALT(1)
+   END;
+
+   win := SDL_CreateWindow("m2sdl3-test", 320, 240,
+                           VAL(LONGCARD, WindowHidden));
+   IF win = NIL THEN
+      printf("FAIL test_window: CreateWindow returned NIL\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+
+   IF NOT SDL_SetWindowTitle(win, "m2sdl3-retitled") THEN
+      printf("FAIL test_window: SetWindowTitle failed\n");
+      SDL_DestroyWindow(win);
+      SDL_Quit;
+      HALT(1)
+   END;
+   title := SDL_GetWindowTitle(win);
+   CStrToM2(title, titleBuf);
+   IF NOT StrEqual(titleBuf, "m2sdl3-retitled") THEN
+      printf("FAIL test_window: title round-trip mismatch\n");
+      SDL_DestroyWindow(win);
+      SDL_Quit;
+      HALT(1)
+   END;
+
+   IF NOT SDL_GetWindowSize(win, w, h) THEN
+      printf("FAIL test_window: GetWindowSize failed\n");
+      SDL_DestroyWindow(win);
+      SDL_Quit;
+      HALT(1)
+   END;
+   IF (w # 320) OR (h # 240) THEN
+      printf("FAIL test_window: size %dx%d, expected 320x240\n", w, h);
+      SDL_DestroyWindow(win);
+      SDL_Quit;
+      HALT(1)
+   END;
+
+   SDL_PumpEvents;
+   SDL_DestroyWindow(win);
+   SDL_Quit;
+   printf("PASS test_window\n")
+END test_window.