PixelProtocol (pipro) is a protocol for defining what is being sent between the GUI client and the game engine.
It's for implementing games where old-school looking pixel art can be appreciated.
- 256 indexed colors.
- A size of 320x200 pixels is recommended.
- Should be possible to implement both in 16-bit assembly for DOS and in a browser.
- 8-channel audio with multiple waveforms, PCM samples and ADSR envelopes.
- Instruments and pattern-based music with tracker effects.
- Text rendering with 8 font slots and scaling.
- Advanced sprite operations including rotation, scaling, and collision detection.
- Tilemap layers with per-sprite flags.
- Clipping, camera offsets, fill patterns, draw palettes and blend modes.
- Textured triangles and bulk pixel transfers.
- Keyboard, mouse and up to four joysticks.
- Timing control and frame synchronization.
- Memory management for palettes and screen buffers.
- 128 KB of persistent key/value storage per game.
-
Q: Wouldn't it be cooler if Vulkan commands was sent instead? Or OpenGL? Or SDL2?
-
A: Protocols for OpenGL over network already exists and I want to keep things really simple.
-
Q: Can't you just use VNC?
-
A: No, I want something specifically for games or demoscene demos that use 320x200 pixels, 256 colors.
- Version: 0.8
| name | type | description |
|---|---|---|
| magic | uint32 | 0x50495052 ("PIPR") |
| ver | uint16 | protocol version, 8 for 0.8 |
| width | uint16 | width |
| height | uint16 | height |
| flags | uint16 | bit 0: stereo, bit 1: client returns values |
| commands | []byte | pairs of bytes: a command, then its argument |
All multi-byte values are little-endian. A command is always the command byte followed by the argument byte, in that order on the wire. The commands can be streamed.
A client implements every command in this document and nothing else. Before 1.0 there is no backwards compatibility: a client that reads a version it does not implement stops and reports an error, and so does a client that reads a command this document does not define.
Every one of the 256 command bytes is now spoken for. A command stays one byte and its argument stays one byte, so making room for something new means dropping whatever earns its place least, and raising the version. Before 1.0 that is a straight break.
The palette starts out as the default VGA mode 13h palette. Color index 0 is transparent for sprites.
If bit 1 of the header flags is set, the client sends a header of its own before returning any value:
| name | type | description |
|---|---|---|
| magic | uint32 | 0x50495052 ("PIPR") |
| ver | uint16 | the protocol version the client implements |
| buffers | uint16 | how many screen buffers it has, at least 2 |
| flags | uint16 | what it can do |
Client flags: bit 0 stereo, bit 1 audio, bit 2 music, bit 3 joysticks, bit 4 rumble, bit 5 mouse, bit 6 persistent storage. Where a bit is clear the matching commands do nothing and their queries return 0. Panning collapses to mono unless both headers have their stereo bit set.
For the few commands that need an argument wider than a byte.
| cmd | uint8 argument | |
|---|---|---|
| 0xf0 | clear the value register | |
| 0xf1 | push a byte into the value register | shift left 8 bits, then or in the byte |
The register is 32 bits. Commands that read it as a signed value read it as two's complement.
| cmd | uint8 argument | |
|---|---|---|
| 0xf2 | begin blob | the argument is the destination |
Followed by a uint16 length and that many bytes, padded with 0x00 to an even count.
Destinations: 0 for a string, 1 for the pixels of the chosen sprite, 2 for the palette (RGB triplets from index 0), 3 for the glyphs of the chosen font, 4 for the data of the chosen sample, 5 for the rows of the chosen pattern, 6 for the drawing target, 7 for the chosen rectangle, 8 for the cells of the chosen map layer, 9 for a storage key, 10 for a storage value.
| cmd | uint8 argument | |
|---|---|---|
| 0x9d | set blob encoding | 0=raw, 1=run-length |
Run-length encoded data is pairs of bytes: a count of 1 to 255 and a value. A count of 0 ends the data early. The uint16 length still counts the encoded bytes. The encoding stays until it is changed.
A blob that holds more than its destination takes is cut, and one that holds less leaves the rest of the destination alone.
Every written pixel goes through the same steps, in this order:
- The position is moved by the camera, then dropped if it falls outside the clip rectangle or outside the drawing target.
- A color index comes from the pixel color, the sprite, the map cell, the texture, or from interpolation along a line or across a triangle.
- The draw palette remaps it.
- It is dropped if it is transparent. Sprites, maps, textured triangles, text and copy region honor transparency, the rest ignore it.
- The fill pattern keeps it, replaces it with the fill pattern color, or drops it. Clear, the linewise fills, lines, circles, rectangles and untextured triangles honor the pattern, the rest ignore it.
- The blend mode mixes it with the pixel already there.
- It is written to the drawing target.
The screen palette and the palette RGB values are applied when a buffer is shown, not when it is written.
Coordinates are signed and kept to at least 32 bits, so the camera can push drawing off either edge. Nothing wraps and nothing is clamped: a pixel outside the clip rectangle or the drawing target is dropped.
So that every client draws the same pixels: lines use Bresenham, circles use the midpoint algorithm, triangles and rectangles use a top-left fill rule, outlines are one pixel wide and lie inside the shape, and sprites and textures are sampled with nearest neighbour. Where blending has to land on a palette entry it takes the lowest index with the smallest squared RGB distance.
| cmd | uint8 argument | |
|---|---|---|
| 0x00 | choose palette color | prepare for filling the palette |
| 0x01 | set red value of chosen palette color | set the color |
| 0x02 | set green value of chosen palette color | set the color |
| 0x03 | set blue value of chosen palette color | set the color |
| cmd | uint8 argument | |
|---|---|---|
| 0x04 | choose pixel color | prepare for drawing |
| 0x05 | set x position | |
| 0x06 | set y position | |
| 0x07 | add to x position | |
| 0x08 | add to y position | |
| 0x09 | plot | draw a pixel |
-
Q: What is the "add to x position" command for?
-
A: since all arguments are bytes, it's needed to be able to specify X coordinates from 256..320.
-
Q: Isn't that a bit impractical?
-
A: Perhaps, but it makes the protocol very simple and uniform. All commands takes a byte as an argument.
| cmd | uint8 argument | |
|---|---|---|
| 0x0a | clear | fill the clip rectangle with the color |
Clear ignores the camera.
| cmd | uint8 argument | |
|---|---|---|
| 0x0b | draw linewise until nonzero or end | for filling the pixel buffer |
| 0x0c | draw backwards linewise until nonzero or end | for filling the pixel buffer |
Both start at the pixel position and walk along the row, 0x0b to the right and 0x0c to the left, drawing the pixel color until they meet a pixel that is not index 0 or the edge of the clip rectangle. The pixel position is left where they stopped.
| cmd | uint8 argument | |
|---|---|---|
| 0x0d | flip | show buffer N, 0 is the default buffer |
| 0x0e | sprite flip | the same, but only where sprites have been drawn |
A flip copies to the display, leaves the buffer alone and does not move the drawing target, so a frame can be drawn on top of the one before it. A sprite flip then forgets where the sprites were, so each one covers only what was drawn since the last flip.
| cmd | uint8 argument | |
|---|---|---|
| 0x0f | choose color for start of line | prepare to draw a line |
| 0x10 | choose color for end of line | prepare to draw a line |
| cmd | uint8 argument | |
|---|---|---|
| 0x11 | set x coordinate for start of line | prepare to draw a line |
| 0x12 | set y coordinate for start of line | prepare to draw a line |
| 0x13 | set x coordinate for end of line | prepare to draw a line |
| 0x14 | set y coordinate for end of line | prepare to draw a line |
| cmd | uint8 argument | |
|---|---|---|
| 0x15 | add to x coordinate for start of line | prepare to draw a line |
| 0x16 | add to y coordinate for start of line | prepare to draw a line |
| 0x17 | add to x coordinate for end of line | prepare to draw a line |
| 0x18 | add to y coordinate for end of line | prepare to draw a line |
| cmd | uint8 argument | |
|---|---|---|
| 0x19 | draw a line | draw the line |
The two colors are interpolated along the line.
| cmd | uint8 argument | |
|---|---|---|
| 0x1a | choose color for p0 | prepare to draw a filled triangle |
| 0x1b | choose color for p1 | prepare to draw a filled triangle |
| 0x1c | choose color for p2 | prepare to draw a filled triangle |
| cmd | uint8 argument | |
|---|---|---|
| 0x1d | set x coordinate for p0 | prepare to draw a filled triangle |
| 0x1e | set y coordinate for p0 | prepare to draw a filled triangle |
| 0x1f | set x coordinate for p1 | prepare to draw a filled triangle |
| 0x20 | set y coordinate for p1 | prepare to draw a filled triangle |
| 0x21 | set x coordinate for p2 | prepare to draw a filled triangle |
| 0x22 | set y coordinate for p2 | prepare to draw a filled triangle |
| cmd | uint8 argument | |
|---|---|---|
| 0x23 | add to x coordinate for p0 | prepare to draw a filled triangle |
| 0x24 | add to y coordinate for p0 | prepare to draw a filled triangle |
| 0x25 | add to x coordinate for p1 | prepare to draw a filled triangle |
| 0x26 | add to y coordinate for p1 | prepare to draw a filled triangle |
| 0x27 | add to x coordinate for p2 | prepare to draw a filled triangle |
| 0x28 | add to y coordinate for p2 | prepare to draw a filled triangle |
| cmd | uint8 argument | |
|---|---|---|
| 0x29 | draw a filled or empty triangle | 0 for empty, 1 for filled |
The three colors are interpolated across the triangle. The winding order does not matter.
The same three points are used, with texture coordinates that span the chosen sprite.
| cmd | uint8 argument | |
|---|---|---|
| 0xb8 | set u coordinate for p0 | 0-255 across the texture |
| 0xb9 | set v coordinate for p0 | 0-255 down the texture |
| 0xba | set u coordinate for p1 | |
| 0xbb | set v coordinate for p1 | |
| 0xbc | set u coordinate for p2 | |
| 0xbd | set v coordinate for p2 | |
| 0xbe | choose texture sprite | the sprite to read texels from |
| 0xbf | draw a textured triangle | 0=draw all, 1=skip transparent colors |
Texturing is affine. Nothing in the protocol carries depth, so a triangle that needs perspective has to be subdivided by the engine.
| cmd | uint8 argument |
|---|---|
| 0x2a | choose a random color for the pixel |
| 0x2b | choose random colors for the line |
| 0x2c | choose random colors for the triangle |
| cmd | uint8 argument |
|---|---|
| 0x2d | choose random coordinates for the pixel |
| 0x2e | choose random coordinates for the line |
| 0x2f | choose random coordinates for the triangle |
| cmd | uint8 argument | |
|---|---|---|
| 0x4b | seed the random generator | the seed is in the value register |
Random colors run from 0 to 255 and random coordinates fall inside the drawing target. The generator is a 32-bit xorshift, so a seeded sequence replays the same everywhere.
| cmd | uint8 argument | |
|---|---|---|
| 0x30 | choose sprite ID | select a sprite to work with |
| 0x31 | set sprite width | set sprite width |
| 0x32 | set sprite height | set sprite height |
| 0x33 | clear sprite | clear contents |
| 0x34 | push pixel | adds N pixels of the selected color |
| 0x35 | push empty | add N transparent pixels |
| 0x36 | set x coordinate for drawing the sprite | |
| 0x37 | set y coordinate for drawing the sprite | |
| 0x38 | add to x coordinate for drawing the sprite | |
| 0x39 | add to y coordinate for drawing the sprite | |
| 0x3a | draw sprite | 0=with the sprite transform, 1=without |
| 0x3b | set sprite flags | a bitmask kept with the sprite |
| 0x3c | get sprite flags | returns the bitmask (uint16) |
| 0x3d | copy sprite | copy the chosen sprite into sprite N |
| 0x3e | get sprite size | 0 for the width, 1 for the height (uint16) |
Pixels are pushed left to right, top to bottom. Clearing a sprite or setting its width or height puts the push cursor back at the top left. Pushing past the end drops the extra, and an argument of 0 pushes nothing. A blob to destination 1 fills a sprite in one go.
The draw position is the top left of the sprite before it is rotated or scaled.
| cmd | uint8 argument | |
|---|---|---|
| 0x3f | get pixel | the color index at the pixel position, or in the chosen sprite if the argument is 1 (uint16) |
Reading uses the camera, ignores the clip rectangle, sees the drawing target as it was written and not as it is shown, and returns 0 from outside it.
| cmd | uint8 argument | |
|---|---|---|
| 0x4c | set clip x | |
| 0x4d | set clip y | |
| 0x4e | add to clip x | |
| 0x4f | add to clip y | |
| 0x50 | set clip width | |
| 0x51 | set clip height | |
| 0x52 | add to clip width | |
| 0x53 | add to clip height | |
| 0x54 | reset clip | clip to the whole drawing target |
| 0x55 | set camera x | signed, from the value register |
| 0x56 | set camera y | signed, from the value register |
| 0x57 | reset camera | back to 0,0 |
The camera is subtracted from every drawing coordinate. Clipping is in target pixels and is not moved by the camera. A clip width or height of 0 stops everything from drawing.
| cmd | uint8 argument | |
|---|---|---|
| 0x58 | set fill pattern | 4x4 bits from the value register, the top row first |
| 0x59 | set fill pattern color | the color used where a bit is set |
| 0x5a | set fill pattern mode | 0=off, 1=two colors, 2=set bits are transparent |
The pattern is aligned to the drawing target, so the camera does not move it.
These remap color indexes and leave the RGB values from 0x01-0x03 alone. They apply to the color chosen with 0x00.
| cmd | uint8 argument | |
|---|---|---|
| 0x5b | set draw palette entry | the chosen color is drawn as this index |
| 0x5c | set screen palette entry | the chosen color is shown as this index |
| 0x5d | set color transparency | 0=opaque, 1=transparent |
| 0x5e | reset the draw palette | also makes only index 0 transparent |
| 0x5f | reset the screen palette |
| cmd | uint8 argument | |
|---|---|---|
| 0x9f | set blend mode | 0=replace, 1=alpha, 2=add, 3=subtract, 4=average |
Blending works on the RGB values behind the two color indexes. Alpha mode uses the sprite alpha, which counts as 255 for everything that is not a sprite.
| cmd | uint8 argument | |
|---|---|---|
| 0x40 | set convolution filter 0 | blur is 0,1,0,1,1,1,0,1,0 div 5 |
| 0x41 | set convolution filter 1 | flame is 0,1,0,1,1,1,0,0,0 div 4 |
| 0x42 | set convolution filter 2 | |
| 0x43 | set convolution filter 3 | the uint8 is treated as a range from |
| 0x44 | set convolution filter 4 | 0.0 to 1.0 |
| 0x45 | set convolution filter 5 | |
| 0x46 | set convolution filter 6 | |
| 0x47 | set convolution filter 7 | |
| 0x48 | set convolution filter 8 | |
| 0x49 | set convolution division | |
| 0x4a | use convolution filter |
The filters work on color indexes, not on RGB values. They run over the clip rectangle of the drawing target, read from a copy of it, and clamp at its edges.
For returning the state of the client:
Commands that return an uint16:
| cmd | uint8 argument | |
|---|---|---|
| 0x60 | is Esc | is Escape being pressed? |
| 0x61 | is up | is W, up or joystick up pressed? args: 0 for any, 1-4 for the player |
| Player1 has WASD and Joy1, Player2 has arrows and Joy2, Player3 and | ||
| Player4 have Joy3 and Joy4 | ||
| 0x62 | is left | is A, left or joystick left pressed? |
| 0x63 | is down | is S, down or joystick down pressed? |
| 0x64 | is right | is D, right or joystick right pressed? |
| 0x65 | is A | is Return, Comma (,) or joystick A pressed? |
| 0x66 | is B | is Space, Dot (.) or joystick B pressed? |
| cmd | uint8 argument | |
|---|---|---|
| 0x67 | is shift held down | uint8 argument: 0 for left, 1 for right, 2 for any, returns 1 for held down |
| 0x68 | is alt held down | |
| 0x69 | is ctrl held down | |
| 0x6a | is super held down |
| cmd | uint8 argument | |
|---|---|---|
| 0x6b | get keybuffer | returns: 0 if empty, keycode of first in keybuffer if not empty |
| 0xfa | is key held down | uint8 argument: keycode, returns: 1 for held down |
| cmd | uint8 argument | |
|---|---|---|
| 0x6c | get mouse x coordinate | |
| 0x6d | get mouse y coordinate | |
| 0x6e | get mouse buttons | returns a bitmask: 1 for left, 2 for right, 4 for middle |
| 0xf9 | get mouse wheel | signed movement since the last read |
| cmd | uint8 argument | |
|---|---|---|
| 0x6f | joy button | uint8 argument: button ID, returns: 1 for pressed |
| 0xf3 | choose player | uint8 argument: 1-4, or 0 for any, used by the commands below |
| 0xf4 | get joystick count | how many joysticks are connected |
| 0xf5 | get joystick buttons | returns a bitmask of the first 16 buttons |
| 0xf6 | get joystick hat | returns a bitmask: 1 up, 2 right, 4 down, 8 left |
| 0xf7 | get joystick axis | uint8 argument: axis ID, returns the signed position |
| 0xf8 | rumble | uint8 argument: strength, 0 stops it |
| cmd | uint8 argument | |
|---|---|---|
| 0xef | set cursor | 0=shown, 1=hidden, 2=hidden and held inside the window |
Mouse coordinates are in screen pixels and are not moved by the camera. Signed values come back as two's complement. Under cursor mode 2 there is no pointer to report, so 0x6c and 0x6d return how far the mouse moved since they were last read, which is what mouse-look wants.
A channel must be set up for receiving the uint16 values that are returned by these functions. One uint16 is returned per command, in the order the commands were received. The two commands that return a variable number of values send the count first, so the channel stays self-framing.
The 8 channels are numbered 0 to 7.
| cmd | uint8 argument | |
|---|---|---|
| 0x70 | set audio channel | select channel 0-7 for audio operations |
| 0x71 | set note | 0-127, 60 is C-4 and 69 is A-4 (440 Hz) |
| 0x72 | set finetune | signed, -128..127 spans one semitone |
| 0x73 | set volume | set volume (0-255) |
| 0x74 | set waveform | 0=pulse, 1=triangle, 2=sawtooth, 3=sine, |
| 4=noise, 5=periodic noise, 6=sample | ||
| 0x75 | set pulse duty | 128 is a 50% square wave |
| 0x76 | set panning | 0=left, 128=center, 255=right |
| 0x77 | note on | hold if 0, else gate for N ticks |
| 0x78 | note off | start the release phase |
| 0x79 | stop channel | stop audio on selected channel |
| 0x7a | set envelope attack | ticks up to full volume |
| 0x7b | set envelope decay | ticks down to the sustain level |
| 0x7c | set envelope sustain | sustain level (0-255) |
| 0x7d | set envelope release | ticks down to silence |
| 0x7e | set channel effect | see the effect table |
| 0x7f | set channel effect parameter | |
| 0xee | get free channel | the lowest silent channel the sequencer does not own, or 255 (uint16) |
A tick lasts 2.5/BPM seconds, so the default 125 BPM gives 50 ticks per second. Clients mix at 44100 Hz or better. Waveform 6 is silent until a sample is bound to the channel.
Channels are picked by hand and are never stolen. A command aimed at a channel the sequencer owns is ignored, so music and sound effects cannot fight over one.
| cmd | uint8 argument | |
|---|---|---|
| 0xd0 | set master volume | 0-255 |
| 0xd1 | choose sample | select a sample slot to work with |
| 0xd2 | set sample format | 0=signed 8-bit, 1=unsigned 8-bit, |
| 2=signed 16-bit | ||
| 0xd3 | set sample rate | Hz, from the value register |
| 0xd4 | set sample base note | the note that plays it untransposed |
| 0xd5 | set sample loop mode | 0=none, 1=forward, 2=ping-pong |
| 0xd6 | set sample loop start | in frames, from the value register |
| 0xd7 | set sample loop length | in frames, from the value register |
| 0xd8 | clear sample | clear contents |
| 0xd9 | choose instrument | select an instrument slot to work with |
| 0xda | store channel voice as instrument | waveform, duty, envelope and sample |
| 0xdb | load instrument into channel | |
| 0xdc | bind chosen sample to instrument | |
| 0xdd | play sample | bind sample ID, set waveform 6 and start it once |
| 0xde | set channel frequency | Hz, from the value register |
| 0xdf | get channel level | current output level 0-255 (uint16) |
Sample data is sent as a blob to destination 4. A loop length of 0 means no loop. Setting the frequency overrides the note until the next note on.
| cmd | uint8 argument | |
|---|---|---|
| 0xe0 | choose pattern | select a pattern slot to work with |
| 0xe1 | set pattern rows | 1-255 |
| 0xe2 | clear pattern | clear contents |
| 0xe3 | set order length | number of entries in the song order |
| 0xe4 | set order position | the entry to write to and play from |
| 0xe5 | set order entry | the pattern at the order position |
| 0xe6 | set tempo | BPM (32-255) |
| 0xe7 | set speed | ticks per row (1-31) |
| 0xe8 | set music volume | 0-255 |
| 0xe9 | play music | 0=stop, 1=play once, 2=loop |
| 0xea | pause music | 0=resume, 1=pause |
| 0xeb | set music channel mask | bit N gives channel N to the sequencer |
| 0xec | get music position | 0=order, 1=row, 2=tick (uint16) |
Pattern rows are sent as a blob to destination 5. A row has one cell per sequencer channel, in ascending channel order, and a cell is 4 bytes: note, instrument, effect, parameter.
In a cell, note 0 means no change, 1-127 is a note and 255 is a note off. Instrument 0 means no change.
The sequencer plays the channels the mask gives it, lowest bit to the lowest channel, and ignores cells beyond them.
| val | effect | uint8 parameter |
|---|---|---|
| 0x00 | arpeggio | two nibbles, semitones to add |
| 0x01 | portamento up | speed |
| 0x02 | portamento down | speed |
| 0x03 | tone portamento | speed towards the note in the cell |
| 0x04 | vibrato | high nibble speed, low nibble depth |
| 0x05 | tremolo | high nibble speed, low nibble depth |
| 0x06 | volume slide | high nibble up, low nibble down |
| 0x07 | set panning | 0-255 |
| 0x08 | sample offset | start frame, in units of 256 |
| 0x09 | retrigger | every N ticks |
| 0x0a | note delay | delay by N ticks |
| 0x0b | note cut | cut after N ticks |
| 0x0c | set volume | 0-255 |
| 0x0d | pattern break | continue at row N of the next pattern |
| 0x0e | pattern jump | continue at order position N |
| 0x0f | set speed | ticks per row, or BPM if above 31 |
Effects 0x00 to 0x0c also work per channel with 0x7e and 0x7f, for sound effects outside the music.
| cmd | uint8 argument | |
|---|---|---|
| 0x80 | set font | select font 0-7 |
| 0x81 | set text color | color for text foreground |
| 0x82 | set text background color | color for text background |
| 0x83 | set text x position | x position for text |
| 0x84 | set text y position | y position for text |
| 0x85 | add to text x position | for positions > 255 |
| 0x86 | add to text y position | for positions > 255 |
| 0x87 | print character | print ASCII character |
| 0x88 | print string | print the last blob sent to destination 0 |
| 0x89 | set text scale | scale factor 1-8 |
| 0x8a | set text background transparency | 0=opaque, 1=transparent |
| 0x8e | set letter spacing | signed, added to the width of each glyph |
| 0x8f | set line height | pixels stepped down by a newline, 0 for the font height |
Font 0 is built in. Fonts 1-7 start out empty and are sent as a blob to destination 3, as 256 glyphs of 8 bytes each, one byte per row and the high bit on the left.
A glyph is 8 by 8 pixels. Printing one steps the text x position by 8 times the scale, plus the letter spacing. A byte of 10 steps y down by the line height and puts x back where 0x83 or 0x85 last set it. A byte of 13 is skipped. The string stays until another blob is sent to destination 0.
| cmd | uint8 argument | |
|---|---|---|
| 0x90 | set circle center x | x coordinate for circle center |
| 0x91 | set circle center y | y coordinate for circle center |
| 0x92 | add to circle center x | for coordinates > 255 |
| 0x93 | add to circle center y | for coordinates > 255 |
| 0x94 | set circle radius | radius in pixels |
| 0x95 | draw circle | 0=outline, 1=filled |
| 0x96 | set rectangle x | top-left x coordinate |
| 0x97 | set rectangle y | top-left y coordinate |
| 0x98 | add to rectangle x | for coordinates > 255 |
| 0x99 | add to rectangle y | for coordinates > 255 |
| 0x9a | set rectangle width | width in pixels |
| 0x9b | set rectangle height | height in pixels |
| 0x9c | draw rectangle | 0=outline, 1=filled |
| 0xfb | add to rectangle width | for widths > 255 |
| 0xfc | add to rectangle height | for heights > 255 |
These use the pixel color chosen with 0x04. A radius, width or height of 0 draws nothing.
The chosen rectangle is the one that copy region and blob destination 7 work on as well, so it has to be able to cover a whole screen.
| cmd | uint8 argument | |
|---|---|---|
| 0x9e | set drawing target | buffer N, 0 is the default buffer |
Every buffer is the size of the screen and keeps its contents until something is drawn to it. Only 0x0d and 0x0e put a buffer on the display. Blobs to destination 6 and 7 go to the drawing target, so a whole frame rendered by the engine can be sent in one go.
| cmd | uint8 argument | |
|---|---|---|
| 0xa0 | wait frames | wait for N display refreshes |
| 0xa1 | set frame rate | set target FPS (0=unlimited) |
| 0xa2 | get frame counter | returns current frame number (uint16) |
| 0xa3 | reset frame counter | reset frame counter to 0 |
| 0xa4 | sync to vblank | wait for vertical blank |
| 0xa5 | get milliseconds | since the frame counter reset (uint16) |
| 0xed | get frames behind | frames still to be shown before the client catches up (uint16) |
The frame rate starts at 60. Both counters wrap at 65536. With a frame rate of 0 the client refreshes as fast as it can and 0xa0 still waits for that many refreshes.
0xa0 and 0xa4 hold up the client, not the engine, so an engine is free to send commands as fast as the transport takes them. An engine that wants to stay in step polls 0xed and slows down when it climbs.
| cmd | uint8 argument | |
|---|---|---|
| 0xa6 | choose map layer | select a layer 0-7 to work with |
| 0xa7 | set map width | in tiles |
| 0xa8 | set map height | in tiles |
| 0xa9 | set tile size | 8 or 16 pixels |
| 0xaa | set map cell x | in tiles, for reading and writing cells |
| 0xab | set map cell y | in tiles, for reading and writing cells |
| 0xac | set cell | the sprite ID at the cell, then step x |
| 0xad | get cell | the sprite ID at the cell, then step x (uint16) |
| 0xae | clear map | fill every cell with the argument |
| 0xaf | draw map | draw only cells whose flags have every bit in the argument, 0 for all |
| 0x8b | set map origin x | signed pixels, from the value register |
| 0x8c | set map origin y | signed pixels, from the value register |
Stepping x past the map width wraps to the start of the next row, so a whole layer can be filled without touching the cursor. Cells are sent as a blob to destination 8, one byte per cell, row by row.
A cell of 0 is empty and draws nothing. Other cells draw the top left tile-size square of the sprite they name, without the sprite transform. Layers are drawn one command at a time, in the order the engine asks for them.
A layer is drawn with its top left cell at the map origin, moved by the camera and cut down by the clip rectangle. Scrolling is therefore smooth, and layers scroll at different speeds by giving them different origins. The cell cursor has no effect on drawing.
| cmd | uint8 argument | |
|---|---|---|
| 0xb0 | set sprite rotation | rotation angle (0-255 = 0-360°) |
| 0xb1 | set sprite scale x | horizontal scale (128=1.0x) |
| 0xb2 | set sprite scale y | vertical scale (128=1.0x) |
| 0xb3 | set sprite flip | 0=none, 1=horizontal, 2=vertical, 3=both |
| 0xb4 | check sprite collision | 0=bounding box, 1=pixel perfect (uint16) |
| 0xb5 | set sprite pivot | 0=top left, 1=center, what it turns around |
| 0xb6 | set sprite alpha | transparency (0=transparent, 255=opaque) |
| 0xb7 | choose the other sprite ID | the sprite that 0x30 is checked against |
Flipping happens first, then scaling, then rotation about the pivot. Collision compares where the two sprites were last drawn, with the transforms they were drawn with, and returns 1 when they touch.
| cmd | uint8 argument | |
|---|---|---|
| 0xc0 | save palette | save current palette to slot N |
| 0xc1 | load palette | load palette from slot N |
| 0xc2 | save screen | copy the drawing target into buffer N |
| 0xc3 | load screen | copy buffer N into the drawing target |
| 0xc4 | copy region | 0=copy all, 1=skip transparent colors |
| 0x8d | set source buffer | what copy region reads from, 0 at first |
The region is the chosen rectangle in the source buffer, copied to the position set with 0x05-0x08 in the drawing target. A region that overlaps itself is copied as if it went through a spare buffer first.
A key/value store of at most 128 KB per storage ID, kept between runs.
| cmd | uint8 argument | |
|---|---|---|
| 0xc5 | set storage ID | from the value register, picks the store |
| 0xc6 | store value | the last value blob under the last key blob |
| 0xc7 | load value | returns the length of the value for the last key blob, then the bytes as uint16s |
| 0xc8 | has key | returns 1 if the last key blob is stored |
| 0xc9 | delete key | remove the last key blob |
| 0xca | get key count | how many keys are stored (uint16) |
| 0xcb | choose key index | select key N for 0xcc |
| 0xcc | get key | returns the length of the chosen key, then the bytes as uint16s |
| 0xcd | clear storage | erase everything for the storage ID |
| 0xce | get storage used | bytes used, in units of 256 (uint16) |
| 0xcf | flush storage | write it out, returns 1 on success |
Keys and values are sent as blobs to destination 9 and 10, and stay until another blob replaces them. A key is at most 255 bytes. A load of a missing key returns a length of 0. A store that would pass 128 KB is ignored. Keys keep the order they were first stored in, so 0xcb and 0xcc can walk them.
| cmd | uint8 argument | |
|---|---|---|
| 0xfd | reset | put all state back to how it started, but keep persistent storage |
| 0xfe | toggle fullscreen | enable or disable fullscreen mode |
| 0xff | exit | end the program |