previous
home : utopia.consolemul.com
home : www.zeograd.com
www.magickit.com/mkit
In this documentation, you'll find how to use this compiler, how to interface it with your own asm routine and such. If you want to see anything added there or just want some explanation, tell me. I'll try my best to make it clearer.
#include(tiles, "tiles.pcx", 1, 16);
load_vram(0x1400, tiles + (1 << 6), 64);
This way, you'll be able to easily create multi pattern sprite
(for animated moves, e.g.)#INCBIN(level1, "level1.fmp");
const char level1[] = { <stuff inside level1.fmp> };
get_tile(x,y)
to return the index of the tile
at position x,y in the current map (for sprite/tile collision checking,
isn't it, gravis ?). get_map_width()
and get_map_height()
which work on the current map.#incbin(level1, "level1.fmp") /* 256x256 */
#incbin(level2, "level2.fmp") /* 256x256 */
#incbin(level3, "level3.fmp") /* 256x256 */
#incbin(level4, "level4.fmp") /* 256x256 */
const int big_map[] = {
/* width & height of all the maps
* put together
*/
512, 512,
/* the following lines declare all
* the small maps, one line per map :
*
* the two first values are the top/left
* coordinates of the small map inside
* the big map, the two followings values
* are bottom/right coordinates (inclusive),
* and the two last values are the map
* address in ROM
*/
0, 0, 255, 255, bank(level1), level1,
256, 0, 511, 255, bank(level2), level2,
0, 256, 255, 511, bank(level3), level3,
256, 256, 511, 511, bank(level4), level4,
/* -1 closes the table */
-1
};
put_number(int num, char width, int vaddr);
put_tile(int tile, int vaddr);
put_digit(char num, char x, char y);
spr_x()
and
spr_y()
has been changed, the syntax is always the same,
but the coordinate system is different. Now 0 corresponds to the top
of the screen. To make a sprite disappear behind the top border you
must use negative value. This way, a char is enough to handle the
position (note that chars are unsigned and can handle until coordinate
256 [while screen is 256x224])newbank
pseudo instruction.You'll find here some trick to understand how this compiler works. Before trying to write your own asm routine, read that first. Even if you don't intend to make asm, I think you should read it at least once, knowledge can't harm.
Whatever you write in a asm block (i.e. beginning by #asm and ended by #endasm) is directly passed to the assembler, without any modification from the C compiler. You can write this way your own assembler lib function (when speed is needed for example) but it's harder to write and support than C code, of course.
You must be aware that C function names are limited to 24 chars and when passed to assembler are preceded with a '_' char. That's why you can't have 2 functions with the same 24 first characters and need to add a _ when calling a function directly from an assembly block. (The part concerning the length of identifier is not as important as when this document was first wrote, it was then limited to 8)
Argument passage is done trough a special stack, not the internal stack of the pc engine (that is 256 bytes long only and must be saved whenever possible) but a stack situated in RAM, at the end of the RAM to be more precise. The head of this stack is pointed by the variable __stack ( 2 '_' characters before ).
Since the tail is fixed at the very end of the RAM ($4000), the head 'goes down' when you add an item in it. Here's the pseudo code for adding an item on it ("pushing" a value).
__stack -= sizeof(var);
*__ stack = var;
This is exactly what is done in assembler before calling a function, to set the value of the various arguments.
outdated : If you want to write your own assembler function with parameters, you must restore the value of __stack because it's the calling program that must 'clean' the stack and not the called sub routine (contrary to the pascal convention) . This is due to the C calling convention which allows variable number of arg for subroutines (which is not supported by HuC and is likely to be never supported).
Now, David Michel decided (for optimisation purpose) to use anew the pascal convention for passing arguments and passing the first argument in the A:X registers instead of using the stack. This means that the stack must be restored in the called function, no more in the calling one.
Once in the subroutine, argument are then situated 'above' the stack (i.e. *__stack, *(__stack+1), and so on.. ). As for the order, rightmost C declared arguments are found on the 'bottom' of the stack. A little example to understand better...
+------------+
dummy(val,offset,x, reg_arg) __stack -> |//// x /////|
char val; +------------+
int offset; __stack + 1 -> |\\\\\\\\\\\\|
char x; +-- offset --+
int reg_arg; __stack + 2 -> |\\\\\\\\\\\\|
+------------+
__stack + 3 -> |/// val ////|
+------------+
| |
You may even consider the following pseudo code is executed to set the value of the argument using the stack :
reg_arg = value of reg X + 256 * value of reg A;
x = *__stack;
offset = (*(__stack + 1) + 256 * (*(__stack + 2))
val = *(__stack + 3)
Sprites are handled trough a table of 64 items, one for each sprite, each entry being 8 bytes long.
We are reusing the method of the magic kit, that's to say that we keep a local copy of this table in RAM on which we work and then we copy this local table to the real one (located in the video ram) each frame. This method allows faster manipulation (RAM is easier to access than video memory).
This local SATB (Sprite Attribute
TaBle) is initialized by satb_init()
, future call to this function
will just clear it.
You can change every attribute of each sprite, individually. There are 4 attributes (using 16 bits each, which make this 64 sprites long table using 512 bytes). 2 are for the position (x,y), one for the pattern and the last one contains various flags...
When you want to change anything
in a sprite, the method is the following : first, you must tell the system what
is the sprite you want to deal with spr_set(no)
where no
is the number of the sprite (in 0-63).
Once done, you have a few functions
you can use to modify the sprite you've chosen. spr_x(x_coord)
and spr_y(y_coord)
respectively change the position of the sprite
on screen.
spr_pri(priority)
allows
to set the priority of the sprite, that's to say behind or in front of tiles.
Priority of 0 makes it behind the tiles map and 1 draw them front of them.
spr_pal(pal_number)
changes the palette for this sprite. There are 16 palettes of 16 colors for
sprites. You can change the palette using set_sprpal(palette)
,
supposing you have a palette defined somewhere else with the incpal
or defpal
statement we'll also talk about.
Last function to control flags is
the more generic spr_ctrl(ctrl_mask, ctrl_value)
function. It controls
the mirror flags (you can flip a sprite, either horizontally or vertically)
and size flags (sprites can be 16x16, 32x16, 16x32, 32x32, 16x64 or 32x64).
To use this, you must provide as the mask, the one corresponding to the flag
you want to change or both (combining them with a logical 'or' i.e. | ) and
on value, well, the value. There's a bunch of #define statements in the file
huc.h
which make it easy to use. A few examples, to draw the 13th
sprite horizontally flipped, write :
spr_set(13);
spr_ctrl(FLIP_MAS, FLIP_X);
As for drawing a sprite flipped both horizontally and vertically, while setting the size to 32x64, simply put :
spr_ctrl(FLIP_MAS | SIZE_MAS,
FLIP_X | FLIP_Y | SZ_32x64);
The last function to handle sprite
is the spr_pattern(pattern_address)
function. It's used to change
the pattern of a sprite, i.e. the real shape of it. This 16 bits address is
the place in the video memory where resides the pattern. The address is in word,
not in bytes, which allows to access the whole 0x20000 bytes of video. You must
not forget to load this pattern from the rom with another asm piece of code.
First, you must define the pattern in a data bank including pieces of pcx files
(or even creating yours with hexa numbers), then loading these datas in the
video memory using the load_sprite
statement. The load_sprite
function takes the label of the data first, then the address in the video ram
where it should be situated, THIS IS THE VERY SAME PARAMETER THAT SHOULD
BE USED IN THE SPR_PATTERN
FUNCTION, and finally the number
of sprite that should be loaded. This number is in 32x64 big sprites. So loading
8 16x16 sprites requires only the value 1 here...
Finally, you shouldn't forget to
update the true satb with the local one using the function satb_update()
,
which should be also accompanied by a vsync(1)
call in order to
let the user see the changes.
This part describes all the instructions which are not real C statements. They are HuC built in instructions and are processed at the compilation time, they are not call to library function for example (although they often produce calls to library functions). Since they're specific to HuC, they deal with all which is pc engine specific such as banking, sprites and tiles...
Outdated :
newbank
: This instruction must be used at top level only (that's to say outside any
function, at the same 'level' than main
function for example).
Its goal is to provide an easy way to take advantage of pc engine banking
ability (in fact, to use more than 8 kilobytes of memory :). You can use it
of 4 different ways :
newbank();
Without argument, all
that follow this instruction is placed in the next free bank, and code
which will be in will be designed to be placed at a compiler chosen position
(If you want to know more, you can check a
brief summary of the pc engine hardware). This is the most used form.
It's typical use is when you get an assembler error such as "Out
of range, bank offset > 0x1FFF"
. This means you have filled
a bank (which is 0x2000 bytes long, that's to say 8192 bytes). In order
to solve this you have to 'break' your code. Indeed, currently, since
newbank
can only occur at top level, you can't have a function
the flow of which goes from one bank to another. Each function must reside
in one bank. It's then up to you to put the 'overflowing' code into a
newer bank including the newbank
instruction before it. From
this moment, you still have another 0x2000 bytes of data for your use
(in the limit of 128 banks, which still represent 1 megabyte). I fear
this point is relatively unprecise but we'll try our best to improve it.newbank(bank_number);
newbank
. Only
people who see what's its use should use it :)newbank(offset_value);
newbank(0x4000);
because we've decided it would make things much more easier to have all
data mapped at 0x4000. This version is recognized from the previous because
the argument is a multiple of 0x2000 and is greater than 128.newbank(bank_number,
offset_value);
incpal
: This
top level instruction allows to include a palette coming from a .pcx file.
It's basically a wrapper for the magic kit equivalent instruction. It exists
in 3 flavors :
incpal(identifier_name, "filename");
identifier_name
coming
from the file called filename
.
It extracts 16 palettes from the 0th to the 15th.incpal(identifier_name,
"filename", start_pal);
start_pal
to the 15thincpal(identifier_name,
"filename", start_pal, number_pal);
number_pal
palettes from palette start_pal
.incspr
: This
instruction includes data for sprite patterns from a .pcx file.
incspr(identifier_name, "filename");
filename
.incspr(identifier_name,
"filename", col, row);
row
rows and col
columns of 16x16
sprites from the file.incspr(identifier_name,
"filename", begin_x, begin_y, col, row);
row
rows and col
columns of 16x16 sprites
starting at position (begin_x
, begin_y)
incchr
: This
instruction includes data for character patterns from a .pcx file.
incchr(identifier_name, "filename");
filename
.incchr(identifier_name,
"filename", col, row);
row
rows and col
columns of 8x8
tiles from the file.incchr(identifier_name,
"filename", begin_x, begin_y, col, row);
row
rows and col
columns of 8x8 tiles
starting at position (begin_x
, begin_y)
incchr_ex
: This
instruction includes data for character patterns from a series of .pcx files, and
sets up for the "new style" of 8x8 scroll-tiling.
incchr(identifier_name,
"filename_1", begin_x, begin_y, col, row, pal_idx1, \
"filename_2", begin_x, begin_y, col, rox, pal_idx2, \
"filename_n", begin_x, begin_y, col, rox, pal_idxn );
row
rows and col
columns of 8x8 tiles
starting at position (begin_x
, begin_y
), and using the stated
pal_idx from filename_1 (etc.), assembling them as a single list of tiles
in memory, available for use in the map and scrolling functionsinctile
: This
instruction includes data for tile patterns from a .pcx file.
inctile(identifier_name, "filename");
filename
.inctile(identifier_name,
"filename", col, row);
row
rows and col
columns of 16x16
tiles from the file.inctile(identifier_name,
"filename", begin_x, begin_y, col, row);
row
rows and col
columns of 16x16 tiles
starting at position (begin_x
, begin_y)
inctile_ex
: This
instruction includes data for character patterns from a series of .pcx files, and
sets up for the "new style" of 16x16 scroll-tiling.
inctile(identifier_name,
"filename_1", begin_x, begin_y, col, row, pal_idx1, \
"filename_2", begin_x, begin_y, col, rox, pal_idx2, \
"filename_n", begin_x, begin_y, col, rox, pal_idxn );
row
rows and col
columns of 16x16 tiles
starting at position (begin_x
, begin_y
), and using the stated
pal_idx from filename_1 (etc.), assembling them as a single list of tiles
in memory, available for use in the map and scrolling functionsincbat
: This
is a special instruction that include a kind of map used to recreate a background
picture in conjunction with inclusion of tiles pattern. We've set another
pseudo instruction that does that but it's not completely flexible (see load_background).
incbat(identifier_name, "filename", pcx_offset);
filename
. pcx_offset
is the position in the video ram where will be stored the pattern to draw
the picture. I advise you to let 0x1000 for this argument, if you want
to use the load_background instruction
for now.incbat(identifier_name, "filename", pcx_offset, col,
row)
col
columns and row
rows from the file.incbat(identifier_name, "filename", pcx_offset, begin_x,
begin_y, col, row);
col
columns and row
rows starting at position (begin_x
,
begin_y
)defpal
: This
instruction allows you to define a palette data in your code. Instead of referencing
it from a picture, you will simply tell the RGB components for each color
of the palette.
#defpal(identifier_name, <color component> * 16);
You are not obliged to define all 16 colors of a palette. As for the components, they are easy to use if you use the hexa decimal notation in which only the 3 last digits are used and represent (from left to right) red, green and blue. The maximum for each component is 7, thus a perfect while is 0x777, while a purple is something like 0x607...
defspr
: With
this one, you'll define your own sprites using also inline data, not coming
from a picture.
#defspr(identifier_name, vram_address, default_palette, <8 pixels data> * 32);
Thevram_address
is a value in the range 0-0x10000 which tell the assembler where you want to see these data in the video ram. Don't forget that before using a sprite and such, you'll have to load data into video ram before the real use. In fact, this argument just tells a preference and its value doesn't oblige you to respect this. This is also true for thedefault_palette
(there are 16 palettes for sprites). As for the data, I recommand you to use the hexadecimal notation, in which each 32 bits number easily represent 8 pixels using a digits for each. I bet a little example will make this appear clearer. To declare 8 pixels beginning by 3 pixels of color 5, then 2 of color 10 and finishing by color 1, you'll use the number 0x555AA111. Since we're defining 16x16 sprites, we need two of these numbers to define a line and thus we'll have 32 arguments to define the whole sprite pattern.
defchr
: basically,
you should have guessed what this one is for :) (I'm also pretty tired to
make documentation ***yawn*** )
#defchr(identifier_name, vram_address, default_palette, <8 pixels data> * 8);
Seedefspr
for most information. I just want to confirm that you also have 16 palettes for tiles data (which makes a total of 32 differents palettes). And since a tile is only 8x8, you have only one hexa number per line and 8 of these numbers. You can have a look at the debug.c file which has the font used for printing defined this way (notice how thevram_address
is useless :)
set_sprpal
: We're arriving to pseudo functions built to be used inside functions, as
the same place you would write down statements, function calls, declarations
and such... This function change a palette using data included or defined.
set_sprpal(first_palette, palette_name [, number_palette ] );
first_palette
is the number of the palette to alter (in the range 0-15) whilepalette_name
is an identifier to a palette to load or to the first palette you want to load (in this case, you should have included or defined all your palettes without any other actions between them). The last argument is optionnal and assumed to be 1 is missing. It's the number of palette you'll load from the rom into the hardware.
set_bgpal
: This one is exactly the same as the previous one expected that it will alter
one of the 16 tile palettes.load_sprites
: This function is just a wrapper for a magic kit macro but it's pretty important.
It's the function that permit to load sprite from the rom to the video ram.
Don't forget it only become useful there. Once in the video ram, you'll be
able to use it in the spr_pattern function (see sprite
handling).
load_sprites(vram_address, data_name, number_sprites);
vram_address
is the address in the video ram where you want to transfert your sprites. This is this value that you need to pass to thespr_pattern
function.data_name
is an identifier of the sprites you want to copy andnumber_sprites
is the amount of sprites to transfert. Be careful however that the unit of transfert data is in 32x64 sprites. This mean that for transfering 4 16x16 sprites, putting 1 fornumber_sprites
is enough.
load_background
: contrary to what you could think about, this function isn't the tiles equivalent
of load_sprites
. It used to load a whole background picture using
tiles. I consider this function as unperfect, it still requires some improvements,
some flexibility but as it is, it works :)
load_background(data_name, palette_name, bat_name, width, height);
data_name
is the identifier of the first tile that compose the picture. As it is currently, it must be beginning a bank; you can't have some data between the lastnewbank(0x4000)
statement and the label you're referencing. Thepalette_name
identifier must refer to the whole palette which have been extracted from the picture. It is HIGHLY recommended to convert your picture to 16 colors only before including them because if you know that each 8x8 tile must use only 16 colors consecutive, you'll quickly understand that it's not so easy to have more than 16 colors on a whole picture.bat_name
is the identifier of the BAT which is in fact the way to use tiles to compose anew the original picture. Finallywidth
andheight
are self explicite ^^, the only thing I could add is that the unit is the tile, that's to say a 8x8 piece of graphic. The best to see how this function works is to check sources of Pong.
The goal of this documentation is not to explain how does the pc engine works but since it's quite near of the machine architecture, I fell obliged to talk a bit about it to fully understand the utility of HuC. For a complete pc engine documentation, don't worry, we're also working on it. Until it's complete, you would like to check the one done my J.C. Restemeyer and on which ours will be based.
Concerning the memory management, you must be aware that the pc engine owns a very little addressable memory, 64 kB. In order to access the whole 1MB which can be present on a HuCard (plus some more special part of memory), there're a system of banking which allow you to access some part of the HuCard at a given moment. The 64 kb is divided in 8 pieces of 8 kb each. Each one of these piece is controlled by a special register indicating the "mapping" that's to say the bank we are accessing trough the memory range controlled by this register. For example, let's say you're setting MMR (Memory Management Unit I think) number 4 to the value 30. Since MMR4 controls 0x8000-0xA000 memory range, you'll be able to read the 15th byte of bank 30 at 0x800F and the last byte of bank 30 at position 0x9FFF.
Normal banks are numbered from 0-127, above these, you can find special banks. The bank numbered 0xFF is the Input/Output bank. When you read or write in memory where this bank is mapped, you won't read or write in some memory but rather access some hardware ports. For example, writing at position 0 of this bank will change the current video register (forget about its use, it's an example), sound stuff is at offset 0x800, cdrom stuff at 0x1800 and such... It's the way the program can communicate with the hardware. Bank 0xF8 is the ram for the coregraphx (there may be more for supergraphx). Whenever available the backup RAM is located at bank 0xF7.
There's also something I didn't explain. In order to make it more easy to handle all of this, we generally "schedule" a bank to be placed at a certain place (such as 0x4000 or 0xC000 and such). We way, we can use absolute jumps and such, we are not limited to relative addressing. Whatever, you must be aware that each bank is prepared for a certain position, not forgetting that some MMR are reserved. Indeed, it's not compulsory but most of roms, not to say absolutely all, uses a certain scheme. MMR0 (controlling 0x0000-0x2000) is mapped to the I/O bank for controlling the hardware. MMR1 (controlling 0x2000-0x4000) is mapped to the RAM bank. MMR7 (the last one) is mapped to bank 0. This one should always be mapped since interruption vectors are taken from the very end of the RAM (and thus in this bank), this bank generally contain general purpose functions (BIOS) and it's the only one which is mapped by default when booting anew. So code at the beginning of this bank will be the 'boot' code.
Outdated : As for easing creation of ROM from HuC, we've also decided to use 0x4000-0x6000 for data such as sprites, tiles. If you've read everything, you'll notice we still have 0x6000-0xDFFF for our purposes. Code can be placed in any of these and when telling HuC to use a new bank, it will try to feed all this bank so that you won't have overlapping function calls. About function calls, we're still working on it but we already have a working solution.
All of this to show you that a
good spreading of the code will make things faster and smaller...
Thanks to the new magic kit used, the assembler is able to map automaticaly data and code. This means that you don't use to specify any "newbank" statement since the assembler will spread code and data in various banks and will take care of mapping stuff before using it. As far as I remember the code only uses one bank and another one will only be used to contain a wraper for each function that maps correctly a bank before calling a function. Two banks are used for startup code (hucard and cd one). One will contain data (such as sprites, tiles and such) and another one for constant data (as the string constant in C programs). Remain the two classical first MMR filled with the hardware access bank and the RAM one.
As you can see, everything is used now and transparently for us, C coders :)