# Analysis of the tigr-m2 project (2026-09-16) A GNU Modula-2 (`gm2 -fiso`) binding of the tigr graphic library, plus a few test programs in `modula2/examples`. The C library is vendored in `tigr-master/` (v3.2). ## How it is built From `modula2/` (working directory must contain `tigr.c`, `tigr.def`, `helper.mod`, `helper.def`): gcc -c tigr.c gm2 -fiso -c helper.mod gm2 -fiso -I. helper.o tigr.o .mod -o -lGL -lX11 The `-s` flag shown in the README is optional and worked fine in testing. ## What works Verified by building and running on this machine (X11 session): - `Essai1`, `demo`, `clip`, `clavier`, `flags`, `update`, `hello`, `shader` all compile, link and run as *windows*; `headless` runs without a window and writes a correct 320x240 PNG; `opengl` opens two windows and drives the raw GL path (magenta `glClearColor`/`glClear` visible in `Window2`). `Essai2` (qualified imports) and `Essai3` build. `invaders` (new, no assets) is a playable Space Invaders game using only `tigrFillRect` + the built-in font: two-frame invader animation, accelerating movement, aimed multiple bombs, bullet-vs-bomb scoring and autofire. Verified in live captures: bombs (orange `#FFA028`) raining on every shot and sprite frames toggling between frames. `essai4.mod` is not a tigr example: it is a standalone PIM-style bit-twiddling test (`SHIFT`/`AND`/`OR` on `CARDINAL`) that the ISO dialect does not accept - left as is. - Window creation, `tigrClear`, `tigrFill`, `tigrLine`, `tigrRect`, `tigrCircle`, `tigrFillCircle`, `tigrClip`, `tigrBlit`, `tigrBlitAlpha`, `tigrMouse`, `tigrKeyDown`, `tigrReadChar`, `tigrTime`, `tigrTextWidth`, `tigrTextHeight`, `tigrPrint`, `tigrLoadImage`, `tigrReadFile`. - `TPixelType` passed/returned by value matches the C ABI (checked with `tigrGet`/`tigrPlot`). - The `Tigr` record layout (`tigr.def`) matches the C `struct Tigr` (`tigr.h`) field-for-field. - `tfont` and the built-in font work; text metrics are correct. ## Bugs found ### BUG 1 - incomplete EXPORT UNQUALIFIED list (fixed 2026-09-16) Only ~35 of ~110 identifiers declared in `tigr.def` were in the `EXPORT UNQUALIFIED` list, so the rest could not be imported at all (GM2 rejects them, even with qualified names). Affected: - Functions: `tigrBeginOpenGL`, `tigrSetPostShader`, `tigrGet`, `tigrPlot`, `tigrBlitTint`, `tigrBlitMode`, `tigrLoadFont`, `tigrFreeFont`, `tigrTouch`, `tigrScrollWheel`, `tigrShowKeyboard`, `tigrLoadImageMem`, `tigrSaveImage`, `tigrInflate`, `tigrDecodeUTF8`. - Types: `TigrType`, `intPtr`, `shortRealPtr`, `TigrFontTypePtr`, `TigrFontType`, `TigrGlyphType`, `TCodepageType`, `TigrTouchPoint`, `TigrTouchPointPtr`. - Constants: `TIGR_FIXED`, `TIGR_KEEP_ALPHA`, `TIGR_BLEND_ALPHA` and almost all `TK_*` key codes (only ESCAPE, SPACE, LEFT, RIGHT were exported). All of them are now in the export list (verified: `tigrGet`, `intPtr`, `tigrLoadFont`, ... now compile in both qualified and unqualified form; `demo`, `flags`, `clavier`, `Essai1`, `Essai2`, `Essai3` still build). ### BUG 2 - `TIGR_BLEND_ALPHA` has the wrong value (fixed 2026-09-16) - `tigr.def`: `TIGR_BLEND_ALPHA = -1` - `tigr.h`: `TIGR_BLEND_ALPHA = 1` (enum, default blit mode) `blitMode` is used arithmetically in `tigr.c` (lines 470, 591, 643), so a client passing `-1` would break alpha blending. Fix: it is now set to `1`. This also made the constant usable (it is exported). ### BUG 3 - `TCodepageType` enum values are wrong (fixed 2026-09-16) - `tigr.def`: `TCodepageType = (TCP_ASCII, TCP_1252, TCP_UTF32)` -> 0, 1, 2 - `tigr.h`: `TCP_ASCII = 0, TCP_1252 = 1252, TCP_UTF32 = 12001` A Modula-2 enumerated type cannot hold non-consecutive ordinals, so the enumeration was replaced by plain constants: TCP_ASCII = 0; TCP_1252 = 1252; TCP_UTF32 = 12001; `tigrLoadFont` takes a `CARDINAL` codepage, so clients can now pass the correct values. The `TCodepageType` type and the stray `TCodepage` variable were removed from `tigr.def`. ### BUG 4 - `DynamicStrings.String` used as a C `const char*` (fixed 2026-09-16) A GNU Modula-2 `String` (from `DynamicStrings`) is a *record* whose character buffer is at offset 0 but is **not NUL-terminated** and is capped at 127 bytes (`MaxBuf`). Passing an `InitString` result to a C function that expects a `const char*` only worked while the freshly-allocated memory happened to be zeroed. Fix applied in `tigr.def` (GNU Modula-2 maps `ARRAY OF CHAR` in a `FOR "C"` module onto `char*`/`const char*`, including NUL-terminated string literals): | procedure | before | after | |---|---|---| | `tigrWindow` | `title: String` | `title: ARRAY OF CHAR` | | `tigrPrint` | `text: String` | `text: ARRAY OF CHAR` | | `tigrReadFile`| returns `String` (raw `char*`) | returns `ADDRESS` | | `tigrDecodeUTF8` | `text: String; ... ): String` | `text: ADDRESS; cp: intPtr): ADDRESS` | | `tigrEncodeUTF8` | `text: String; ... ): String` | `text: ADDRESS; cp: INTEGER): ADDRESS` | Consequences for users: - Window title and `tigrPrint` text are now ordinary string literals / `ARRAY OF CHAR` (e.g. `tigrWindow(320, 240, "Clip", 0)`), instead of requiring `InitString`. - `tigrReadFile` returns a NUL-terminated `char*` as `ADDRESS`. To use the contents in M2, wrap it: `InitStringCharStar(raw)` then `CopyOut(arr, s)` (used this way in `demo.mod`). - `tigrDecodeUTF8`/`tigrEncodeUTF8` operate on `ADDRESS` buffers (pass `ADR(buffer)`); they return the next position as `ADDRESS`. Do not use them with `DynamicStrings.String` values. The `FROM DynamicStrings IMPORT String;` import was removed from `tigr.def` (no longer needed). All examples were updated to the new API: `demo.mod`, `Essai1.mod`, `clavier.mod`, `flags.mod` (they were passing `InitString(...)` results before), and `clip.mod` now compiles as-is because its string literal/constant arguments are accepted directly. ### BUG 5 - example programs (fixed 2026-09-16) - `examples/clip/clip.mod` did not compile (`tigrWindow(320, 240, "Clip", 0)` passed a string literal where `String` was expected). This was the same `String`/`char*` mismatch as BUG 4, so fixing `tigr.def` fixed `clip.mod` and it now compiles and runs unchanged. - `examples/flags/flags.mod` crashed immediately at old line 184: the `toggle` (`TogglePtr`) variable was never initialized (dereferences NIL), `modeChange` was never set, the loop ran `0..numToggles` (one too many iterations), keys never un-set `checked`, and window recreation only ran when `flags > newFlags`. Fixed: - `toggle := CAST(TogglePtr, ADR(toggles[i]));` inside the loop. - loop is `FOR i := 0 TO numToggles - 1`. - pressing a toggle key flips it: `toggle^.checked := 1 - toggle^.checked`. - recreate on any change (`flags # newFlags`). - `modeFlags := TIGR_AUTO + TIGR_RETINA` and `modeChange := (flags AND modeFlags) # (newFlags AND modeFlags)`; since ISO rejects `CARDINAL` `AND`/`OR`, bit masking is done via `CAST(BITSET, n) * CAST(BITSET, mask)` (set intersection). - `drawToggle`'s `stride` parameter is `CARDINAL` (was `INTEGER`, receiving a `CARDINAL`). - loop condition now `AND` (window open AND ESC not pressed) so ESC quits, matching the C example. - `examples/unicode/update.mod` was an unfinished stub (missing `:` in the VAR block, empty body). Completed into a working bouncing-ball example that updates position each frame on the tigr window (keeps the original `x/y/posx/posy` variable names). - `examples/Essai2/Essai2.mod` was a stub. Completed into a minimal window test that exercises the qualified-import style (`IMPORT helper, tigr`): builds a `TPixelType` with `helper.tigrRGB(80H, 90H, 0A0H)`, clears the window with it, and closes on ESC. ### Examples from the original tigr tree (ported and tested 2026-09-16) The original `tigr-master/` tree (sources identical to the bundled copy) also shipped `headless`, `hello`, `opengl` and `shader`. `modula2/examples/` had empty placeholders for `headless/`, `opengl/` and `shader/`; `hello/` did not exist. All four were ported and verified: - `headless/headless.mod` - off-screen bitmap (`tigrBitmap`) + `tigrPrint` + `tigrSaveImage("headless.png", bmp)`; no window at all. Produces a valid 320x240 RGBA PNG whose corner pixel is exactly `tigrRGB(80H, 90H, 0A0H)` = (128, 144, 160) and which contains the rendered white text. Note: C returns `!err`, i.e. **1 = success** - check `result = 0` for failure, not `# 0`. - `hello/hello.mod` - the trivial windowed "Hello, world." (blue clear + white text). Runs fine. - `opengl/opengl.mod` - two windows; `Window2` clears the raw GL framebuffer magenta via `tigrBeginOpenGL` + `glClearColor(1,0,1,1)` + `glClear(GL_COLOR_BUFFER_BIT)` and then blits tigr text over it. Verified by screenshot: the window is dominated by `#FF00FF`. The GL functions are called through a tiny `DEFINITION MODULE FOR "C" gl;` (`opengl/gl.def`). - `shader/shader.mod` - `tigrSetPostShader` with the GLSL `fxShader` string + `tigrSetPostFX`/`tigrTime` loop ("Shady" window). Needs `size := HIGH(fxShader)` (gm2 computes the literal's length - there is no `LENGTH`/`strlen` for `ARRAY OF CHAR` in ISO M2). GM2 gotchas encountered: - `DEFINITION MODULE FOR "C" gl;` puts `FOR "C"` *before* the module name (the order `MODULE gl FOR "C";` only produces syntax warnings). - A `FOR "C"` definition without `EXPORT UNQUALIFIED` is mangled as `_` (e.g. `gl_glClear`), so it cannot reach a plain C function like `glClear`; with `EXPORT UNQUALIFIED glClear, ...;` GM2 generates the plain C names and links straight against `-lGL`. - ISO Modula-2 has no `CARDINAL`/`INTEGER` bitwise `AND`/`OR`/`SHIFT`; use `CAST(BITSET, n) * ...` (intersection) / `+` (union). - Adjacent string literals are **not** concatenated under ISO - keep strings on one line (done for the `fxShader`). - `SHORTREAL` literals (`1.0`) pass straight into C `float` parameters. - `tigrTime()` is **not** an absolute clock: each call returns the number of seconds *since the previous call* (i.e. a per-frame delta; the first call returns 0). Use it directly as `dt := tigrTime();` per frame (as `demo.mod` and `shader.mod` do) - do not subtract two calls to build a "now" you then difference, or timers effectively never fire (seen in the first `invaders` version: `dt = now - lastTime` collapsed to ~0 and bombs never spawned). ## Notes / minor oddities - `tigr.def` still declares stray global variables (`Tigr : TigrType`, `TigrGlyph`, `TigrFont`) that do not correspond to anything in the C library. They are not exported; they could be removed (`TCodepage` was removed along with BUG 3). - `Essai1` uses `tigrError` to quit once the window is closed; on Linux `tigrError` prints to stderr and calls `exit(1)`.