The LLVM-MOS compiler toolchain and platform libraries.
- Atari 2600
- Atari 8-bit
- DOS ".XEX" file
- 8-KiB or 16-KiB standard cartridge
- XEGS bank-switched cartridge from 32 KiB to 512 KiB (8 KiB banks)
- Ben Eater's Breadboard 6502 Computer
- Commander X16
- Commodore 64
- Commodore 128
- Commodore PET
- Commodore VIC-20
- CP/M-65
- Dodo 6502 Game System
- MEGA65
- NES
- Ohio Scientific Challenger 1P
- PC Engine
- PC Engine CD
- RPC/8e (RedPower 2)
- 6502 simulator (included)
- Broad C99 and C++11 freestanding standards compatibility
- Interrupt handling
- C++ templates
- C++ virtual functions
- C++ new/delete
- C++ Run-Time Type Information (dynamic_cast, typeid)
- C++ static constructors/destructors (run before and after main)
- C++ "magic" function local static constructors/destructors
- The high and low-level optimizations expected of a young-ish LLVM backend
- Sophisticated register allocation over A, X, Y, and a field of 16 2-byte zero-page (imaginary) registers
- The imaginary registers can be placed anywhere and need not be contiguous.
- The calling convention passes through registers whenever possible.
- Loop optimizations to select 6502 addressing modes
- Whole program "static stack" optimization
- Automatically identifies non-reentrant functions and allocates their frames as static globals
- Programs without recursion or complex function pointers may not need a soft stack at all.
- No manual annotations required
- Link time inlining and optimization across the whole program
- Includes SDK libraries. Library calls can be often optimized away completely!
- Excellent compiler usability
- Clang's world-class error messages
- IDE integration through the included custom clangd's Language Server Protocol
- Straightforward invocations to compile for various targets:
mos-c64-clang++ -Os -o game.prg game.cc
- A small standard library sufficient to provide the above and a few extras
- Simple printf
- Simple malloc/free
- exit, _Exit, and atexit
- An ELF file format implementation
- All the usual POSIX tools for working with object files: readelf, nm, etc.
- A GAS-compatible assembler for the 6502 with a complete macro system
- A lld linker implementation for the 6502
- All the usual trimmings of an ELF lld backend
- Link-time garbage collection
- Symbol map exports
- Linker scripts
- GCC ld compatibility
- All the usual trimmings of an ELF lld backend
- A hosted C with all the standard library bells and whistles.
- C++ Exceptions
First, download and extract the archive for your platform.
On macOS, downloading a package automatically applies a quarantine to the file. This will also affect the extracted binaries from the package, which causes GateKeeper to prevent them from running. To avoid this, run the following on the downloaded package before extracting it:
$ xattr -d com.apple.quarantine llvm-mos-macos.tar.xz
If you like, you can add LLVM-MOS to your path. This will make accessing LLVM-MOS from the command line easier.
WARNING: Don't install LLVM-MOS into your path if you already have LLVM/Clang installed. LLVM-MOS conflicts with other LLVM/Clang installations.
Add the following line to your shell profile (~/.bashrc
, ~/.zshrc
, etc...):
export PATH=$PATH:<arbitrary-install-directory>/bin
To work with CMake-enabled IDEs, it may also need to be added to your desktop
profile (~/.gnomerc
,
KDE, etc...).
rundll32.exe sysdm.cpl,EditEnvironmentVariables
# Edit "Path" user variable
# Add entry for "<arbitrary-install-directory>\bin"
Afterwards, new shells will have direct access to LLVM-MOS.
Once installed, you can compile a sample program with a direct command. You will
need to prefix clang
(or clang++
) with a specific MOS platform provided by
the SDK. This will ensure clang loads the correct configuration to generate
executables and libraries for that target.
Platform | Variant | Command |
---|---|---|
Atari 2600 | 4K | mos-atari2600-4k-clang |
Atari 2600 | TigerVision 3E | mos-atari2600-3e-clang |
Atari 8-bit | DOS | mos-atari8-dos-clang |
Atari 8-bit | Standard cartridge | mos-atari8-stdcart-clang |
Atari 8-bit | XEGS cartridge | mos-atari8-xegs-clang |
Ben Eater's 6502 Breadboard Kit | - | mos-eater-clang |
Commander X16 | - | mos-cx16-clang |
Commodore | 64 | mos-c64-clang |
Commodore | 128 | mos-c128-clang |
Commodore | PET | mos-pet-clang |
Commodore | VIC-20 | mos-vic20-clang |
CP/M-65 | - | mos-cpm65-clang |
Dodo 6502 Game System | - | mos-dodo-clang |
MEGA65 | - | mos-mega65-clang |
NES | Action53 mapper | mos-nes-action53-clang |
NES | CNROM mapper | mos-nes-cnrom-clang |
NES | GTROM mapper | mos-nes-gtrom-clang |
NES | MMC1 mapper | mos-nes-mmc1-clang |
NES | MMC3 mapper | mos-nes-mmc3-clang |
NES | NROM mapper | mos-nes-nrom-clang |
NES | UNROM mapper | mos-nes-unrom-clang |
NES | UNROM-512 mapper | mos-nes-unrom-512-clang |
Ohio Scientific Challenger 1P | - | mos-osi-c1p-clang |
PC Engine | Standard | mos-pce-clang |
PC Engine | CD | mos-pce-cd-clang |
RPC/8e (RedPower 2) | - | mos-rpc8e-clang |
6502 simulator | - | mos-sim-clang |
$ cat <install_dir>/examples/hello-putchar.c
#include <stdio.h>
int main(void) {
const char *cur = "HELLO, PUTCHAR!\n";
while (*cur)
__putchar(*cur++);
return 0;
}
$ mos-c64-clang -Os -o hello.prg <install_dir>/examples/hello-putchar.c
$ ls -l hello.prg
... 77 ... hello.prg
$ hexdump -C hello.prg
00000000 01 08 0b 08 5d 1e 9e 32 30 36 31 00 00 00 20 1e |....]..2061... .|
00000010 08 4c 14 08 60 8d 4c 08 20 13 08 ad 4c 08 60 a2 |.L..`.L. ...L.`.|
00000020 01 a9 48 c9 0a f0 10 20 d2 ff bd 3b 08 e8 e0 11 |..H.... ...;....|
00000030 d0 f1 a2 00 a9 00 60 a9 0d 4c 26 08 48 45 4c 4c |......`..L&.HELL|
00000040 4f 2c 20 50 55 54 43 48 41 52 21 0a 00 |O, PUTCHAR!..|
0000004d
$ mos-c64-clang -Os -o hello.s -Wl,--lto-emit-asm <install_dir>/examples/hello-putchar.c
$ cat hello.s
.text
.file "ld-temp.o"
.section .text.main,"ax",@progbits
.globl main
.type main,@function
main:
ldx #1
lda #72
.LBB0_1:
cmp #10
beq .LBB0_4
.LBB0_2:
;APP
jsr __CHROUT
;NO_APP
lda .L.str,x
inx
cpx #17
bne .LBB0_1
ldx #0
lda #0
rts
.LBB0_4:
lda #13
jmp .LBB0_2
.Lfunc_end0:
.size main, .Lfunc_end0-main
...Superfluous ASM...
.type .L.str,@object
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "HELLO, PUTCHAR!\n"
.size .L.str, 17
Note that the generated ASM output will contain more than actually ends up in the binary. This is because this assembly is emitted before a link-time garbage collection pass discards functions that aren't actually referenced.
A CMake package and toolchain file are provided to make targeting MOS from CMake easy.
Create a new source directory with a CMakeLists.txt
like the following where
LLVM_MOS_PLATFORM
is set to any platform supported by the SDK:
cmake_minimum_required(VERSION 3.18)
set(LLVM_MOS_PLATFORM c64)
find_package(llvm-mos-sdk REQUIRED)
project(llvm-mos-sdk-foo)
add_executable(foo foo.c)
Note: If LLVM-MOS was not added to PATH, set
-DCMAKE_PREFIX_PATH=<arbitrary-install-directory>
to match the install prefix
of LLVM-MOS so find_package
will work correctly.
To modify the SDK, you'll need to be able to build it yourself. This requires a working LLVM-MOS compiler, which can be found in the current SDK release. Accordingly, make sure to install the SDK first using the instructions above.
For the steps below to work as-is, you'll need to install Ninja, the fast, parallel build tool favored by LLVM developers. Instructions for your platform will vary; see https://ninja-build.org/.
Alternatively, you can set -G "Makefile" in each CMake command to use standard UNIX Makefiles, or you can substitute any other CMake-supported generator. Your compile times may take a hit, and LLVM is already very slow to build, so Ninja is highly recommended.
Set CMAKE_INSTALL_PREFIX
below to the LLVM-MOS installation directory. This will replace the SDK portion of the installation with the newly built artifacts on ninja install
.
$ git clone https://github.com/llvm-mos/llvm-mos-sdk.git
$ cd llvm-mos-sdk
$ mkdir build
$ cd build
$ cmake -G "Ninja" -DCMAKE_INSTALL_PREFIX=<sdk-install-directory> ..
$ ninja install
The complete SDK will now be present in the install prefix.
Install Mesen-X and its dependencies.
Set the MESEN_DIR
environment variable to the folder containing the Mesen.exe executable before running CMake for the first time.
Copy test/mesen_settings.xml
to this folder.
Install emutest (requires Go 1.21):
$ go install github.com/kivutar/emutest@latest
Make sure $GOBIN
(usually ~/go/bin
) is included in your PATH
environment variable so that CMake can find the binary, or set the
EMUTEST_DIR
environment variable to point to this directory before
running cmake -G
for the first time.
You can verify emutest with emutest -h
on the command-line.
You should see -T Test runner mode (script must call os.exit)
in the
output.
Build Libretro cores for desired target(s):
- Atari 2600 - https://github.com/libretro/stella2014-libretro
- NES - https://github.com/NovaSquirrel/Mesen-X
Copy the output Libretro core library files (they have extensions .so | .dylib | .dll) to a shared directory, maybe $HOME/libretro
.
Set the LIBRETRO_CORES_DIR
environment variable to this folder before running cmake -G
for the first time.
$ ninja test # run all test projects
$ ninja test-nes-nrom # run a specific test project, e.g. nes-nrom