DEV Community

Cover image for Building a TI-84 Plus CE Emulator in WebAssembly: Lessons from 100% Browser-Based Calculator Emulation
Furqan
Furqan

Posted on

Building a TI-84 Plus CE Emulator in WebAssembly: Lessons from 100% Browser-Based Calculator Emulation

Building a TI-84 Plus CE Emulator in WebAssembly: Lessons from 100% Browser-Based Calculator Emulation

TL;DR

I built a fully functional TI-84 Plus CE calculator emulator that runs entirely in your web browser using WebAssembly. It provides identical functionality to the $120+ physical calculator at zero cost. Try it live.


The Problem: Accessibility in Education

The TI-84 Plus CE graphing calculator costs $120-150, creating a financial barrier for many students who need it for math courses, standardized tests (SAT/ACT), and homework. I wanted to create a solution that:

  • Provides identical functionality to the physical device
  • Works on any device (phones, tablets, computers)
  • Requires no downloads or installations
  • Costs zero dollars

The solution? WebAssembly.


Why WebAssembly?

Performance Requirements

Calculator emulation is computationally intensive. We need to:

  • Emulate the eZ80 CPU (8-bit processor with 24-bit memory addressing)
  • Handle complex mathematical operations in real-time
  • Render a color LCD display at 60 FPS
  • Process touch/click input with minimal latency
  • Manage flash memory, RAM, and peripheral hardware

Traditional JavaScript couldn't deliver the performance needed. WebAssembly provides near-native C performance in the browser, making true hardware emulation possible.

Cross-Platform Compatibility

With WASM, the same compiled binary runs identically on:

  • Windows, macOS, Linux (desktop browsers)
  • iOS Safari, Android Chrome (mobile browsers)
  • No platform-specific builds needed
  • No app store approval required

Architecture Overview

Core Emulator (C/C++)

The core is based on CEmu, a third-party emulator focused on developer features. I compiled the desktop C core to WebAssembly using Emscripten.

Key Components:

// Core emulation modules
- CPU (eZ80 processor emulation)
- Memory management (flash, RAM, ports)
- LCD rendering (320x240 color display)
- Hardware peripherals (timers, interrupts, USB)
- Keypad input handling
- File I/O (.8xp program files)
Enter fullscreen mode Exit fullscreen mode

Web Interface (JavaScript)

The web layer handles:

  • Canvas rendering for calculator screen
  • Touch/mouse input → emulator keypad mapping
  • File upload/download for TI-BASIC programs
  • PWA (Progressive Web App) installation
  • Service worker for offline functionality

The Critical Bridge

WebAssembly doesn't have direct DOM access. We use Emscripten's bindings to bridge WASM ↔ JavaScript:

// JavaScript → WebAssembly
Module._keypad_press(keyCode);        // Send button press to emulator
Module._lcd_get_frame();              // Get LCD buffer pointer

// WebAssembly → JavaScript  
window.requestAnimationFrame();       // Render loop at 60 FPS
Enter fullscreen mode Exit fullscreen mode

The Build Process

Step 1: Compiling C to WASM with Emscripten

Makefile configuration:

CC = emcc

# Aggressive optimization for size (calculator needs to load fast)
CFLAGS = -W -Wall -Oz -flto

# WASM output with ES6 modules
EMFLAGS := -s WASM=1 
          -s EXPORT_ES6=1 
          -s MODULARIZE=1 
          -s TOTAL_MEMORY=33554432
          -s EXPORT_NAME="'WebCEmu'"
          -s "EXPORTED_RUNTIME_METHODS=['FS', 'callMain', 'ccall', 'HEAPU8']"

OUTPUT := WebCEmu

$(OUTPUT).js: $(OBJS)
    $(CC) $(CFLAGS) $(EMFLAGS) $^ -o $@
Enter fullscreen mode Exit fullscreen mode

Result: Single WebCEmu.js file with embedded WASM binary (~2MB gzipped)

Step 2: Canvas Rendering Loop

The emulator renders to a 320x240 pixel buffer in WASM memory. We copy this to a HTML5 Canvas every frame:

function startRendering() {
    const LCD_WIDTH = 320;
    const LCD_HEIGHT = 240;

    // Get pointer to LCD buffer in WASM memory
    const lcdBufferPtr = Module._lcd_get_frame();

    // Create typed array view into WASM memory
    const lcdBuffer = new Uint32Array(
        Module.HEAPU8.buffer, 
        lcdBufferPtr, 
        LCD_WIDTH * LCD_HEIGHT
    );

    const ctx = canvas.getContext('2d');
    const imageData = ctx.createImageData(LCD_WIDTH, LCD_HEIGHT);
    const imageData32 = new Uint32Array(imageData.data.buffer);

    function renderLoop() {
        // Copy emulator screen to canvas (60 FPS)
        imageData32.set(lcdBuffer);
        ctx.putImageData(imageData, 0, 0);

        requestAnimationFrame(renderLoop);
    }

    requestAnimationFrame(renderLoop);
}
Enter fullscreen mode Exit fullscreen mode

Performance: Consistent 60 FPS on modern hardware, ~30 FPS on mobile devices.

Step 3: Input Handling

Mapping web input to calculator buttons required careful touch event handling:

