Write TMS9900 assembly for the TI-99/4A in VS Code. Build cartridges, disk images and Editor/Assembler programs, and launch them in an emulator without leaving the editor.
Uses xdt99 as the assembler and disk manager. xdt99 is a separate project and is not bundled — see Requirements.
Hover any mnemonic for the full picture: instruction format, operand pattern, cycle count, address-mode penalties and which status flags it touches.
MOV source,destination
Move Word — format 1
Copies source to destination and sets status from the value moved.
Cycles: 14
Address mode penalty: *R +4, *R+ +8, @LABEL +8, @LABEL(R) +8
Status flags: L> (logical greater than), A> (arithmetic greater than), = (equal)
The Editor/Assembler utilities are built in too, so REF VMBW resolves and
hovers with its calling convention instead of raising a false error.
Label at column 1, opcode at 8, operand at 13, comment at 31 — all configurable. Trailing comments on definitions become hover documentation, so the comments you already wrote start earning their keep.
Editor/Assembler strict, xas99 extended and xas99 relaxed disagree about where the operand field ends. One setting drives tokenizing, formatting and the assembler flag together, so they cannot drift apart.
This matters more than it sounds. Legacy TI source frequently separates a
trailing * comment from the operand by a single blank. Under xas99's default
syntax that blank belongs to the expression, so this:
MOV @SSNKSZ,@SNKSIZ * Reset the snake size
is read as a multiplication, and the assembler goes looking for a symbol called
RESET THE SNAKE SIZE. On a real 1,500-line game that is a dozen errors that
look nothing like the actual problem.
The extension flags those lines as you type, offers a one-character quick fix, and can migrate a whole file with TI-99: Convert Source to xas99 Syntax — in a minimal mode that touches only the lines that would change meaning.
| Output | Runs on |
|---|---|
| MAME RPK cartridge | MAME |
| Padded raw cartridge binary | Classic99, FinalGROM 99, FlashROM 99 |
| Editor/Assembler option 5 image | Any E/A environment |
| Editor/Assembler option 3 object | Any E/A environment |
| TI disk image | MAME, real hardware, HxC |
| TIFILES | Classic99 FIAD folders |
Press F5. The extension assembles, packages, verifies the artifact exists, reports any errors in the Problems panel, and launches your emulator.
Assembler errors arrive as proper diagnostics with the offending token underlined, not the whole line — and without the duplicates xas99 emits once per pass.
A TI-99 program reaches its user in one of three ways, and each needs a
different prologue: a cartridge needs AORG >6000 and a standard header, an
Editor/Assembler option 5 image needs a branch at offset 0 because option 5
executes from the load address, and anything that resolves the entry by name
needs only DEF.
A project can declare targets in ti99.json — distribution routes that each
override part of the configuration and inherit the rest:
TI-99: Build builds them all; TI-99: Build Target... builds one. Each
target gets its own build/ and dist/ folder, and each is offered as a
separate task under Run Task.
Projects that declare no targets behave exactly as before.
xdt99 ships as the default profile, but the toolchain model is declarative.
Register another assembler in ti99.toolchain.profiles with its own detection
rules, argument templates and problem matcher. Capabilities drive the UI, so a
tool that cannot produce RPKs simply will not offer cartridge output.
| Python 3.8+ | xdt99 is written in Python |
| xdt99 3.6.0+ | github.com/endlos99/xdt99 — a folder of .py files, not a pip package |
| An emulator | Optional, but needed for Build and Run |
Run TI-99: Configure Toolchain after installing. The extension searches
PATH, XDT99_HOME, and <workspace>/tools/xdt99 before asking.
No TI ROMs, GROMs, cartridge images or system firmware are distributed with this extension, and it will not download them. Point the settings at your own copies.
From a release. Download the .vsix from the
Releases page, then
either drop it on the Extensions view, or:
code --install-extension ti99-dev-suite-<version>.vsix
From the Marketplace. Not published yet — see Project status.
From source. See docs/DEVELOPMENT.md.
- TI-99: Create New Project, name it, pick a syntax dialect, and choose which route Build and Run should use by default.
- Edit
src/main.a99. - Press F5.
The new project prints HELLO WORLD! and already builds three ways — a
cartridge, an Editor/Assembler program, and an Extended BASIC disk — from that
one file:
src/
main.a99 the program - the file you edit
targets/
cart.a99 cartridge -> dist/cart/
ea.a99 Editor/Assembler -> dist/ea/
disk-xb.a99 Extended BASIC disk -> dist/disk-xb/
boot/
LOAD.b99 Extended BASIC loader, tokenised onto the disk
src/main.a99 carries no AORG, no cartridge header and no DEF: each
wrapper supplies the prologue its loader needs and then COPYs the body, so
adding to the program means editing one file. It is commented for someone new
to the machine — the VDP ports, what lives in scratchpad, why a cartridge
cannot write to its own data, and where to put your code — which also gives an
assistant enough context to help you write it.
The utility routines it ships with (VSBW, VMBW, VWTR, CLS) mean the
program needs nothing from the Editor/Assembler, which is what lets the same
source run from a cartridge on a bare console.
Importing existing code instead? TI-99: Import Existing Source detects the dialect of your source and never modifies a file.
A TI-99/4A cannot run assembly from disk on its own. The console boots to TI
BASIC, which has no CALL LOAD, and the disk controller serves files rather
than executing them. Something in the cartridge port has to supply the loader,
so the route you choose is really a choice about what your user already owns.
| Route | They need | You ship |
|---|---|---|
| Cartridge | nothing | .rpk for MAME, or a C.BIN for Classic99 and FinalGROM 99 |
| E/A option 5 | Editor/Assembler + 32K | a memory image, loaded by filename |
| E/A option 3 | Editor/Assembler + 32K | a tagged object, entered by program name |
| Extended BASIC | Extended BASIC + 32K | a disk with a LOAD program |
Extended BASIC was in far more homes than Editor/Assembler, so an XB disk usually reaches more people than an E/A one. A cartridge reaches everybody.
Only some loaders resolve the entry point by name:
| Loader | Finds the entry by | Needs |
|---|---|---|
| console (cartridge) | DATA in the >AA header |
a standard header |
| E/A option 3 | name, from DEF |
nothing |
XB CALL LINK("NAME") |
name, from DEF |
nothing |
| E/A option 5 | the load address | entry first, or a branch to it |
xas99 --embed-xb |
the code base | entry first, or a branch to it |
If your entry point is not the first thing emitted — and it usually is not,
because data tables tend to come first — the last two rows execute your data as
instructions. A single B @MAIN ahead of everything satisfies both.
Embedded, one file. xas99 --embed-xb stores the code inside an XB program.
The disk holds only LOAD. Simple to ship, but XB reads the whole program
before running a single statement, so a large program means a long silence with
nothing on screen.
Loader plus object, two files. A small XB program prints progress and
CALL LOADs the object. It costs a second file, and buys feedback: the loader
can print each step, and sprites set moving with CALL SPRITE keep moving
during the load because the console interrupt routine drives them. If the
machine hangs, they stop — which tells a user more than a frozen screen does.
XB does not have to survive. If your program never returns, it can own the machine, but it must stop XB's interrupt routine first or the two will fight over scratchpad. A bootstrap ahead of the game, and no change to the game itself, is usually enough:
XBBOOT LIMI 0 * before touching anything
LWPI MYWS * our own workspace
CLR R0
MOV R0,@>83C4 * address of the user interrupt routine
MOVB R0,@>83C2 * flag byte controlling it
B @MAINThose two locations are documented in appendix 24.3.1 of the Editor/Assembler manual. Clearing them leaves the console interrupt routine doing only its own work — the timer, sprite motion, and the sound list — which is what a program written for a cartridge already expects.
Every build that produces a disk image prints this, with the paths filled in:
xhm99.py -T snake.dsk -o snake.hfe
HFE is what a Gotek, a greaseweazle or an HxC floppy emulator expects, and a
greaseweazle writes it to a physical disk. Converting back with -F returns
the .dsk unchanged, so nothing is lost in the round trip. The .dsk itself
works directly in Classic99, MAME and js99er.
| Command | Default key |
|---|---|
| TI-99: Build | Ctrl+Shift+B |
| TI-99: Build and Run | F5 |
| TI-99: Build Target... | |
| TI-99: Rebuild / Rebuild Target... / Clean | |
| TI-99: Create New Project | |
| TI-99: Import Existing Source | |
| TI-99: Convert Source to xas99 Syntax | |
| TI-99: Detect Source Dialect | |
| TI-99: Check for Dialect Hazards | |
| TI-99: Show Toolchain Status | |
| TI-99: Open Build Listing | |
| TI-99: Show Symbols | Load the symbol table from the last build into the Symbols view. The older ti99.showMemoryMap command id still works as an alias; it never produced a memory map, and the real memory map remains future work. |
| TI-99: Show Disk Catalog | |
| TI-99: Export to Real Hardware |
New projects use the canonical extension for their language. The aliases are recognised as first-class equivalents, not as deprecated spellings.
| Language | Canonical | Alias |
|---|---|---|
| TMS9900 Assembly | .a99 |
.asm |
| TI BASIC | .b99 |
|
| TI Extended BASIC | .xb99 |
.xb |
| GPL | .g99 |
.gpl |
| BASIC, dialect unspecified | .bas |
.a99 and .g99 are the extensions xas99 and xga99 already look for, so the
assembly and GPL naming is xdt99 naming rather than something invented here.
.xb99 is a modern convention introduced by this extension for consistency
with that family; it was never used on the original TI.
.b99 needs one caution. xbas99 writes it when detokenizing either
dialect, because xbas99 does not distinguish them, so an existing .b99 file
may well hold Extended BASIC. This extension adopts .b99 for new TI BASIC
source but never assumes an existing one is TI BASIC: project configuration, a
per-file override, or an Extended BASIC construct in the source all take
precedence over the name.
.bas says nothing about dialect and is treated as neutral. When the dialect
cannot be established the extension asks, because absence of Extended BASIC
syntax is not evidence of TI BASIC. Every valid TI BASIC program is also a
valid Extended BASIC program, so there is nothing to infer from.
See docs/source-naming.md for the full precedence rules.
Right-clicking a TI source file gives a TI-99/4A submenu whose entries depend on what the file actually is:
- An entry source offers Build, Run, Build and Run, Build and Run As, Package and Validate.
- A module belonging to one or more targets offers Build Containing Target rather than pretending to be a standalone program.
- A
.basfile of unknown dialect offers only the dialect question.
Build and Run uses the default target, so ordinary work is one click.
Build and Run As... always shows the target list. A target that is
compatible but unconfigured stays in the list and names the setting to fix.
The same commands work from the Command Palette. Both surfaces call one resolver, so they cannot offer different targets for the same file.
Classic99 is launched with classic99.exe -rom <file>. This is not in the
Classic99 manual; it was confirmed by the author. Cartridge type is inferred
from the last character of the filename (C plain ROM, 8 non-inverted 378
banking, 3 inverted 379, G GROM), and the extension names the output
accordingly. Classic99 cannot write to .dsk images, so disk projects drop
TIFILES straight into your configured DSK1 folder.
MAME takes the RPK directly and is the better default for cartridge work.
Disk projects need your own Editor/Assembler cartridge image; set
ti99.emulator.eaCartridgePath.
Win994a is launch-only. It has no documented command line for loading a program; files go in through its own Disk Manager.
Js99er runs in a browser, so the extension opens it and reveals the build output for you to drag in.
- Analysis is per-file. Cross-file symbol resolution is limited to
COPYnavigation until the language server lands. - No source-level debugging.
- GPL (
xga99) projects are not yet supported. - Desktop VS Code only. Web extensions cannot start local processes, which rules out running Python, xdt99 or an emulator.
The single authoritative specification for what this product is intended to be, and where the implementation currently stands, is docs/requirements-master.md. It carries the requirements, the implementation status, the product decisions and the phased roadmap.
Supporting documents: the implementation audit, the BASIC design report, and the source naming convention.
The original assembly-only specification is archived at docs/archive/requirements-assembly-original.md and is historical reference only.
Early but usable. The version number is honest: the language support, formatting, dialect handling, build pipeline and emulator launching all work and are used on real projects, but the extension has had few users besides its author, so expect rough edges on setups unlike mine.
Not yet on the VS Code Marketplace. Install from a release in the meantime.
Reports from people running different toolchain versions, emulators and source dialects are the most useful thing anyone can contribute right now.
Both are welcome, and both go through GitHub Issues.
- Report a bug — the output from the TI-99 channel in the Output panel is usually the single most useful thing to attach, along with your xdt99 version and emulator.
- Request a feature — describing the TI-99/4A workflow you are trying to complete helps more than proposing an implementation.
Suspected security issues should be reported privately rather than in a public issue. See SECURITY.md.
A note on code contributions: the project is maintained directly by KF1TZ Software and is not currently accepting external pull requests. Please open an issue rather than sending a patch — CONTRIBUTING.md explains the reasoning. Forking for your own use is welcome under the MIT licence.
Maintained by KF1TZ Software.
Released under the MIT licence — you may use, modify and redistribute it, including commercially, provided the copyright notice and licence text are kept.
Third party software this extension works with, and the trademarks it references, are recorded in NOTICE.md. Notably xdt99 is GPL v3 and is invoked as a separate program, never bundled or modified.
xdt99 by Ralph Benzinger, GPL v3 — invoked as an external program, not bundled or modified. The xdt99 IntelliJ plugin was the reference for tokenizing this language correctly.
Classic99 by Mike Brent. TMS9900 cheat sheet by Stefan "SteveB" Bauch with Lee Stewart and the AtariAge TI forums.
TI-99/4A and Texas Instruments are trademarks of Texas Instruments Incorporated. This project is not affiliated with or endorsed by Texas Instruments, the xdt99 project, the MAME project, or any emulator author.