Skip to main content

Writing Paint Programs

This guide walks through how to write Paint programs, building from minimal single-tile layouts to multi-tile pipelines with routing, replication, and auto-routing. Each section introduces new concepts with working examples.

For the full primitive reference, see Paint Language Reference.


1. Hello World: One Tile​

The simplest possible paint program — a single PE running one kernel:

let t = tile()
t : code('add [N:N])

What happens:

  1. tile() creates a 1×1 region at position (0, 0).
  2. code('add ...) attaches the Tungsten kernel add.w to that tile.
  3. N:N binds the kernel parameter N to the paint variable N, which a test.rc row sets (N=4) and the Makefile's PAINTVARS passes to paint.

There is no routing because the kernel operates only on local memory.

Makefile integration​

The Makefile of the tutorial's lesson 1, Hello, wafer!, abridged:

include $(FLUTEMK)
include $(TESTMK)

PAINTVARS=N
PAINTFLAGS=lib=.. $(PAINTOPTS) verbose arch=${ARCH}

and the first row of its test.rc, which names the test and sets its variables:

tutorial N=4 ARCH=sdr

PAINTVARS lists the row variables that paint imports. PAINTFLAGS adds the library search path where paint finds the .w files, any PAINTOPTS given on the command line, and the target architecture.


2. Two Tiles: Send and Receive​

To move data between tiles you need a color and routing:

let txcolor = color()

let send = tile()
send : code('send [tx:txcolor N:N])
send : paint(txcolor [C>R])
send : name('send)

let recv = tile()
recv : code('recv [rx:txcolor N:N])
recv : paint(txcolor [L>C])
recv : name('recv)

hstack(send recv)

Concepts introduced​

ConceptSyntaxPurpose
Colorcolor()Allocates a routing channel
Socket bindingtx:txcolorBinds a Tungsten socket to a color; paint picks its physical number
Route rule[C>R]PE sends from Center to Right
Route rule[L>C]PE receives from Left into Center
Namingname('send)Labels the region for debug and miget
Layouthstack(send recv)Places tiles side by side

How data flows​

send recv
┌──────┐ ┌──────┐
│ C>R │ ───▶ │ L>C │
│ │ │ │
└──────┘ └──────┘
x=0 x=1

The sender's C>R route sends what its CE puts on txcolor to its right (east) neighbor. The receiver's L>C route takes what arrives from its left (west) neighbor on the same color and delivers it to its CE.


3. Three-Tile Pipeline​

A compute tile in the middle receives on one color and sends on another:

let rxcolor = color()
let txcolor = color()

let send = tile()
send : code('send [tx:rxcolor N:N])
send : paint(rxcolor [C>R])
send : name('send)

let compute = tile()
compute : code('compute [rx:rxcolor tx:txcolor N:N])
compute : paint(rxcolor [L>C])
compute : paint(txcolor [C>R])
compute : name('compute)

let recv = tile()
recv : code('recv [rx:txcolor N:N])
recv : paint(txcolor [L>C])
recv : name('recv)

hstack(send compute recv)

Key pattern: two colors through one tile​

When data passes through a tile's CE (received, then sent on), it needs two distinct colors:

  • rxcolor carries data into the tile (L>C)
  • txcolor carries data out of the tile (C>R)

A switch position takes one source, so one color cannot carry both L>C and C>R in the same position. On one color this would take two switch positions (;) and an advance between them. Data that the router carries past the CE, as in [L>R], needs only one color.


4. Rectangles and Boundary Routing​

For 2D grids, use rect() and boundary operators:

let [a b c d a' b' c' d'] = color()

let grid = rect(0 0 W H 'x 'y)

(* Outbound routing with boundary suppression *)
grid : paint(a' [CXR|R]) (* send right, suppress at right edge *)
grid : paint(b' [CXL|L]) (* send left, suppress at left edge *)
grid : paint(c' [CXU|U]) (* send up, suppress at top edge *)
grid : paint(d' [CXD|D]) (* send down, suppress at bottom edge *)

(* Inbound routing with boundary suppression *)
grid : paint(a [L|LXC]) (* receive from left, suppress at left edge *)
grid : paint(b [R|RXC]) (* receive from right, suppress at right edge *)
grid : paint(c [D|DXC]) (* receive from below, suppress at bottom edge *)
grid : paint(d [U|UXC]) (* receive from above, suppress at top edge *)

(* Declare swap pairs *)
grid : colorpair(a a')
grid : colorpair(b b')
grid : colorpair(c c')
grid : colorpair(d d')

Boundary operator |​

| before > means "don't accept from this direction at the boundary":

grid : paint(c [L|L>R]) (* leftmost column doesn't accept from LEFT *)

| after > means "don't send in this direction at the boundary":

grid : paint(c [L>R|R]) (* rightmost column doesn't send RIGHT *)

The boundary is the edge of the rect() the rule is painted on. Apply boundary rules to one rect() that covers the grid, as here. On a region joined with union, hstack or vstack, Paint cuts the route inside the region (see Boundary operators).

Color swap (X, H, V)​

X replaces >. In [CXR|R] on a', it makes a' take what the core sends on a. In [L|LXC] on a, it makes a take a' arriving from the left neighbor. So the kernel sends right and receives from the left with the one socket a, and every tile runs the same rules. A color takes input from one port at a time, so without swap a uniform row needs colors that alternate from tile to tile.


5. Using Functions​

Paint supports user-defined functions to encapsulate common patterns:

define make_pipeline_tile(kernel rx_color tx_color params) {
let t = tile()
t : code(kernel params)
t : paint(rx_color [L>C])
t : paint(tx_color [C>R])
t
}

define make_sender(kernel color params) {
let t = tile()
t : code(kernel params)
t : paint(color [C>R])
t
}

define make_receiver(kernel color params) {
let t = tile()
t : code(kernel params)
t : paint(color [L>C])
t
}

Usage:

let c1 = color()
let c2 = color()

let s = make_sender('send c1 [N:N tx:c1])
s : name('send)

let m = make_pipeline_tile('compute c1 c2 [N:N rx:c1 tx:c2])
m : name('compute)

let r = make_receiver('recv c2 [N:N rx:c2])
r : name('recv)

hstack(s m r)

6. Replication Patterns​

rep — simple replication​

let row = tile()
row : code('pe [N:N])
let grid_row = rep(row 'x 8 1 0) (* 8 copies, each +1 in x *)

hstackrep / vstackrep — stacking with bounding-box awareness​

let cell = tile()
cell : code('pe [N:N])

let row = hstackrep(cell 8) (* 8 copies horizontally *)
let col = vstackrep(cell 4) (* 4 copies vertically *)
let row_named = hstackrep(cell 8 0 'col) (* named dimension *)
let spaced = hstackrep(cell 4 2) (* a 2-tile gap between copies *)

arrayplace — non-uniform placement​

let t = tile()
t : code('pe [idx:@rank])

let offsets = [0 3 5 10]
let ranks = [0 1 2 3]
let layout = arrayplace(t offsets [rank:ranks])

Each copy is placed at the corresponding Y offset and named x0, x1, etc.

Optional geo-variable with arrayplace​

let ids = [100 200 300 400]
let layout_with_ids = arrayplace(t offsets [rank:ranks id:ids])

This continues the preceding example: both @rank and @id are defined for every copy; placement still uses offsets, not the ID values.


7. Geo-Variables in Detail​

Geo-variables pass tile-dependent values to kernels. Paint compiles each distinct resolved parameter combination once and reuses matching variants:

(* Define regions with different geo-values *)
let left = tile(0 0)
left : geovar('rank 0)

let right = tile(1 0)
right : geovar('rank 1)

(* Both tiles use the same code, but @rank differs *)
let both = rect(0 0 2 1)
both : code('kernel [id:@rank])
both : union(left right)

The @ prefix in code() references the geo-variable. The geo-variable must be defined on every PE where the corresponding code attribute is applied. Here the geo-variables sit on left and right, beside the code region both. Setting them on parts of a region and then applying code() to that whole region does not work: paint ignores them without a message (see Geo-Variables).


8. Dispatch Pattern: RPC over a Color​

This is the pattern used for wavelet-driven dispatch in Tungsten:

let cmdcolor = color()
let rxtx = color()

let send = tile()
send : code('send [tx:cmdcolor])
send : paint(cmdcolor [C>R])
send : name('send)

let compute = tile()
compute : code('spmv [dotrx:cmdcolor y:rxtx])
compute : paint(cmdcolor [L>C])
compute : paint(rxtx [C>R])
compute : name('spmv)

let recv = tile()
recv : code('recv [rx:rxtx N:N])
recv : paint(rxtx [L>C])
recv : name('recv)

hstack(send compute recv)

The compute tile uses cmdcolor for incoming command wavelets (dispatch) and rxtx for outgoing result data. This maps directly to Tungsten's dispatch() block and socket model.


9. Auto-Routing with Terminals and Transports​

For complex layouts where tiles are not adjacent, use terminals and transports instead of manual routing:

let c = color()

(* Sender tile with a terminal *)
let a = tile(0 0)
a : paint(c [C>R])
a : terminal('tx 'data RIGHT c [1 1] [1 1] 1)
a : name('a)

(* Receiver tile with a terminal *)
let b = tile(5 0)
b : paint(c [L>C])
b : terminal('rx 'data CENTER c [1 1] [1 1] 1)
b : name('b)

(* Connect them *)
let arc = transport('a.tx 'b.rx 'arc)

(* Merge everything *)
let all = union(a b arc)

Terminal rules​

  • Terminals must share the same tag to be connected.
  • Both terminals use one color: paint gives the two terminals of a transport the same physical color.
  • Each terminal needs a paint() rule on its PE that routes its color through its side, such as [C>R] for RIGHT; without one, paint stops with has no pins defined.
  • The region must be named before the terminal can be referenced.
  • Apply name() after terminal().
  • A transport names each terminal by its path of region names, such as 'a.tx or 'outer.inner.rx.

If paint cannot connect a transport, it prints port(s) of arc a.tx -> b.rx not found and still writes the image, which then has no route: the receiver waits forever. A transport left out of the union gives no message at all.

Using aliases for hierarchy​

alias() gives a nested terminal a name on the outer region, so a transport can name 'outer.rx without knowing what is inside outer:

let inner = tile(5 0)
inner : paint(c [L>C])
inner : terminal('rx 'data CENTER c [1 1] [1 1] 1)
inner : name('inner)

let outer = rect(5 0 2 1)
outer : union(inner)
outer : alias('rx 'inner.rx)
outer : name('outer)

let arc = transport('a.tx 'outer.rx 'arc)

10. Dictionaries for Configuration​

Dictionaries provide keyword-argument-style parameter passing:

define make_module(options) {
let [width height depth] = query(options [width:4 height:4 depth:1])
let r = rect(0 0 width height)
r : code('kernel [W:width H:height D:depth])
r
}

(* Use with custom values *)
let m1 = make_module([width:8 height:8]) (* depth defaults to 1 *)

(* Use with all defaults *)
let m2 = make_module([])

query(dict defaults) looks up each key from defaults in dict, falling back to the default value if not found. Keys of dict that are not in defaults are ignored without a warning, so a misspelt key such as [widht:3] leaves the default.


11. Conditional Logic​

Paint has no if/else statement, but setif() provides conditional expressions:

let use_buffer = lt(N 16)
let r = setif(use_buffer make_small(N) make_large(N))

Warning: both branches are always evaluated. Both must be valid expressions even if only one is used.

(* Conditional region inclusion *)
let optional = setif(ENABLE make_extra_tile() rect(0 0 0 0))
let all = union(main optional)

An empty rectangle rect(0 0 0 0) serves as a no-op region. hstackrep(r 0) is another way to conditionally exclude a region.


12. Switch Routing (Multi-Phase)​

For kernels that change a tile's routing while they run, for example to take turns on a shared color:

t : paint(c [L>R; L>U; D>U; ring pop=all])

This configures three switch positions:

  1. Accept from left, send right
  2. Accept from left, send up
  3. Accept from down, send up

The ring flag makes the switch cycle back to position 0 after the last position. ring and pop=all are modifiers, not positions.

Pop modes​

ModeDescription
pop=allUnconditional pop
pop=advPop on advance
pop=advnopPop on advance or nop

A pop removes command 0 of a control wavelet as it leaves the router, so the next router acts on the next command; see Switch Positions.


13. Includes and Library Structure​

Break large paint files into reusable modules:

(* common.paint *)
define make_sender(kernel color N) {
let t = tile()
t : code(kernel [tx:color N:N])
t : paint(color [C>R])
t
}

(* main.paint *)
include "common.paint"

let c = color()
let s = make_sender('send c N)
s : name('send)

Library paths are specified in the Makefile:

PAINTFLAGS = lib=../common lib=../kernels

14. Passthrough Routing​

passthrough(region color_in color_out) links colors for bus and entry-point assignment. It does not program any router routes. To forward data without touching the PE, use an explicit route:

(* Pass data through without touching the PE *)
let c = color()
let t = tile()
t : paint(c [L>R]) (* horizontal pass-through *)
let vertical = tile(0 1)
vertical : paint(c [D>U]) (* vertical pass-through on another tile *)
union(t vertical)

15. Complete Real-World Example: 9-Point Stencil​

A 2D stencil computation where each PE communicates with its four neighbors. This section shows the layout; the bounds and ninept kernels it attaches are not shown, and W and H are set on the paint command line:

(* Allocate 8 colors: 4 directions × send/receive *)
let [a b c d a' b' c' d'] = color()

(* Boundary tiles run a special kernel *)
let shore = union(
rect(0 0 W 1) (* bottom row *)
rect(0 H-1 W 1) (* top row *)
rect(0 1 1 H-2) (* left column *)
rect(W-1 1 1 H-2) (* right column *)
)
shore : code('bounds [a:a b:b c:c d:d])

(* Interior tiles run the stencil kernel *)
let lake = rect(1 1 W-2 H-2)
lake : code('ninept [a:a b:b c:c d:d])

(* All tiles share the same routing *)
let beach = rect(0 0 W H 'x 'y)
beach : paint(a' [CXR|R])
beach : paint(b' [CXL|L])
beach : paint(c' [CXU|U])
beach : paint(d' [CXD|D])
beach : paint(a [L|LXC])
beach : paint(b [R|RXC])
beach : paint(c [D|DXC])
beach : paint(d [U|UXC])

beach : colorpair(a a')
beach : colorpair(b b')
beach : colorpair(c c')
beach : colorpair(d d')

let prog = union(beach lake shore)
prog : name('ninept)

Anatomy​

  • 8 colors: Each direction needs a send color and a receive color (paired via colorpair).
  • X swap: on each tile, a' carries what the core sends on a, and a delivers to the core what arrives on a'. The kernels bind only a, b, c and d.
  • Boundary |: Prevents routing off the edge of the grid. The rules are painted on beach, one rect() over the whole grid, because a | rule acts at the edges of each rectangle it is painted on; on union(lake shore) it would cut routes inside the grid.
  • Two code regions: bounds on the perimeter, ninept in the interior. They share the same routing since the route rules are applied to the full beach rectangle.

Common Patterns Summary​

PatternPaint idiom
Single tiletile() + code()
Send rightpaint(c [C>R])
Receive from leftpaint(c [L>C])
Pipeline tileTwo colors: paint(c1 [L>C]) + paint(c2 [C>R])
Horizontal chainhstack(a b c)
2D gridrect(0 0 W H) with boundary | routing
Neighbor swapcolorpair() + X swap modifier
Conditional regionsetif(flag region rect(0 0 0 0))
Reusable moduledefine f(...) { ... } + include
Position-dependent paramsgeovar() + @var in code
Complex layoutterminal() + transport() + union()