Introduction to Rivl Development

This document provides an introduction to the internals of Rivl for developers. It assumes familiarity with the Rivl language.

Table of Contents

1. The signal abstraction

The main abstraction implemented by Rivl is the signal. A signal is a continuous function of two real values, called x and y, to some type, such as byte, long, or double. The signal abstraction is general enough to implement all three of Rivl's media types -- images, sequences, and audio streams -- with a bit of creative interpretation for the last two.

2. The signal representation - graphs

Consider the following code, which performs a picture-in-a-picture operation on two images:
set im1 [im_read tiger.jpg]
set im2 [im_read football.jpg]
im_scale! im1 0.3
im_trans! im1 200 50
set out [im_overlay $im1 $im2]
im_write $out out.jpg
Rivl uses a demand-driven evaluation model. Commands such as im_trans and im_scale do not perform their operations when they are called. Rather, the operations are stored in a data structure, a directed acyclic graph, to be performed later. For example, the first five lines of the program above generates a graph that looks like:

In the graph, nodes represent signal operations and edges represent signals. For example, the edge following the scale node represents a signal that has been read from tiger.jpg and scaled down. Throughout the Rivl C interface and this document, the terms "signal" and "edge" are used interchangeably.

A node has zero or more input signals and exactly one output signal. A node with zero inputs is called a leaf node and its output signal is called a leaf signal. The descendents and ancestors of a signal refer to the subgraphs to its left and right, respectively.

Commands such as im_read, im_scale, im_trans, and im_overlay are called node commands. A node command adds a new node and a new outgoing signal to the graph. The node contains information which allows data to be computed later. The signal is given a unique name, like rivl_im5, which is returned to Tcl. If and when rivl_im5 is passed back to C in another command, a hash table lookup is used to find its signal.

Commands such as im_write and im_display are called content commands. A content command computes the data in some region of a signal and then acts on the data - by writing it to a file, displaying it, returning its sum, etc. Rivl computes a signal's data by propagating data up from the signal's descendents, starting with the leaf nodes. As the data passes through the graph, each node transforms it.

2.1. Graph datatypes

This section describes structures used to implement the graph.

2.1.1. Signals

The visible portion of a signal is:

typedef struct {
    Rivl_Node fromNode;
    Rivl_Signal *inputs;
    int inputCount;
} Rivl_Signal;
The fromNode field points to the node to the left of the signal. The inputs field points to an array of input signals, where inputCount gives the number of inputs.

Note that there is currently no way to determine the ancestors (the subgraph to the right) of a given signal or node. This may change in the near future.

2.1.2. Nodes

The visible portion of a node is:

typedef struct {
    Rivl_NodeClass classInfo;
    void *data;
} Rivl_Node;
Nodes are object-oriented in a limited sense. The classInfo field points to a Rivl_NodeClass structure containing information pertaining to all nodes of a class. The data field points to an optional structure containing data that differentiates a node from other instances of its class. Each node class decides whether its instances will make use of the data pointer and what kind of structure it will point to. For example, scale nodes are differentiated by how much to scale, so the data field in every scale node points to a structure containing x and y scale factors. However, dup nodes do not use the data field since their behavior is completely defined by their class.

A class is usually associated with one or more node commands that add a new node of that class to the graph. See node commands, below.

Note that there is currently no way to determine the input or output signals of a given node. The graph connectivity information is only maintained in the signal type.

3. Extending Rivl

3.1 Types of extensions

There are three ways to extend Rivl in C: Writing a node command, writing a content command, and creating a file format.

3.1.1 Node commands

A node command adds a new node and new outgoing signal to the graph, returning the name of the new signal. The inputs to the node, if any, as well as parameters that specify its behavior are given as arguments to the command. Writing a node command usually entails defining a new node class that the command will create instances of.

It is difficult to describe exactly how to add a new node command here. The best way to learn is by example. RIVL/doc/examples/node.c contains a complete, well-documented example of a module that adds a node command. In addition, RIVL/nodes.c contains code for all the built-in nodes in Rivl.

All of the Rivl commands necessary for building node commands should be documented in the manual pages.

3.1.2 Content commands

A content command is a Tcl command which computes some data from one or more signals as part of its action. For example, it might write some signal data to a file (like im_write) or return a value based on the data (like sig_hash).

Once again, the best way to get started with content commands is to look at examples. RIVL/doc/examples/content.c contains a well-documented example of a content command. You can also examine the source code for built-in Rivl content commands, such as Sig_HashCmd (in misc.c) and Im_FillPhotosCmd (in tkutil.c).

All of the Rivl commands necessary for building content commands should be documented in the manual pages.

3.1.3 File formats

A file format handler is code to read and write an I/O format. For example, Rivl has built-in handlers to read and write with JPEG images and MPEG sequences. Building a file format handler entails writing a read procedure (a node command) and a write procedure (a content command), and registering the pair with Rivl_CreateFileFormat. For examples, look at ppm.c and mjpgc.c in the Rivl source code.

3.2 Dynamic loading

After you create your extension, you need to incorporate it into Rivl. The best way to do this is to use dynamic loading. Compile and link your module separately to create a shared object, for example a ".so" file on Sun machines. You can then use rivl_dl to load the module into a running Rivl interpreter. See the rivl_dl manual page for more information.

Back to contents