Skip to content
SightemPublic

About

A LaTeX rendering engine for the TI-84 Plus CE calculator. Renders mixed text and math expressions - fractions, integrals, matrices, Greek letters, and more, directly on the calculator's hardware

Topics

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

libtexce

libtexce demo animation

License: AGPLv3

A LaTeX rendering engine for the TI-84 Plus CE calculator. Renders mixed text and math expressions - fractions, integrals, matrices, Greek letters, and more, directly on the calculator's hardware.

libtexce is written in C, targets the ez80 via the CE C/C++ Toolchain, and can also be built natively (via PortCE + SDL2) or for the browser (via Emscripten) for development and testing.

Quick Start

The full rendering pipeline is five steps: load fonts, configure, format, create a renderer, draw.

#include <graphx.h>
#include <keypadc.h>
#include <tice.h>

#include "tex/tex.h"

int main(void)
{
    gfx_Begin();
    gfx_SetDrawBuffer();
    gfx_SetTransparentColor(255);

    TeX_Font font_main;
    TeX_Font font_script;
    if (!tex_font_open(&font_main, "TeXFonts") ||
        !tex_font_open(&font_script, "TeXScrpt"))
    {
        gfx_PrintStringXY("Missing font packs!", 10, 10);
        gfx_SwapDraw();

        while (!os_GetCSC()) {}
        gfx_End();
        return 1;
    }

    // Source is borrowed read-only and must outlive the layout.
    const char* source =
        "Quadratic Formula\n"
        "$$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$$\n"
        "\n"
        "Taylor Series\n"
        "$$f(x) \\approx f(a) + f'(a)(x-a) + \\frac{f''(a)}{2}(x-a)^2$$";

    // format parses the document and computes layout metrics
    TeX_Config cfg = {
        .palette = { 255, 254, 253, 0 }, // background, two reserved shades, ink
        .justify = true, // optional: fill automatically wrapped text lines
        .font_main = font_main,
        .font_script = font_script,
    };

    int margin = 10;
    TeX_Layout* layout = tex_format(source, GFX_LCD_WIDTH - margin * 2, &cfg);

    // create a renderer (manages a transient memory pool for drawing)
    TeX_Renderer* renderer = tex_renderer_create();

    int scroll_y = 0;
    int total_h  = tex_get_total_height(layout);
    int max_scroll = total_h > GFX_LCD_HEIGHT ? total_h - GFX_LCD_HEIGHT : 0;

    // draw loop
    while (true) {
        kb_Scan();
        if (kb_Data[6] & kb_Clear) break;
        if (kb_Data[7] & kb_Up)   scroll_y -= 10;
        if (kb_Data[7] & kb_Down) scroll_y += 10;
        if (scroll_y < 0) scroll_y = 0;
        if (scroll_y > max_scroll) scroll_y = max_scroll;

        gfx_FillScreen(255);
        tex_draw(renderer, layout, margin, 0, scroll_y);
        gfx_SwapDraw();
    }

    // Renderer and layout may be destroyed in either order.
    tex_renderer_destroy(renderer);
    tex_free(layout);
    gfx_End();
    return 0;
}

Real-World Usage

If you are building a notes app (the most common use case), start with libtexce_notes_template. It gives you a ready to use TI-84 Plus CE notes workflow with formatting and CI-built transfer artifacts

For a larger production example, see matrix, a full linear algebra app that uses libtexce to render step by step formatted math on device

API Reference

The public API is declared in tex/tex.h. All functions use C linkage.

Core Lifecycle

Function Description
TeX_Layout* tex_format(const char* input, int width, const TeX_Config* config) Measure a mixed text/math document without modifying its source. Returns NULL for invalid arguments or initialization OOM. Check tex_get_last_error() on the returned layout.
int tex_get_total_height(TeX_Layout* layout) Total rendered height in pixels. Use for scroll bounds.
void tex_free(TeX_Layout* layout) Free all resources associated with a layout

Rendering

Function Description
TeX_Renderer* tex_renderer_create(void) Create a renderer with the default 40 KB slab
TeX_Renderer* tex_renderer_create_sized(size_t slab_size) Create a renderer with a custom slab size
void tex_renderer_destroy(TeX_Renderer* r) Destroy the renderer and free its slab.
void tex_draw(TeX_Renderer* r, TeX_Layout* layout, int x, int y, int scroll_y) Draw visible portion of the document to the current draw buffer

Error Handling

Function Description
TeX_Error tex_get_last_error(TeX_Layout* layout) Error code from last operation (TEX_OK, TEX_ERR_OOM, TEX_ERR_FONT, TEX_ERR_PARSE, TEX_ERR_INPUT, TEX_ERR_DEPTH).
const char* tex_get_error_message(TeX_Layout* layout) Human readable error string (static, never NULL).
int tex_get_error_value(TeX_Layout* layout) Detail value (byte offset, nesting depth, etc.)

