MaxTile - Enhanced Software/Hardware Tilemap
Introduction to the MinTile Series...

MaxTile is an extended Tilemap demo. It is designed to create platform games and other scrolling tilemap games like RPGs.

While Mintile only used 8 bits per tile, and supported 256 tiles per object. Maxtile supports 4096, and has a '4 bit drawing mode'. All tiles are 8x8 pixels, and simulated sprite objects can be formed and drawn via mini movable tilemaps.

Maxtile is optimized for 4 color tiles, however works with 16 colors too, depending on  the platform driver.

Unlike Mintile, Maxtile does not need a second 'Draw cache' tilemap, instead a single bit 'flag' is used to mark if a tile needs redrawing, so effectively the tilemap IS the cache.

The simplest drawing mode is 'Normal', but X-flip, Y-flip and XY-flip are also supported, which realtime convert the normal tile for display.

Extended drawing modes include 'Fastfill' to quicly fill a tile, 'DoubleHeight' to fill an area with a stretched tile (intended to save memory for less important tiles' and 'Transparent' where 0 byte data is not drawn for simple transparency.

Extended modes do not support flipping.

Drawing to the screen can be done in two ways:

'DoubleBuffered' is the simplest, everything is drawn to the vram every frame, as it's intended a double buffer is being used. This is also handy for testing as there is less to go wrong!

'Cached' is more advanced. The screen is split vertically into 3 caches. Required draws are cached to these caches, and the caches are then flushed to the screen. This minimizes flicker, and the cache flushes can by synced to the raster to further cancel flicker without need for a double buffer.

Maxtile works on all systems. Where hardware tile support is available (eg SMS) it will be used, where it is not (eg CPC) sofware tiles will be used,
however both work in the exact same way to allow a game to be ported more easily

The main code is multiplatform, and has few changes per system, but there is a 'platform driver' which efficiently draws a single tile to the screen depending on the system capabilities

Due to it's 2 byte per tile tilemap, MaxTile is NOT suited to low end systems. if your machine only supports 256 hardware tile patterns or has little memory, it is probably not desirable to use MaxTile.


Tile Format:

%TTTTTTTT TTTTMMMD

T=Tilenum
M=Mode
D='Dirty flag' (Tile needs redraw)

Tile definitions in detail:

The two byte tile definitions contain 4 bits which define the tile type.
These work as a 'binary tree', so the simplest mode is drawn most quickly.

Some bits, such as BB can be used for a bank, palette or just as extra tile number bits depending on the platform driver.

%NNBBXYPU    U=update P=Program YX=flip NN NN=Tile H
BB = Bank / palette / top 2 bits of tile num

&???0 %???????0 ???????? = Tile to be skipped
&???1 %???????1 ???????? = Tile to be drawn
&nnn1 %nnnnBB01 nnnnnnnn = Normal tile       (&-1)
&nnP3 %BBPP0011 nnnnnnnn = Custom Program PP (&-3) (PP=0... Fill / PP=1... Stretch / pp=3+bb=3... empty (for sprites / pp=2 unused)
&nn03 %BBPP0011 nnnnnnnn = Program Fill
&nn13 %BBPP0011 nnnnnnnn = Program Stretch
&FFF3 %11110011 11111111 = Program Empty tile    (&F3 FF  /  F2 FF)
&nn33 %nn110011 nnnnnnnn = Transparent tile    (&33 nn  / &7C nn  /  BC nn  /  F3nn)
&nnn7 %nnnn0111 BBnnnnnn = Y flipped tile    (&-7)
&nnnB %nnnn1011 BBnnnnnn = X flipped tile    (&-B)
&nnnF %nnnn1111 nnnnnnnn = XY flipped tile   (&-F)


Lesson MaxTile1 - The Minimal Test 1
Lets take a look at the 'Minimal Test.' for MaxTile

This produces a simple scrolling background on a system, and is intended to test the Flip, Fill and stretch tile functions during development of MaxTile

MaxTile_Test1.asm



MaxTile is an extended tile drawing routine, it's designed to work in the same way on all systems, whether they have hardware tilemap or not!

It provides 4096 patterns per tilemap, Flipping and more, however it is not suited to games which need to maximally use the hardware's capabilities. This is because it's designed as 'multiplatform' code, so must function on the more basic systems, often to the determent of the most advanced ones.


Here is Test1 running on the Amstrad CPC.

Here we'll look at the multiplatform code, and we'll see how the shared modules of MaxTile are used and their function.
RunTest1 Executes the test routine.

First we do some setup before the first draw.

We define the source tilemap address, loading the base address and width in bytes (36*2)... We run SetScroll to configure the tilemap for drawing.

Next we prep the cropping routines to configure them for a full screen.

We draw the first screen.

The InfLoop is the main test loop.

We change the X,Y offset of the tilemap source to effect a scroll. The format of the 'offset' is %TTTTTTPP, where T is a full tile shift, and P is a partial (2 pixel) shift

and run 'SetScroll' or 'ResetScroll' to perform a redraw of the tilemap as required


DrawScreen will draw the tilemap to the screen in double buffered mode, or to the cache in cached mode.
If we're using Cached drawing, FastDrawCaches will dump the 3 caches to the screen, without trying to sync to the raster draw.
BackgroundTilemapBase points to the memory address of the first tile to draw to the screen.


Recalc scroll updates the BackgroundTilemapBase using the ScrollX and ScrollY offset
SetScroll will redraw the full tilemap.

We Recalculate the draw area with 'RecalcScroll'

flag the tiles for redraw with 'ResetTilemap'


Finally we draw the tiles with 'Cache_TilemapCLS'
ReSetScroll can only be used when tiles have moved 1 full tile.

It attempts to compare the old and new position, and only redraws areas where the two do not match.

This reduces draw time with no visual difference.

As each tile uses 2 bytes there are 2 compares per tile.
'FastDrawCaches' will empty the caches to the screen.

It does not attempt to sync to the raster beam, so may cause some flicker, but is fast and simple.


The Tilemap

The Tilemap contains the sample data. Each tile is 2 bytes.

The example tilemap is split into lines by type, these lines can be remmed out to disable types of tile during testing/development of a new platform driver








Lesson MaxTile2 - The Midpoint Test 2
Lets take a look at the Extended Test.

This provides a scrolling tilemap, and various moving 'sprite objects'

One is controllable, and flipable by the user, The others are controlled by the CPU, and one uses 0-byte transparency.

MaxTile_Test2.asm



Here is Test2 running on the Amstrad CPC.

Here we'll look at the multiplatform code, and we'll see how the shared modules of MaxTile are used and their function.
First we set the scroll position of the background tilemap with 'SetScroll'


Next we set up the miniTilemap of the player with 'Expand Tilemap'... This takes a 1 byte per tile source tilemap and expands it to 2 bytes per tile (using IX as a tile pattern offset). It can also optionally perform an X/Y flip of the source data.
We set the cropping area for the full screen tilemap.
Before we move the sprites, we need to update the crop-cache which records the area of the background tilemap which will need redrawing.

To do this we point IX to a sprite object and run 'CalcCropRemoveIX'
At the start of our main loop we move the CPU sprites
We read in from the joystick, and move the player XY pos accordingly.
If the player pressed fire, we first toggle between the 4 XY flip options.

We use 'ExpandTilemapA' to update the players tilemap and flip as required.
We scroll the background one tick.
ResetTilemap Marks the Tiles in the tilecache for redrawing.

ResetScroll performs the draw

'Speedscroll' is only for the CPC, Rather than redraw the tilemap one tile at a time,
it uses LDIR to do a memory-memory copy of the entire tilemap effecting the scroll, as the sprites will be in the wrong position, we remove them before this copy.
After handling the player, we redraw all the computer sprites and redraw the screen.

"ClearBorder" is an optional extra, it draws a 4-6 pixel border around the screen (depending on if half or quarter tile scroll is enabled) - this is to cover the 'glitchy' edges of the screen - which occur because our drawing code only works in full tiles.
MovePlayerSprite updates the X,Y position of the player in the sprite object pointed to by IX from BC

The old sprite is then removed from the screen
Draw sprite performs all the tasks of redrawing the screen,

All the sprite locations are updated, the tilemap is drawn, then the sprites are drawn.

If we're using the cache, all these 'draws' won't appear on the screen yet, so we use 'Fast Draw Caches' to flush the caches to the screen
BackgroundTilemapBase points to the memory address of the first tile to draw to the screen.


Recalc scroll updates the BackgroundTilemapBase using the ScrollX and ScrollY offset
SetScroll will redraw the full tilemap.

We Recalculate the draw area with 'RecalcScroll'

flag the tiles for redraw with 'ResetTilemap'


Finally we draw the tiles with 'Cache_TilemapCLS'
ReSetScroll can only be used when tiles have moved 1 full tile.

It attempts to compare the old and new position, and only redraws areas where the two do not match.

This reduces draw time with no visual difference.

As each tile uses 2 bytes there are 2 compares per tile.
'FastDrawCaches' will empty the caches to the screen.

It does not attempt to sync to the raster beam, so may cause some flicker, but is fast and simple.

PrepSpriteIX will perform the tasks for a redraw of the sprite object pointed to by IX.

CalcCropIX will calculate the draw area, based on screen cropping.

ZeroSpriteInCache works out the areas of the background tilemap covered by the sprite, and clears the update flag so they are not drawn.

FlagSpriteForRedraw flags the sprite tilemap to be redrawn.
CalcCropIX will calculate the area of the sprite to draw, and store the details in the sprites crop cache.
CalcCropRemoveIX also uses the same cropping routine, however this version calculates the area BEHIND the sprite.

It calculates the area of the background tilemap which will need to be redrawn to remove the sprite.
RemoveSpriteIX performs a redraw of object IX

Data

The Tilemap contains the sample data. Each tile is 2 bytes.

One Byte per tile tilemaps are convenient for storage, but must be 'Expanded' in to 2 bytes per tile for drawing
These are the expanded tilemaps used by the cpu sprites
MaxTile is pretty complex! If you're looking for something a bit simpler see MinTile.

If you're looking for something far simpler see Grime, which was more character block based.






Lesson MaxTile3 - Maxtile main code
Lets take a look at the core shared module which handles tilemap drawing.

V1_MaxTile.asm

We draw simulated sprites using the tilemap.

We pass a pointer to a data structure which defines the attributes of the sprite.

X,Y are the position, W,H are the size - all in logical units (Pairs of pixels)

TilemapHL is the address of the tilemap which defines the sprite object.
PatternHL is the base address of the pattern data for the sprite.
CropCache is the data related to drawing the sprite.
CropCacheBack is the data related to removing the sprite.
GetTilemapUnderSprite returns the HL address of the top left corner of the background tilemap under the current sprite.

This is used for removing a sprite.
DoCropLogicalToTile converts XYWH from logical units to tiles.

as Logical units are pairs of pixels This effectively divides each by 4.

If the Width or Height is zero, nothing should be drawn, so this routine sets the Z flag.
Shift Tilemap calculates the starting position in the background tilemap for our sprite.

If X,Y are in tiles, If effectively calculates:

HL=BackgroundTilemapBase + (BackgroundTilemapWidth*Y) + X*2

For speed, This routine is hardcoded to support only a tilemap 32 or 36 tiles wide.
When we want to remove a sprite, we do so by flagging the tiles in the tilemap for redraw.

Each tile is defined by 2 bytes, and we set bit 0 to one to flag the tile for redraw.
FlagSpriteForRedraw sets bit 0 of the Sprite tilemap.

This makes the sprite redraw next time.

Before flagging, we check if the tile is $FFF2 or $FFF3 - these are transparent empty tiles, so it would be a waste of time processing them for redraw (though it does no harm)
ResetTilemap is simpler, it flags the entire tilemap for redraw.

It's intended for the background, so we don't check for empty tiles.

it resets BC tiles from HL.  it can also be used for flagging a strip at the top or bottom of the tilemap when scrolling.
There may be times (like scrolling left or right) where we want to update a block of the tilemap.

ResetTilemapPart does this.

We pass the tilemap address in HL and the width and height of the area to flag in DE
Our sprite may not have moved, but another sprite may have covered it and moved away.

We call this a 'Dirty Sprite' as it needs to be redrawn cleanly.

To detect this, we check all the tiles under a sprite, if the tile in the tilemap has been flagged for redraw, then that part of the sprite also needs redrawing.


BUT there's a catch!

As we can move in 2 pixel jumps, we may not be exactly aligned on the tile that changed, therefore we flag a 2x2 block of tiles in our sprite tilemap for redraw to ensure we redraw everything that was lost.

When we're going to redraw a sprite, part of the tileamp will be covered by the newly drawn sprite.

We don't want to redraw that part of the tilemap - so ZeroSpriteinCache will clear the redraw flags of the areas covered by the sprite.

Cache_TilemapCLS will redraw the tilemap into the Cache or directly to screen.

Most of this code is platform specific shifts to the HL Vram destination for half and quarter tile shifts!


The DrawTilemap routine is used to perform the actual draw.


There are two versions of this, one draw directly to VRAM (for double buffering) the other calculates the draw and stores it in a cache.

Well look at those next time!


DrawSpriteIX draws a sprite from it's mini tilemap.

It uses the same DrawTilemap routine... unless we're using hardware sprites, in which case it used DrawTilemapH


Logical Cropping

To save time, we calculate the cropping when a sprite moves, and store the results in a 'Cache'

There are two caches, one for drawing the sprite, and one for redrawing the background tilemap behind it.
CalcCrop Calculates the crop and fills the cache width the results
Our cropping routine will work out the X,Y pos in logical units, and width and height + any skipped tiles from the source tilemap

registers B,C is the X,Y co-ordinate in logical units
registers H,L is the Width,Height in logical units
register IY is the address of the source  tilemap  data.

First we zero D,E - they are used for temp values

We load IXH with the top of screen, and IXL with the height of the screen.
Ok... lets crop the top of the sprite...
First we remove the ypos of the first visible pixel from the draw ypos (C)... if the result is greater than zero, then nothing is off the screen at the top.

if the result is less than zero we need to crop... we convert the negative to a positive and compare to the height of our sprite (L), if the amount to crop is not less than the height then the sprite is completely offscreen.

Anything else is the number of lines we need to remove from the top, we store this in E

As we can only draw full tiles, We calculate the correct offset for the first draw and set the new 'draw position' in C

Next we do the same for the bottom,

We add the height to the Ypos, and subtract the height of the logical screen, if it's over the screen height (greater than zero) we need to crop again - the result is the amount to crop

We've calculated the top (E) and bottom (D) crop... we now use these to calculate the new height of the sprite (L).
We then skip over any bytes in the source (IY) based on the number of lines of tiles we need to remove from the top.
now we do the same for the X axis

First we remove the xpos of the first visible pixel from the draw Xpos (B) ... if the result is greater than zero, then nothing is off the screen at the left.

if the result is less than zero we need to crop... we convert the negative to a positive and compare to the width of our sprite (H), if the amount to crop is not less than the width then the sprite is completely offscreen.

Anything else is the number of lines we need to remove from the left, we store this in E.

We calculate any needed X offset for the first draw in B


Next we do the same for the right,

We add the width (H) to the Xpos, and subtract the width of the logical screen, if it's over the screen width (greater than zero) we need to crop again - the result is the amount to crop from the right (D)
We need to crop, so we calculate the new with in H, and update the First source byte in IY if required


We return Carry clear to confirm the crop found something to draw.

Otherwise we set the carry to mark the sprite offscreen.

Lesson MaxTile4 - DirectDriver and Expanders
Lets take a look at the simplest Draw routine, the one which writes straight to VRAM - intended for Double Buffered screens.

We'll also look at the 'expanders' which convert a 1 byte per tile tilemap to a 2 byte one - This is used for expanding the player sprite tilemaps into the cache for drawing, it also X and Y flips them

V1_MaxTile_Expanders.asm
V1_MaxTile_DirectDriver.asm

Draw Tilemap

DE=Source tilemap
DE'=Pattern Data
HL=Screen Base
IYH=Tilemap Width
IXH/IXL=Draw Width / Height
   
At the start of each line, we need to prepare the values in our registers, we also back up the stack pointer, as our drawing routines often use stack misuse.

On the CPC we have an alternative version of the drawing routines for a 4 pixel vertical shift.
Each tile has two bytes defining it in the tilemap.

If the bit 0=1 then the tile needs redrawing, otherwise we skip over it.
There is a some platform code for each system - This moves the vram address HL across the screen one tile (8 pixels.

We repeat for all the remaining tiles on the line.

After the line we restore HL and DE
After each line there is some move platform code - this move the VRAM address DOWN one tile (8 pixels)

We add the width of the tilemap to the tilemap source in DE, and repeat for the remaining lines

Expanders

The expanders take a 1 byte per tile tilemap, and expand it into the 2 byte per tile format used by the drawing routines.
the value in IX is used as an offset to the tile numbers in the 1 byte tilemap.
ExpandTileMap is the simplest version... ExpandOne coverts a single tile.
ExpandTilemapA also allows for X,Y or XY flip of the tilemap.
A defines the Flip Mode (0/1/2/3 =Normal/Xflip/Yflip/XYflip)


If we're XY flipping the source data, we need to move HL to the end of the source data, we also need to set the bits of the tile offset in IX to enable flipping of the bitmap data at draw.


We work through each line in reverse

The X-flip routine works in a similar way, moving from right to left through the source tilemap data.
The Y-flip routine is also similar.

It first moves to the first tile on the last line, and works backwards through the tilemap.
Each routine uses 'ExpandOne' to convert the individual tiles.

A tile value of 0 is a special case - this is a 'transparent tile', and is converted to &FFF2

All others are multiplied by 16 (as MaxTile is optimized for 4 color - 16 byte patterns), and added to iX, before being stored into the destination tile map.


Lesson MaxTile5 - Cache Driver
The Cache driver calculates all the tile draws required, and stores them in 3 caches. This allows the delays during drawing to be reduced, meaning less flicker during redraw, it also means we can time the draw of the caches to the raster beam.

V1_MaxTile_CacheDriver.asm

The Cache Code

First we define the format of our cache.

Each Cach has 6 bytes, and we define offsets to some of these for reading via (IY+n) addressing
We need to define the structure of the cache table. This differs depending on the system, and must be writable, so cannot be held in ROM.

There are 3 entries in the tables.

The first byte is the last H byte of the HL VRAM address for this section of cache, once the VRAM destination reaches this value, this cache is finished, and the next cache entry is used. "CacheTableAddr" points to the current entry.

The second and third entry are the RAM area used by this cache, it's recommended the cache is 256 bytes in size.
The second entry is updated as new data is entered into the cache, the third is the original value for resetting the cache.

The fourth entry is the Raster timing, it is optional and only used if we're attempting to sync cache draws to the raster beam position.c
GetCacheIY is used when drawing the cache. IY should point to an entry (Section) in the cache.

This command will return a pointer to the cache data in IX... if the first two bytes are &00 &00, then the cache is empty, and we set the zero flag
'DrawTilemap' will process a tilemap and send the tile data to the cache.

When executed registers should have the following values
  DE=Source tilemap
  DE'=Pattern Data
  HL=Screen Base
  IYH=Tilemap Width
  IXH/IXL=Draw Width / Height

First we load in the address of the entry in the cache, and get the next free memory address for new data in that cache
We're going to process a line of the tilemap
First we check the H part of the HL vram destination, and see if we're over the limit of this cache.

We use BC' to keep track of what tiles (if any) need drawing in this line of the tilemap
We check the tile, if it's Update flag is not set, it doesn't need drawing so we skip it.

If we need to draw this tile, we check if it's the first we've needed to draw in the line, if not, we skip the next bit.
We've found the first tile in this line... but is this the first tile in this tilemap?

If it is, we need to write the pattern information. Byte sequence &00 &zz &yy defines the pattern data as at address &yyzz

16 color systems may use an extra 'tint' byte, in which case the format would be &00 &zz &yy &tt

Note: Byte sequence &00 &00 &00 denotes the end of the cache data, so the pattern data cannot be &0000 or the caches will never draw!
If it's not the first tile in the tilemap, but the first tile on the line we write the info to draw this line.
If we found a valid tile, we update C'  (which is used to caclulate the tile count to draw.


Whatever happened We then move to the next tile in the map, and update the VRAM destination HL - this is platform specific code.
We repeat until we've processed all the tiles.

We then look at BC' and calculate the total tiles that need drawing in this line.

If this is>0 we write the count into the cache, and move to the next point in the cache.

We also check if the cache is full, if it is we'll have to flush it now
We've processed a line, so it's time to move down a line

There's platform specific code to move down a strip of tiles.
We now move the source tilemap down a line, and repeat for the next line of the tilemap
Once we've finished processing we update the address of the next data in the cache for next time.

We then write an 'end of cache marker' &00 &00 &00
If we've reached the last line of a cache, we write an end marker and move to the next one, then continue processing
If the cache is full during processing the tilemap, we need to stop what we're doing

We write a 'cache end' marker, process the cache and reset the address of the cache data (as it's now empty)

As we're starting again with an empty cache, we need to write a new pattern address, so we clear H'
When we're processing the caches in normal time (not because they are full) we will want to reset them all.

We do this by resetting the 'current addresses' from the backup defaults.
When we want to process a cache, we load IX with the address of the cache

We load in the parameters of the line. If we get a 0 length this is a special command - either the end of the cache or a new pattern address.
We're going to draw tiles to the screen, so we back up the stack pointer as we may be using stack misuse.

The CPC has a special 'Half tile Y shift' version to cope with the strange memory layout of the CPC, when a 4 pixel Y move occurs
We check the draw flag, and draw the tile if required.
We now need to move across one tile, This is platform specific code.
Once we've done a line, we restore the SP.

We now move to the next line in the cache (5 bytes per line data)

We repeat for this line.
If we read a 'Zero count' this is a special command.

&00 &00 &00  is the end of cache command

anything else (&00 &zz &yy) defines the pattern address for the tilemap as &yyzz - 16 color systems may use an extra 'tint' byte (&00 &zz &yy &tt)