v1.1.0
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Modules Pages
Scripting

Automate, introspect and debug apps via Lua scripting

Running Lua code

The Sifteo platform simulator Siftulator includes scripting support, via an embedded Lua interpreter. This scripting support is intended primarily for automated testing, and as a way to interface emulated Sifteo games with the outside world for debugging, testing, and exploration.

Within Siftulator, there are two ways to use Lua scripting.

Shell mode

You can start Siftulator with the -e command line option. This instructs Siftulator to run the specified Lua script, in the main thread, instead of starting the usual simulation environment.

In this mode, the Siftulator binary acts as a very basic shell around the Lua interpreter. By creating objects like System() and Frontend(), the script can then explicitly set up the parts of the simulation that it needs.

This mode is used internally, for unit-testing of Siftulator and the Sifteo Cube hardware model. It may also be suitable for black box testing of Sifteo games which have no debug instrumentation at all. These external tests can inspect the display contents of each cube, and they may read or write any memory in the system. They also have some limited capacity for interacting with the hardware simulation or with the UI Frontend.

Sample shell-mode script:

1 -- Lua script file
2 
3 print "Hello"
4 
5 sys = System()
6 fe = Frontend()
7 c = Cube(0)
8 
9 sys:setOptions{numCubes=1}
10 
11 sys:init()
12 fe:init()
13 sys:start()
14 
15 print(string.format("Radio address: %s", c:getRadioAddress()))
16 
17 repeat
18  fe:postMessage(string.format("Hello from Lua! (%.2f sec)",
19  sys:vclock()))
20  print(string.format("LCD: %10d pixels, %10d frames",
21  c:lcdPixelCount(), c:lcdFrameCount()))
22 until not fe:runFrame()
23 
24 sys:exit()
25 fe:exit()

Inline scripting

In this mode, script fragments are interleaved with normal C++ game code, using some macro, linker, and runtime tricks. It all starts with the SCRIPT() and SCRIPT_FMT() macros, which allow inline execution of Lua code in your game, with limited ways for game code and Lua code to exchange data.

It is often useful to include out-of-line Lua code via require(). All inline scripting runs in its own context, distinct from the context used by shell-mode scripting. This single context is shared between all blocks of inline Lua code. The code is always parsed and run in the order that the enclosing C++ code runs, so these opening declarations commonly happen at the top of main().

Sample in-line scripting code:

// Example fragments of inline scripting from C++
SCRIPT(LUA,
package.path = package.path .. ";scripts/?.lua"
require('my-test-library')
);
for (unsigned i = 0; i < 10; ++i)
SCRIPT(LUA, invokeTest());
SCRIPT(LUA, System():setAssetLoaderBypass(true));
SCRIPT(LUA, Cube(0):saveScreenshot("myScreenshot.png"));
SCRIPT(LUA, Cube(0):testScreenshot("myScreenshot.png"));
SCRIPT_FMT(LUA, "Frontend():postMessage('Power is >= %d')", 9000);
int luaGetInteger(const char *expr) {
int result;
SCRIPT_FMT(LUA, "Runtime():poke(%p, %s)", &result, expr);
return result;
}
void luaSetInteger(const char *varName, int value) {
SCRIPT_FMT(LUA, "%s = %d", varName, value);
}

Interfacing with the outside world

The Lua Input / Output library is available, for reading or writing files on disk..

Additionally, environment variables (via os.getenv) are a convenient way to read parameters at any point, whether you're using shell mode or inline scripting.

Execution Environment

Standard Lua packages

The Lua interpreter is based on Lua 5.1.

Standard Lua libraries:

Built-in extension modules:

System object

This is a singleton object which represents the simulated state of the system, at a high level.

System()

The constructor for System does not actually allocate any resources in Siftulator, it just allocates a Lua object which can then be used as a proxy for the simulated system state. Takes no parameters.

System():setOptions{ key = value, ... }

Set one or more key-value pairs which describe global simulation options.