Renderer Statistics

Function Description
void tex_renderer_get_stats(TeX_Renderer* r, size_t* peak_used, size_t* capacity, size_t* alloc_count, size_t* reset_count) Query pool statistics. Pass NULL for stats you dont need. Useful for tuning tex_renderer_create_sized()

Configuration

typedef struct
{
    uint8_t palette[4];           // Background, 1/3 ink, 2/3 ink, foreground
    bool justify;                // Expand word spaces on automatic wraps (default false)

    TeX_Font font_main;           // Borrowed validated main-font view
    TeX_Font font_script;         // Borrowed validated script-font view

    TeX_ErrorLogFn error_callback; // Optional error/warning callback
    void* error_userdata;         // Passed to callback
} TeX_Config;

Colors are 8 bit palette indices matching the graphx palette. Use four distinct indices: the renderer uses them to distinguish background, partial coverage and full ink when strokes overlap. tex_draw() sets the two intermediate colors from your background and foreground. Clear to palette[0] before drawing, and don't change the intermediate colors while text using them remains visible.

Set justify = true to fill automatically wrapped text lines. Paragraph endings and explicit newlines are not stretched.

The error callback receives a severity level (0 = info, 1 = warning, 2 = error), a message string, and in debug builds, the source file and line number where the error occurred.

Ownership and Lifetime Rules

Understanding buffer ownership is critical for correct usage.

The Input Buffer

tex_format() reads the input without modifying it, so string literals are valid inputs.

The source and font bytes must remain unchanged and at the same address for the entire lifetime of the TeX_Layout. The layout borrows them rather than making copies, and tex_draw() reparses the source on cache misses.

Free the layout before releasing its source or font storage. The renderer can be destroyed before or after the layout.

The Renderer

A TeX_Renderer owns a slab of memory used as a transient pool. Each call to tex_draw() may reset and reuse this pool. A single renderer can be shared across multiple layouts.

Summary

Object Owns Must outlive
Source text The caller's immutable bytes TeX_Layout
Font data The caller's validated, stationary font bytes TeX_Layout
TeX_Layout Checkpoint index, config/font context, error state Neither renderer nor source storage
TeX_Renderer Transient slab pool and line array Neither layout nor source storage

How Rendering Works

libtexce uses a streaming two pass architecture designed for the calculator's constrained memory.

Pass 1: tex_format() Dry Run Layout

When you call tex_format(), the engine tokenizes and parses the entire document, measuring each lines height and accumulating the total document height. No nodes or render trees are retained, only the total height and a sparse checkpoint index are stored in the TeX_Layout

Checkpoints record (y_position, source_pointer) pairs at regular pixel intervals (~200px). These allow tex_draw() to jump into the middle of a long document without reparsing from the beginning

Pass 2: tex_draw() Windowed Reparse

Each time tex_draw() is called, the renderer:

  1. Checks its cache. If the visible region falls within the previously hydrated window, the existing render tree is reused without reparsing
  2. Otherwise, rehydrates. The renderer finds the nearest checkpoint before scroll_y - padding, reparses from that point forward, and builds a render tree covering the viewport plus 240px of padding in each direction
  3. Draws the visible lines from the render tree to the current graphx draw buffer

This means the renderer only ever holds nodes for ~3 screens of content, regardless of total document length. the tradeoff is that scrolling to a completely new region triggers a reparse, but checkpoint indexing keeps this fast

Why This Matters to You

  • Render tree storage does not grow with document length the existing document height limit is 20000 pixels.
  • The input buffer must stay alive because tex_draw() reads from it on every cache miss.
  • Renderer slab sizing (tex_renderer_create_sized()) controls the upper bound on how much visible content can be rendered. The default 40 KB is too generous for most documents and you should consider using the sized init function. Use tex_renderer_get_stats() to measure actual usage.

Font System and Assets

libtexce uses two Latin Modern fonts stored as AppVars:

AppVar Description Nominal Size
TeXFonts.8xv Main text and math font 16 px
TeXScrpt.8xv Script/subscript/superscript font 11 px

Both must be transferred to the calculator before running any program that uses libtexce. Prebuilt copies are in the assets/ directory. Keep them archived to read the font data directly from flash without a font-sized RAM allocation.

What is in the Fonts

Each font provides ASCII text, Greek letters, calculus symbols, operators, arrows and delimiters, plus italic Latin letters and larger display operators.

The header include/texfont.h defines named constants for custom glyphs, though you generally won't need these. The LaTeX parser maps \alpha, \int, etc. automatically.

Rebuilding Fonts

Install Pillow and fontTools on the build host, then run:

python3 tools/process_fonts.py

