# 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. `snake` (new, no assets) is a classic Snake game on an 8px grid - verified moving, eating/growing and the wall-collision "GAME OVER" state. `pong` (new, no assets) is a classic Pong game in a 640x400 window - verified serves, wall/paddle bounces, scoring and the match-over restart. `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. - `pong/pong.mod` (new, no assets) - classic Pong in a 640x400 window. Two-player capable: W/S move the left paddle, UP/DOWN take over the right paddle from the CPU opponent. Ball and paddle positions are `SHORTREAL`, moved with `tigrTime` deltas; the ball speeds up (x1.08, capped) on every paddle hit and bounces off the paddle surface at an angle derived from the impact offset; first to 5 points wins. Verified live: serving, wall/paddle bounces, scoring and match restart all work. 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). - 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). - GM2 16.0.1 (experimental, 2026-03-25) internal-compiler-errors (`wide_int_to_tree_1` at `tree.cc:1900`, via `fold_negate_const`) when folding constant integer arithmetic: a compile-time `a - b` or a negated integer literal (e.g. `Serve(-1)`, `winH - 28`, `winW / 2 - 20`) crashes `cc1gm2`. Workarounds used in `pong.mod`: keep `CONST` entries as plain literals, use precomputed literals at call sites (`CenterPrint(372, ...)`), and pass directions as `BOOLEAN` (`TRUE` = down/right, `FALSE` = up/left) instead of `-1`/`1` `INTEGER` literals. Negating runtime variables (`-bvy`, `-diff`) is fine. - Real-typed comparisons need real literals: `bvx > 0.0`, not `bvx > 0` (the latter is an ordinary ISO type error, `SHORTREAL` vs `INTEGER`, not a compiler bug). ## 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)`. # microui port (2026-09-16) `microui` v2.02 (immediate-mode UI, rxi, MIT) was rewritten from the C in `microui-master/` into ISO Modula-2: | file | role | |---|---| | `microui.def` | public API: types, constants, widget procedures | | `microui.mod` | full port of `microui.c` (widgets, layout, command list) | | `microuiHelpers.def/.mod` | bit ops (ISO has none), string/memory, number formatting | | `examples/microui-demo/microui-demo.mod` | interactive demo rendered with tigr | | `examples/microui-demo/microui-headless.mod` | off-screen smoke test writing `microui-demo.png` | Build: cd modula2 gcc -c ../tigr-master/tigr.c -o tigr.o gm2 -fiso -c helper.mod gm2 -fiso -I. -c microuiHelpers.mod gm2 -fiso -I. -c microui.mod cd examples/microui-demo gm2 -fiso -I ../../ ../../tigr.o ../../helper.o \ ../../microuiHelpers.o ../../microui.o \ microui-demo.mod -o microui-demo -lGL -lX11 gm2 -fiso -I ../../ ../../tigr.o ../../helper.o \ ../../microuiHelpers.o ../../microui.o \ microui-headless.mod -o microui-headless -lGL -lX11 The demo is rendered by tigr (not SDL): `COMMAND_RECT` -> `tigrFillRect`, `COMMAND_TEXT` -> `tigrPrint` with `tfont`, `COMMAND_ICON` -> line drawings, `COMMAND_CLIP` -> `tigrClip`. Mouse keys/buttons from `tigrMouse`, `tigrKeyHeld`/`tigrKeyDown`/`tigrReadChar` are translated into `microui.input*` calls. ## Design adaptations - `mu_Command` is a C union; Modula-2 has none. It is represented by a `BaseCommand` header and the concrete command records; command buffers are walked with `CAST(CommandPtr, ...)` and each command is cast to its record. - C callbacks take several arguments, but GM2 rejects procedure types with more than one (unnamed) formal parameter (see G1 below). Each callback is therefore invoked through a one-argument record (`TextWidthArgs`, `TextHeightArgs`, `FrameArgs`). - `drawText` takes the string as `ADDRESS` + length (as in C) rather than an open `ARRAY OF CHAR`, so substrings can be passed without copying. - `TCodepageType`-style enums with non-consecutive ordinals do not apply here; all microui constants are plain `CONST`. ## GM2 16.0.1 issues hit while porting - **G1 - procedure types**: when a `DEFINITION MODULE` is parsed as part of compiling its `.mod`, a procedure *type* only accepts a single, unnamed formal parameter. Both `PROCEDURE(a : T; b : T)` and `PROCEDURE(ADDRESS; INTEGER)` are rejected (the latter only in that context). Workaround: one type-only parameter per procedure type; bundle extra arguments in a record. - **G2 - inline `POINTER TO T`**: rejected in formal parameter lists and in `CAST(...)` (`CAST(POINTER TO BYTE, p)` fails). Declare a named pointer type alias (`BytePtr = POINTER TO BYTE`) and use it. - **G3 - `SHORTREAL(integer)` is miscompiled**: it silently yields `0`. Conversion from `INTEGER`/`CARDINAL` to `SHORTREAL` must go through the standard `FLOAT` function and be assigned to a `SHORTREAL` (`microuiHelpers.IntToReal` / `CardToReal`). This silently broke the slider thumb position and every `%.2f`/`%.3g` display until found. - **G4 - `TRUNC` and type aliases**: `TRUNC(x)` where `x`'s type is an alias (`Real = SHORTREAL`) gives "argument to TRUNC must be a float point type". Wrapping the expression in `SHORTREAL(...)` (or using a direct `SHORTREAL` local) works. `ROUND` is not defined at all in ISO mode. - **G5 - `EXIT`** is only allowed inside a `LOOP`, not in `WHILE`/`REPEAT`. - **G6 - address arithmetic**: `ADDRESS` does not support `+`; use `ADDADR`. `ADDRESS(array)` / `CAST(P, ADDRESS(array))` fails; use `ADR(array)` (`ADR` must be imported `FROM SYSTEM`). - **G7 - function results**: `f(x)^.field` cannot be dereferenced directly; assign the result to a local first. Function return values also cannot be discarded - assign them to a dummy variable. - **G8 - `WHILE`/`REPEAT` conditions** mixing `INTEGER` and `CARDINAL` (e.g. `i <= HIGH(a)` with an `INTEGER` index) are rejected; convert `HIGH` with `VAL(INTEGER, HIGH(a))`. ## Port bugs found and fixed - `beginRootContainer` pushed `unclippedRect` through `pushClipRect`, which intersects with the (empty) clip stack and produced a garbage clip rect; every draw was then clipped away (the first headless render was blank). It now pushes the unclipped rect directly, matching `begin_root_container`. - `mu_update_control` had the hover/no-interact logic inverted; rewritten to match the C. - `checkbox`/`textbox`/`slider`/`number` hashed the pointee value instead of the pointer; they now hash `ADR(pointer)` like the C. - `RealToStr` (added for the port) was rewritten to round correctly and to format `%f` / `%g` faithfully (fixed an off-by-one zero-pad and a lost minus sign in `%g`), and no longer relies on the broken `SHORTREAL(int)`. - The tigr renderer used `tigrFillRect` alone to paint microui rectangles. tigr's `tigrFillRect` fills only the *interior* of a rectangle (it does `x+=1; y+=1; w-=2; h-=2`), so every rectangle was shrunk by one pixel and all 1px-high/wide rectangles (the control and window borders) disappeared - which showed up as the top of the window being "eaten" by the title bar. The fix is to draw `tigrRect` (1px outline) followed by `tigrFillRect` (interior): together they fill the exact rectangle, with clipping and alpha blending, and single-pixel rects still show. - The demo created its window with `TIGR_2X`, which forces a 2X scale (1600x1200 for the 800x600 bitmap). On a screen whose client area is shorter than that, tigr crops the bitmap, cutting off the top rows of the framebuffer - which is what "ate" the window title. `TIGR_FIXED` avoided the crop but letterboxes the fixed bitmap (resizing just recentres it and jumps between integer scales). The demo now uses `TIGR_AUTO`: tigr resizes the bitmap to match the window (1:1), so the UI draws on the full, growing canvas and resizing behaves like a normal resizable window. The renderer must use `bmp^.w` / `bmp^.h` (not the compile-time size) for its clip and clear.