Option Meaning
numCubes Number of cubes to simulate. Also set by the -n command line option.
turbo Boolean value. If false, the simulation runs as close to real-time as possible. If true, the simulation runs as fast as possible.
paintTrace Boolean value. If true, dump detailed Paint Controller logs.
radioTrace Boolean value. If true, log the contents of all radio packets.
svmTrace Boolean value. If true, log all executed SVM instructions.
svmFlashStats Boolean value. If true, dump statistics about flash memory usage.
svmStackMonitor Boolean value. If true, monitor SVM stack usage.

System():numCubes()

Retrieve the current number of simulated cubes. This value can be set with System():setOptions{numCubes=N}, the -n command line option, or keyboard commands in the UI.

System():init()

Initialize the simulation subsystem. This includes the simulated Cubes, radio, and Base. The system must be initialized before most other methods are invoked. Note that this function is only needed when using scripting in shell mode. With inline scripting, you're running from within the simulated environment, so it by necessity is already initialized.

System():start()

Begin running the simulation, on a separate thread pool. Like init(), this is only applicable in shell mode. Other modules, such as Frontend, may be initialized between calling init() and start().

System():exit()

Halt the simulation, and free resources associated with it. Only useful in shell mode.

System():setAssetLoaderBypass( true | false )

Enable or disable asset loader bypass mode. In this mode, all asset downloads will appear to complete instantaneously. Instead of fully simulating the asset download process using Siftulator's hardware-accurate simulation engine, the assets are decompressed using native code and written directly to the Cube's simulated Asset Flash memory.

Note that your game code must still go through the same procedure to install assets; this just reduces the amount of time taken by the install process, making for a quicker dev/test cycle.

System():vclock()

Return the current virtual time, in seconds. This is the elapsed time, from the perspective of the simulated system. If the simulation is running at 50% real-time, for example, this value will increase at a rate of 0.5 virtual seconds per real second.

The virtual clock's resolution is approximately 60 nanoseconds.

System():vsleep( seconds )

Block the caller for the specified number of seconds, in virtual time. This is not an exact delay. It tries to sleep for the minimum amount of time which is greater than or equal to the specified duration. The Lua scripting engine is not precisely synchronized with the simulation engine, however.

System():sleep( seconds )

Block the caller for a specified number of real wall-clock seconds. This depends on the underlying operating system's sleep primitive, and the accuracy will vary depending on the platform.

Frontend object

This is a singleton object which represents the graphical frontend to Siftulator. In shell mode, the frontend must be explicitly initialized, and your script is responsible for running the frontend's main loop. With inline scripting, the frontend is run automatically on a separate thread.

Frontend()

The constructor for Frontend does not actually allocate any resources in Siftulator, it just allocates a Lua object which can then be used as a proxy for the grapical frontend state. Takes no parameters.

Frontend():init()

Set up the frontend. Creates a window, allocates GPU resources, etc. If any of this fails, init() will throw a Lua error.

Frontend():exit()

Terminate the graphical frontend. Closes the window, frees textures, and so on.

Frontend():runFrame()

Run the frontend's main loop, for a single frame. This automatically includes a frame-rate throttling feature, which dynamically decreases the frame rate during periods of no interaction.

Returns true if the frontend should still be running or false if the user has asked Siftulator to exit.

Frontend():postMessage( string )

Post a message string to the frontend's heads-up display. A posted message will display for a few seconds before automatically clearing. Posting a new message will instantly replace the previously posted message.

This method is suitable for either transient messages, or for information displays that stay visible for long periods of time.

Cube object

The Cube object is an accessor for a single simulated Sifteo Cube. This is a lightweight Lua object which acts as a proxy for the internal object which simulates a single cube.

Cube( number )

The constructor for Cube does not actually allocate any resources in Siftulator, it just allocates a Lua object and binds it to a particular cube ID. Cube IDs are zero-based.

Cube(N):reset()

Reset the state of this cube's simulation. Equivalent to removing and reinserting the cube's batteries.

Cube(N):lcdFrameCount()

Read this cube's LCD frame counter. Every time the cube hardware finishes drawing one full frame, this counter increments. It is a 32-bit unsigned integer, which will wrap around.