The generator reads the Latin Modern fonts in tools/fonts/ and writes both .8xv files to assets/. Use --outdir PATH to write elsewhere. The fonts use the included GUST Font License.

Supported LaTeX

libtexce supports a substantial subset of LaTeX math mode. The full list is maintained in LATEX_COMMANDS_SUPPORTED.md.

Highlights:

  • Fractions: \frac{a}{b}, \tfrac, \binom{n}{k}
  • Roots: \sqrt{x}, \sqrt[3]{x}
  • Scripts: x^2, x_n, x_i^2
  • Greek: \alpha through \omega, \Gamma through \Omega
  • Big operators: \int, \sum, \prod with limits, plus \iint, \iiint, \oint variants
  • Functions: \sin, \cos, \lim, \log, \exp, \det, \gcd, and many more
  • Boolean logic: \land / \wedge, \lor / \vee, \neg / \lnot, \Rightarrow / \implies, \Leftrightarrow / \iff, \supset (horseshoe/superset), \top, and \bot
  • Accents: \hat, \bar, \vec, \dot, \tilde, \overline, \underline
  • Decorations: \overbrace{...}^{label}, \underbrace{...}_{label}
  • Delimiters: \left( ... \right) with auto-sizing for (), [], \{\}, ||, \langle\rangle, \lfloor\rfloor, \lceil\rceil
  • Matrices: pmatrix, bmatrix, Bmatrix, vmatrix, matrix, array (with column separators via |). Leading, interior, and trailing empty cells retain their column positions.
  • Spacing: \,, \:, \;, \!, \quad, \qquad
  • Text mode: \text{...} for roman text within math

Input uses standard LaTeX delimiters: $...$ for inline math, $$...$$ for display math (centered). everything outside $ delimiters is rendered as plain text with automatic word wrapping

Boolean logic symbols require the current TeXFonts.8xv and TeXScrpt.8xv AppVars

Common Patterns

Scrollable Document Viewer

The Quick Start example covers this pattern. Key points:

  • Use tex_get_total_height() to compute scroll bounds
  • Clamp scroll_y between 0 and total_height - viewport_height
  • Pass scroll_y to tex_draw() the renderer handles windowed rendering automatically

Multiple Layouts with a Shared Renderer

A single TeX_Renderer can draw different layouts on different frames. This is useful for chat style UIs:

TeX_Renderer* renderer = tex_renderer_create();

// String literals already have sufficient lifetime; no source copies needed.
TeX_Layout* msg1 = tex_format("What is $E = mc^2$?", width, &cfg);
TeX_Layout* msg2 = tex_format("Einstein's mass-energy equivalence:\n$$E = mc^2$$", width, &cfg);

// draw them at different positions using the same renderer
// note: scroll_y=0 since we position each message manually via the y parameter
tex_draw(renderer, msg1, x, y1, 0);
tex_draw(renderer, msg2, x, y2, 0);

// cleanup
tex_renderer_destroy(renderer);
tex_free(msg1); tex_free(msg2);

Note: When switching between layouts, the renderer invalidates its cache and reparses. For a scrolling view of a single layout, the cache avoids redundant work

Error Handling

TeX_Layout* layout = tex_format(buf, width, &cfg);

if (!layout) {
    // catastrophic failure (OOM during initialization)
    // cannot proceed
}

if (tex_get_last_error(layout) != TEX_OK) {
    // parse error, font error, etc.
    // the layout may still be partially renderable
    dbg_printf("TeX error: %s (code %d, detail %d)\n",
        tex_get_error_message(layout),
        tex_get_last_error(layout),
        tex_get_error_value(layout));
}

for richer diagnostics, use the error callback in TeX_Config:

void my_error_handler(void* userdata, int level, const char* msg,
                      const char* file, int line) {
    (void)userdata;
    const char* prefix = level == 2 ? "ERROR" : level == 1 ? "WARN" : "INFO";
    dbg_printf("[%s] %s\n", prefix, msg);
}

TeX_Config cfg = {
    .palette = {255, 254, 253, 0},
    .font_main = font_main,
    .font_script = font_script,
    .error_callback = my_error_handler,
    .error_userdata = NULL,
};

Tuning Renderer Memory

If you're rendering complex expressions (deeply nested fractions, large matrices) and suspect the renderer pool is too small, measure it:

size_t peak, cap;
tex_renderer_get_stats(renderer, &peak, &cap, NULL, NULL);
dbg_printf("Pool: %u / %u bytes\n", (unsigned)peak, (unsigned)cap);

Though from real world testing, this is basically never a problem unless the input is maliciously nested.

Building

libtexce uses CMake with presets. There are two independent build systems: the native/WASM host build (for development and testing), and the CE build (for the actual calculator)

Prerequisites

