Picotron v0.1.1d
https://www.picotron.net
(c) Copyright 2024 Lexaloffle Games LLP
Author: Joseph White // [email protected]
Picotron is made with:
SDL2 http://www.libsdl.org
Lua 5.4 http://www.lua.org // see license.txt
lz4 by Yann Collet https://www.lz4.org // see license.txt
z8lua by Sam Hocevar https://github.com/samhocevar/z8lua
GIFLIB http://giflib.sourceforge.net/
libb64 by Chris Venter
Latest version of this manual: https://www.lexaloffle.com/picotron.php?page=resources
Picotron is a Fantasy Workstation for making pixelart games, animations, music, demos and other curiosities. It can also be used to create tools that run inside Picotron itself, and to customise things like live wallpapers and screensavers. It has a toy operating system designed to be a cosy creative space, but runs on top of Windows, MacOS or Linux. Picotron apps can be made with built-in tools, and shared with other users in a special 256k png cartridge format.
Display: 480x270 / 240x135 64 definable colours
Graphics: Blend tables, tline3d, stencil clipping
Audio: 64-node synth and 8-channel tracker
Code: Lua 5.4 w/ PICO-8 compatability features
CPU: 8M Lua vm insts / second
Cart: .p64.png (256k) / .p64 ("unlimited")
The first time Picotron is run, it will automatically create a configuration file that specifies where to store files if one does not already exist:
Windows: C:/Users/Yourname/AppData/Roaming/Picotron/picotron_config.txt
OSX: /Users/Yourname/Library/Application Support/Picotron/picotron_config.txt
Linux: ~/.lexaloffle/Picotron/picotron_config.txt
The default configuration:
mount / C:/Users/Yourname/AppData/Roaming/Picotron/drive
From inside Picotron, any folder in the file navigator can be opened in a regular non-Picotron file browser with "View in Host OS", or by typing "folder" from terminal. Picotron's filenav gui is a work in progress, so you might want to do any complex file management from the host OS for now!
ALT+ENTER: Toggle Fullscreen
ALT+F4: Fast Quit (Windows)
CTRL-Q: Fast Quit (Mac, Linux) -- need to enable in settings
CTRL-R: Reload / Run / Restart cartridge
CTRL-S: Quick-Save working cartridge (or current file if not inside /ram/cart)
ALT+LEFT/RIGHT Change Workspace (use option on Mac)
ESCAPE: Toggle between desktop / terminal
ENTER: Pause menu (fullscreen apps)
CTRL-6: Capture a Screenshot, saved as {Your Desktop}/picotron_desktop
CTRL-7: Capture a Cartridge Label
CTRL-8: Start recording a GIF (SHIFT-CTRL-8 to select a region first)
CTRL-9: End recording GIF
Picotron is a cartridge-orientated workstation. A cartridge is like an application bundle, or a project folder: it is a collection of Lua source code files, graphics, audio and any other data files the cartridge needs to run. The "present working cartridge" is always in a RAM folder named /ram/cart.
Click on the code editor workspace at top right, which looks like this: ()
Paste in a program (select here, CTRL-C, and then CTRL-V inside Picotron)
function _init()
bunny = unpod(
"b64:bHo0AEIAAABZAAAA-wpweHUAQyAQEAQgF1AXQDcwNzAHHxcHM"..
"AceBAAHc7cwFwFnAQcGAPAJZx8OJ0CXcF8dkFeQFy4HkBcuB5AXEBdA"
)
x = 232
y = 127
hflip = false
end
function _draw()
cls(3) -- clear the screen to colour 3 (green)
srand(0) -- reset the random number generator sequence
for i=1,50 do
print("\\|/",rnd(480),rnd(270),19) -- grass
print("*",rnd(480),rnd(270),rnd{8,10,14,23}) -- flower
end
ovalfill(x+2,y+15,x+12,y+18, 19) -- shadow
spr(bunny, x, y - (flr(hop)&2), hflip) -- bunny
end
function _update()
if (btn(0)) x -= 2 hflip = true
if (btn(1)) x += 2 hflip = false
if (btn(2)) y -= 2
if (btn(3)) y += 2
if (btn()>0) then
hop += .2 -- any button pressed -> hop
else
hop = 0
end
end
Now, press CTRL-R to run it. CTRL-R runs whatever is in /ram/cart, and the entry point in the cartridge is always main.lua (the file you were editing).
After hopping around with the cursor keys, you can halt the program by pressing ESCAPE, and then ESCAPE once more to get back to the code editor.
There is a lot going on here! The _init() function is always called once when the program starts, and here it creates an image stored as text (a "pod"), and then sets the bunny's initial x,y position.
_draw() is called whenever a frame is drawn (at 60fps or 30fps if there isn't enough available cpu). _update() is always called at 60fps, so is a good place to put code that updates the world at a consistent speed.
Normally graphics are stored in .gfx files included in the cartridge at gfx/*.gfx
Click on the second workspace to draw a sprite. By default, the sprite editor has /ram/cart/gfx/0.gfx open which is a spritebank that is automatically loaded when a cartridge is run (and can be modified with set_spr).
Now, instead of using the "bunny" image, the index of the sprite can be used:
spr(1, x, y, hflip) -- hflip controls horizontal flipping
The map works the same way: map/0.map is automatically loaded, and can be drawn with:
map()
Map files can also be loaded directly, as a table of layers. Each layer has a Userdata .bmp that can be passed to map() as the first parameter:
layers = fetch("map/0.map")
map(layers[1].bmp)
To adjust the draw position to keep the player centered, try using camera() at the start of _draw():
camera(x - 240, y - 135)
Multiple code tabs can be created by making a lua file for each one. Click on the [+] tab button near the top and type in a name for the new file (a .lua extension will be added automatically if needed), and then include them at the top of your main.lua program:
include "title.lua"
include "monster.lua"
include "math.lua"
The filename is relative to the present working directory, which starts as the directory a program is run from (e.g. /ram/cart).
To save a cartridge to disk, open a terminal from the picotron menu (top left), and type:
save mycart.p64
(or just "save mycart" ~ the .p64 extension will be added automatically)
The save command simply copies the contents of /ram/cart to mycart.p64.
Note that you can not save the cart while you are inside /ram/cart (e.g. after you press escape to halt a program). That would mean copying a folder somewhere inside itself! Also, saving to anything inside /ram or /system will only save to memory which disappears the next time Picotron is restarted.
Once a cartridge has been saved, the filename is set as the "present working cartridge", and subsequent saves can be issued with the shortcut: CTRL-S. To get information about the current cartridge, type "info" at the terminal prompt.
When editing code and graphics files inside a cartridge, those individual files are auto-saved to /ram/cart so that CTRL-R will run the current version (there's no need to save before each run).
When using an editor to edit a file that is outside /ram/cart, CTRL-S saves that individual file.
Picotron is still in alpha -- please keep plenty of backups and save often! If an editor crashes but the window manager is still running, there is a good chance you can still save to disk with CTRL-S.
Some handy commands: // "directory" means "folder"
ls list the current directory
cd foo change directory (e.g. cd /desktop)
mkdir foo create a directory
folder open the current directory in your Host OS
open . open the current directory in filenav
open fn open a file with an associated editor
rm filename remove a file or directory (be careful!)
cp f0 f1 copy file / directory f0 to f1
mv f0 f1 move file / directory f0 to f1
info information about the current cartridge
load cart load a cartridge into /ram/cart
save cart save a cartridge
To create your own commands, see: Custom_Commands
Cartridges can be shared on the lexaloffle BBS:
https://www.lexaloffle.com/bbs/?cat=8
First, capture a label while your cart is running with CTRL-7. For windowed programs, the label will include a screenshot of your desktop, so make sure you don't have anything personal lying around!
You can give the cartridge some metadata (title, version, author, notes) using about:
> about /ram/cart
Hit CTRL-S to save the changes made to the label and metadata.
Then make a copy of your cartridge in .p64.png format just by copying it:
> cp mycart.p64 releaseable.p64.png
The label will be printed on the front along with the title, author and version metadata if it exists. You can check the output by opening the folder you saved to, and then double clicking on releaseable.p64.png (it is just a regular png)
> folder
Finally, go to https://www.lexaloffle.com/picotron.php?page=submit to upload the cartridge. Cartridges are not publicly visible until a BBS post has been made including the cartridge (unless someone can guess the cartridge id that you give it). Cartridges can be loaded directly from the BBS using:
> load #bbs_cart_id
Or a specific version of the cart with the revision suffix:
> load #bbs_cart_id-0
Cartridges can also be shared as stand-alone html pages.
To export the currently loaded cartridge as a single file:
> export blah.html
View the page by opening the folder and double clicking on it:
> folder
Open the system settings via the Picotron menu (top left) or by typing "settings" at the prompt.
To create your own lists of themes, wallpapers and screensavers, create the following folders:
/appdata/system/themes
/appdata/system/wallpapers
/appdata/system/screensavers
Wallpapers and screensavers are regular .p64 cartridges -- you can copy anything in there that runs in fullscreen.
Widgets are programs that run in the slide-out tooltray (pull the toolbar down from the top), and are windowed programs that are not moveable and do not have a frame. See /system/startup.lua for examples of how to launch widgets. Additional widgets and other programs can be launched at startup by creating /appdata/system/startup.lua (which is run after /system/startup.lua).
Snap to grid mode for the desktop items can be enabled with the following command in terminal:
> store("/appdata/system/filenav.pod", {snap_to_grid=true})
To create your own terminal commands, put .p64 or .lua files in /appdata/system/util.
When a command is used from commandline (e.g. "ls"), terminal first looks for it in /system/util and /system/apps, before looking in /appdata/system/util and finally the current directory for a matching .lua or .p64 file.
The present working directory when a program starts is the same directory as the program's entry point (e.g. where main.lua is, or where the stand-alone lua file is). This is normally not desireable for commandline programs, which can instead change to the directory the command was issued from using env().path. For example:
cd(env().path)
print("args: "..pod(env().argv))
print("pwd: "..pwd())
Save it as /appdata/system/util/foo.lua, and then run it from anywhere by typing "foo".
Text input (using peektext() / readtext()) defaults to the host OS keyboard layout / text entry method.
Key states used for things like CTRL-key shortcuts (e.g. key("ctrl") and keyp("c")) are also mapped to the host OS keyboard layout by default, but can be further configured by creating a file called /appdata/system/keycodes.pod which assigns each keyname to a new scancode. The raw names of keys (same as US layout) can alternatively be used on the RHS of each assignment, as shown in this example that patches a US layout with AZERTY mappings:
store("/appdata/system/keycodes.pod", {a="q",z="w",q="a",w="z",m=";",[";"]=",",[","]="m"})
Note: you probably do not need need to do this! The default layout should work in most cases. Raw scancodes themselves can also be remapped in a similar way using /appdata/system/scancodes.pod, but is also normally not needed. The raw mapping is used in situations where the physical location of the key matters, such as the piano-like keyboard layout in the tracker. See /system/lib/events.lua for more details.
When opening a file via filenav or the open command, an application to open it with is selected based on the extension. To change or add the default application for an extension, use the default_app command. The following will associate files ending with ".sparkle" with the program "/apps/tools/sparklepaint.p64":
default_app sparkle /apps/tools/sparklepaint.p64
The table of associations is stored in: /appdata/system/default_apps.pod
Anywhen is a tool for reverting to earlier versions of cartridges that are saved to disk from inside Picotron. Every time a file is changed, Picotron records a delta between the last known version and the current one, and is able to fetch any earlier version of a cartridge as long as anywhen was active at that point in time. It can be turned off via settings.p64, but it is recommended during early alpha (0.1.*) to leave it running as it might be helpful in recovering lost data caused by bugs or lack of safety features (e.g. there is currently no confirmation required when saving over files).
To load an earlier version of a cartridge even after it has been deleted or moved, use a timestamp (written as local time) at the end of the load command:
load foo.p64@2024-04-10_12:00:00
An underscore is used between the date and time parts as spaces are not allowed in location strings.
When an earlier version from the same (local) day is needed, the date and optionally seconds parts can be omitted:
load foo.p64@12:00
Anywhen only stores changes made to files from within Picotron; it does not proactively look for changes made in external editors except when generating the first log file per day. Also, it only stores changes made to files saved to disk, and not to /ram.
The anywhen data is stored in the same folder as the default picotron_config.txt location (type "folder /" and then go up one folder in the host OS). The history is orgnised by month, and it is safe to delete earlier month folders if they are no longer needed (but they normally shouldn't take up too much space).
The code editor is open on boot in the first workspace, defaulting to /ram/cart/main.lua.
Like all of the standard tools, it runs in a tabbed workspace, where each tab is a separate process editing a single file.
To open or create a file in the editor from terminal:
> code foo.lua
Hold shift Select (or click and drag with mouse)
CTRL-X,C,V Cut copy or paste selected
CTRL-Z,Y Undo, redo
CTRL-F Search for text in the current tab
CTRL-L Jump to a line number
CTRL-W,E Jump to start or end of current line
CTRL-D Duplicate current line
TAB Indent a selection (SHIFT-TAB to un-indent)
CTRL-B Comment / uncomment selected block
SHIFT-ENTER To automatically add block end and indent
CTRL-UP/DOWN Jump to start or end of file (same as CTRL-HOME/END)
CTRL-CURSORS Jump left or right by word
The code editor can render gfx pod snippets (e.g. copied from the gfx editor) embedded in the code. See /system/demos/carpet.p64 for an example of a diagram pasted into the source code.
Those snippets contain a header string using block comments "--[[pod_type=gfx]]", so can not appear inside the same style of block comments and still parse and run. Use variations with some matching number of ='s between the square brackets instead:
--[==[
a picture:
--[[pod_type="gfx"]]unpod("b64:bHo0AC4AAABGAAAA-wNweHUAQyAQEAQQLFAsIEwwTBAEAAUR3AIAUxwBfAEcBgCg3CBMEUxAPBE8IA==")
]==]
The simplest way to use a text editor outside of Picotron is to store the files outside of a cartidge and then include() them. For example, the following snippet could be used at the top of main.lua:
cd("/myproj") -- files stored in here will be accessible to the host OS text editor
include("src/draw.lua") -- /myproj/src/draw.lua
include("src/monster.lua") -- /myproj/src/monster.lua
Just remember to copy them to the cartridge (and comment out the "cd(...)") before releasing it!
-- the -f switch copies over any existing directory
cp -f /myproj/src /ram/cart/src
As a general rule, released cartridges should be self-contained and not depend on anything except for /system.
The second workspace is a sprite and general-purpose image editor. Each .gfx file contains up to 256 sprites, and if the filename starts with a number, it is automatically loaded into that bank slot and used from the map editor. See map() and spr() for notes about sprite indexing.
Don't forget to save your cartridge after drawing something -- the default filenames all point to /ram/cart and isn't actually stored to disk until you use the save command (or CTRL-S to save the current cartridge)
SPACE Shortcut for the pan tool
MOUSEWHEEL To zoom in and out
S Shortcut for the select tool (hold down)
CTRL-A Select all
ENTER Select none
CURSORS Move selection
BACKSPACE Delete selection
CTRL-C Copy selection
CTRL-V Paste to current sprite
CTRL-B Paste big (2x2)
TAB Toggle RH pane
-,+ Navigate sprites
1,2 Navigate colours
RMB Pick up colour
F/V Flip selection horizontally or vertically
The drawing tools can be selected using icons under the palette:
PENCIL Draw single pixels
BRUSH Draw using a fill pattern and brush shape
LINE Draw a line // SHIFT to snap closest axis, diagonal, or 2:1 slope
RECT Draw a rectange // SHIFT to snap to a square
ELLIPSE Draw an elipse // SHIFT to snap to a circle
BUCKET Fill an area
STAMP Stamp a copy of the clipboard at the cursor
SELECT Select a rectangular region; press Enter to remove
PAN Change the camera position
RECT and ELLIPSE tools can be drawn filled by holding CTRL
To select multiple sprites at once, hold shift and click and drag in the navigator. Resizing and modifying sprite flags apply to all sprites in that region.
Each sprite has its own undo stack. Operations that modify more than one sprite at once (paste multiple, batch resize) create a checkpoint in each individual undo stack, and can only be undone once (ctrl-z) as a group immediately after the operation.
The map editor uses similar shortcuts to the gfx editor, with a few changes in meaning.
The F and V flip selected tiles, but also set special bits on those tiles to indicate that the tile itself should also be drawn flipped. The map() command also observes those bits.
To select a single tile (e.g. to flip it), use the picker tool (crosshair icon) or hold S for the section tool and use right mouse button.
Sprite 0 means "empty", and that tile is not drawn. The default sprite 0 is a small white x to indicate that it is reserved with that special meaning. This can be disabled; see map() for notes.
Each map file can contain multiple layers which are managed at the top right using the add layer ("+") button and the delete (skull icon) button. Currently only a single undo step is available when deleting layers, so be careful!
Layers can be re-ordered using the up and down arrow icon buttons, named using the pencil icon button, and hidden using the toggle button that looks like an eye.
Each layer can have its own size, and is drawn in the map editor centered.
A global tile size is used for all layers of a map file, taken from the size of sprite 0. The width and height do not need to match.
Sprites that do not match the global tile size are still drawn, but stretched to fill the target size using something equivalent to a sspr() call.
The SFX editor can be used to create instruments, sound effects (SFX), and music (SFX arranged into "patterns").
Each of these has their own editing mode that can be switched between by pressing TAB, or by clicking on the relevant navigator header on the left. Instruments can also be edited by CTRL-clicking on them, and SFX and pattern items always jump to their editing modes when clicked.
In the SFX and pattern editing modes, press SPACE to play the current SFX or pattern, and SPACE again to stop it.
Picotron has a specialised audio component called PFX6416 used to generate all sound. It takes a block of RAM as input (containing the instrument, track and pattern definitions). The .sfx file format is simply a 256k memory dump of that section of ram starting at 0x30000.
An instrument is a mini-synthesizer that generates sound each time a note is played. It is made from a tree of up to 8 nodes, each of which either generates, modifies or mixes an audio signal.
For example, a bass pluck instrument might have a white noise OSC node that fades out rapidly, plus a saw wave OSC node that fades out more slowly.
The default instrument is a simple triangle wave. To adjust the waveform used, click and drag the "WAVE" knob. In many cases this is all that is needed, but the instrument editor can produce a variety of sounds given some experimentation. Alternatively, check the BBS for some instruments that you can copy and paste to get started!
The root node at the top is used to control general attributes of the instrument. It has an instrument name field (up to 16 chars), and toggle boxes RETRIG (reset every time it is played), and WIDE (give child nodes separate stereo outputs).
To add a node, use one of the buttons on the top right of the parent:
+OSC: Add an oscillator (sound generator) to the parent.
+MOD: Modulate the parent signal using either frequency modulation or ring modulation.
+FX: Modify the parent signal with a FILTER, ECHO, or SHAPE effect.
An instrument that has two oscillators, each with their own FX applied to it before sending to the mix might look like this:
ROOT
OSC
FX:FILTER
OSC
FX:ECHO
During playback, the tree is evaluated from leaves to root. In this case, first the FX nodes are each applied to their parents, and then the two OSCs are mixed together to produce the output signal for that instrument.
Sibling nodes (a group with the same parent) can be reordered using the up and down triangle buttons. When a node is moved, it brings the whole sub-tree with it (e.g. if there is a filter attached to it, it will remain attached). Likewise, deleting a node will also delete all of its children.
The parameters for each node (VOL, TUNE etc) can be adjusted by clicking and dragging the corresponding knob. Each knob has two values that define a range used by Envelopes; use the left and right mouse button to adjust the upper and lower bounds, and the range between them will light up as a pink arc inside the knob.
Parameters can be evaluated relative to their parents. For example, a node might use a tuning one octave higher than its parent, in which case the TUNE will be "+ 12". The operator can be changed by clicking to cycle through the available operators for that knob: + means add, * means multiply by parent.
Below the numeric value of each knob there is a hidden multiplier button. Click it to cycle between *4, /4 and none. This can be used to alter the scale of that knobs's values. For example, using *4 on the BEND knob will give a range of -/+ 1 tone instead of -/+ 1/2 semitone. There are more extreme multipliers available using CTRL-click (*16, *64), which can produce extremely noisey results in some cases.
The default parameter space available in the instrument designer (without large multipliers) shouldn't produce anything too harsh, but it is still possible to produce sounds that will damage your eardrums especially over long periods of time. Please consider taking off your headphones and/or turning down the volume when experimenting with volatile sounds!
By default, instruments are rendered to a mono buffer that is finally split and mixed to each stereo channel based on panning position. To get stereo separation of voices within an instrument, WIDE mode can be used. It is a toggle button in the root node at the top of the instrument editor.
When WIDE mode is enabled, OSC nodes that are children of ROOT node have their own stereo buffers and panning position. FX nodes that are attached to ROOT are also split into 2 separate nodes during playback: one to handle each channel. This can give a much richer sound and movement between channels, at the cost of such FX nodes costing double towards the channel maximum (8) and global maxmimum (64).
There is only one type of oscillator (OSC), which reads data from a table of waveforms (a "wavetable"), where each entry in the table stores a short looping waveform. Common waveforms such as sine wave and square wave are all implemented in this way rather than having special dedicated oscillator types.
VOL volume of a node's output
PAN panning position
TUNE pitch in semitones (48 is middle C)
BEND fine pitch control (-,+ 1/2 semitone)
WAVE position in wavetable. e.g. sin -> tri -> saw
PHASE offset of wave sample
Noise is also implemented as a wavetable containing a single entry of a random sample of noise. Every process starts with 64k of random numbers at 0xf78000 that is used to form WT-1. Click the wavetable index (WT-0) in the oscilloscope to cycle through the 4 wavetables. WT-2 and WT-3 are unused by default.
At higher pitches, the fact that the noise is a repeating loop is audible. A cheap way to add more variation is to set the BEND knob's range to -/+ maximum and then assign an envelope to it. An LFO (freq:~40) or DATA envelope (LP1:16, LERP:ON, scribble some noisey data points) both work well.
A frequency modulator can be added to any oscillator. This produces a signal in the same way as a regular oscillator, but instead of sending the result to the mix, it is used to rapidly alter the pitch of its parent OSC.
For example, a sine wave that is modulating its parent OSC at a low frequency will sound like vibrato (quickly bending the pitch up and down by 1/4 of a semitone or so). The volume of the FM MOD signal determines the maximum alteration of pitch in the parent.
As the modulating frequency (the TUNE of the FM:MOD) increases, the changes in pitch of the parent OSC are too fast to hear and are instead perceived as changes in timbre, or the "colour" of the sound.
Similar to FM, but instead of modulating frequency, RING MOD modulates amplitude: the result of this oscillator is multiplied by its parent. At low frequencies, this is perceived as fluctuation in the parent's volume and gives a temelo-like effect.
// The name "ring" comes from the original implementation in analogue circuits, which uses a ring of diodes.
The filter FX node can be used to filter low or high frequencies, or used in combination to keep only mid-range frequencies. Both LOW and HIGH knobs do nothing at 0, and remove all frequencies when set to maximum.
>
LOW Low pass filter
HIGH High pass filter
RES Resonance for the LPF
Copy the signal back over itself from some time in the past, producing an echo effect. At very short DELAY values this can also be used to modify the timbre, giving a string or wind instrument feeling. At reasonably short delays (and layered with a second echo node) it can be used to approximate reverb.
DELAY How far back to copy from; max is around 3/4 of a second
VOL The relative volume of the duplicated siginal. 255 means no decay at all (!)
A global maximum of 16 echo nodes can be active at any one time. Echo only applies while the instrument is active; swtiching to a different instrument on a given channel resets the echo buffer.
Modify the shape of the signal by running the amplitude through a gain function. This can be used to control clipping, or to produce distortion when a low CUT (and high MIX) value is used. CUT is an absolute value, so the response of the shape node is sensitive to the volume of the input signal.
GAIN Multiply the amplitude
ELBOW Controls the gradient above CUT. 64 means hard clip. > 64 for foldback!
CUT The amplitude threshold above which shaping should take effect
MIX Level of output back into the mix (64 == 1.0)
Envelopes (on the right of the instrument designer) can be used to alter the value of a node parameter over time. For example, an oscillator might start out producing a triangle wave and then soften into a sine wave over 1 second. This is achieved by setting an upper and lower value for the WAVE knob (see Node_Parameters), and then assigning an evelope that moves the parameter within that range over time.
To assign an envelope to a particular node parameter, drag the "ENV-n" label and drop it onto the knob. Once an envelope has been assigned, it will show up as a blue number on the right of the knob's numeric field. Click again remove it, or right click it to toggle "continue" mode (three little dots) which means the envelope is not reset each time the instrument is triggered.
When an envelope is evaluated, it takes the time in ticks from when the instrument started playing (or when it was retriggered), and returns a value from 0 to 1.0 which is then mapped to the knob's range of values.
Click on the type to cycle through the three types:
ADSR (Attack Decay Sustain Release) envelopes are a common way to describe the change in volume in response to a note being played, held and released.
When the note is played, the envelope ramps up from 0 to maximum and then falls back down to a "sustain" level which is used until the note is released, at which point it falls back down to 0.
............................. 255 knob max
/\
/ \
/ \______ ....... S sustain level
/ \
/ \
../................\......... 0 knob min
|-----|--| |--|
A D R
Attack: How long to reach maximum. Larger values mean fade in slowly.
Decay: How long to fall back down to sustain level
Sustain: Stay on this value while the note is held
Release: How long to fall down to 0 from current value after release
For a linear fade in over 8 ticks, use: 8 0 255 0
For a linear fade out over 8 ticks: 0 8 0 0
The duration values are not linear. 0..8 maps to 0..8 ticks, but after that the actual durations start jumping up faster. 128 means around 5.5 seconds and 255 means around 23 seconds.
Low frequency oscillator. Returns values from a sine wave with a given phase and frequency.
freq: duration to repeat // 0 == track speed
phase: phase offset
A custom envelope shape defined by 16 values. Indexes that are out of range return 0.
LERP: lerp smoothly between values instead of jumping
RND: choose a random starting point between 0 and T0 (tick 0 .. T0*SPD-1)
SPD: duration of each index // 0 == track speed
LP0: loop back to this index (0..)
LP1: loop back to LP0 just before reaching this index when note is held
T0: starting index (when RND is not checked)
These attributes that control playback of data envelopes are also available to ADSR and LFO, accessible via the fold-out button that looks like three grey dots.
This is not an envelope, but works in a similar way. Right clicking on an envelope button (to the right of the knob's numeric field) when no envelope is assigned toggles random mode. When this mode is active, a pink R is shown in that spot, and a random value within the knob's range is used every time the instrument is triggered. This can be used to produce chaotic unexpected sounds that change wildly on every playthrough, or subtle variation to things like drum hits and plucks for a more natural sound.
A single track (or "SFX") is a sequence of up to 64 notes that can be played by the sfx() function.
SFX can be be played slowly as part of a musical pattern, or more quickly to function as a sound effect. The SPD parameter determines how many ticks (~1/120ths of a second) to play each row for.
Each row of a track has a pitch (C,C#,D..), instrument, volume, effect, and effect parameter. Instrument and volume are written in hexidecimal (instrument "1f" means 31 in decimal). Volume 0x40 (64) means 100% volume, but larger values can be used.
The pitch, instrument and volume can each be set to "none" (internally: 0xff) by typing a dot ("."). This means that the channel state is not touched for that attribute, and the existing value carries over.
An instrument's playback state is reset (or "retriggered") each time the instrument index is set, and either the pitch or instrument changes. When RETRIG flag is set on the instrument (node 0), only the instrument attribute index to be set for it to retrigger, even if the pitch is the same as the previous row (e.g. for a hihat played on every row at the same pitch).
Notes can be entered using a workstation keyboard using a layout similar to a musical keyboard. For a QWERTY keyboard, the 12 notes C..B can be played with the following keys (the top row are the black keys):
2 3 5 6 7
Q W E R T Y U
An additional octave is also available lower down on the keyboard:
S D G H J
Z X C V B N M
Use these keys to preview an instrument, or to enter notes in the SFX or pattern editing modes.
Notes are played relative to the global octave (OCT) and volume (VOL) sliders at the top left.
Some instruments do not stop playing by themselves -- press SPACE in any editor mode to kill any active sound generation.
Each effect command takes either a single 8-bit parameter or two 4-bit parameters.
PICO-8 effects 1..7 can be entered in the tracker using numbers, but are replaced with s, v, -, <, >, a and b respectively. The behaviour for those effects matches PICO-8 when the parameter is 0x00 (for example, a-00 uses pitches from the row's group of 4).
s slide to note (speed)
v vibrato (speed, depth)
- slide down from note (speed)
+ slide up from note (speed)
< fade in (start_%, speed)
> fade out (end_%, speed)
a fast arp (pitch0, pitch1)
b slow arp (pitch0, pitch1)
t tremelo (speed, depth)
w wibble (speed, depth) // v + t
r retrigger (every n ticks)
d delayed trigger (after n ticks)
c cut (after n ticks)
p set channel panning offset
The meaning of "speed" varies, but higher is faster except for 0 which means "fit to track speed".
Arpeggio pitches are in number of semitones above the channel pitch.
A pattern is a group of up to 8 tracks that can be played with the music() function.
Click on the toggle button for each track to activate it, and drag the value to select which SFX index to assign to it.
SFX items can also be dragged and dropped from the navigator on the left into the desired channel.
The toggle buttons at the top right of each pattern control playback flow, which is also observed by music():
loop0 (right arrow): loop back to this pattern
loop1 (left arrow): loop back to loop0 after finishing this pattern
stop (square): stop playing after this pattern has completed
Tracks within the same pattern have different can lengths and play at different speeds. The duration of the pattern is taken to be the duration (spd * length) of the left-most, non-looping track.
Picotron uses a slightly extended version of Lua 5.4, and most of the standard Lua libraries are available. For more details, or to find out about Lua, see www.lua.org.
The following is a primer for getting started with Lua syntax.
-- use two dashes like this to write a comment
--[[ multi-line
comments ]]
To create nested multi-line comments, add a matching number of ='s between the opening and closing square brackets:
--[===[
--[[
this comment can appear inside another multi-line comment
]]
]===]
Types in Lua are numbers, strings, booleans, tables, functions and nil:
num = 12/100
s = "this is a string"
b = false
t = {1,2,3}
f = function(a) print("a:"..a) end
n = nil
Numbers can be either doubles or 64-bit integers, and are converted automatically between the two when needed.
if not b then
print("b is false")
else
print("b is not false")
end
-- with elseif
if x == 0 then
print("x is 0")
elseif x < 0 then
print("x is negative")
else
print("x is positive")
end
if (4 == 4) then print("equal") end
if (4 ~= 3) then print("not equal") end
if (4 <= 4) then print("less than or equal") end
if (4 > 3) then print("more than") end
Loop ranges are inclusive:
for x=1,5 do
print(x)
end
-- prints 1,2,3,4,5
x = 1
while(x <= 5) do
print(x)
x = x + 1
end
for x=1,10,3 do print(x) end -- 1,4,7,10
for x=5,1,-2 do print(x) end -- 5,3,1
Variables declared as local are scoped to their containing block of code (for example, inside a function, for loop, or if then end statement).
y=0
function plusone(x)
local y = x+1
return y
end
print(plusone(2)) -- 3
print(y) -- still 0
In Lua, tables are a collection of key-value pairs where the key and value types can both be mixed. They can be used as arrays by indexing them with integers.
a={} -- create an empty table
a[1] = "blah"
a[2] = 42
a["foo"] = {1,2,3}
Arrays use 1-based indexing by default:
> a = {11,12,13,14}
> print(a[2]) -- 12
But if you prefer 0-based arrays, just write something the zeroth slot (or use the Userdata):
> a = {[0]=10,11,12,13,14}
Tables with 1-based integer indexes are special though. The length of such a table can be found with the # operator, and Picotron uses such arrays to implement add, del, deli, all and foreach functions.
> print(#a) -- 4
> add(a, 15)
> print(#a) -- 5
Indexes that are strings can be written using dot notation
player = {}
player.x = 2 -- is equivalent to player["x"]
player.y = 3
See the Table_Functions section for more details.
Picotron offers some shorthand forms following PICO-8's dialect of Lua, that are not standard Lua.
"if .. then .. end" statements, and "while .. then .. end" can be written on a single line:
if (not b) i=1 j=2
Is equivalent to:
if not b then i=1 j=2 end
Note that brackets around the short-hand condition are required, unlike the expanded version.
Shorthand assignment operators can also be used if the whole statement is on one line. They can be constructed by appending a '=' to any binary operator, including arithmetic (+=, -= ..), bitwise (&=, |= ..) or the string concatenation operator (..=)
a += 2 -- equivalent to: a = a + 2
Not shorthand, but Picotron also accepts != instead of ~= for "not equal to"
print(1 != 2) -- true
print("foo" == "foo") -- true (string are interned)
A Picotron program can optionally provide 3 functions:
function _init()
-- called once just before the main loop
end
function _update()
-- called 60 times per second
end
function _draw()
-- called each time the window manager asks for a frame
-- (normally 60, 30 or 20 times per second)
end
Graphics operations all respect the current clip rectangle, camera position, fill pattern fillp(), draw color, Colour_Tables and Masks.
sets the clipping rectangle in pixels. all drawing operations will be clipped to the rectangle at x, y with a width and height of w,h.
clip() to reset.
when clip_previous is true, clip the new clipping region by the old one.
sets the pixel at x, y to colour index col (0..63).
when col is not specified, the current draw colour is used.
for y=0,127 do
for x=0,127 do
pset(x, y, x*y/8)
end
end
returns the colour of a pixel on the screen at (x, y).
while (true) do
x, y = rnd(128), rnd(128)
dx, dy = rnd(4)-2, rnd(4)-2
pset(x, y, pget(dx+x, dy+y))
end
when x and y are out of bounds, pget returns 0.
get or set the colour (col) of a sprite sheet pixel.
when x and y are out of bounds, sget returns 0.
get or set the value (val) of sprite n's flag f.
f is the flag index 0..7.
val is true or false.
the initial state of flags 0..7 are settable in the sprite editor, so can be used to create custom sprite attributes. it is also possible to draw only a subset of map tiles by providing a mask in map().
when f is omitted, all flags are retrieved/set as a single bitfield.
fset(2, 1 | 2 | 8) -- sets bits 0,1 and 3
fset(2, 4, true) -- sets bit 4
print(fget(2)) -- 27 (1 | 2 | 8 | 16)
print a string str and optionally set the draw colour to col.
shortcut: written on a single line, ? can be used to call print without brackets:
?"hi"
when x, y are not specified, a newline is automatically appended. this can be omitted by ending the string with an explicit termination control character:
?"the quick brown fox\0"
additionally, when x, y are not specified, printing text below 122 causes the console to scroll. this can be disabled during runtime with poke(0x5f36,0x40).
print returns the right-most x position that occurred while printing. this can be used to find out the width of some text by printing it off-screen:
w = print("hoge", 0, -20) -- returns 16
set the cursor position.
if col is specified, also set the current colour.
set the current colour to be used by shape drawing functions (pset, circ, rect..), when one is not given as the last argument.
if col is not specified, the current colour is set to 6.
clear the screen and reset the clipping rectangle.
col defaults to 0 (black)
set a screen offset of -x, -y for all drawing operations
camera() to reset
draw a circle or filled circle at x,y with radius r
if r is negative, the circle is not drawn.
When bit 0x800000000 in col is set, circfill draws inverted (everything outside the circle is drawn).
draw an oval that is symmetrical in x and y (an ellipse), with the given bounding rectangle.
When bit 0x800000000 in col is set, ovalfill is drawn inverted.
draw a line from (x0, y0) to (x1, y1)
if (x1, y1) are not given, the end of the last drawn line is used.
line() with no parameters means that the next call to line(x1, y1) will only set the end points without drawing.
function _draw()
cls()
line()
for i=0,6 do
line(64+cos(t()+i/6)*20, 64+sin(t()+i/6)*20, 8+i)
end
end
draw a rectangle or filled rectangle with corners at (x0, y0), (x1, y1).
When bit 0x800000000 in col is set, rectfill draws inverted.
pal() swaps colour c0 for c1 for one of three palette re-mappings (p defaults to 0):
0: draw palette
The draw palette re-maps colours when they are drawn. For example, an orange flower sprite can be drawn as a red flower by setting the 9th palette value to 8:
pal(9,8) -- draw subsequent orange (colour 9) pixels as red (colour 8)
spr(1,70,60) -- any orange pixels in the sprite will be drawn with red instead
Changing the draw palette does not affect anything that was already drawn to the screen.
1: display palette
The display palette re-maps the whole screen when it is displayed at the end of a frame.
Set transparency for colour index c to is_transparent (boolean) transparency is observed by spr(), sspr(), map() and tline3d()
palt(8, true) -- red pixels not drawn in subsequent sprite/tline draw calls
When c is the only parameter, it is treated as a bitfield used to set all 64 values. for example: to set colours 0 and 1 as transparent:
-- set colours 0,1 and 4 as transparent
palt(0x13)
palt() resets to default: all colours opaque except colour 0. Same as palt(1)
Draw sprite s at position x,y
s can be either a userdata (type "u8" -- see Userdata) or sprite index (0..255 for bank 0 (gfx/0.gfx), 256..511 for bank 1 (gfx/1.gfx) etc).
Colour 0 drawn as transparent by default (see palt())
When flip_x is true, flip horizontally. When flip_y is true, flip vertically.
Stretch a source rectangle of sprite s (sx, sy, sw, sh) to a destination rectangle on the screen (dx, dy, dw, dh). In both cases, the x and y values are coordinates (in pixels) of the rectangle's top left corner, with a width of w, h.
s can be either a userdata (type "u8") or the sprite index.
Colour 0 drawn as transparent by default (see palt())
dw, dh defaults to sw, sh.
When flip_x is true, flip horizontally. When flip_y is true, flip vertically.
Get or set the sprite (a 2d userdata object of type "u8") for a given index (0..16383).
When a cartridge is run, files in gfx/ that start with an integer (0..63) are automatically loaded if they exist. Each file has 256 sprites indexes, so the sprites in gfx/0.gfx are given indexes 0..255, the sprites in gfx/1.gfx are given indexes 256..511, and so on up to gfx/63.gfx (16128..16383).
Set a 4x4 fill pattern using PICO-8 style fill patterns. p is a bitfield in reading order starting from the highest bit.
Observed by circ() circfill() rect() rectfill() oval() ovalfill() pset() line()
Fill patterns in Picotron are 64-bit specified 8 bytes from 0x5500, where each byte is a row (top to bottom) and the low bit is on the left. To define an 8x8 with high bits on the right (so that binary numbers visually match), fillp can be called with 8 arguments:
fillp(
0b10000000,
0b01011110,
0b00101110,
0b00010110,
0b00001010,
0b00000100,
0b00000010,
0b00000001
)
circfill(240,135,50,9)
Two different colours can be specified in the last parameter
circfill(320,135,50,0x1c08) -- draw with colour 28 (0x1c) and 8
To get transparency while drawing shapes, the shape target mask (see Masks) should be set:
poke(0x550b,0x3f)
palt()
--> black pixels won't be drawn
Colour tables are applied by all graphics operations when each pixel is drawn. Each one is a 64x64 lookup table indexed by two colours:
1. the colour to be drawn (0..63)
2. the colour at the target pixel (0..63)
Each entry is then the colour that should be drawn. So for example, when drawing a black (0) pixel on a red (8) pixel, the colour table entry for that combination might also be red (in effect, making colour 0 transparent).
Additionally, one of four colour tables can be selected using the upper bits 0xc0 of either the source or destination pixel. Using custom colour table data and selection bits allows for a variety of effects including overlapping shadows, fog, tinting, additive blending, and per-pixel clipping. Functions like pal() and palt() also modify colour tables to implement transparency and colour switching.
Colour tables and masks are quite low level and normally can be ignored! For more details, see: https://www.lexaloffle.com/dl/docs/picotron_gfx_pipeline.html
When each pixel is drawn, three masks are also used to determine the output colour. The draw colour (or pixel colour in the case of a sprite) is first ANDed with the read mask. The colour of the pixel that will be overwritten is then ANDed by the target mask. These two values are then used as indexes into a colour table to get the output colour. Finally, the write mask determines which bits in the draw target will actually be modified.
0x5508 read mask
0x5509 write mask
0x550a target mask for sprites
0x550b target_mask for shapes
The default values are: 0x3f, 0x3f, 0x3f and 0x00. 0x3f means that colour table selection bits are ignored (always use colour table 0), and the 0x00 for shapes means that the target pixel colour is ignored so that it is possible to draw a black rectangle with colour 0 even though that colour index is transparent by default.
The following program uses only the write mask to control which bits of the draw target are written. Each circle writes to 1 of the 5 bits: 0x1, 0x2, 0x4, 0x8 and 0x10. When they are all overlapping, all 5 bits are set giving colour 31.
function _draw()
cls()
for i=0,4 do
-- draw to a single bit
poke(0x5509, 1 << i)
r=60+cos(t()/4)*40
x = 240+cos((t()+i)/5)*r
y = 135+sin((t()+i)/5)*r
circfill(x, y, 40, 1 << i)
end
end
A map in Picotron is a 2d userdata of type i16. Each value refers to a single sprite in the "sprite registry" (see get_spr, set_spr).
The default tile width and height are set to match sprite 0.
The bits in each cel value:
0x00ff the sprite number within a bank (0..255)
0x3f00 the bank number (0..63)
0x4000 flip the tile horizontally
0x8000 flip the tile vertically
The tile flipping bits are observed by the map editor, map() and tline3d().
Both map() and mget() can be used in PICO-8 form that assumes a single global map, which defaults to the first layer of map/0.map if it exists, and can be set during runtime by memory-mapping an int16 userdata to 0x100000:
mymap = fetch("forest.map")[2].bmp -- grab layer 2 from a map file
memmap(mymap, 0x100000)
map() -- same as map(mymap)
?mget(2,2) -- same as mymap:get(2,2)
Draw section of a map (starting from tile_x, tile_y) at screen position sx, sy (pixels), from the userdata src, or from the current working map when src is not given. Note that the src parameter can be omitted entirely to give a PICO-8 compatible form.
To grab a layer from a .map file:
layers = fetch("map/0.map") -- call once when e.g. loading a level
map(layers[2].bmp)
To draw a 4x2 blocks of tiles starting from 0,0 in the map, to the screen at 20,20:
map(0, 0, 20, 20, 4, 2)
tiles_x and tiles_y default to the entire map.
map() is often used in conjunction with camera(). To draw the map so that a player object (drawn centered at pl.x in pl.y in pixels) is centered in fullscreen (480x270):
camera(pl.x - 240, pl.y - 135)
map()
p8layers is a bitfield. When given, only sprites with matching sprite flags are drawn. For example, when p8layers is 0x5, only sprites with flag 0 and 2 are drawn. This has nothing to do with the list of layers in the map editor -- it follows PICO-8's approach for getting more than one "layer" out of a single map.
tile_w and tile_h specify the integer width and height in pixels that each tile should be drawn. Bitmaps that do not match those dimensions are stretched to fit. The default values for tile_w and tile_h are 0x550e, 0x550f (0 means 256), which are in turn initialised to the dimensions of sprite 0 on run.
Sprite 0 is not drawn by default, so that sparse maps do not cost much cpu as only the non-zero tiles are expensive. To draw every tile value including 0, set bit 0x8 at 0x5f36:
poke(0x5f36), peek(0x5f36) | 0x8
PICO-8 style getters & setters that operate on the current working map. These are equivalent to using the userdata methods :get and :set directly:
mymap = userdata("i16", 32,32)
mymap:set(1,3,42)
?mymap:get(1,3) -- 42
memmap(mymap, 0x100000)
?mget(1,3) -- 42
Draw a textured line from (x0,y0) to (x1,y1), sampling colour values from userdata src. When src is type u8, it is considered to be a single texture image, and the coordinates are in pixels. When src is type i16 it is considered to be a map, and coordinates are in tiles. When the (src) is not given, the current map is used.
Both the dimensions of the map and the tile size must be powers of 2.
u0, v0, u1, v1 are coordinates to sample from, given in pixels for sprites, or tiles for maps. Colour values are sampled from the sprite present at each map tile.
w0, w1 are used to control perspective and mean 1/z0 and 1/z1. Default values are 1,1 (gives a linear interpolation between uv0 and uv1).
Experimental flags useful for polygon rendering / rotated sprites: 0x100 to skip drawing the last pixel, 0x200 to perform sub-pixel texture coordinate adjustment.
Unlike map() or PICO-8's tline, tline3d() does not support empty tiles: pixels from sprite 0 are always drawn, and there is no p8layers bitfield parameter.
Play sfx n (0..63) on channel (0..15) from note offset (0..63 in notes) for length notes.
Giving -1 as the sfx index stops playing any sfx on that channel. The existing channel state is not altered: stopping an sfx that uses an instrument with a long echo will not cut the echo short.
Giving -2 as the sfx index stops playing any sfx on that channel, and also clears the channel state state: echos are cut short and the channel is immediately silent.
Giving nil or -1 as the channel automatically chooses a channel that is not being used.
Negative offsets can be used to delay before playing.
When the sfx is looping, length still means the number of (posisbly repeated) notes to play.
Play music starting from pattern n.
n -1 to stop music
fade_len is in ms (default: 0). so to fade pattern 0 in over 1 second:
music(0, 1000)
channel_mask is bitfield that specifies which channels to reserve for music only, low bits first.
For example, to play only on the first three channels 0..2, the lowest three bits should be set:
music(0, nil, 0x7) -- bits: 0x1 | 0x2 | 0x4
Reserved channels can still be used to play sound effects on, but only when that channel index is explicitly requested by sfx().
This provides low level control over the state of a channel. It is useful in more niche situations, like audio authoring tools and size-coding.
Internally this is what is used to play each row of a sfx when one is active. Use 0xff to indicate an attribute should not be altered.
Every parameter is optional:
pitch channel pitch (default 48 -- middle C)
inst instrument index (default 0)
vol channel volume (default 64)
effect channel effect (default 0)
effect_p effect parameter (default 0)
channel channel index (0..15 -- default 0)
retrig (boolean) force retrigger -- default to false
panning set channel panning (-128..127)
To kill all channels (including leftover echo and decay envelopes):
note() -- same as sfx(-2, -1)
Global mixer state:
stat(464) -- bitfield indicating which channels are playing a track (sfx)
stat(465, addr) -- copy last mixer stereo output buffer output is written as
-- int16's to addr. returns number of samples written.
stat(466) -- which pattern is playing (-1 for no music)
stat(467) -- return the index of the left-most non-looping music channel
Per channel (c) state:
stat(400 + c, 0) -- note is held (0 false 1 true)
stat(400 + c, 1) -- channel instrument
stat(400 + c, 2) -- channel vol
stat(400 + c, 3) -- channel pan
stat(400 + c, 4) -- channel pitch
stat(400 + c, 5) -- channel bend
stat(400 + c, 6) -- channel effect
stat(400 + c, 7) -- channel effect_p
stat(400 + c, 8) -- channel tick len
stat(400 + c, 9) -- channel row
stat(400 + c, 10) -- channel row tick
stat(400 + c, 11) -- channel sfx tick
stat(400 + c, 12) -- channel sfx index (-1 if none finished)
stat(400 + c, 13) -- channel last played sfx index
stat(400 + c, 19, addr) -- fetch stereo output buffer (returns number of samples) stat(400 + c, 20 + n, addr) -- fetch mono output buffer for a node n (0..7)
Returns the state of button b for player index pl (default 0 -- means Player 1)
0 1 2 3 LEFT RIGHT UP DOWN
4 5 Buttons: O X
6 MENU
7 reserved
8 9 10 11 Secondary Stick L,R,U,D
12 13 Buttons (not named yet!)
14 15 SL SR
A secondary stick is not guaranteed on all platforms! It is preferable to offer an alternative control scheme that does not require it, if possible.
The return value is false when the button is not pressed (or the stick is in the deadzone), and a number between 1..255 otherwise. To get the X axis of the primary stick:
local dx = (btn(1) or 0) - (btn(0) or 0)
Stick values are processed by btn so that the return values are only physically possible positions of a circular stick: the magnitude is clamped to 1.0 (right + down) even with digital buttons gives values of 181 for btn(1) and btn(3), and it is impossible for e.g. LEFT and RIGHT to be held at the same time. To get raw controller values, use peek(0x5400 + player_index*16 + button_index).
Keyboard controls are currently hard-coded:
0~5 Cursors, Z/X
6 Enter -- disable with window{pauseable=false}
8~11 ADWS
12,13 F,G
14,15 Q,E
btnp is short for "Button Pressed"; Instead of being true when a button is held down, btnp returns true when a button is down and it was not down the last frame. It also repeats after 30 frames, returning true every 8 frames after that. This can be used for things like menu navigation or grid-wise player movement.
The state that btnp() reads is reset at the start of each call to _update60, so it is preferable to use btnp only from inside that call and not from _draw(), which might be called less frequently.
Custom delays (in frames 60fps) can be set by poking the following memory addresses:
poke(0x5f5c, delay) -- set the initial delay before repeating. 255 means never repeat.
poke(0x5f5d, delay) -- set the repeating delay.
In both cases, 0 can be used for the default behaviour (delays 30 and 8)
returns the state of key k
function _draw()
cls(1)
-- draw when either shift key is held down
if (key("shift")) circfill(100,100,40,12)
end
The name of each k is the same as the character it produces on a US keyboard with some exceptions: "space", "delete", "enter", "tab", "ctrl", "shift", "alt", "pageup", "pagedown".
By default, key() uses the local keyboard layout; On an AZERTY keyboard, key("a") is true when the key to the right of Tab is pressed. To get the raw layout, use true as the second parameter to indicate that k should be the name of the raw scancode. For example, key("a", true) will be true when the key to the right of capslock is held, regardless of local keyboard layout.
if (key"ctrl" and keyp"a") printh("CTRL-A Pressed")
keyp() has the same behaviour key(), but true when the key is pressed or repeating.
To read text from the keyboard via the host operating system's text entry system, peektext() can be used to find out if there is some text waiting, and readtext() can be used to consume the next piece of text:
while (peektext())
c = readtext()
printh("read text: "..c)
end
When "clear" is true, any remaining text in the queue is discarded.
Returns mouse_x, mouse_y, mouse_b, wheel_x, wheel_y
mouse_b is a bitfield: 0x1 means left mouse button, 0x2 right mouse button
when lock is true, Picotron makes a request to the host operating system's window manager to capture the mouse, allowing it to control sensitivity and movement speed.
returns dx,dy: the relative position since the last frame
event_sensitivity in a number between 0..4 that determines how fast dx, dy change (1.0 means once per picotron pixel)
move_sensitivity in a number between 0..4: 1.0 means the cursor continues to move at the same speed.
local size, col = 20, 16
function _draw()
cls()
circfill(240, 135, size*4, col)
local _,_,mb = mouse()
dx,dy = mouselock(mb > 0, 0.05, 0) -- dx,dy change slowly, stop mouse moving
size += dx -- left,right to control size
col += dy -- up,down to control colour
end
Strings in Lua are written either in single or double quotes or with matching [[ ]] brackets:
s = "the quick"
s = 'brown fox';
s = [[
jumps over
multiple lines
]]
The length of a string (number of characters) can be retrieved using the # operator:
>print(#s)
Strings can be joined using the .. operator. Joining numbers converts them to strings.
>print("three "..4) --> "three 4"
When used as part of an arithmetic expression, string values are converted to numbers:
>print(2+"3") --> 5
Convert one or more ordinal character codes to a string.
chr(64) -- "@"
chr(104,101,108,108,111) -- "hello"
Convert one or more characters from string STR to their ordinal (0..255) character codes.
Use the index parameter to specify which character in the string to use. When index is out of range or str is not a string, ord returns nil.
When num_results is given, ord returns multiple values starting from index.
ord("@") -- 64
ord("123",2) -- 50 (the second character: "2")
ord("123",2,3) -- 50,51,52
grab a substring from string str, from pos0 up to and including pos1. when pos1 is not specified, the remainder of the string is returned. when pos1 is specified, but not a number, a single character at pos0 is returned.
s = "the quick brown fox"
print(sub(s,5,9)) --> "quick"
print(sub(s,5)) --> "quick brown fox"
print(sub(s,5,true)) --> "q"
Split a string into a table of elements delimited by the given separator (defaults to ","). When separator is a number n, the string is split into n-character groups. When convert_numbers is true, numerical tokens are stored as numbers (defaults to true). Empty elements are stored as empty strings.
split("1,2,3") -- {1,2,3}
split("one:two:3",":",false) -- {"one","two","3"}
split("1,,2,") -- {1,"",2,""}
Returns the type of val as a string.
> print(type(3))
number
> print(type("3"))
string
create_delta returns a string encoding all of the information needed to get from str0 to str1 ("delta"). The delta can then be used by apply_delta to reproduce str1 given only str0.
For example, given the two strings:
str0 = the quick brown fox
str1 = the quick red fox
create_delta(str0, str1) will return a string that instructs apply_delta() to replace "brown" with "red".
d = create_delta(str0, str1)
print(apply_delta("the quick brown fox", d) --> the quick red fox
Note that the string given to apply_delta must be exactly the same as the one used to create the delta; otherwise apply_delta returns nil.
deltas can be used together with pod() to encode the difference between two tables of unstructured data:
a = {1,2,3}
b = {1, "banana", 2, 3}
d = create_delta(pod(a), pod(b))
-- reconstruct b using only a and the delta (d)
b2 = apply_delta(pod(a), d)
foreach(unpod(b2), print)
1
banana
2
3
This makes deltas useful for things like undo stacks and perhaps (later) changes in game state to send across a network. The binary format of the delta includes a few safety features like crc and length checks to ensure that the input and output strings are as expected. The first 4 bytes of the delta string are always "dst\0".
The backend for delta encoding is also used internally by anywhen to log incremental changes made to each file. There is a lot riding on its correctness ~ please let me know if you discover any odd behaviour with deltas!
With the exception of pairs(), the following functions and the # operator apply only to tables that are indexed starting from 1 and do not have NIL entries. All other forms of tables can be considered as unordered hash maps, rather than arrays that have a length.
Add value val to the end of table tbl. Equivalent to:
tbl[#tbl + 1] = val
If index is given then the element is inserted at that position:
foo={} -- create empty table
add(foo, 11)
add(foo, 22)
print(foo[2]) -- 22
Delete the first instance of value VAL in table TBL. The remaining entries are shifted left one index to avoid holes.
Note that val is the value of the item to be deleted, not the index into the table. (To remove an item at a particular index, use deli instead). del() returns the deleted item, or returns no value when nothing was deleted.
a={1,10,2,11,3,12}
for item in all(a) do
if (item < 10) then del(a, item) end
end
foreach(a, print) -- 10,11,12
print(a[3]) -- 12
Like del(), but remove the item from table tbl at index. When index is not given, the last element of the table is removed and returned.
Returns the length of table t (same as #tbl) When val is given, returns the number of instances of VAL in that table.
Used in for loops to iterate over all items in a table (that have a 1-based integer index), in the order they were added.
t = {11,12,13}
add(t,14)
add(t,"hi")
for v in all(t) do print(v) end -- 11 12 13 14 hi
print(#t) -- 5
For each item in table tbl, call function func with the item as a single parameter.
> foreach({1,2,3}, print)
Used in for loops to iterate over table tbl, providing both the key and value for each item. Unlike all(), pairs() iterates over every item regardless of indexing scheme. Order is not guaranteed.
t = {["hello"]=3, [10]="blah"}
t.blue = 5;
for k,v in pairs(t) do
print("k: "..k.." v:"..v)
end
Output:
k: 10 v:blah
k: hello v:3
k: blue v:5
A POD ("Picotron Object Data") is a string that encodes Lua values: tables, userdata, strings, numbers booleans, and nested tables containing those types.
PODs form the basis of all data transfer and storage in Picotron. Every file is a single POD on disk, the contents of the clipboard is a POD, images embedded in documents are PODs, and messages sent between processes are PODs.
Returns a binary string encoding val.
flags determine the encoding format (default: 0x0)
metadata is an optional value that is encoded into the string and stores additional information about the pod.
?pod({a=1,b=2})
{a=1,b=2}
pod() returns nil when the input value contains functions, circular references, or other values that can not be encoded.
flags:
0x1 pxu: encode userdata in a compressed (RLE-style) form
0x2 lz4: binary compression pass (dictionary matching)
0x4 base64 text encoding (convert back into a text-friendly format)
Plaintext PODs can get quite large if they contain images or map data. A compressed binary encoding can be generated using flags 0x1 and 0x2, which are normally used together as the pxu format aims to produce output that can be further compressed by lz4. store() uses this format by default.
The resulting string contains non-printable characters and starts with the header "lz4\0", so only the first 3 characters are printed here:
?pod({a=1,b=2}, 0x3)
lz4
returns the decoded value, and the decoded metadata as a second result:
str = pod({4,5,6}, 0, {desc = "an uninteresting sequence"})
c,m = unpod(str) -- returns content and metadata
?m.desc -- an uninteresting sequence
?c[1] -- 4
A file in picotron is a single POD (see the previous section), and uses the metadata part of the POD as a metadata fork. As such, files are stored and fetched atomically; there is no concept of a partial read, write or append.
store a lua object (tables, strings, userdata, booleans and numbers are allowed) as a file.
filenames can contain alphanumeric characters, "_", "-" and "."
When metadata is given, each field is added to the file's metadata without clobbering any existing fields.
store("foo.pod", {x=3,y=5})
a = fetch("foo.pod")
?a.x -- 3
When a cartridge needs to persist data (settings, high scores etc), it can use store() to write to /appdata:
store("/appdata/mygame_highscores.pod", highscore_tbl)
If the cartridge needs to store more than one or two files, a folder can be used:
mkdir("/appdata/mygamename")
store("/appdata/mygamename/highscores.pod", highscore_tbl)
Either method is fine -- in future versions, cartridges running directly from BBS will be sandboxed, in which case these folders will automatically be mapped to something like /appdata/bbs/cart_id/ (and not be able to read or clobber data written by other bbs carts).
When running under web, /appdata (and only /appdata) is persisted using Indexed DB storage. This applies to both html exports and carts running on the BBS.
Return a lua object stored in a given file. Returns the object and metadata.
Store and fetch just the metadata fork of a file or directory. This can be faster in some cases.
Create a directory
list files and folders in given path relative to the current directory.
Copy a file from src to dest. Folders are copied recursively, and dest is overwritten.
Move a file from src to dest. Folders are moved recursively, and dest is overwritten.
Delete a file or folder (recursive).
Mount points are also deleted, but the contents of their origin folder are not deleted unless explicitly given as a parameter to rm.
Return the present working directory. Relative filenames (that do not start with "/") all resolve relative to this path.
Change directory.
Resolve a filename to its canonical path based on the present working directory (pwd()).
returns 3 attributes of given filename (if it exists):
string: "file" or "folder"
number: size of file
string: origin of path
Load and run a lua file.
The filename is relative to the present working directory, not the directory that the file was included from.
Note that include() is quite different from PICO-8's #include, although it is used in a similar way. The difference is that include() is a regular function that is called at runtime, rather than PICO-8's #include which inserts the raw contents of the included file at the preprocessing stage.
include(filename) is roughly equivalent to:
load(fetch(filename))()
print a string to the host operating system's console for debugging.
Returns a table of environment variables given to the process at the time of creation.
?pod(env()) -- view contents of env()
stop the cart and optionally print a message
if condition is false, stop the program and print message if it is given. this can be useful for debugging cartridges, by assert()'ing that things that you expect to be true are indeed true.
assert(actor) -- actor should exist and be a table
actor.x += 1 -- definitely won't get a "referencing nil" error
Returns the number of seconds elapsed since the cartridge was run.
This is not the real-world time, but is calculated by counting the number of times _update60 is called. multiple calls of time() from the same frame return the same result.
Returns the current day and time formatted using Lua's standard date strings.
format: specifies the output string format, and defaults to "!%Y-%m-%d %H:%M:%S" (UTC) when not given. Picotron timestamps stored in file metadata are stored in this format.
t: specifies the moment in time to be encoded as a string, and can be either an integer (epoch timestamp) or a string indicating UTC in the format: "!%Y-%m-%d %H:%M:%S". When t is not given, the current time is used.
delta: number of seconds to add to t.
-- show the current UTC time (use this for timestamps)
?date()
-- show the current local time
?date("%Y-%m-%d %H:%M:%S")
-- convert a UTC date to local time
?date("%Y-%m-%d %H:%M:%S", "2024-03-14 03:14:00")
-- local time 1 hour ago
?date("%Y-%m-%d %H:%M:%S", nil, -60*60)
Each process in Picotron has a limit of 16MB RAM, which includes both allocations for Lua objects, and data stored directly in RAM using memory functions like poke() and memcpy(). In the latter case, 4k pages are allocated when a page is written, and can not be deallocated during the process lifetime.
Memory below 0x80000 and above 0xf00000 is mostly reserved for system use, but anything in the 0x80000..0xefffff range can be safely used for arbitrary purposes.
0x000000 ~ 0x003fff Legacy PICO-8 range, but probably safe to use!
0x004000 ~ 0x0047ff Primary P8SCII Font (2k)
0x005000 ~ 0x0053ff ARGB display palettes (1k)
0x005400 ~ 0x005477 Per-scanline rgb display palette selection (120 bytes)
0x005480 ~ 0x0054bf Indexed display palette (64 bytes)
0x0054c0 ~ 0x00553f Misc draw state (128 bytes)
0x005580 ~ 0x0055ff Raw controller state (128 bytes)
0x005600 ~ 0x005dff Secondary P8SCII font (2k)
0X005e00 ~ 0x005eff Reserved: P8 persistent state (256 bytes)
0x005f00 ~ 0x005f7f P8 draw State (some used by Picotron)
0x005f80 ~ 0x007fff Reserved: legacy P8 gpio, video memory
0x008000 ~ 0x00bfff Colour tables (16k)
0x00c000 ~ 0x00ffff Reserved (16k)
0x010000 ~ 0x02ffff Display / Draw Target (128k)
0x030000 ~ 0x07ffff Default audio data range
0x080000 ~ 0xefffff Available for arbitrary use
0xf00000 ~ 0xffffff Wavetable data
read a byte from an address in ram. if n is specified, peek() returns that number of results (max: 65536). for example, to read the first 2 bytes of video memory:
a, b = peek(0x10000, 2)
write one or more bytes to an address in base ram. if more than one parameter is provided, they are written sequentially (max: 65536).
i16,i32 and i64 versions
copy len bytes of base ram from source to dest. sections can be overlapping (but is slower)
write the 8-bit value val into memory starting at dest_addr, for len bytes.
for example, to fill half of video memory with 0xc8:
> memset(0x10000, 0xc8, 0x10000)
Each process in Picotron has a single window, and a single display that always matches the size of the window. The display is a u8 userdata that can be manipulated using the regular userdata methods, or using the gfx api while the display is also the draw target.
When a program has a _draw function but a window does not exist by the end of _init(), a fullscreen display and workspace is created automatically. To explicitly create a fullscreen display before then, window() with no parameters can be used.
Although switching between fullscreen and windowed modes is possible, the window manager does not yet support that and will produce unexpected results (a window in a fullscreen workspace, or a fullscreen window covering the desktop).
Returns the current display as a u8, 2d userdata. There is no way to set the display userdata directly; it can be resized using the window() function.
Set the draw target to ud, which must be a u8, 2d userdata. When ud is not given, set_draw_target() defaults to the current display.
Create a window and/or set the window's attributes. attribs is table of desired attributes for the window:
window{
width = 80,
height = 160,
resizeable = false,
title = "Palette"
}
function _draw()
cls(7)
for y=0,7 do
for x=0,3 do
circfill(10 + x * 20, 10 + y * 20, 7, x+y*4)
end
end
end
width -- width in pixels (not including the frame)
height -- height in pixels
title -- set a title displayed on the window's titlebar
pauseable -- false to turn off the app menu that normally comes up with ENTER
tabbed -- true to open in a tabbed workspace (like the code editor)
has_frame -- default: true
moveable -- default: true
resizeable -- default: true
wallpaper -- act as a wallpaper (z defaults to -1000 in that case)
autoclose -- close window when is no longer in focus or when press escape
z -- windows with higher z are drawn on top. Defaults to 0
cursor -- 0 for no cursor, 1 for default, or a userdata for a custom cursor
System cursors are named, and can be requested using a string:
pointer hand with a finger that presses down while mouse button is pressed
grab open hand that changes into grabbing pose while mouse button is pressed
dial hand in a dial-twirling pose that disappears while mouse button is held down
crosshair
Set a fullscreen video mode. Currently supported modes:
vid(0) -- 480x270
vid(3) -- 240x135
vid(4) -- 160x90
Userdata in Picotron is a fixed-size allocation of memory that can be manipulated as a 1d or 2d array of typed data. It is used to repesent many things in Picotron: vectors, matrices, to store sprites, maps and the contents of display. Therefore, all of these things can be manipulated with the userdata API. It is also possible to expose the raw binary contents of a userdata to RAM (by giving it an address with memmap), in which case userdata API can be used to directly manipulate the contents of RAM.
u = userdata("i16", 4, 8) -- a 4x8 array of 16-bit signed integers
u:set(2,1,42) -- set the elements at x=2, y=1 to 42
?#u -- the total number of elements (32)
Userdata can be indexed as a 1d array using square brackets, and the first 7 elements of a userdata can be accessed using special names: x y z u v w t.
The following assignments and references are equivalent for a 2d userdata of width 4:
u:set(2,1,42)
u[6] = 42
u.t = 42
?u:get(2,1)
?u[6]
?u.t
Create a userdata with a data type: "u8", "i16", "i32", "i64", or "f64". The first 4 are integers which are unsigned (u) or signed(i), and with a given number of bits. The last one is for 64-bit floats, and can be used to implement vectors and matrices.
data is a string of hexvalues encoding the initial values for integer values, or a list of f64s separated by commas.
A 2d 8-bit userdata can also be created by passing a PICO-8 [gfx] snippet as a string (copy and paste from PICO-8's sprite editor):
s = userdata("[gfx]08080400004000444440044ffff094f1ff1944fff9f4044769700047770000a00a00[/gfx]")
spr(s, 200, 100)
A convenience function for constructing 1d vectors of f64s.
v = vec(1.0,2.0,3.5)
-- euivalent to:
v = userdata("f64", 3)
v:set(0, 1.0,2.0,3.5)
returns the width, height of a userdata
height() returns nil for a 1d userdata.
?userdata(get_display():width()) -- width of current window
returns the width, height, type and dimensionality of a userdata. Unlike :height(), :attribs() returns 1 as the height for 1d userdata.
Return n values starting at x (or x, y for 2d userdata), or 0 if out of range.
?get_display():get(20, 10) -- same as ?pget(20, 10)
Set one or more value starting at x (or x, y for 2d userdata).
Values set at locations out of range are clipped and have no effect.
get and set are also available as global functions: set(u,0,0,3) is the same as u:set(0,0,3).
Return a row or column of a 2d userdata (0 is the first row or column), or nil when out of range.
Copy a region of one userdata to another. The following copies a 8x7 pixel region from sprite 0 to the draw target at 100, 50:
blit(get_spr(0), get_draw_target(), 0, 0, 100, 50, 8, 7)
Both src and dest must be the same type.
When dest is the draw target, the current clipping state is applied. Otherwise no clipping is performed (except to discard writes outside the destination userdata). In either case, no other aspects of the draw state are observed, and it is much faster than an equivalent sspr call.
All arguments are optional: width and height default to the src width and height, and the two userdata parameters default to the current draw target.
Change the type or size of a userdata. When changing data type, only integer types can be used.
The binary contents of the userdata are unchanged, but subsequent operations will treat that data using the new format:
> ud = userdata("i32", 2, 2)
> ud:set(0,0, 1,2,3,-1)
> ?pod{ud:get()}
{1,2,3,-1}
> ud:mutate("u8", 8,2)
> ?pod{ud:get()}
{1,0,0,0,2,0,0,0,3,0,0,0,255,255,255}
The total data size given by the new data type and dimensions must be the same as or smaller than the old one. In the above example, the userdata starts with 2x2 i32's (16 bytes) and is changed to 8x2 u8's (also 16 bytes).
When the width and height is not given, the existing width is used multiplied by the ratio of old data type size to new one, and the existing height is used as-is. Note that this can result in a loss of total data size when the width is not evenly divisible.
> ud = userdata("u8", 200, 50)
> ud:mutate("i64")
> ?{ud:attribs()}
{25,50,"i64",2}
linearly interpolate between two elements of a userdata
offset is the flat index to start from (default: 0)
len is the length (x1-x0) of the lerp, including the end element but not the start element.
el_stride is the distance between elements (default: 1)
Multiple lerps can be performed at once using num_lerps, and lerp_stride. lerp_stride is added to offset after each lerp.
> v = vec(2,0,0,0,10):lerp()
?pod{v:get()} -- 2,4,6,8,10
> v = vec(0,2,0,4,0):lerp(1,2)
?pod{v:get()} -- 0,2,3,4,0
> v = vec(2,0,0,0,10):lerp(0,2,2)
?pod{v:get()} -- 2,0,6,0,10
> v = vec(1,0,3,0,10,0,30):lerp(0,2,1, 2,4)
?pod{v:get()} -- 1,2,3, 0, 10,20,30
Userdata can be used with arithmetic operators, in which case the operator is applied per element:
v = vec(1,2,3) + vec(4,4,4)
?v -- (5.0, 6.0, 7.0)
When one of the terms is a scalar, that value is applied per element:
v = vec(1,2,3) + 10
?v -- (11.0, 12.0, 13.0)
Supported operators for any userdata type: + - * / %
Operators for integer userdata types: & | ^^
Each operator has a corresponding userdata metamethod that can take additional parameters:
:add :sub :mul :div :mod :band :bor :bxor
Additional operations that do not have an operator:
:copy -- equivalent to :add(0, ...) when u1 is nil
:max -- return the largest of each element / scalar
:min -- return the smallest of each element / scalar
Applies {userdata_op} (add, mult etc) to each element and written to a new userdata. All parameters are optional.
?vec(1,2,3):add(vec(4,4,4)) -- same as ?vec(1,2,3) + vec(4,4,4)
u0 or u1 can be a scalar (number) in which case it is treated as a userdata with a single element and stride of 0. In other words, that number is used as the LHS / RHS operand for each element:
v = vec(1,2,3)
v = v:add(10) -- add 10 to each element
v += 10 -- same thing
u2 is an optional output userdata, which can be the boolean value true to mean "write to self". This can be used to avoid the cpu overhead of creating new userdata objects.
v:add(10, v) -- add 10 to each element of v, written in-place
v:add(10, true) -- same thing
offset1 is the flat index of u1 to read from, and offset2 is the flat index of u2 (or u0) to write to. When a scalar is given as u1, offset1 is ignored and assumed to be 0.
len is the number of elements to process
The last 3 parameters (stride1, stride2 and spans) can be used together to apply the operation to multiple, non-contiguous spans of length len. stride1, and stride2 specify the number of elements between the start of each span for u1 and u2 respectively. Both are expressed as flat indices (i.e. for 2d userdata the element at x,y has a flat index of x + y * width).
This is easier to see with a visual example:
foo = userdata("u8", 4, "01020304")
function _draw()
cls()
circfill(240 + t()*10,135,100,7)
get_display():add(foo, true, 0, 0, 4, 0, 16, 10000)
end
This is an in-place operation -- reading and writing from the display bitmap (which is a 2d userdata).
It modifies the first 4 pixels in each group of 16, 10000 times (so 40000 pixels are modified).
First, 1 is added to the first pixel, 2 to the second, up to the 4th pixel. The second span starts at the 16th pixel, and reading again from the start of foo (because the stride for u1 is 0), which means the same 4 values are added for every span.
Note that this example is a pure userdata operation -- no graphical clipping is performed except to stay within the linear range of each input userdata.
Operations on userdata cost 1 cycle for every 24 operations, except for mult (16 ops), div (4 ops) and mod (4 ops), plus any overhead for the function call itself.
When :copy is given a table as the first argument (after self), it is taken to be a lookup table into that userdata for the start of each span.
** experimental for 0.1.1d ** out of range behaviour is currently undefined
src = vec(0,10,20,30,40)
idx = userdata("i32",8)
idx:set(0, 0,2,4,0,2,4,1,3) -- flat indexes into src
dest = src:copy(idx) -- defaults to idx->width spans
?pod{dest:get()} -- 0,20,40,0,20,40,10,30
Instead of reading from src directly, src:copy(idx) instead reads from idx and uses each value as a flat index into src for the start of each span. len elements are then copied from that position in src.
The default len (length of each span is 1), in which case the default shape of the output is the same as the shape of idx.
idx_stride is applied between each index, and defaults to 1.
dest_stride is applied after writing each span. It defaults to len.
To copy 3 spans, each of length 4:
src = vec(0,1,2,3,4,5,6,7)
idx = userdata("i32",3)
idx:set(0, 3,1,4)
dest = src:copy(idx,nil, 0,0, 4)
When the length of each span is > 1, the default shape of the output is a row for each span, in this case 3 rows with 4 elements in each row:
3 4 5 6
1 2 3 4
4 5 6 7
This is a common pattern when using indices as an efficient way to make references to larger data (e.g. 3d vectors that are shared by multiple faces, but should only be stored and transformed once each).
Matrices and vectors can be represented as 2d and 1d userdata of type f64:
mat = userdata("f64", 4, 4)
set(mat, 0, 0,
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
pos = vec(3,4,5)
?pos.x -- 3
pos += vec(10,10,10) -> 13.0, 14.0, 15.0
pos *= 2 -> 26.0, 28.0, 30.0
?v
Multiply two matrixes together. matmul is part of the userdata metatable, so it can also be called using the equivalent form: m0:matmul(m1).
When m_out is given, the output is written to that userdata. Otherwise a new userdata is created of width m1:width() and height m0:height().
As per standard matrix multiplication rules, the width of m0 and the height of m1 must match -- otherwise no result is returned.
mat = mat:matmul(mat)
v2 = vec(0.7,0.5,0.5,1):matmul(mat) -- vector width matches matrix height
For 3d 4x4 transformation matrices, matmul3d can be used.
matmul3d implements a common optimisation in computer graphics: it assumes that the 4th column of the matrix is (0,0,0,1), and that the last component of LHS vector (the mysterious "w") is 1. Making these assumptions still allows for common tranformations (rotate, scale, translate), but reduces the number of multiplies needed, and so uses less cpu.
matmul3d can be used on any size vector as only the first 3 components are observed, and anything larger than a 3x4 userdata for the RHS matrix; again, excess values are ignored.
So apart from the cpu and space savings, matmul3d can be useful for storing extra information within the same userdata (such as vertex colour or uv), as it will be ignored by matmul3d(). matmul() is less flexible in this way, as it requires unambiguous matrix sizes.
See /system/demos/carpet.p64 for an example.
:magnitude()
:distance(v)
:dot(v)
:cross(v, [v_out])
:matmul(m, [m_out])
:matmul2d(m, [m_out])
:matmul3d(m, [m_out])
:transpose([m_out])
Like the per-component operation methods, v_out or m_out can be "true" to write to self.
Matrix methods always return a 2d userdata, even when the result is a single row.
The contents of an integer-typed userdata can be mapped to ram and accessed using regular memory functions like peek and memcpy. This can be useful for things like swapping colour tables in and out efficiently.
Map the contents of an integer-type userdata to ram.
addr is the starting memory address, which must be in 4k increments (i.e. end in 000 in hex).
Userdata does not need to be sized to fit 4k boundaries, with one exception: addresses below 0x10000 must always be fully mapped, and memmap calls that break that rule return with no effect.
Unmap userdata from ram. When an address is given, only the mapping at that address is removed. This is relevant only when there are multiple mappings of the same userdata to different parts of memory.
unmap(ud) is needed in order for a userdata to be garbage collected, as mapping it to ram counts as an object reference. Overwriting mappings with memmap() is not sufficient to release the reference to the original userdata.
read or write from ram into an integer-typed userdata.
addr is the address to peek / poke
offset is the userdata element to start from (flattened 1d index), and len is the number of elements to peek/poke.
For example, to poke a font (which is a pod containing a single u8 userdata) into memory:
fetch("/system/fonts/p8.font"):poke(0x4000)
Or to load only the first 4 instruments of a .sfx file:
fetch("foo.sfx"):poke(0x40000, 0x10000, 0x200 * 4)
A userdata can be used to represent lists of arguments to be passed to gfx functions, so that multiple draws can be made with only the overhead of a single function call. This is supported by pset, circfill,
The following draws 3 circles:
args = userdata("f64", 4, 3)
args:set(0,0,
100,150,5,12, -- blue circle
200,150,5,8, -- red cricle
300,150,5,9) -- orange circle
circfill(args)
p is the f64 userdata -- normally 2d with a row for each call
offset is the flat offset into the userdata for the first call. Default: 0
num is the number of gfx calls to make. Default: p:height()
params is the number of parameters to pass to the gfx function. Default: p:width()
stride is the number of elements to jump after each call. Default: p:width()