Procházet zdrojové kódy

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

- src: SDL2 (init/error/timer), SDLVersion, SDLVideo,
  SDLEvents (opaque pump), SDLRect
- lib: SDLUtils (CStrToM2, GetErrorString)
- tests: init/version/rect/window + runner, 4/4 green
  (also headless via SDL_VIDEODRIVER=dummy)
- examples: hello_window, version, event_loop
- Makefile with two-step (-c then link) gm2 build
Eric Streit před 2 týdny
revize
c4aa35b7a3

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+build/
+*.o
+*.s
+*.a
+*~

+ 51 - 0
Makefile

@@ -0,0 +1,51 @@
+# m2SDL — GNU Modula-2 bindings for SDL2
+GM2      ?= gm2
+SDLCONF  := $(shell pkg-config --cflags sdl2 2>/dev/null)
+SDLLIBS  := $(shell pkg-config --libs sdl2 2>/dev/null || echo -lSDL2)
+GM2FLAGS ?=
+
+SRC_DIR := src
+LIB_DIR := lib
+BLD     := build
+OBJDIR  := $(BLD)/objs
+OBJS    := $(OBJDIR)/SDLRect.o $(OBJDIR)/SDLUtils.o
+
+INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR)
+
+TESTS    := test_init test_version test_rect test_window
+EXAMPLES := hello_window version event_loop
+
+.PHONY: all tests examples clean check
+
+all: tests examples
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(BLD)/tests:
+	mkdir -p $(BLD)/tests
+
+$(BLD)/examples:
+	mkdir -p $(BLD)/examples
+
+$(OBJDIR)/SDLRect.o: $(SRC_DIR)/SDLRect.mod $(SRC_DIR)/SDLRect.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/SDLUtils.o: $(LIB_DIR)/SDLUtils.mod $(LIB_DIR)/SDLUtils.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) $(SDLCONF) $< $(OBJS) -o $@ $(SDLLIBS)
+
+examples: $(BLD)/examples $(OBJS) $(addprefix $(BLD)/examples/,$(EXAMPLES))
+
+$(BLD)/examples/%: examples/%.mod $(OBJS) | $(BLD)/examples
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $(SDLCONF) $< $(OBJS) -o $@ $(SDLLIBS)
+
+check: tests
+	./tests/run_tests.sh
+
+clean:
+	rm -rf $(BLD)

+ 83 - 0
README.md