Target Requirements
Native Clang with C23 support, CMake >= 3.21, Ninja, SDL2, SDL2_mixer
CE CE C/C++ Toolchain (provides ez80-clang, fasmg, convbin)
WASM Emscripten SDK, CMake, Ninja

Native Build (Development + Tests)

cmake --preset native
cmake --build build/native

this builds the core engine, all host unit tests, and the PortCE SDL2 demo. variants:

Preset Description
native Default debug build (system clang)
native-clang20 Explicit clang-20
native-asan-clang20 AddressSanitizer + LeakSanitizer enabled

Run tests:

cd build/native && ctest
# or
cmake --build build/native --target run_tests

Run the SDL2 demo:

./build/native/bin/demo_text_portce

CE Build (Calculator)

the CE build lives in demo/ce/ and uses the CEdev toolchain:

cd demo/ce
cmake --preset ce
cmake --build ../../build/ce

This produces .8xp files in build/ce/<target>/bin/. Transfer the .8xp program along with TeXFonts.8xv, TeXScrpt.8xv, and clibs.8xg to the calculator.

WASM Build (Browser)

source /path/to/emsdk/emsdk_env.sh
cmake --preset wasm
cmake --build build/wasm

Produces an HTML file you can serve locally... for whatever reason

Testing

libtexce has two test tiers:

Host Unit Tests

Located in tests/, these test individual pipeline stages against the internal API:

Test What it covers
test_token Tokenizer (text, math delimiters, escaping)
test_parse Parser, required operands, balanced groups, and positional empty matrix cells
test_measure Measurement pass (node dimensions)
test_layout Checkpoint replay, cache lifetime, font switching, antialiasing, palette ownership, clipping, delimiter clearance, text ink bounds, and justification
test_fonts Borrowed font/glyph views and malformed payload bounds
test_symbols Symbol table lookup
test_pool Pool allocator (nodes, strings, lists, OOM)

Run with:

cd build/native && ctest --output-on-failure

On Device Autotests

The autotests/ directory contains a regression test suite that runs on CEmu via its autotester

Each test case renders a LaTeX expression on the calculator, then validates the LCD framebuffer against an expected CRC32 hash. Test cases are defined in autotests/casegen/cases.c:

{ "quadratic", NULL, "$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$", "025A9B2B", 10, 5 },

Running autotests:

cd autotests
make generate   # Generate test case directories from cases.c
make build      # Build all test .8xp programs
AUTOTESTER_ROM=/path/to/rom make test-all   # Run all tests in parallel

Adding a new test case:

  1. Add an entry to the appropriate suite in autotests/casegen/cases.c
  2. make generate && make build
  3. Run with --dry-run or set the CRC to "00000000", then use ./update_hashes.py to capture the actual CRC

Integrating into Your CE Project

To use libtexce in your own CE project:

1. Add the Source Files

Copy (or git submodule) the src/tex/ directory and include/texfont.h into your project. The core engine is these files:

src/tex/tex_util.c      src/tex/tex_pool.c
src/tex/tex_symbols.c   src/tex/tex_metrics.c
src/tex/tex_fonts.c
src/tex/tex_token.c     src/tex/tex_parse.c
src/tex/tex_measure.c   src/tex/tex_layout.c
src/tex/tex_renderer.c  src/tex/tex_draw.c

2. Include Paths

Your build must be able to find:

  • src/ and src/tex/ (internal headers)
  • include/ (public texfont.h)

3. Required Assets

Transfer these to the calculator alongside your .8xp:

  • TeXFonts.8xv and TeXScrpt.8xv (from assets/; keep archived for minimal RAM use)
  • clibs.8xg matching your CE toolchain's library versions (the bundled copy may be older than a current toolchain)

4. CMake Integration (Recommended)

if your CE project uses the provided CEdevToolchain.cmake, see demo/ce/CMakeLists.txt for a complete working example of cedev_add_program() with libtexce sources

Demos

The repository includes two demo programs that build for both the CE and PortCE (native SDL2):

Demo Description
demo_text Scrollable document renderer. Showcases fractions, integrals, Taylor series, matrices, and more in a paginated view.
demo_thread Chat-style threaded conversation. Multiple independent TeX_Layout objects drawn with a shared renderer, demonstrating the multi-layout pattern.

Build and run natively:

cmake --preset native && cmake --build build/native
./build/native/bin/demo_text_portce

License

libtexce is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only).

See LICENSE for the full text. See CONTRIBUTING.md for SPDX header guidance on new files.

The Latin Modern source fonts and renamed derived AppVars use the GUST Font License, with its incorporated LPPL.

About

A LaTeX rendering engine for the TI-84 Plus CE calculator. Renders mixed text and math expressions - fractions, integrals, matrices, Greek letters, and more, directly on the calculator's hardware

Topics

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages