Varvara is a clean-slate computing stack based on the Uxn CPU.
This personal computer system, built on top of the Uxn virtual machine, was designed to run small audio and visual applications. To see a list of compatible software, see roms, and the community projects.
While this project aspires to act as a target that may last, it is in its infancy, the design could still change and break compatibility.
Devices are external systems connected to the Uxn CPU, such as the screen, the mouse and the keyboard. Each device has 16 bytes, also called ports, of I/O memory.
Vectors are ports holding an address in memory to evaluate when a device event is triggered, such as when the mouse is moved, or a key is pressed. Writing to vector ports does not trigger an event, it only tell the emulator which address to evaluate from when an event happens with that device.
The two reserved devices can be used for implementation specific features that do not need to be part of the specs, or other Uxn/Varvara instances.
A device mask, is a short in which each bit represent one of the sixteen devices, and each toggled bit represents a required device to run a rom, for more details, see metadata.
The System device is used to control the execution of a varvara program.
halt* vector is evaluated when uxn errors. If the vector is unset, the emulator handles the error, if an address in found in the halt port, the vector is evaluated. In that scenario, stacks are emptied, a short of the address where the error occured, a byte of the instruction that errored, and a byte for the error code are put on the working stack.
The address in
title* points to text, which may be printed as the emulator window title. The address in
metadata* points to text that contains metadata about the rom.
This device is holding 3 shorts to be used for application customization. For a device that does not use a screen, these bytes may be used for speech controls, or other system globals, for simplicity we call them the Red*, Green* and Blue* shorts.
Sending a non-null byte to the
0x0e port will print the content of the stacks or pause the evaluation if the emulator includes a step-debugger, sending a non-null byte to the
0x0f port will terminate the application.
expansion* port allows to do special memory management operations, it expects an address to an operation. This is used mostly by larger roms that want to keep assets outside of addressable range. An example operation is in the following format:
|copy||01||length*||src page*||src addr*||dst page*||dst addr*|
The console device is the standard I/O device, it allows for communication between programs on the host computer. The console vector triggers when a stdio event occurs.
#41 .Console/write DEO ( to send the letter "A" )
To send data from one Uxn to another, when using a Unix host, use the following pattern:
uxnemu orca.rom | uxnemu piano.rom
For example, a program sending the line of text "hello", will trigger the console's vector 6 times; one for each character and a line ending character.
The screen device is made of two layers, each displaying 2-bits graphics in up to 4 colors. Drawing to the screen is done by writing values to the screen's
pixel/sprite ports of the screen device. The screen vector triggers 60 times per seconds.
M L Y X D C B A | | | +---- Flipx | | | +---- Blend | | +------ Flipy | | +------ Blend | +-------- Layer | +-------- Blend +---------- Mode +---------- Blend
To draw a single teal pixel on the foreground, at the position 20,30:
@paint-pixel #0020 .Screen/x DEO2 ( set x* position ) #0030 .Screen/y DEO2 ( set y* position ) #42 .Screen/pixel DEO ( draw 4-fg 2-color2 ) JMP2r
The screen can also draw 8x8 sprites by writing an
addr* which points to the sprite data in memory. The sprite byte defines the layer to draw on, the type of data to draw, and the colors to use.
@paint-sprite #0020 .Screen/x DEO2 ( set x* position ) #0030 .Screen/y DEO2 ( set y* position ) ;sprite .Screen/addr DEO2 ( set addr* position ) #03 .Screen/sprite DEO ( draw 0-bg 3-color3 ) JMP2r @sprite 0f38 675f dfbf bfbf
Sprites are 8x8 tiles, stored in memory as sequences of 8 bytes for a 1-bit sprite, and 16 bytes for a 2-bits sprite. For example, the color byte
#01 will paint a 1bpp sprite on the background with the first system color, and
#c2 will paint a 2-bits sprite on the foreground with the second system color.
The screen's auto byte automates the position and sprite address whenever a drawing command is sent, so the program does not need to manually move to the next sprite, or the next tile. A pixel drawing will increment the positions by 1, and a sprite drawing, by 8. A 1bpp sprite drawing will increment the address by 8, and a 2bpp drawing, by 16.
L L L L * A Y X | | | +---- Length | | | +---- Auto X | | +------ Length | | +------ Auto Y | +-------- Length | +-------- Auto Addr +---------- Length +---------- Unused
The high nibble of the auto byte is the length of sprites to draw at once. This allows to draw a 2x3 sprite without needing complex logic:
@paint-sprite #0020 .Screen/x DEO2 ( set x* position ) #0030 .Screen/y DEO2 ( set y* position ) #16 .Screen/auto DEO ( set 2w y addr auto ) ;23x-icns .Screen/addr DEO2 ( set addr* ) #01 .Screen/sprite DEOk DEOk DEO ( draw 3 rows ) JMP2r @23x-icns 0010 2847 2810 0001 0000 00e0 2040 8000 0204 080f 0000 0001 0000 00e0 2040 8000 0204 080f 0000 0000 0010 28c4 2810 0000
pitch is written to any of the audio devices, it starts playing an audio sample from Uxn's memory, pointed to by
length*. It loops the sample (unless told not to) until it reaches the end of the ADSR envelope defined by
adsr*. The audio vector triggers when a note ends.
Several fields contain more than one component:
Each of the ADSR components is measured in 15ths of a second, so writing
adsr* will play a note that lasts for exactly four seconds, with each section of the envelope lasting one second. If
#0000 then no envelope will be applied: this is most useful for longer samples that are set to play once by setting the most significant bit of
pitch to 1.
The envelope varies the amplitude as follows: starting at 0%, rising to 100% over the Attack section, falling to 50% over the Decay section, remaining at 50% throughout the Sustain section and finally falling to 0% over the Release section. The envelope is linearly interpolated throughout each section.
volume components set how loudly the next sample will play.
#ff sets maximum volume for both speakers.
pitch is written, any sample that is currently playing will be replaced with the sample defined by all the values set in the device. While the sample is playing, the
output byte can be read to find the loudness of the envelope at that moment.
Audio Sample Format
All samples used by the audio devices are mono and unsigned 8-bit (also known as u8), so the space taken up by samples is minimized. The sample rate of the samples depends on
|Sample type||Sample rate|
|> 256||Middle-C pitched sample||44,100 Hz|
Long samples are assumed to be already pitched to Middle C and will loop (unless No Loop is 1) until the end of the envelope. To play the sample at the same rate as it was recorded, write the Middle C MIDI note number,
pitch. To play at double or half speed, for example, write an octave higher or lower to
The minimum sample size that can be pitched at 44.1 kHz to Middle C with reasonable accuracy is 337 bytes long, which represents two cycles of the 261 Hz wave. The single wavelength mode in Uxn allows much smaller samples to be used, even down to only two bytes for a square wave. In this mode the length of the entire sample is taken to be one cycle of the Middle C note, so the pitch is not heard to vary even if
length* were to change between sample plays.
This device is currently a work in progress, more details shortly.
The button byte works similarly to a NES controller, where there the state of each one of the 8 buttons is stored as a bit in a single byte. The keys byte contains the ascii character that is currently pressed. The controller vector triggers when a button, is pressed or released, and when a key is pressed.
Would the need for multi-player games arise, the P2, P3 and P4 ports, will host button-type byte values received from the other controllers.
The mouse device's state port holds a byte in which each bit is a button state. The byte value of holding down the mouse1 button is
01, and holding down mouse1+mouse3 button is
05. The scroll values are signed shorts, normally
0001, for -1 and +1. The mouse vector triggers when the mouse is moved and when a button is pressed or released.
The file device supports reading and writing files, listing directories, obtaining file information and deleting files. The device may not access files outside of the working directory. The vector is unused.
The general approach is to write
name* with the address of the filename in memory,
length* with the length of the memory region to use in the data exchange, and finally one of the
addr* shorts with the address of that memory region. Once the operation has completed, the
success* short can be read to find the number of bytes successfully exchanged.
name* resolves to a file, writing the address to
read(addr)* will read the file's data into the memory region.
success* will be less than
length* if the file is shorter than
length*, and will be zero if the file does not exist or the filename is invalid. If the file is longer than
length*, subsequent writes to
read(addr)* will read the next chunk of data into the memory region, so it is possible to read the contents of very large files one chunk at a time.
name* resolves to a directory, writing the address to
read(addr)* will read the directory as if it were a text file listing each of the directory's contents:
001a small text file.txt ---- subdirectory ???? huge file.mp4
The listing has each file or directory on its own line, prefixed with the file size in four hex characters and a space. The ending newline is always present. If the file is too big to fit in four hex characters (> 64 kB) then
???? will be used instead; for directories,
---- takes the place of the file size. As for reading file data, if the listing length exceeds
length* then subsequent writes to
read(addr)* will read more entries. Unlike file data, directory entries will be returned as atomic units that won't be broken across chunks, so
success* will usually be lower than
length* even when more data is available. When
success* reads zero, the listing is complete.
The directory listing for a single file or directory can be obtained when
stat(addr)* is written, and will write the same format as above, including the newline, into the memory buffer. If
success* reads zero, the file or directory doesn't exist or the region is too small to fit the line.
Writing files is performed by writing to
append is set to 0x01, then the data in the memory region will be written after the end of the file, if it is 0x00 (the default) it will replace the contents of the file. If the file doesn't previously exist then it will be created and
append makes no difference.
success* will be set to
length* if the write was successful, otherwise it will read as zero. As with reading files and directories, subsequent writes to
addr(write)* will write more chunks of data to the file.
In all cases, writing to
name* closes the file/directory and new calls to the
addr* shorts will start from the beginning (or writing after the end when
append is 0x01).
Finally, to delete a file, write any value to the
The week, in the dotw port, begins on sunday.
Incoming: donsol roms left noodle nasu nebu adelie metadata metadata metadata icn format chr format gly format ufx format tga format paradise potato basic basic chip8 uxn beetbug beetbug devlog computer