When comparing two frame counts, the easiest way to handle wrap-around is to ensure that you're using 32-bit arithmetic as well. For example:

1 local c = Cube(0)
2 local count = c:lcdFrameCount()
3 local framesRendered = bit.band(count - lastCount, 0xFFFFFFFF)
4 lastCount = count

Cube(N):lcdPixelCount()

Read this cube's LCD pixel counter. This is much like lcdFrameCount(), except instead of incrementing once per completed frame, it increments every time a pixel is written to the display hardware. The pixel count will change continuously while a frame is being rendered.

This is also an unsigned 32-bit integer. Note that integer wraparound could occur in as little as 1 hour.

Cube(N):saveScreenshot( filename )

Save a screenshot of this cube, to a 128x128 pixel PNG file with the given name.

Cube(N):testScreenshot( filename, tolerance = 0 )

Capture a screenshot of this cube, and compare it to an existing 128x128 pixel PNG file with the given name.

If the images match, returns nothing. If there was an error opening the reference image file, raises a Lua error.

If the reference image was loaded successfully, this function compares the reference to the actual screenshot, pixel by pixel. By default, an exact match is required. Even a slight difference in the 16-bit value of a pixel would cause a pixel mismatch.

The optional tolerance parameter can be used to allow inexact matches. The images are still compared pixel-by-pixel. For each difference, an error value is computed:

  1. The two pixels, reference and actual, are both converted to 24-bit (8 bit per channel) color.
  2. For each of the three color channels: The two pixel values are subtracted, and that result is squared.
  3. The squared differences for each of the three channels are summed.

This is equivalent to the Mean Squared Error for that single pixel, multiplied by three. The computed error value is always an integer. If this error is less than or equal to tolerance, the comparison continues successfully. If it is greater, a pixel mismatch occurs.

In the event of a pixel mismatch, this function returns five parameters:

Position Name Meaning
1 x Zero-based X coordinate
2 y Zero-based Y coordinate
3 lcdPixel Actual pixel on the LCD, as a 16-bit RGB565 value
4 refPixel Reference pixel from the provided PNG, after conversion to 16-bit RGB565 format
5 errValue The actual error value for this pixel (greater than tolerance)

Cube(N):getNeighborID()

Returns the low-level neighbor ID for a cube. This is the 8-bit number used internally to identify a cube to its neighbors. The low 5 bits of this number will match the cube's CubeID in userspace. (The top three bits are reserved.) It will be zero if the cube is not sending any neighbor signal.

Cube(N):xbPoke( address, byte )

Write one byte to the cube's Video RAM, at the specified byte address. Byte addresses must be in the range [0, 1023]. Out-of-range addresses will wrap around.

Cube(N):xwPoke( address, word )

Write one 16-bit word to the cube's Video RAM, at the specified word address. Word addresses must be in the range [0, 511]. Out-of-range addresses will wrap around.

Cube(N):xbPeek( address )

Read one byte from the cube's Video RAM, at the specified byte address. Byte addresses must be in the range [0, 1023]. Out-of-range addresses will wrap around.

Cube(N):xwPeek( address )

Read one 16-bit word to the cube's Video RAM, at the specified word address. Word addresses must be in the range [0, 511]. Out-of-range addresses will wrap around.

Cube(N):fbPoke( address, byte )

Write one byte to the cube's Asset Flash memory, at the specified byte address.

Cube(N):fwPoke( address, word )

Write one 16-bit word to the cube's Asset Flash memory, at the specified word address.

Cube(N):fbPeek( address )

Read one byte from the cube's Asset Flash memory, at the specified byte address.

Cube(N):fwPeek( address )

Read one 16-bit word to the cube's Asset Flash memory, at the specified word address.

Runtime object

This is a singleton object which represents the simulated state of the Execution Environment.

Runtime()

The constructor for Runtime does not actually allocate any resources in Siftulator, it just allocates a Lua object which can then be used as a proxy for the simulated runtime state. Takes no parameters.

Runtime():poke( address, word )

