This command line tool is a pre-processor, assembler and scriptable emulator for Octo-flavoured CHIP-8 assembly language. You (should) have obtained this software from https://github.com/Timendus/chipcode/tree/main/octopus2.
Octopus 2 is a rewrite in Go of my original JavaScript implementation of the Octopus pre-processor. In this new version I have compiled in the assembler from John Earnest's c-octo and my Silicon8 emulator.
The pre-processors in versions one and two are functionally nearly identical, except that this implementation runs quite a bit faster and has image support built-in. No need to install an additional plugin. It also has beta support for colour images with multiple planes for XO-CHIP and dithering, which the original does not, and a rudimentary "standard library" of CHIP-8 routines at your disposal.
All in all this version is much more a one-stop shop for writing CHIP-8 ROMs than the original Octopus.
- Installing and running
- Pre-processor features
- Assembler features
- Emulator features
- Documentation generator features
You can download a binary of Octopus 2 from Github. It gets built automatically
each time the repository gets updated. Go to the Github actions
tab
and select the top succesful workflow on the main branch. Scroll down a bit to
where it says "Artifacts", and download Octopus 2 for your system.
Once you have obtained a working copy of Octopus, put it somewhere in your PATH so you can then run it on the command line using this syntax:
octopus -i <input file> -o <output file> <option 1> <option 2>Input file should be an assembly language file in Octo syntax with the extension
.8o. Output file can have the extensions .8o or .ch8 to output either the
pre-processed intermediate assembly language or the resulting binary. Or .md
to generate documentation from the input file. If you do not specify an output
file, it will dump the pre-processed assembly to standard output. Unless you
specify a -template, in which case it will dump the generated documentation to
standard output.
Valid parameters:
-color- Use ANSI codes for color output to the terminal (default behaviour on Linux and MacOS)-i string- Alias for-input-input string- The path of the input file-no-color- Do not use ANSI codes for color output to the terminal (default behaviour on Windows)-o string- Alias for-output(default "STDOUT")-output string- The path of the output file (default "STDOUT")-run string- Run the given code or binary in the embedded emulator instead (default "disabled")-template string- The path to a documentation generation template
Alternatively, you can clone this repository and build the binary youself:
git clone git@github.com:Timendus/chipcode.git
cd chipcode/octopus2
make linux # or make windows, or make macosThis requires that you have Go installed as well
as a functional C compiler. I believe the Linux and MacOS builds use GCC, the
Windows build uses Zig. This should not be hard to change by specifying a
different CC in the Makefile. It will build the program to a
folder called dist.
Options are boolean values that Octopus understands. You can set options to
true by providing them as parameters to the Octopus invocation on the command
line. For example:
octopus -i input.8o DEBUGAn option named DEBUG will now be true. Options you do not provide will
automatically be false.
You can also set options in your code with :const. If you set a constant to
zero, it will be false from the perspective of Octopus. Any other value will
be considered true.
You can use the :dump-options command to instruct Octopus to output all known
options and their values at that point in the program:
:const OPTION_1 1
:const OPTION_2 0
:dump-options
:const OPTION_2 1
:dump-options
octopus -i input.8o OPTION_3This will output:
Octopussifying 'input.8o' 🡆 'output.8o'
Options in 'input.8o' at line 3:
OPTION_1: true
OPTION_2: false
OPTION_3: true
Options in 'input.8o' at line 6:
OPTION_1: true
OPTION_2: true
OPTION_3: true
With Octopus, you can use :if, :unless, :else and :end in your code to
switch on options, like so:
: store-values
i := target
save v2 # This doesn't increment i for superchip
:if SCHIP # Conditionally fix that issue
vF := 3
i += vF
:end
return
Now the conditional code between :if and :end will be included in the output
file only if the option SCHIP is set.
Note that you can't use expressions. Only the immediate boolean value of an option can be used.
Here is a more complicated example, also showing the use of :else and
:unless (which is "if not", for lack of a not-operator).
: store-values
i := target
:if XOCHIP
save v3 - v4 # This XO-CHIP opcode doesn't increment i
:else
v0 := v3
v1 := v4
save v1 # This doesn't increment i on SUPERCHIP
:end
:unless VIP # So on anything but VIP, we need to increment i manually
vF := 2
i += vF
:end
return
When writing XO-CHIP code, you need to keep the code that executes in the first
3.5K of memory. Beyond that you can have another 60K of data. This is because
XO-CHIP does provide an instruction to load a 16-bit address into i (i := long <label>), but no instructions to jump to or call a 16-bit label.
However, when writing code it is much more convenient to be able to keep the
code and the data that the code operates on close together. Octopus can
automatically solve this issue for you, if you annotate your code with :segment code and :segment data.
Each file is considered to automatically start with :segment code, so you can
leave that out if your file does indeed start with executable code.
i := long table
load v4
# Do something intelligent with data...
:segment data
: table
0 1 2 3 4
When a project gets too large for a single source file, it is nice to be able to
split it up into more logical segments. The Octopus :include command allows
you to include another file into the current source file.
:include "renderer.8o" # Include as source file
:include "images/bitmaps.bin" # Include as binary data
If a file ends in .bin or .ch8, it will be included as a binary. Other file
extensions will be interpreted and included as text files. Except for some image
formats, see below.
Note that paths that start with std/ are reserved. Those will be interpreted
as an attempt to use the standard library instead, and will
not include your local files.
If a file ends in the extension .jpg, .jpeg, .bmp, .png, .gif, .tif
or .tiff, it will be interpreted as an image file and included in the source
file as image data. You can control the inclusion behaviour using modifiers:
:include "path/to/some-file.png" <optional modifiers>
Note that any alpha channels (transparency) will be ignored.
The image loader tries to make an educated guess as to what resolution sprites
you are trying to get out of the input image. Let's say you include, for
example, a file called horse.jpg that is 24 pixels wide and 16 pixels high.
Given the dimensions, Octopus will guess that you want to have six 8x8 sprites.
But it may not always guess correctly.
To override this behaviour and specify your own sprite dimensions, provide a
string with the format <width>x<height> as a modifier. Note that the width
needs to be a multiple of 8 to get useful results.
For example:
:include "horse.jpg" 8x4
The image you supply may not use the two colours you use in your CHIP-8 program. In fact, it may not be monochrome at all. Octopus will still try to reduce the image to a monochrome black and white image, rounding each pixel's colour to either black or white, whichever is closer:
This may not be what you want. To get more fine grained control over the rounding, or to include XO-CHIP images that use more than two colours, you can provide a palette of colours in the form of hexadecimal numbers between brackets. Octopus accepts both three and six digit colours, as long as they are hexadecimal RGB or RRGGBB values.
:include "clock.png" [000 AAA] # These are
:include "clock.png" [000000 AAAAAA] # Identical
In this example Octopus will round the colours to black and gray instead, resulting in a different cut-off point between black and white in the resulting binary data. If we nevertheless were to render it in black and white again, we would get a different image:
In the next example, the colours in our clock image get rounded to black, white, red and brown, and converted to a two-plane, four colour XO-CHIP image.
:include "clock.png" [000000 a22632 b86f51 FFFFFF]
The default behaviour is for the colours to get rounded to the nearest colour in either the given palette or the default palette of black and white. You can however instruct Octopus to apply a Floyd-Steinberg dither to the image, which is an often used technique to give the illusion of having more colours available than can really be represented.
Give dithered as a modifier to your image and Octopus will dither the image
prior to conversion.
:include "clock.png" [000000 a22632 b86f51 FFFFFF] dithered
Octopus will, by default, generate labels for the sprites included from an
image. Assuming the horse.jpg image is still 24x16 pixels, the following
labels will be generated, which relate to the X and Y positions of the sprites
within the image:
:include "horse.jpg"
horse-0-0horse-1-0horse-2-0horse-0-1horse-1-1horse-2-1
So in this example, horse-2-0 is the top-rightmost 8x8 sprite.
Using more colours will predictably produce more planes and more data. Labels are provided for each plane.
:include "horse.jpg" [c45b56 c64f4d 370816 000000]
The first digit in the label name will now indicate its plane, followed again by its X and Y position:
horse-0-0-0horse-1-0-0horse-0-1-0horse-1-1-0horse-0-2-0horse-1-2-0horse-0-0-1horse-1-0-1horse-0-1-1horse-1-1-1horse-0-2-1horse-1-2-1
The rightmost sprite now consists of horse-0-2-0 for the first plane and
horse-1-2-0 for the second plane. They follow each other in the order the
XO-CHIP sprite opcode expects them to, provided you have specified your
palette in the right order.
If you don't want Octopus to generate these labels and instead define your own,
provide the modifier no-labels:
: horse-image
:include "horse.jpg" no-labels
Provide the word debug as a modifier to let Octopus output the image to the
console, as well as the selected sprite resolution, the detected modifiers and
all the sprites that it has cut from the image. This quickly and easily lets you
inspect if the conversion was a success, and if everything went as you expected.
:include "horse.jpg" debug
This is a beta feature that I'm not 100% sure should live inside Octopus, but we'll see how it goes 😄
The pre-processor now also comes with a library of helper files. When you
:include a path that starts with std/, Octopus will instead look into its
internal library to load the path you requested.
The files that are currently available in the library and their usage are documented here:
The assembler is John Earnest's Octo assembler, which accepts Octo syntax. The
C-Octo version of it, to be precise.
This syntax was popularized by his excellent web based IDE and
emulator and the CHIP-8 gamejams
(Octojams) that he organized from
2014 to 2023. It's relatively powerful for an assembler, with some higher level
concepts like if / else statements and while loops. I believe he calls it
a compiler himself, but since nearly every line in the source file gets
converted one-to-one into one or two opcodes, I consider it to be a good
assembler with macro support.
Here are the necessary resources to get started:
- Octo syntax manual
- Beginner's guide
- more documentation can be found on Octo's github for those wanting to get into the weeds with it
Octopus 2 also comes with a version of Silicon8 built in, which is a CHIP-8, SCHIP and XO-CHIP emulator (or virtual machine if you want to be precise). This version runs in the terminal and can be scripted to go through a sequence of steps. Ideal for automating tests or automatically generating screenshots.
Note that it does require a terminal that supports 24-bit ANSI colours if you wish to show the emulator's screen in the terminal.
To use the emulator, add -run with a sequence of comma separated commands to
your Octopus invocation:
octopus -i input.8o -run "100, press: 1, 10, release: 1, 100, display"The emulator can also accept a binary ROM file (.ch8) as an input file.
As a bonus feature, newline (\n) is also accepted as a separator between
commands, which makes it really easy to write a little script in a file and
cat it in. The below is functionally equivalent to the one-liner above.
$ cat steps
100 # comments are allowed
# as are empty lines
press: 1
10
release: 1
100
display
$ octopus -i input.8o -run "`cat steps`"Due to the limitations of a terminal, it's not amazing at being an interactive
emulator, but you can use it as such nevertheless. Use the command
interactive:
octopus -i input.8o -run "interactive"Press Escape or Ctrl+C to leave interactive mode.
The QWERTY keys are mapped to CHIP-8 keys as follows:
1 = 1 |
2 = 2 |
3 = 3 |
4 = C |
q = 4 |
w = 5 |
e = 6 |
r = D |
a = 7 |
s = 8 |
d = 9 |
f = E |
z = A |
x = 0 |
c = B |
v = F |
Enter = 4 |
Up = 5 |
Space = 6 |
Left = 7 |
Down = 8 |
Right = 9 |
Also, the keys 5 - 9 and 0 map to the CHIP-8 keys 5 - 9 and 0.
You can combine interactive with other commands, and have an interactive part in an otherwise automated sequence. Maybe you script your way to the part of your program that you're working on, so you don't have to go through the motions every time, and then give control to the user for manual testing?
To just let the ROM run for a number of clock cycles, give it a number.
octopus -i input.8o -run "100"This will run your program for 100 cycles and then terminate. You will not see any real output...
...which is where display comes in. If you run this sequence:
octopus -i input.8o -run "100, display"Your program will run for 100 cycles and then it will show the display on the terminal.
The other option is to save the contents of the display to an image. Which is very useful if your terminal does not support ANSI colours, or for generating screenshots of your game.
The screenshot command takes a parameter, separated by a colon, with the name
of a file to save the image to. The currently supported image types are JPEG,
PNG, BMP, GIF.
An optional third parameter can specify the magnification of the image. CHIP-8
screenshots at 100% zoom on a modern display are usually microscopic and
unreadable. By applying some magnification we get more useful screenshots.
Defaults to 6x for hires and 12x for lores ROMs.
octopus -i input.8o -run "100, screenshot: image.png" # default scale
octopus -i input.8o -run "100, screenshot: image.png: 10x" # explicit scaleYou can also send press and release events to the emulator. These commands
require a numeric parameter, separated by a colon, that specifies which key you
want to press or release. Don't forget to give the program a few cycles in
between press and release to actually register the keypress:
octopus -i input.8o -run "100, press: 1, 10, release: 1, 100, display"Commands can also be used to manipulate and read memory. For this we have the
commands save and load. Both take two parameters, separated by colons.
save expects a memory address followed by a list of numbers to write to memory
(separated by whitespace, brackets optional). load expects a memory address
and the number of bytes you wish to read. For example:
octopus -i input.8o -run "save: 0x200: [1 2 3 4 5], load: 0x200: 5"This will output:
Octopussifying 'input.8o'...
Assembling 'input.8o'...
Finished processing in 3.770906ms
Running emulation sequence...
0200: 01
0201: 02
0202: 03
0203: 04
0204: 05
DoneThis is especially useful for writing tests, so you don't have to rely on visual output. Just feed your ROM some numbers, run the thing, read the output.
To further aid in this, Octopus sends all its regular output to stderr, and only
output generated by emulation sequence steps to stdout. This means you can
easily capture just the output you care about, either by caputuring or piping
stdout, or by sending stderr to /dev/null:
$ octopus -i input.8o -run "save: 0x200: [1 2 3 4 5], load: 0x200: 5" 2> /dev/null
0200: 01
0201: 02
0202: 03
0203: 04
0204: 05
$Finally, you can change the behaviour of the emulator itself. For this we have
the commands cpf and mode.
CPF stands for Clock cycles Per Frame and its parameter defines how many cycles you want the virtual CPU to run for 60 times per second. The default is set to 30, which results in a "real" clock frequency of 30 x 60 = 1.8 kHz.
The mode defines the type of emulation you want, which can be one of these:
vip- Original CHIP-8 behaviour as implemented on the Cosmac VIPblindvip- The same as VIP, but without the display wait quirk enabledschip- Super-chip behaviour (the "modern" version, not the "legacy" version)xochip- XO-CHIP behaviour as defined by Octo
Here's an example of both settings in use:
octopus -i input.8o -run "mode: schip, cpf: 100, interactive"To aid in the documentation of the standard library, I've
added a parser to Octopus that can generate documentation from CHIP-8 assembly
source files. It takes a template and a source file, and generates an output
file based on your provided template. If you request a .md file as the output
file, a default built-in markdown template will be used:
octopus -i input.8o -o output.mdThe generator understands three kinds of comment blocks:
- a block at the top of your file that defines a title and a description for the whole file
- a comment that defines a section in your file
- a comment that describes a routine, a macro or a constant
We'll go through them one by one.
At the top of the source file, in the first five lines, there may be a comment. This comment can be one of:
- A single line, which gets interpreted as the file's title
- A single paragraph, which gets interpreted as the file's description
- Or a single line, followed by a paragraph, which gets interpreted as a title and description
So for example, having this comment close to the top of your source file:
# Tile renderer
#
# This is the tile rendering subsytem, which renders the world around the
# player. Everything that moves or is animated is not a tile, and is not part
# of this subsystem.
will result in documentation that has the title Tile renderer and the
description that follows it.
Sections are an optional way to organize your documentation. You start a new section by using a comment that starts and ends with at least three hash symbols:
### Animations ###
All the routines, macros and constants following this section header will be considered part of this section, until a new section header is encountered.
You can add comments for the documentation generator to routines, macros and constants by putting them directly above the thing they pertain to (a single empty line in between is allowed) and starting them with at least three hash symbols:
###
# Render the given sprite to the display buffer.
#
# Inputs:
# - `v0` - number in the tilemap
# - `v1` - X coordinate
# - `v2` - Y coordinate
# - `v3` - `1` if tile should animate
#
# Destroys: `v0`, `v4`, `vF`, `i`
: render-sprite
# Your code here
return
Since I know I'm rendering to Markdown, I can use Markdown in my comments to give them some structure for the reader. All of this text will be part of the description of this routine in the output documentation.
In the case of constants, the value of the constant will also be shown. In the case of macros, the documentation will show the names of the parameters to the macro, so you don't have to add those manually.
Note that not all routines, macros and constants will be shown in the documentation, not even all the ones that have comments. Only those that have a valid comment above them that starts with the three hash symbols. This is by design, so you can document some things internally that don't need to end up in the generated documentation.
Also note that this works the other way around for the file level comment; this comment should not start with three hash symbols for it to show up in the generated documentation. Otherwise it is interpreted as the documentation for some entity level thing, not as a file level comment.
You can specify your own template to generate documentation with:
octopus -i input.8o -o output.md -template template.mdIn this case, if you omit the output file it will send the resulting documentation to standard output.
The generator is not restricted to markdown files, you could use this to generate all kinds of formats. If you prefer to generate HTML files or JSON or plain text, that's all fine. Just write the template for it.
The default markdown template can be found here, and can be a good starting point for writing your own templates. Octopus uses the Golang templating system, which is not very hard to read and understand, but it can be a bit confusing when it comes to whitespace.
The template gets fed a data structure that can be found at the top of this
file. It starts with a Doc, which has some
properties, like a filename and a title, and contains Sections that each can
hold Macros, Routines and Constants. Each of these will have a line
number, a name, a description and other properties.




