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).
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 <example>.mod -o <example> -lGL -lX11
The -s flag shown in the README is optional and worked fine in testing.
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. snake (new, no assets) is a classic
Snake game on an 8px grid - verified moving, eating/growing and the
wall-collision "GAME OVER" state. 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.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).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.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:
tigrBeginOpenGL, tigrSetPostShader, tigrGet, tigrPlot,
tigrBlitTint, tigrBlitMode, tigrLoadFont, tigrFreeFont,
tigrTouch, tigrScrollWheel, tigrShowKeyboard, tigrLoadImageMem,
tigrSaveImage, tigrInflate, tigrDecodeUTF8.TigrType, intPtr, shortRealPtr, TigrFontTypePtr,
TigrFontType, TigrGlyphType, TCodepageType, TigrTouchPoint,
TigrTouchPointPtr.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).
TIGR_BLEND_ALPHA has the wrong value (fixed 2026-09-16)tigr.def: TIGR_BLEND_ALPHA = -1tigr.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).
TCodepageType enum values are wrong (fixed 2026-09-16)tigr.def: TCodepageType = (TCP_ASCII, TCP_1252, TCP_UTF32) -> 0, 1, 2tigr.h: TCP_ASCII = 0, TCP_1252 = 1252, TCP_UTF32 = 12001A 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.
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:
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.
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.FOR i := 0 TO numToggles - 1.toggle^.checked := 1 - toggle^.checked.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).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.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).snake/snake.mod (new, no assets) - classic Snake on an 8px grid in the
320x240 window. Fixed-size body arrays (whole board = win condition),
per-tick pending turn stored as INTEGER deltas (no 180 degree reversal),
deterministic LCG food placement, tigrTime-deltas to pace movement,
speed increases with score. Verified live: head advances one cell per
step, eating grows it by one cell (forced food in the head's line), and a
straight run ends in "GAME OVER" with the head frozen at the wall.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).FOR "C" definition without EXPORT UNQUALIFIED is mangled as
<module>_<proc> (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.CARDINAL/INTEGER bitwise AND/OR/SHIFT; use
CAST(BITSET, n) * ... (intersection) / + (union).DOWNTO in ISO: write FOR k := n - 1 TO 1 BY -1 DO.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).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).