Vbank usage demo in Lua with TIC-80

Tic-80 Vbank is an hardware video, fantasy simulation, and this is a usage demo in Lua

Date: 2025-11-08
Tags: Lua, procedural generation, tic80, videofx

A simple tic-80 fantasy console card in Lua scrtipting language, that display some possible effects of vbank() usage. That's an equivalent to Amiga, hardware video bitmap mixing. This kind of video mixing technology still exists on several ARM SoCs. At least seen on AllWinner and Rockchip SoCs.

Vbank are

It replace former OVR() "overlay" function mode. See this old demo

Table of Content


press several tume ESC key, then choose close game and then press ESC key again to go to source code, and play with it. Enjoy.

This TIC-80 is also available on official tic80 website

Warning, the default, lower vbank is vbank 0, the upper bank is vbank1

Filling of the background

To laverage as much as possible TIC-80 C compiled code instead of Lua script, for heavy loads we use tilemap for filing the two vbank at each frame. More optimal way, would be to fill background the first time, and then only what changed, but even slower microcontroller can fill this fast enough to have 30fps.

Prepare the tiles map

descriptive text
The Sprite Editor, with number of the top left corner of a sprite displayed
On this demo backgroundis filled with loop, for example, on this Spride Editor picture, you can see above the drawing area, the number (here #32) of the upper-left part of the sprite, this will be the reference given in map tile table. The map tile, simulate 16bits area hardware video tile mixing. The reference of the table is sent to video processor that read it and send it directly to the screen. In TIC-80 fantasy console, the map is blitted on pre-screen buffer instead. You can modify it after that.

Table of tiles are a list that are loop read sqeuentially in main TIC() function and send to map_fill() every t/n frames (where t is time and n is number of frames before changes).

-- number, sx, sy
fill_list0={
{34,2,2},
{36,2,2},
{32,2,2},
{96,4,4},
}

The map area to fill is given to map_fill function with scr parameter 2×2 screens of 30×17 tiles (of 8×8pixels = 240×136 pixels in total) are used for vbank0 and 2×2 screen starting at tile 34,0 for bank1:

-- fill background tilemap
function map_fill(scr,tile)
 start=scr*30*2
 -- sx * 17
 for y=0,33,tile[3] do
  -- sy * 30
  for x=0,59,tile[2] do
   for j=1,tile[3] do
    for i=1,tile[2] do
     mset(start+x+(i-1), y+(j-1),
          tile[1]+(i-1)+(j-1)*16)
    end
   end
  end
 end
end
ii + 1i + 3
i + 16i + 17i + 18 
i + 32i + 33i + 34

Tiles relative to tile i of a sprite

Test to change to t/n frames:

if t%120==0 then
  cur1=(cur1+1)%#fill_list1
  map_fill(1,fill_list1[cur1+1])
end

TIP: As can be seen with help ram command in console, the memory of the tile map start at 0x08000, so it can also be set here by using the poke() function.

Copy tile map to the screen

The map() function is then used to fill screen buffer with tile map. As for any function you can type help map to have the function call parameters. help api will show you all available TIC-80 API functions.

Lua function are math.sin() and math.cos() but an alias was made at top: sin=math.sin

the 60,34 (2×2 tile map screens) starting at tile 60,0 are mapped to the screen coordinate:

so -50,-50 center on which is added the rotation of 50 pixel ray wide at a speed of t/50 (1 whole circle is 2×π ~= 6.28)

The ending 0 is for colorkey. Warning for vbank(1), this will add transparency.

map(60,0, 60,34,
   -50+50*cos(t/50), -50+50*sin(t/50)
   ,0)

Using vbank() to switch video banks

Blending: bank 0 is bellow, bank 1 is upper, and colour 0 is transparent

We use about the same function on both vbank, with stlightly different parameters to have them vary a different way. The function vbank() is used to switch current drawing vbank.

As you can see, on the vbank 1, we draw a circle with color 0 (last parameter here). This what make the upper layer transparent:

-- move to vbank0 at each frame
vbank(0)
...
map(0,0,60,34,
  -50-20*sin(t/20),-50+20*sin(t/20))
spr(1+t%60//30*2,x,y,14,3,0,0,2,2)
print("HELLO WORLD!",84,84)

-- pass to vbank1	for upper layer
vbank(1)

-- fill the background with map
map(60,0, 60,34,
  -50+50*cos(t/50), -50+50*sin(t/50)
  ,0)

-- draw a circle mask with color 0 for transparency
circ(120+30*sin(t/50),50,40+20*sin(t/60),0)

Sprite trick

descriptive text
The Sprite Editor, currently overhead color is "color [10]" at the top.
At the right, applying palette color swaps.
TIC-80 Sprite blue colours set to 0 for layer transparency
Sprite applied on vbank(1) after swapping color 8 and 10 to 0

Finally we swap palette map ccolors for drawing the sprite, and have a mask of its shape too. More generally. Every kind of drawing with color 0 will allow to see the lower vbank. Video processor memory layout and registers can be see by the command help vram

At top of program, we define one time the VRAM PALETTE_MAP registers, we multiply by 2 here because we will use the 4 bits only poke4() function to write in, not the poke() 8 bits (one byte), function.

PALETTE_MAP = 0x3ff0*2

Then we remap color 8 and 10 to color 0, draw the sprite, and reset it to their own color.

-- shift blues colors of TIC sprite to
-- 0, for adding it to the mask
poke4(PALETTE_MAP+8,0)
poke4(PALETTE_MAP+10,0)
  spr(1+t%60//30*2,20,80,14,3,0,0,2,2)
-- set back to normal color
poke4(PALETTE_MAP+8,8)
poke4(PALETTE_MAP+10,10)

By default, colour 0 is transparency, but it can be set to another colour by poking 0x03FF8: poke(0x03FF8,transparent_colour_number)

Realtime fantasy hardware accelerated twist and colors effects

The function BDR(y) is called at each virtual begining of scanline. This allow you to change screen context fot the whole line. This include video pointers, video parameters etc... y is the current screen scanline number.

Warning: Scanlines are overscan vertically they start 4 lines before the line 0 of the buffer drawable screen (lines 0 to 3), and end (from 140 to 143) after it, you need to in your changes calculation. y=0 is before the 0 line

We set vbank(0) to apply to it a sinusoidal offset at each line, depending on frame time t, and current line y. 0x3ff9 is x offset register and 0x3ffa is y offset register. Only the x offset is then set for vbank(1), as you can see they can be set a different way for each vbank.

vbank(0) --standard layer
-- horizontal and vertical screen offset
poke(0x3ff9,8*sin((t+y)/10)%256)
poke(0x3ffa,8*sin((t+y)/20)%256)

vbank(1) --upper layer
-- slower but wider horizontal offset
poke(0x3ff9,5*sin((t+y)/20)%256)

Finally, we change the color palettte number 5 (middle green by defaut), to have a smooth color gradiant

-- change coloour #5
local base=0x3fc0+ 5*3
poke(base, 0,(255*y/140)//1)    --R
poke(base+1,220+(255-220)*y/140)--G
poke(base+2,(255*y/140)//1)     --B

The demo

- CLICK TO PLAY -