XXIIVV
The Sound Of Plumpkins
The Sound Of Plumpkins

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 audio and visual applications. To see a list of compatible software, see roms, and the community projects. If you are implementing your own Varvara emulator, you can find a series of roms to test your devices implementations here.

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.

Masks are shorts in which each bit correspond to one of the 16 ports in a device, the device mask tells the emulator when to trigger a device event, such as drawing a pixel or playing a sound. They are drawn in black in the follow tables.

Varvara
00system80controller
10console90mouse
20screena0file
30audiob0
40c0datetime
50d0Reserved
60e0
70f0

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.

System Device mask 0xff28

The System device is used to control the execution of a varvara program.

system 00halt*08red*
0109
02expansion*0agreen*
030b
04friend*0cblue*
050d
06metadata*0edebug
070fstate

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

int
uxn_halt(Uxn *u, Uint8 instr, Uint8 err, Uint16 addr)
{
	Uint8 *d = &u->dev[0x00];
	Uint16 handler = PEEK2(d);
	if(handler) {
		u->wst.ptr = 4;
		u->wst.dat[0] = addr >> 0x8;
		u->wst.dat[1] = addr & 0xff;
		u->wst.dat[2] = instr;
		u->wst.dat[3] = err;
		return uxn_eval(u, handler);
	} else {
		system_inspect(u);
		fprintf(stderr, "%s %s, by %02x at 0x%04x.\n", (instr & 0x40) ? "Return-stack" : "Working-stack", errors[err - 1], instr, addr);
	}
	return 0;
}

The 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:

nameoperationfields
copy01length*src page*src addr*dst page*dst addr*
static void
system_cmd(Uint8 *ram, Uint16 addr)
{
	if(ram[addr] == 0x01) {
		Uint16 i, length = PEEK2(ram + addr + 1);
		Uint16 a_page = PEEK2(ram + addr + 1 + 2), a_addr = PEEK2(ram + addr + 1 + 4);
		Uint16 b_page = PEEK2(ram + addr + 1 + 6), b_addr = PEEK2(ram + addr + 1 + 8);
		int src = (a_page % RAM_PAGES) * 0x10000, dst = (b_page % RAM_PAGES) * 0x10000;
		for(i = 0; i < length; i++)
			ram[dst + (Uint16)(b_addr + i)] = ram[src + (Uint16)(a_addr + i)];
	}
}

Sending the address of a vector to the friend* port spawns a new Uxn instance to evaluate it concurrently. Sending a null short, will wait for the threads to finish before continuing. An emulator may choose to not implement threads and evaluate the vectors linearly.

Sending a non-null byte to the debug 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 state port will terminate the application, on systems that can handle exit codes, the error code is the 0x7f portion of the byte. So, 0x01 terminates the program with an error, and 0x80 terminates the program succesfully.

The metadata* port allows points the emulator to metadata about the rom. The emulator can choose to utilize this information to update the window name.

0123
#fff #000 #7ec #f00

This device is holding 3 shorts to be used for application customization, for simplicity we call them the Red*, Green* and Blue* shorts. These colors are typically used by the screen device to form four application colors.

#f07f .System/r DEO2
#f0e0 .System/g DEO2
#f0c0 .System/b DEO2

Console Device mask 0x0300

console 10vector*18write
1119error
12read1a--
13--1b--
14--1c--
15--1d--
16--1e--
17type1f--

The console vector* is evaluated when a byte is received. The type port holds one of 5 known types: no-queue(0), stdin(1), argument(2), argument-spacer(3), argument-end(4). During the reset vector, a program should be able to query the type port and get a null byte when there is no arguments to be expected.

uxncli file.rom arg1 arg2
                ^   ^^   ^
                2   32   4

The write port is used to send data through the console, 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.

LIT "H .Console/write DEO ( to send the letter "H" )

Screen Device mask 0xc028

screen 20vector*28x*
2129
22width*2ay*
232b
24height*2caddr*
252d
26auto2epixel
27--2fsprite

The console vector* is evaluated 60 times per second. The screen device is capable of displaying 2-bit graphics with the 4 system colors defined by the system device. The screen is made of two independent layers. The foreground layer treats color0 as transparent.

The pixel byte defines the pixel or fill mode, the layer to draw on, optional horizontal and vertical flipping for the quadrant to fill, and the color to use. When the fill bit is active, the operation will fill a portion of the screen starting at the x,y position until the edges of the screen. The default quadrant is bottom-right, flipping the x bit will fill the buttom-left quadrant, and so on.

          M L Y X * * C1 C0
Mode -----+ | | |     |  +---- Color bit 0
Layer ------+ | |     +------- Color bit 1
Flip Y -------+ |
Flip X ---------+