@@ -0,0 +1,83 @@
+# m2SDL — SDL2 bindings for GNU Modula-2
+
+Thin `DEFINITION MODULE FOR "C"` bindings of SDL2 for `gm2`
+(GCC 16 / GNU Modula-2), starting with init, video, version,
+timer, error and a minimal event pump.
+
+## Status
+
+Starter set, verified with gm2 + SDL 2.32.4 on Linux:
+
+| Module | C header | Contents |
+|---|---|---|
+| `SDL2` | `<SDL.h>`, `<SDL_error.h>`, `<SDL_timer.h>` | init/quit, `GetError`, `Delay`, ticks, performance counter |
+| `SDLVersion` | `<SDL_version.h>` | `SDLVersion` record, `GetVersion`, `GetRevision` |
+| `SDLVideo` | `<SDL_video.h>` | window create/destroy, title, size, show/hide |
+| `SDLEvents` | `<SDL_events.h>` | `PumpEvents`, `PollEvent`, `WaitEvent`, quit/key/mouse type ids (opaque buffer for now) |
+| `SDLRect` | `<SDL_rect.h>` | pure-M2 `SDLPoint`/`SDLRect` + helpers |
+| `SDLUtils` (`lib/`) | — | `CStrToM2()` + `GetErrorString()` helpers |
+
+Full `SDL_Event` union mapping, renderer, audio and gamepad come next.
+
+## Layout
+
+```text
+m2SDL/
+  src/        FOR "C" .def bindings (+ pure-M2 SDLRect)
+  lib/        pure-M2 helpers built on the bindings (SDLUtils)
+  tests/      gm2 smoke tests + run_tests.sh
+  examples/   hello_window, version, event_loop
+  build/      generated binaries (git-ignored)
+```
+
+## Prerequisites
+
+- `gm2` (tested: 16.0.1 experimental, `~/bin/Modula2/Gm2/bin/gm2`)
+- SDL2 dev files (`pkg-config --cflags --libs sdl2`, tested: 2.32.4)
+
+```sh
+export PATH=$HOME/bin/Modula2/Gm2/bin:$PATH
+```
+
+## Build & test
+
+```sh
+make            # tests + examples -> build/
+./tests/run_tests.sh
+SDL_VIDEODRIVER=dummy ./tests/run_tests.sh   # headless
+./build/examples/hello_window
+./build/examples/version
+./build/examples/event_loop
+```
+
+`make check` is `make tests` + the runner script.
+
+## Usage
+
+```modula2
+FROM SDL2 IMPORT SDL_Init, SDL_Quit, SDLInitVideo;
+FROM SDLVideo IMPORT SDL_CreateWindow, SDL_DestroyWindow,
+   WindowShown, WindowPosUndefined;
+```
+
+Compile your program with the binding search path and the C library:
+
+```sh
+gm2 -Isrc -Ilib myprog.mod src/SDLRect.mod lib/SDLUtils.mod -o myprog -lSDL2
+```
+
+Notes:
+
+- `CARDINAL <-> Uint32`, `INTEGER <-> int` (see gm2 "Elementary data
+  types" node); opaque `SDL_Window *` / `char *` travel as `ADDRESS`.
+- `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_GetVersion(v)`,
+  `SDL_GetWindowSize(win, w, h)`.
+
+## Roadmap
+
+1. Full `SDL_Event` variant record + typed poll/wait.
+2. Renderer + texture + surface subset.
+3. Audio + joystick/gamepad subset.
+4. SDL3 bindings alongside (sources at `../../SDL3-3.2.28`, not yet installed).

+ 55 - 0
examples/event_loop.mod

@@ -0,0 +1,55 @@
+MODULE event_loop ;
+
+(*
+   m2SDL example: event loop until window close or 5 seconds.
+   Uses the opaque SDLEvents subset: PollEvent over a raw buffer,
+   first CARDINAL of SDL_Event is the Uint32 type - offset 0 on
+   little and big endian alike.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SDL2 IMPORT SDL_Init, SDL_Quit, SDL_Delay,
+   SDL_GetTicks64, SDLInitVideo, SDLInitEvents;
+FROM SDLVideo IMPORT SDL_CreateWindow, SDL_DestroyWindow,
+   WindowShown, WindowPosUndefined;
+FROM SDLEvents IMPORT SDL_PollEvent, EventQuit, EventBufferBytes;
+FROM libc IMPORT printf;
+
+VAR
+   win: ADDRESS;
+   buf: ARRAY [0..EventBufferBytes-1] OF CHAR;
+   evType: CARDINAL;
+   start, now: LONGCARD;
+   quit: BOOLEAN;
+
+BEGIN
+   IF SDL_Init(SDLInitVideo + SDLInitEvents) # 0 THEN
+      printf("SDL_Init failed\n");
+      HALT(1)
+   END;
+   win := SDL_CreateWindow("Close me, or wait 5s",
+                           WindowPosUndefined, WindowPosUndefined,
+                           640, 480, WindowShown);
+   IF win = NIL THEN
+      printf("CreateWindow failed\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+   quit := FALSE;
+   start := SDL_GetTicks64();
+   WHILE NOT quit DO
+      WHILE SDL_PollEvent(ADR(buf)) # 0 DO
+         evType := 0;
+         evType := VAL(CARDINAL, ORD(buf[0]))
+                 + VAL(CARDINAL, ORD(buf[1])) * 256
+                 + VAL(CARDINAL, ORD(buf[2])) * 65536;
+         IF evType = EventQuit THEN quit := TRUE END
+      END;
+      now := SDL_GetTicks64();
+      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 ;
+
+(*
+   m2SDL example: open a window for ~2 seconds, then quit.
+   Build:  make examples   or see Makefile.
+   Run:    ./build/examples/hello_window
+*)
+
+FROM SDL2 IMPORT SDL_Init, SDL_Quit, SDL_Delay, SDLInitVideo;
+FROM SDLVideo IMPORT SDL_CreateWindow, SDL_DestroyWindow,
+   WindowShown, WindowPosUndefined;
+FROM SYSTEM IMPORT ADDRESS;
+FROM libc IMPORT printf;
+
+VAR win: ADDRESS;
+
+BEGIN
+   IF SDL_Init(SDLInitVideo) # 0 THEN
+      printf("SDL_Init failed\n");
+      HALT(1)
+   END;
+   win := SDL_CreateWindow("Hello from GNU Modula-2 + SDL2",
+                           WindowPosUndefined, WindowPosUndefined,
+                           640, 480, WindowShown);
+   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.

+ 15 - 0
examples/version.mod

@@ -0,0 +1,15 @@
+MODULE version ;
+
+(* m2SDL example: print linked SDL version + revision. *)
+
+FROM SDLVersion IMPORT SDLVersion, SDL_GetVersion, SDL_GetRevision;
+FROM libc IMPORT printf;
+
+VAR v: SDLVersion;
+
+BEGIN
+   SDL_GetVersion(v);
+   printf("SDL version %d.%d.%d", VAL(INTEGER, v.major),
+          VAL(INTEGER, v.minor), VAL(INTEGER, v.patch));
+   printf(" revision %s\n", SDL_GetRevision())
+END version.

+ 16 - 0
lib/SDLUtils.def

@@ -0,0 +1,16 @@
+DEFINITION MODULE SDLUtils ;
+
+(*
+   m2SDL - small pure-Modula-2 helpers over the FOR "C" bindings.
+   Copies NUL-terminated C strings (ADDRESS) into Modula-2 CHAR
+   arrays (NUL-terminated, truncated to the destination length).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED CStrToM2, GetErrorString;
+
+PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
+PROCEDURE GetErrorString (VAR buf: ARRAY OF CHAR);
+
+END SDLUtils.

+ 24 - 0
lib/SDLUtils.mod

@@ -0,0 +1,24 @@
+IMPLEMENTATION MODULE SDLUtils ;
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SDL2 IMPORT SDL_GetError;
+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;
+
+END SDLUtils.

+ 85 - 0
src/SDL2.def

@@ -0,0 +1,85 @@
+DEFINITION MODULE FOR "C" SDL2 ;
+
+(*
+   m2SDL - core SDL2 subsystem control, error handling and timing.
+   GNU Modula-2 binding, mapped onto <SDL.h>, <SDL_error.h>, <SDL_timer.h>.
+
+   Mapping notes
+   -------------
+   * CARDINAL  <-> Uint32 / unsigned int
+   * INTEGER   <-> int
+   * ADDRESS   <-> opaque pointers (char *, SDL_Window * escapes via SDLVideo)
+   * ARRAY OF CHAR parameters map onto (char * ) - pass string literals
+     or CHAR arrays directly, as documented in the GM2 "Interface to C" node.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   (* --- init flags (SDL_INIT_* from SDL.h) --- *)
+   SDLInitTimer, SDLInitAudio, SDLInitVideo,
+   SDLInitJoystick, SDLInitHaptic, SDLInitGameController,
+   SDLInitEvents, SDLInitSensor, SDLInitEverything,
+
+   (* --- core --- *)
+   SDL_Init, SDL_InitSubSystem, SDL_QuitSubSystem,
+   SDL_WasInit, SDL_Quit,
+
+   (* --- errors --- *)
+   SDL_GetError, SDL_ClearError,
+
+   (* --- timing --- *)
+   SDL_Delay, SDL_GetTicks, SDL_GetTicks64,
+   SDL_GetPerformanceCounter, SDL_GetPerformanceFrequency;
+
+
+CONST
+   SDLInitTimer          = 000000001H;  (* 0x00000001 *)
+   SDLInitAudio          = 000000010H;  (* 0x00000010 *)
+   SDLInitVideo          = 000000020H;  (* 0x00000020 *)
+   SDLInitJoystick       = 000000200H;  (* 0x00000200 *)
+   SDLInitHaptic         = 000001000H;  (* 0x00001000 *)
+   SDLInitGameController = 000002000H;  (* 0x00002000 *)
+   SDLInitEvents         = 000004000H;  (* 0x00004000 *)
+   SDLInitSensor         = 000008000H;  (* 0x00008000 *)
+   SDLInitEverything     = 00000F231H;  (* TIMER|AUDIO|VIDEO|EVENTS|
+                                           JOYSTICK|HAPTIC|GAMECONTROLLER|SENSOR *)
+
+
+(* int SDL_Init(Uint32 flags) *)
+PROCEDURE SDL_Init (flags: CARDINAL) : INTEGER;
+
+(* int SDL_InitSubSystem(Uint32 flags) *)
+PROCEDURE SDL_InitSubSystem (flags: CARDINAL) : INTEGER;
+
+(* void SDL_QuitSubSystem(Uint32 flags) *)
+PROCEDURE SDL_QuitSubSystem (flags: CARDINAL);
+
+(* Uint32 SDL_WasInit(Uint32 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);
+
+(* Uint32 SDL_GetTicks(void) *)
+PROCEDURE SDL_GetTicks () : CARDINAL;
+
+(* Uint64 SDL_GetTicks64(void) - LONGCARD <-> unsigned long long *)
+PROCEDURE SDL_GetTicks64 () : LONGCARD;
+
+(* Uint64 SDL_GetPerformanceCounter(void) *)
+PROCEDURE SDL_GetPerformanceCounter () : LONGCARD;
+
+(* Uint64 SDL_GetPerformanceFrequency(void) *)
+PROCEDURE SDL_GetPerformanceFrequency () : LONGCARD;
+
+END SDL2.

+ 48 - 0
src/SDLEvents.def

@@ -0,0 +1,48 @@
+DEFINITION MODULE FOR "C" SDLEvents ;
+
+(*
+   m2SDL - event pump, minimal starter subset (<SDL_events.h>).
+
+   Full SDL_Event is a 56-byte union; mapping the whole union onto a
+   Modula-2 variant RECORD comes later. For now events are pumped
+   through an opaque ADDRESS buffer plus the small set of helpers
+   below - enough for a quit-loop without lying about the layout.
+
+   Event type constants used here (from SDL_events.h):
+     SDL_QUIT = 0x100, SDL_KEYDOWN = 0x300, SDL_KEYUP = 0x301,
+     SDL_MOUSEBUTTONDOWN = 0x401, SDL_MOUSEBUTTONUP = 0x402.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   EventQuit, EventKeyDown, EventKeyUp,
+   EventMouseButtonDown, EventMouseButtonUp,
+   EventBufferBytes,
+   SDL_PumpEvents, SDL_PollEvent, SDL_WaitEvent, SDL_HasEvents;
+
+
+CONST
+   EventQuit            = 100H;
+   EventKeyDown         = 300H;
+   EventKeyUp           = 301H;
+   EventMouseButtonDown = 401H;
+   EventMouseButtonUp   = 402H;
+
+   (* Size of SDL_Event in SDL 2.32: 56 bytes. *)
+   EventBufferBytes = 56;
+
+
+(* void SDL_PumpEvents(void) *)
+PROCEDURE SDL_PumpEvents ();
+
+(* int SDL_PollEvent(SDL_Event *event) - pass ADR(buf), 1 = event, 0 = none *)
+PROCEDURE SDL_PollEvent (ev: ADDRESS) : INTEGER;
+
+(* int SDL_WaitEvent(SDL_Event *event) *)
+PROCEDURE SDL_WaitEvent (ev: ADDRESS) : INTEGER;
+
+(* SDL_bool SDL_HasEvents(Uint32 minType, Uint32 maxType) - 0/1 as CARDINAL *)
+PROCEDURE SDL_HasEvents (minType, maxType: CARDINAL) : CARDINAL;
+
+END SDLEvents.

+ 30 - 0
src/SDLRect.def

@@ -0,0 +1,30 @@
+DEFINITION MODULE SDLRect ;
+
+(*
+   m2SDL - pure Modula-2 geometry records matching <SDL_rect.h>.
+
+   C layout:
+     typedef struct SDL_Point  { int x, y; } SDL_Point;
+     typedef struct SDL_Rect   { int x, y, w, h; } SDL_Rect;
+   INTEGER <-> int, so these RECORDs are layout-compatible and can be
+   passed by ADDRESS to future SDL_Rect helpers.
+*)
+
+EXPORT UNQUALIFIED
+   SDLPoint, SDLRect,
+   MakePoint, MakeRect,
+   PointEquals, RectEquals, RectEmpty;
+
+
+TYPE
+   SDLPoint = RECORD x, y: INTEGER END;
+   SDLRect  = RECORD x, y, w, h: INTEGER END;
+
+
+PROCEDURE MakePoint (x, y: INTEGER) : SDLPoint;
+PROCEDURE MakeRect (x, y, w, h: INTEGER) : SDLRect;
+PROCEDURE PointEquals (a, b: SDLPoint) : BOOLEAN;
+PROCEDURE RectEquals (a, b: SDLRect) : BOOLEAN;
+PROCEDURE RectEmpty (VAR r: SDLRect) : BOOLEAN;
+
+END SDLRect.

+ 30 - 0
src/SDLRect.mod

@@ -0,0 +1,30 @@
+IMPLEMENTATION MODULE SDLRect ;
+
+PROCEDURE MakePoint (x, y: INTEGER) : SDLPoint;
+VAR p: SDLPoint;
+BEGIN
+   p.x := x; p.y := y; RETURN p
+END MakePoint;
+
+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 PointEquals (a, b: SDLPoint) : BOOLEAN;
+BEGIN
+   RETURN (a.x = b.x) AND (a.y = b.y)
+END PointEquals;
+
+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 RectEmpty (VAR r: SDLRect) : BOOLEAN;
+BEGIN
+   RETURN (r.w <= 0) OR (r.h <= 0)
+END RectEmpty;
+
+END SDLRect.

+ 43 - 0
src/SDLVersion.def

@@ -0,0 +1,43 @@
+DEFINITION MODULE FOR "C" SDLVersion ;
+
+(*
+   m2SDL - SDL_version mapping (<SDL_version.h>).
+
+   C layout:
+     typedef struct SDL_version { Uint8 major, minor, patch; } SDL_version;
+     void SDL_GetVersion(SDL_version *ver);
+     const char *SDL_GetRevision(void);
+
+   CARDINAL8 is exactly 8 bits (see SYSTEM module), so the RECORD
+   layout matches the C struct field-for-field. Pass callers use
+   ADR(v) / VAR parameters - GM2 maps VAR record params onto (T * ).
+*)
+
+FROM SYSTEM IMPORT ADDRESS, CARDINAL8;
+
+EXPORT UNQUALIFIED
+   SDLVersion, VersionPtr,
+   SDLCompiledMajor, SDLCompiledMinor, SDLCompiledPatch,
+   SDL_GetVersion, SDL_GetRevision;
+
+
+TYPE
+   SDLVersion = RECORD
+      major : CARDINAL8;
+      minor : CARDINAL8;
+      patch : CARDINAL8;
+   END;
+   VersionPtr = POINTER TO SDLVersion;
+
+CONST
+   SDLCompiledMajor = 2;
+   SDLCompiledMinor = 32;
+   SDLCompiledPatch = 4;
+
+(* void SDL_GetVersion(SDL_version *ver) *)
+PROCEDURE SDL_GetVersion (VAR ver: SDLVersion);
+
+(* const char *SDL_GetRevision(void) *)
+PROCEDURE SDL_GetRevision () : ADDRESS;
+
+END SDLVersion.

+ 76 - 0
src/SDLVideo.def

@@ -0,0 +1,76 @@
+DEFINITION MODULE FOR "C" SDLVideo ;
+
+(*
+   m2SDL - window management (<SDL_video.h>), minimal starter subset.
+
+   Opaque handle: SDL_Window * is never dereferenced from Modula-2,
+   it travels as ADDRESS (NIL == failure). Title strings are passed
+   as ARRAY OF CHAR (GM2 maps them onto char * ).
+
+   Position helpers SDL_WINDOWPOS_CENTERED / UNDEFINED are macros in C;
+   their expanded values are given here as constants:
+     CENTERED  = 0x2FFF0000, UNDEFINED = 0x1FFF0000.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   WindowHandle,
+   WindowPosCentered, WindowPosUndefined,
+   WindowFullscreen, WindowOpenGL, WindowShown, WindowHidden,
+   WindowBorderless, WindowResizable, WindowMinimized, WindowMaximized,
+   WindowInputGrabbed, WindowAllowHighDPI,
+
+   SDL_CreateWindow, SDL_DestroyWindow,
+   SDL_GetWindowTitle, SDL_SetWindowTitle,
+   SDL_GetWindowSize, SDL_SetWindowSize,
+   SDL_ShowWindow, SDL_HideWindow;
+
+
+TYPE
+   WindowHandle = ADDRESS;
+
+CONST
+   WindowPosCentered  = 2FFF0000H;
+   WindowPosUndefined = 1FFF0000H;
+
+   WindowFullscreen   = 000000001H;
+   WindowOpenGL       = 000000002H;
+   WindowShown        = 000000004H;
+   WindowHidden       = 000000008H;
+   WindowBorderless   = 000000010H;
+   WindowResizable    = 000000020H;
+   WindowMinimized    = 000000040H;
+   WindowMaximized    = 000000080H;
+   WindowInputGrabbed = 000000100H;
+   WindowAllowHighDPI = 000002000H;
+
+
+(* SDL_Window *SDL_CreateWindow(const char *title, int x, int y,
+                                int w, int h, Uint32 flags) *)
+PROCEDURE SDL_CreateWindow (title: ARRAY OF CHAR;
+                            x, y, w, h: INTEGER;
+                            flags: CARDINAL) : 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;
+
+(* void SDL_SetWindowTitle(SDL_Window *window, const char *title) *)
+PROCEDURE SDL_SetWindowTitle (win: WindowHandle; title: ARRAY OF CHAR);
+
+(* void SDL_GetWindowSize(SDL_Window *window, int *w, int *h) *)
+PROCEDURE SDL_GetWindowSize (win: WindowHandle; VAR w, h: INTEGER);
+
+(* void SDL_SetWindowSize(SDL_Window *window, int w, int h) *)
+PROCEDURE SDL_SetWindowSize (win: WindowHandle; w, h: INTEGER);
+
+(* void SDL_ShowWindow(SDL_Window *window) *)
+PROCEDURE SDL_ShowWindow (win: WindowHandle);
+
+(* void SDL_HideWindow(SDL_Window *window) *)
+PROCEDURE SDL_HideWindow (win: WindowHandle);
+
+END SDLVideo.

+ 33 - 0
tests/run_tests.sh

@@ -0,0 +1,33 @@
+#!/bin/sh
+# m2SDL 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}"
+
+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" $GM2FLAGS -c "$ROOT/src/SDLRect.mod" -o "$OBJ/SDLRect.o"
+# shellcheck disable=SC2086
+$GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS -c "$ROOT/lib/SDLUtils.mod" -o "$OBJ/SDLUtils.o"
+pass=0; fail=0
+for t in test_init test_version test_rect test_window; do
+  echo "== $t =="
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS "$ROOT/tests/$t.mod" \
+       "$OBJ/SDLRect.o" "$OBJ/SDLUtils.o" \
+       -o "$BIN/$t" -lSDL2
+  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 ;
+
+(*
+   m2SDL smoke test: SDL_Init(SDLInitTimer) / SDL_Quit round-trip
+   plus SDL_WasInit mask check. Exit code 0 = pass, 1 = fail.
+*)
+
+FROM SDL2 IMPORT SDL_Init, SDL_Quit, SDL_WasInit, SDLInitTimer;
+FROM libc IMPORT printf;
+
+VAR rc: INTEGER; mask: CARDINAL;
+
+BEGIN
+   rc := SDL_Init(SDLInitTimer);
+   IF rc # 0 THEN
+      printf("FAIL test_init: SDL_Init returned %d\n", rc);
+      HALT(1)
+   END;
+   mask := SDL_WasInit(SDLInitTimer);
+   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.

+ 23 - 0
tests/test_rect.mod

@@ -0,0 +1,23 @@
+MODULE test_rect ;
+
+(* m2SDL unit test: pure-Modula-2 SDLRect helpers, no SDL needed. *)
+
+FROM SDLRect IMPORT MakePoint, MakeRect, PointEquals, RectEquals, RectEmpty;
+FROM libc IMPORT printf;
+
+VAR a, b: CARDINAL;
+
+BEGIN
+   a := 0; b := 0;
+   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;
+   IF RectEmpty(MakeRect(0, 0, 10, 20)) THEN
+      printf("FAIL test_rect: RectEmpty false positive\n"); HALT(1) END;
+   IF NOT RectEmpty(MakeRect(0, 0, 0, 20)) THEN
+      printf("FAIL test_rect: RectEmpty zero width\n"); HALT(1) END;
+   printf("PASS test_rect\n")
+END test_rect.

+ 23 - 0
tests/test_version.mod

@@ -0,0 +1,23 @@
+MODULE test_version ;
+
+(*
+   m2SDL smoke test: SDL_GetVersion returns 2.x.y and the
+   SDLVersion RECORD mapping reads the same bytes C wrote.
+*)
+
+FROM SDLVersion IMPORT SDLVersion, SDL_GetVersion;
+FROM libc IMPORT printf;
+
+VAR v: SDLVersion;
+
+BEGIN
+   v.major := 0; v.minor := 0; v.patch := 0;
+   SDL_GetVersion(v);
+   printf("version %d.%d.%d\n",
+          VAL(INTEGER, v.major), VAL(INTEGER, v.minor), VAL(INTEGER, v.patch));
+   IF v.major # 2 THEN
+      printf("FAIL test_version: expected major 2\n");
+      HALT(1)
+   END;
+   printf("PASS test_version\n")
+END test_version.

+ 64 - 0
tests/test_window.mod

@@ -0,0 +1,64 @@
+MODULE test_window ;
+
+(*
+   m2SDL smoke test: hidden window create -> title round-trip ->
+   size query -> destroy. Uses a hidden window so it also passes
+   headless (SDL_VIDEODRIVER=dummy).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SDL2 IMPORT SDL_Init, SDL_Quit, SDLInitVideo;
+FROM SDLVideo IMPORT SDL_CreateWindow, SDL_DestroyWindow,
+   SDL_GetWindowTitle, SDL_SetWindowTitle, SDL_GetWindowSize,
+   WindowHidden, WindowPosUndefined;
+FROM SDLEvents IMPORT SDL_PumpEvents;
+FROM SDLUtils IMPORT CStrToM2;
+FROM StrLib IMPORT StrEqual;
+FROM libc IMPORT printf;
+
+VAR
+   win: ADDRESS;
+   title: ADDRESS;
+   titleBuf: ARRAY [0..63] OF CHAR;
+   w, h: INTEGER;
+   rc: INTEGER;
+
+BEGIN
+   rc := SDL_Init(SDLInitVideo);
+   IF rc # 0 THEN
+      printf("FAIL test_window: SDL_Init video -> %d\n", rc);
+      HALT(1)
+   END;
+
+   win := SDL_CreateWindow("m2sdl-test",
+                           WindowPosUndefined, WindowPosUndefined,
+                           320, 240, WindowHidden);
+   IF win = NIL THEN
+      printf("FAIL test_window: CreateWindow returned NIL\n");
+      SDL_Quit;
+      HALT(1)
+   END;
+
+   SDL_SetWindowTitle(win, "m2sdl-retitled");
+   title := SDL_GetWindowTitle(win);
+   CStrToM2(title, titleBuf);
+   IF NOT StrEqual(titleBuf, "m2sdl-retitled") THEN
+      printf("FAIL test_window: title round-trip mismatch\n");
+      SDL_DestroyWindow(win);
+      SDL_Quit;
+      HALT(1)
+   END;
+
+   SDL_GetWindowSize(win, w, h);
+   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.