JS99'er
JS99'er, by Rasmus Moustgaard, emulates the TI-99/4A entirely in the browser, including a number of peripherals and near-complete support for the F18A video upgrade. Now built with Angular and TypeScript, it loads cartridges and disk images and runs in any modern browser.
Runs on: Web browser
JS99'er Online Emulator
Play JS99'er using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| TI-99/4A · TI BASIC | JS99'er | TI-99/4A | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
js99er is an Angular/TypeScript application, not a single script. We bundle a small boot entry with Vite (library mode, formats:['es']) into js99er-boot.js, load it as <script type="module">, and drive the machine ourselves rather than using js99er's Angular UI, so the loop can be paused and single-stepped, which the debugger needs.
Boot. The entry constructs just the TI994A console (the same class the app's ConsoleFactoryService builds), passing a plain Settings object and stubs for the Angular services it does not need. It then owns a loop built on the console's own frame() method:
import { TI994A } from "./emulator/classes/ti994a";
const ti = new TI994A(document, canvas, [], settings, dbStub, wasm, onBreakpoint);
ti.reset(false);
ti.getKeyboard().start(); // attach js99er's own key listeners
(function loop(){ ti.frame(); requestAnimationFrame(loop); })();
The machine is plain objects. Everything the debugger needs is reachable off the console, no wasm heap to reach into for the CPU state:
| Member | Kind | What it does |
|---|---|---|
ti.frame(skipBp) | method | Run one video frame: draws every scanline and steps the TMS9900 in lockstep. The frame-step primitive. |
cpu.run(1, true) | method | Run one CPU instruction (skipping the breakpoint check). The single-instruction step. |
cpu.getPc() / getWp() | methods | The Program Counter and Workspace Pointer, readable and writable (setPc/setWp). |
cpu.st | field | The 16-bit status register (L> A> EQ C OV OP X + interrupt mask). |
cpu.setBreakpoints([...]) | method | Give js99er's CPU a list of Breakpoint objects (INSTRUCTION or CPU_MEMORY_WRITE); run() stops on them. |
memory.getWord(a) | method | Side-effect-free read of the 16-bit big-endian CPU bus, what the hex/disassembly views use. |
memory.writeWord(a, w, cpu) | method | Bus write, poke memory live. |
vdp.getRAM() | method | The 16 KB VDP VRAM (a view into the render WASM's memory). |
keyboard.simulateKeyDown/Up(keyCode) | methods | Drive the TI key matrix from a JS keyCode, used by the on-screen keyboard. |
Because the CPU, bus and RAM are ordinary JavaScript, the debugger single-steps with cpu.run(1), reads WP/PC/ST straight off the CPU, and implements breakpoints and watchpoints through js99er's own setBreakpoints - no changes to the emulator core.
Debugger integration
Wiring js99er into the shared in-browser debugger needed two pieces of custom work, plus a set of techniques for reaching into the live machine.
1 · A boot shim, because js99er is an Angular app. Its machine core is framework-agnostic TypeScript, but it is normally wired together by Angular services and driven by an internal requestAnimationFrame loop that cannot be paused or single-stepped from outside, which the debugger needs. So we wrote a small boot entry that constructs the TI994A directly, replaces the three @Injectable services it touches (WASM, database, dialog) with plain build shims, and drives it from a loop we control:
// our loop, not js99er's, so pause / step / breakpoints work
function stepInsn(n){ while(n-- > 0) cpu.run(1, true); }
function loop(){ if(!running) return; ti.frame(bps.size === 0); if(running) requestAnimationFrame(loop); }
2 · Bundled as an ES module, not an IIFE. js99er's modules import each other and pull in a small AssemblyScript WASM module for video. Building with Vite in library mode with formats:['es'] keeps live module bindings and lets the boot instantiate the WASM (an imported 21-page memory that holds VRAM + the framebuffer, exactly as js99er's WasmService does). Because a module is deferred, the boot publishes window.EMU_BOOT asynchronously, so the debugger plug-in polls for it.
Techniques for deeper access. The plug-in reaches into the running machine through js99er's own surfaces:
- No fixed registers. The TMS9900 has no on-chip general-purpose registers; its sixteen "workspace" registers live in RAM at the address in WP. We expose WP, PC and ST (plus the status flags) as the CPU registers, and surface R0–R15 as a live view of the words at
WP+2n. - Side-effect-free reads. The hex and disassembly views scrub memory with
memory.getWord()(not the bus read that runs I/O), so auto-polling the view is safe. The bus is 16-bit big-endian, so a byte view picks the high byte at even addresses. - Native breakpoints. Execution breakpoints and memory-write watchpoints are handed to js99er's CPU as
Breakpointobjects viasetBreakpoints; itsrun()stops on them and calls ouronBreakpoint, which pauses the loop. - Single instruction step.
cpu.run(1, true)executes exactly one instruction.
Everything the debugger shows - WP/PC/ST and the workspace registers, the CPU bus and VDP VRAM in hex/disassembly, follow-PC, single-step, breakpoints and watchpoints - is built from these, using the shared tms9900 disassembler and with no changes to js99er itself.
Architecture
js99er is a full TI-99/4A by Rasmus Moustgaard. Each hardware block is its own class hanging off the TI994A console:
TMS9900- the 16-bit CPU interpreter. Big-endian, with the workspace-register model (WP points at sixteen registers in RAM) and the BLWP/RTWP context-switch mechanism.Memory- the console address map: system ROM, the 256-byte scratchpad, the 32K expansion RAM, GROMs and cartridge space, plus peripheral (DSR) cards.TMS9918A- the VDP video chip; its 16 KB VRAM and the RGBA framebuffer both live in a small AssemblyScript WASM module that rasterises each scanline. (js99er can also emulate the F18A and V9938.)TMS9919(SN76489) sound,TMS5200speech,CRU,Keyboard, disk controllers and RAM disks.System- the console ROM and GROM, embedded as byte arrays (the TI operating system and TI BASIC GPL).
Each frame() draws the visible scanlines and runs the CPU (and, on an F18A, its GPU) in lockstep. Because every component is an ordinary object, the whole machine state is inspectable, which is what the debugger reads each refresh.