BytePusher
BytePusher is a minimalist virtual machine invented by user:Javamannen in 2010. You can think of it as a simplified CHIP-8 with bigger memory, bigger screen with colors, and sampled sound. BytePusher has a ByteByteJump one-instruction CPU. The barebones hardware abstraction layer provides only memory-mapped pixel and audiosample buffers, keystates and a fixed framerate. All higher-level stuff (text, sprites, tiles, scrolling, sound synthesis etc.) will have to be done in software.
Features
BytePusher is:
- Simple: BytePusher takes minimalism to the extreme. A complete VM including graphics and sound can be written in less than 100 lines of C code.
- Cross-platform: BytePusher's simple structure makes it very easy to write a VM for any OS (or even without an OS). It would also be simple to implement in actual hardware. All programs run cycle-identical and bit-identical across all platforms.
- Persistent: The complete machine state can be saved to a file, and later be reproduced exactly.
- Fixed-size: BytePusher's memory size, screen resolution and keyboard layout are fixed.
- Fixed-speed: BytePusher is specified as a realtime system with fixed framerate, CPU speed and audio rate. It executes a fixed number of instructions per frame, which means that a BytePusher program can't "hog" the host CPU.
- 100% sandboxed: A BytePusher VM has its own fixed-size "sandbox" memory, which it can never read or write outside of.
Intended uses
- Games: BytePusher is well suited for implementation of 2D games.
- Demos: BytePusher could serve as an "old-school" platform for the demoscene.
- Education: Its simplicity and directness makes BytePusher an excellent tool for learning and experimentation.
Specifications
Framerate: | 60 frames per second |
CPU: | ByteByteJump with 3-byte addresses |
CPU speed: | 65536 instructions per frame (3932160 instructions per second, ~3.93 MHz). |
Byte ordering: | Big-endian |
Memory: | 16 MiB RAM |
Graphics: | 256*256 pixels, 1 byte per pixel, 216 fixed colors |
Sound: | 8-bit mono, signed values. 256 samples per frame (15360 samples per second) |
Keyboard: | 16 keys, organized into 4 rows by 4 columns |
Colors
Each pixel takes up 1 byte. This would theoretically allow for a maximum of 256 different colors, but BytePusher's fixed palette uses only the first 216 of these. Indices 216..255 are unused; they are set to black (#000000).
The 216 colors are organized into a 6*6*6 color cube (a.k.a the "websafe" palette). Each of the red, green and blue components can have an intensity value from 0 to 5. The formula to calculate a pixel's color value is: Red*36 + Green*6 + Blue. If the actual display device has 8-bit (00h-FFh) color components, we have to multiply each intensity value by 33h when blitting to the screen:
0 1 2 3 4 5 00 33 66 99 CC FF
Keyboard layout
BytePusher's hex keyboard has the same layout as that of CHIP-8:
1 | 2 | 3 | C |
4 | 5 | 6 | D |
7 | 8 | 9 | E |
A | 0 | B | F |
Memory map
The memory-mapped I/O area of BytePusher consists of 8 (!) bytes at the start of memory. Multibyte values are stored with big-endian byte ordering.
Address | Bytes | Description |
---|---|---|
0 | 2 | Keyboard state. Key X = bit X. |
2 | 3 | The program counter is fetched from this address at the beginning of each frame. |
5 | 1 | A value of ZZ means: pixel(XX, YY) is at address ZZYYXX. |
6 | 2 | A value of XXYY means: audio sample ZZ is at address XXYYZZ. |
Outer loop
- Wait for the next timer tick (60 ticks are generated per second).
- Poll the keys and store their states as a 2-byte value at address 0.
- Fetch the 3-byte program counter from address 2, and execute exactly 65536 instructions.
- Send the 64-KiB pixeldata block designated by the byte value at address 5 to the display device. Send the 256-byte sampledata block designated by the 2-byte value at address 6 to the audio device.
- Go back to step 1.
A program must be finished with all its calculations for a frame before the next periodic reset of the program counter occurs. In essence this makes BytePusher a hard realtime system. When a program has finished its work for one frame, it should simply wait in a loop for the next reset. A loop counter can be used to measure CPU utilization.
Inner loop
An instruction consists of 3 big-endian 24-bit addresses A,B,C stored consecutively in memory. The operation performed is: Copy 1 byte from A to B, then jump to C.
An instruction should be able to modify its own jump address before jumping. Thus the copy operation must be completed before the jump address is read.
Since the three address operands are limited to 24 bits,
- We can never copy to/from an address past FFFFFFh, and
- We can never jump to an instruction starting at an address greater than FFFFFFh.
However, since each instruction takes up exactly 9 bytes, jumping to an address ≥ FFFFF8h results in the CPU attempting to read past address FFFFFFh. In the worst case (if the instruction starts at FFFFFFh) the CPU will try to read 8 extra bytes. The way we deal with this is to simply add 8 extra padding bytes beyond FFFFFFh, all initialized to 0. A buggy application can then "crash" within the sandbox, without affecting the VM or the host machine in any way.
Here's an implementation in C of the inner loop:
unsigned char mem[0x1000008], *pc; ... pc = mem + (mem[2]<<16 | mem[3]<<8 | mem[4]); int i = 65536; do { mem[pc[3]<<16 | pc[4]<<8 | pc[5]] = mem[pc[0]<<16 | pc[1]<<8 | pc[2]]; pc = mem + (pc[6]<<16 | pc[7]<<8 | pc[8]); } while (--i);
Since we read each address one byte at a time, this code works on both big-endian and little-endian host machines.
Persistence
A snapshot of the memory contents of a running BytePusher machine can be saved to a file. To ensure a consistent state, this must only be done in between frames (i.e. not while the inner loop is running). A saved snapshot file can later be loaded into RAM to recreate the exact same machine state.
File formats
Note that file formats are not part of the actual machine specification. Anyone is free to develop their own format and publish it here.
File extension | Format description |
---|---|
".BytePusher" | A ".BytePusher" file contains a snapshot of the memory contents of a BytePusher machine. Address X in memory is mapped to byte position X in the file. To decrease the filesize, any trailing zeros should be omitted from the file when saving. When loading a file which is less than the full 16 MiB, the upper part of RAM must be initialized to 0. |
".BytePusherPage" | A ".BytePusherPage" file contains a 256-byte lookup table, intended to reside on a 256-byte aligned page in memory. |
".BytePusherBank" | A ".BytePusherBank" file contains a 65536-byte lookup table, intended to reside in a 65536-byte aligned bank in memory. |
Programs
Below is a list of all known BytePusher programs, with screenshots. If you've made your own program, please post a link here.
Palette Test This program shows the colors for all 256 possible pixel values (including the top 40 unused ones, which should show as black). The sourcecode is here. | |
Scrolling Logo This program shows a scrolling BytePusher logo, which wraps around the edges of the screen. The sourcecode is here. | |
Keyboard Test This program shows the state of the sixteen keys. The sourcecode is here. | |
Munching Squares This program written in PUSHEM implements the munching squares display hack. | |
Audio Test This program plays 8 bars of music in an endless loop, while displaying the changing waveform in the style of an oscilloscope. The sourcecode is here. | |
SineScroller This program implements a multi-colored sine scroller. The sourcecode is here. | |
Sprites This program moves some sprites around in various patterns on top of a background image. The sourcecode is here. | |
"Invert Loop" sine (from the Wayback Machine; retrieved on 25 August 2011) This is an alternative audio test which unfortunately doesn't appear to catch a stuttering VM very well. The sourcecode is here (from the Wayback Machine; retrieved on 25 August 2011). | |
Nyan Cat This shows a simple "Nyan Cat" animation (no sound yet). The sourcecode is here. | |
Console Test This is a sort-of text editor. It has no real use except for the code, which other BytePusher programmers can use to make more useful programs. | |
Langton ant Implementation of Langton ant cellular automata for BytePusher. |
Machines
Below is a list of all known BytePusher (virtual or physical) machines. If you've made your own machine, please post a link here.
- BytePusher VM implementation in C and Allegro by User:Javamannen
- Windows executable. Requires the Allegro 4.2 DLL.
- BytePusher VM implementation in Enhanced CWEB and SDL by User:Zzo38
- PuteBysh interpreter in C + Allegro (from the Wayback Machine; retrieved on 25 August 2011) by Ben Russell - try this if you get stuttery audio from the others.
(Note: This interpreter does the copy after setting the program counter, which doesn't match specification. To fix, move lines 175-179 (pc = ...) to after 180 (vmmem[dp] = ...). -- some random editor)
- BytePusher VM implementation in C# using the Microsoft XNA framework by Mark Middleton
- BytePusher VM implementation in java. Works on Windows, Linux, Mac by Mark Middleton
- Mark Middleton's BytePusher VM implementation in java ported to Android by Blake W. Ford
- BytePusher VM implementation in D and SDL 2 by User:Nucular
- BytePusher VM implementation in JavaScript for Chrome by User:Nucular
- BytePusher libretro core implemented in Rust by User:Shadow0133
- BytePusher libretro core implemented in Zig (~130 lines) by User:AnyPuter
- EcmaByter - a small (999 bytes long) VM implementation in JavaScript (ES6) for modern browsers by User:Plugnburn
- Short BytePusher VM implementation in C and SDL (<100 lines, minified<1500 bytes by User:Emg
- BytePusher VM implementation in C++ and SDL by User:majestic53
- BytePusher VM implementation in Pharo by User:JulienDelplanque
- BytePusher VM implementation in JavaScript by User:Duck3456
- BytePusher VM implementation in Flutter (web performance is not great, mobile and desktop seems fine) by User:Bradcypert
- BytePusher VM in WebAssembly text, runs the VM in an audio worklet, with the result of smooth audio by User:Sgeo
See also
- PUSHEM - a macro assembler for BytePusher by User:Zzo38
- ByteByteJump - the heart of BytePusher
- BytePushCore - runtime generator of opcode tables for BytePusher (under development)