canvas.addEventListener('touchstart', (e) => {
    e.preventDefault();
    const touch = e.touches[0];
    const x = touch.clientX - rect.left;
    const y = touch.clientY - rect.top;

    // Map click coordinates to calculator keypad
    const keyCode = findKeyCode(x, y);
    if (keyCode !== null) {
        pressKey(keyCode);
    }
});

function pressKey(keyCode) {
    Module._keypad_press(keyCode);  // Send to emulator
    visualFeedback();                // Show button press animation
}
Enter fullscreen mode Exit fullscreen mode

Step 4: PWA Implementation

Making it installable as a native app required:

{
  "name": "TI-84 Plus CE Calculator Online",
  "short_name": "TI-84 CE",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait-primary",
  "icons": [...],
  "screenshots": [...]
}
Enter fullscreen mode Exit fullscreen mode

Result: Users can "Add to Home Screen" and use it offline after initial load.


Key Challenges & Solutions

Challenge 1: Large WASM Binary Size

Problem: Initial WASM file was 8MB uncompressed, causing slow load times.

Solution: Aggressive optimization flags

-Oz                          # Maximum size optimization
-flto                        # Link-time optimization
--closure 1                  # Google Closure Compiler
-s MINIFY_WASM_IMPORTS=1     # Shrink WASM exports
Enter fullscreen mode Exit fullscreen mode

Result: 2MB gzipped, loads in ~2-3 seconds on mobile.

Challenge 2: Memory Management

Problem: eZ80 uses banked memory addressing that doesn't map cleanly to WASM's linear memory.

Solution: Custom memory mapping functions

uint8_t *mem_get_ptr(uint16_t page, uint16_t offset) {
    // Map banked address space to WASM linear memory
    return wasm_memory_ptr + (page << 16) + offset;
}
Enter fullscreen mode Exit fullscreen mode

Challenge 3: File I/O

Problem: WebAssembly has no filesystem access.

Solution: Emscripten's virtual filesystem

// Upload .8xp file from user
const fileInput = document.getElementById('file-upload');
const arrayBuffer = await fileInput.files[0].arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);

// Write to emulator's virtual filesystem
Module.FS.writeFile('program.8xp', uint8Array);
Module._load_file('program.8xp');  // Tell emulator to load it
Enter fullscreen mode Exit fullscreen mode

Challenge 4: Touchscreen Support

Problem: Calculator UI designed for buttons, not touch.

Solution: Responsive button sizing + visual feedback

.keypad-button {
    min-width: 48px;    /* Thumb-friendly touch targets */
    min-height: 48px;
    touch-action: none; /* Prevent scrolling during button press */
}
Enter fullscreen mode Exit fullscreen mode

Performance Results

Metric Desktop Mobile
Initial Load 2-3 seconds 4-6 seconds
Render FPS 60 FPS 30-45 FPS
Memory Usage ~40 MB ~50 MB
Battery Impact Minimal Moderate

Mobile performance is playable but not ideal. The calculator is designed for desktop/tablet use primarily.


Security & Legal Considerations

ROM Distribution

Critical: We do NOT distribute TI's proprietary ROM firmware. Users must provide their own ROM dump from a physical calculator they own.

Implementation:

// ROM file handling (server-side validation)
if (!isValidROMFile(romFile)) {
    throw new Error('Invalid ROM file');
}
// Only accept ROMs with correct checksums
Enter fullscreen mode Exit fullscreen mode

Acceptable Use

  • Educational purposes
  • Student homework and practice
  • Teaching demonstrations
  • Developer testing

Not acceptable:

  • Test cheating (physical calculator required)
  • Copyright infringement (distributing ROMs)

What I Learned

WebAssembly is Production-Ready

WASM delivers the performance needed for complex applications. Modern browser support is excellent (96%+ global).

PWA > Native Apps (Sometimes)

For simple tools like calculators, PWAs offer:

  • Zero app store friction
  • Instant updates
  • Cross-platform codebase
  • Smaller development time

Education Needs Open Alternatives

Creating free alternatives to proprietary educational tools removes financial barriers for students worldwide.


Future Improvements

  1. Performance: WebGPU rendering for faster graphics
  2. Features: Link cable emulation for multi-calculator collaboration
  3. Mobile: Native iOS/Android apps for better battery life
  4. Accessibility: Screen reader support, high contrast mode

Try It Live

Demo: https://ti84ce.com

Tech Stack:

  • WebAssembly (Emscripten)
  • Canvas API
  • PWA (Service Worker + Manifest)
  • Vanilla JavaScript (no frameworks)

Source: Based on CEmu (GPL v3)


Discussion

Questions for the community:

  1. What other desktop applications should be web-emulated?
  2. How can we improve WASM tooling for better DX?
  3. What are the limits of browser-based emulation?

Share your thoughts in the comments!


Tags

webassembly #wasm #education #javascript #emulation #opensource #webdev #calculator #stem #accessibility


Note: This article discusses WebAssembly compilation techniques only. It does not reveal proprietary Texas Instruments code, ROM firmware, or trade secrets. All code examples are either publicly documented WebAssembly features or standard Emscripten compilation flags.

About the author: I'm a developer passionate about making educational tools accessible. Find me at ti84ce.com.

Top comments (0)