Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Octopus 2

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.

Index

Installing and running

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

Building

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 macos

This 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.

Pre-processor features

Options

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 DEBUG

An 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_3

This 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

Conditional code inclusion

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

Automatic re-ordering of code

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

Including files

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.

Image 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.

Sprite resolution

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

Colours

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:

Original pixelart of a clock The clock forced into black and white

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:

Original pixelart of a clock The clock forced into black and gray

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]

Original pixelart of a clock The clock forced into four colours

Dithering

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

Original pixelart of a clock The clock in four colours with dithering

Labels

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-0
  • horse-1-0
  • horse-2-0
  • horse-0-1
  • horse-1-1
  • horse-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-0
  • horse-1-0-0
  • horse-0-1-0
  • horse-1-1-0
  • horse-0-2-0
  • horse-1-2-0
  • horse-0-0-1
  • horse-1-0-1
  • horse-0-1-1
  • horse-1-1-1
  • horse-0-2-1
  • horse-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

Debug

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

"Standard library"

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:

library/docs

Assembler features

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:

Emulator features

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`"

Interactive mode

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?

Run N cycles

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...

Display

...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.

Screenshots

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 scale

Key input

You 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"

Memory

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
Done

This 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
$

Settings

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 VIP
  • blindvip - The same as VIP, but without the display wait quirk enabled
  • schip - 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"

Documentation generator features

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.md

Writing documentation

The generator understands three kinds of comment blocks:

  1. a block at the top of your file that defines a title and a description for the whole file
  2. a comment that defines a section in your file
  3. a comment that describes a routine, a macro or a constant

We'll go through them one by one.

1. File level comment

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.

2. Section level comments

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.

3. Entity level comments

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.

Using custom templates

You can specify your own template to generate documentation with:

octopus -i input.8o -o output.md -template template.md

In 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.