ANALYSIS.md 11 KB

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 <example>.mod -o <example> -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. 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.
  • 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).
  • 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).
  • A 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.
  • ISO Modula-2 has no CARDINAL/INTEGER bitwise AND/OR/SHIFT; use CAST(BITSET, n) * ... (intersection) / + (union).
  • There is no DOWNTO in ISO: write FOR k := n - 1 TO 1 BY -1 DO.
  • 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).