Write a 32-bit word into RAM, at the specified virtual address. When using inline scripting, this address is equivalent to the value of a C++ pointer to integer or unsigned integer.

If the virtual address is invalid, raises a Lua error.

Runtime():peek( address )

Read a 32-bit word from RAM, at the specified virtual address. When using inline scripting, this address is equivalent to the value of a C++ pointer to integer or unsigned integer.

If the virtual address is invalid, raises a Lua error.

Runtime():faultString( code )

Given a numeric fault code, returns a string describing that fault.

Runtime():formatAddress( address )

Given a virtual address uses the currently loaded symbol tables, if any, to format the address as a string. Even if no suitable symbols can be found, this function will return a readable hexadecimal offset.

Runtime():onFault( code )

By default this function does nothing, but it can be overridden in order to handle VM faults in a special way. By returning 'true', the fault is considered to be handled and program execution will continue. You may wish to use functions like branch() to modify control flow first. By returning 'false', the default fault handler will proceed to run, and the application will be terminated.

You can use this to build unit tests that expect a fault to occur:

#include <sifteo.h>
using namespace Sifteo;
void NOINLINE buggyFunction()
{
LOG("About to crash...\n");
*(volatile int*)0 = 0;
LOG("Shouldn't get here!\n");
}
void NOINLINE handler()
{
LOG("Recovering from the fault...\n");
// This returns from buggyFunction()'s stack frame.
}
void main()
{
SCRIPT_FMT(LUA, "pHandler = 0x%x", &handler);
SCRIPT(LUA,
// On faults in Siftulator, transfer flow control to the C++ handler()
function Runtime:onFault(code)
print("Fault handled in Lua: " .. rt:faultString(code))
rt:branch(pHandler)
return true
end
// Creating an instance installs the callback
rt = Runtime()
);
// We expect this function to fail
buggyFunction();
}

Runtime():getPC()

Return the virtual CPU's program counter. Only recommended for use with inline scripting. This will reflect the state of the program at the point where it was interrupted to run the inline script.

Runtime():getSP()

Return the virtual CPU's stack pointer register.

Runtime():getFP()

Return the virtual CPU's frame pointer register. This will point to an 8-word structure containing state to be restored by the VM at the next return instruction.

Runtime():getGPR( n )

Return the value of a general purpose register in the virtual CPU. N must be between 0 and 7, inclusive.

Runtime():branch( pc )

Issue a branch to the specified code pointer. Only recommended for use with inline scripting. This is equivalent to an unconditional branch instruction that happened to execute at the exact place and time where the VM was stopped to run the script.

Runtime():setGPR( n, value )

Change the value of a general purpose register in the virtual CPU. N must be between 0 and 7, inclusive.

Runtime():runningVolume()

Return the filesystem's block code for the volume containing our current ELF binary.

Runtime():previousVolume()

Return the filesystem's block code for the volume which executed this one.

Runtime():virtToFlashAddr( virtual address )

Convert an SVM virtual address to a physical Flash memory address, using the currently active mappings. If this VA is not mapped, returns zero.

Runtime():flashToVirtAddr( flash address )

Convert a physical Flash memory address to an SVM virtual address. If the supplied flash address is not part of any virtual address space, returns zero.

Filesystem object

This is a singleton object which can be used to script the Base's filesystem.

This is the same filesystem which can be accessed with the SDK's Filesystem objects. By scripting the filesystem, you can automatically test portions of your game which depend on persistent data storage.

Filesystem()

The constructor for Filesystem does not actually allocate any resources in Siftulator, it just allocates a Lua object which can then be used as a proxy for the filesystem layer in the Base's operating system. Takes no parameters.

Filesystem():newVolume( type, payload data, type-specific data = "", parent = 0 )

Creates, writes, and commits a new Sifteo::Volume in the filesystem. The specified type is the same 16-bit type code used by the Sifteo::Volume::Type enumeration. Payload data is a string with binary data to load the Volume with.

The type-specific data parameter is an optional binary string, used by some volume types. Omitting this parameter is equivalent to passing in the empty string.