The sprite byte defines the 1-bit or 2-bit mode, the layer to draw on, optional horizontal and vertical flipping, and the colors to use. The screen can draw 8x8 sprites by first writing an addr* which points to either 1-bit or 2-bit sprite data in memory. Sprite data is stored as 8 bytes for a 1-bit sprite, and 16 bytes for a 2-bits sprite. To find the color for each pixel in a sprite, you can use the following formula, or use a table.

           M L Y X B3 B2 B1 B0
  Mode ----+ | | | |  |  |  +---- Blending bit 0
 Layer ------+ | | |  |  +------- Blending bit 1
Flip Y --------+ | |  +---------- Blending bit 2
Flip X ----------+ +------------- Blending bit 3
0 4 8 c
1 5 9 d
2 6 a e
3 7 b f

The low nibble of the sprite byte defines the blending mode, meaning which color to use for each value of the sprite data. The following table presents all possible blendings, assuming a sprite has a background of value 0 and three concentric circles of values 1, 2, and 3 (counting from the outside). For 1-bit sprites, only values 0 and 1 are applicable.

c = !ch ? (color % 5 ? color >> 2 : 0) : color % 4 + ch == 1 ? 0 : (ch - 2 + (color & 3)) % 3 + 1;

static Uint8 blending[4][16] = {
	{0, 0, 0, 0, 1, 0, 1, 1, 2, 2, 0, 2, 3, 3, 3, 0},
	{0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3},
	{1, 2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1},
	{2, 3, 1, 2, 2, 3, 1, 2, 2, 3, 1, 2, 2, 3, 1, 2}};

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 position. Pixel drawing will increment the positions by 1, and sprite drawing by either 8 or 16 depending on the mode.

                L3 L2 L1 L0 * A Y X
Length bit 3 ----+  |  |  |   | | +---- Auto X
Length bit 2 -------+  |  |   | +------ Auto Y
Length bit 1 ----------+  |   +-------- Auto Addr
Length bit 0 -------------+

The high nibble of the auto byte is the number of sprites to draw at once minus one. 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 length 2 with auto y and addr )
	;23x-icns .Screen/addr DEO2 ( set addr )
	#01 .Screen/sprite DEOk DEOk DEO ( draw 3 rows of 1-bit sprites )
JMP2r

@23x-icns
	      (        0        ) (        1        )
	( 0 ) 0010 2847 2810 0001 0000 00e0 2040 8000
	( 1 ) 0204 080f 0000 0001 0000 00e0 2040 8000
	( 2 ) 0204 080f 0000 0000 0010 28c4 2810 0000

Audio Device mask 0x8014

audio 30vector*38adsr*
3139
32position*3alength*
333b
34output3caddr*
35--3d
36--3evolume
37--3fpitch

When pitch is written to any of the audio devices, it starts playing an audio sample from Uxn's memory, pointed to by addr* and 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:

ADSR*PitchVolume
SubfieldAttackDecaySustainReleaseLoopNoteLeftRight
Size (bits)44441744

Each of the ADSR components is measured in 15ths of a second, so writing #ffff to adsr* will play a note that lasts for exactly four seconds, with each section of the envelope lasting one second. If adsr* is #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.

The two volume components set how loudly the next sample will play. #ff sets maximum volume for both speakers.

When 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 length*:

length*Sample typeSample rate
> 256Middle-C pitched sample44,100 Hz
2–256Single wavelengthVariable

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, #3c, to pitch. To play at double or half speed, for example, write an octave higher or lower to pitch.

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.

Controller Device mask 0x0000

controller 80vector*88--
8189--
82button8a--
83key8b--
84func8c--
85P28d--
86P38e--
87P48f--

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.

0x01A Ctrl0x10Up
0x02B Alt0x20Down
0x04Select Shift0x40Left
0x08Start Home0x80Right

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.

Mouse Device mask 0x0000

mouse 90vector*98--
9199--
92x*9ascrollx*
939b
94y*9cscrolly*
959d
96state9e--
97--9f--

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 ffff and 0001, for -1 and +1. The mouse vector triggers when the mouse is moved and when a button is pressed or released.

File Device mask 0xa260

file a0vector*a8name(addr)*
a1a9
a2success*aalength*
a3ab
a4stat(addr)*acread(addr)*
a5ad
a6deleteaewrite(addr)*
a7appendaf

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.

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

When 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 file.txt
---- directory/
???? large 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 write(addr)*. If 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 delete byte.

Datetime Device mask 0x07ff

datetime c0year*c8doty
c1c9
c2monthcaisdst
c3daycb--
c4hourcc--
c5minutecd--
c6secondce--
c7dotwcf--

The week, in the dotw port, begins on sunday.

Incoming: donsol roms left noodle nasu nebu adelie potato metadata metadata metadata oquonie paradise basic basic icn format chr format gly format ufx format tga format chip8 uxn uxntal syntax beetbug beetbug devlog computer