The parent parameter is a block code for another volume that is hierarchically above this new volume. If the parent volume is deleted, this volume (and its children, if any) are also deleted.

On success, returns the volume's block code. This is an opaque identifier which is indirectly related to the identity of a Sifteo::Volume object. It is guaranteed to be nonzero.

On error (out of filesystem space), returns no results.

Filesystem():listVolumes()

List all volumes on the filesystem. Returns an array of block codes.

Filesystem():deleteVolume( block code )

Mark a volume as deleted. If the provided block code is not valid, raises a Lua error.

Filesystem():volumeType( block code )

Given a volume's block code, return the associated 16-bit type identifier. If the provided block code is not valid, raises a Lua error.

Filesystem():volumeParent( block code )

Given a volume's block code, return its parent's block code, if any. If this volume is not parented to another, returns zero.

Filesystem():volumePayload( block code )

Given a volume's block code, return its payload data as a string.

Filesystem():simulatedBlockEraseCounts()

Return an array of simulated erase counts for every 64K block in the Base's flash memory. This data is tracked by Siftulator's simulation engine itself.

This can be used to measure how much flash wear and tear is being caused by a particular test.

If Siftulator is running with persistent flash storage (-F command line option), the erase counts are also persisted in the same file.

Filesystem():rawRead( address, count )

Read count bytes from the raw Flash device, starting at the specified device address. Returns the data as a string.

Filesystem():rawWrite( address, data )

Write the string data to the raw Flash device, starting at address. Does not automatically erase the device. This effectively performs a bitwise AND of the supplied data with the existing contents of the device.

Use this function with care. Remember you're bypassing the filesystem and writing to low-level flash memory. If you use rawWrite(), you likely must also use rawErase() and invalidateCache() correctly as well.

Filesystem():rawErase( address )

Erase one 64 kB block of the raw Flash device, starting at the specified block-aligned address.

Filesystem():invalidateCache()

Force the system to discard or reload all cached Flash data. Any memory blocks which are still in use will be overwritten with freshly-loaded data, while unused cached memory blocks are simply discarded.

In addition to the systemwide block cache, this flushes other special-purpose caches used by the filesystem.

Filesystem():setCallbacksEnabled( true | false )

This enables a set of Lua callbacks which fire on any low-level access to flash memory. These can be used for low-level tracing and debugging, or for collecting metrics on how memory is being used during a particular operation.

By default these callbacks are disabled for performance reasons. When enabled, the system will call onRawRead(), onRawWrite(), and onRawErase() during the corresponding low-level events. These functions all have default implementations which do nothing. They are meant to be overridden by user code.

For example:

1 function Filesystem:onRawRead(addr, data)
2  print(string.format("Read %08x, %d bytes", addr, string.len(data)))
3 end
4 
5 function Filesystem:onRawWrite(addr, data)
6  print(string.format("Write %08x, %d bytes", addr, string.len(data)))
7 end
8 
9 function Filesystem:onRawErase(addr)
10  print(string.format("Erase %08x", addr))
11 end
12 
13 fs = Filesystem()
14 fs:setCallbacksEnabled(true)

This overrides the three callback methods on Filesystem, then enables those callbacks. Any Flash memory operations after this point will be accompanied by logging.

The addresses supplied are physical flash addresses. Where applicable, you can translate them back to virtual addresses (a.k.a C++ pointers) using Runtime():flashToVirtAddr().

Filesystem():readMetadata( volume, key )

Read a metadata value from an ELF binary identified by a volume bock code. Returns the raw binary contents of the metadata key as a string, or nil if the key does not exist. Note that metadata values may be padded at the end.

Filesystem():readObject( volume, key )

Read a StoredObject from a particular ELF binary's object storage. Returns the raw binary contents of the object as a string, an empty string if the object has been deleted, or nil if the object doesn't exist in the filesystem at all.

Filesystem():writeObject( volume, key, data )

Write a StoredObject to a particular ELF binary's object storage. Automatically causes filesystem garbage collection if we're low on space. Raises a Lua error if we're actually out of storage space.