After spending over a year and a half working with this wonderful platform, I was requested to make a "from scratch" guide to Neo-Geo programming. This guide does not use any libraries (e.g. the NeoBitz kit, DATLib, or freemlib for Neo-Geo). If the idea of doing everything in assembly language scares you, there's a few alternatives out there for coding Neo-Geo games in C. (All of them either spawn from or require parts of the original kit by Fabrice Martinez, Jeff Kurtz, et al.)
If the white-on-black color scheme annoys you, please switch to the "Bright" color scheme.
This guide still assumes a lot of information. These gaps will (hopefully) be covered in the future.
Things I can think of off the top of my head:
If there is anything missing from this list that you think should be mentioned, please let me know via e-mail (ajk187 on gmail) or on IRC: chat.freenode.org #neogeodev
The actual code is being worked on as well.
This guide is meant to be a beginner's first port of call for learning how to program for the Neo-Geo. Some sort of previous programming experience would be good to have, even if it's not assembly language.
I can't possibly teach you everything there is to know about the Neo-Geo, sadly.
That's outside the scope of this document for many reasons, not the least of which
is that I dislike writing Z80 sound code. :D
Plus, introducing too many things this early will likely scare you off.
At this time, the guide assumes you know how to use your computer and how to set up various things (e.g. adding programs to and/or changing the PATH).
This guide uses a bit of programming jargon that you might not be familiar with.
In this guide, binary numbers (0 or 1 for each bit) are displayed with a % prefix (e.g. %00001111, which is $0F (decimal 15)).
In this guide, hex numbers ($0-$F, representing decimal numbers 0-15) are displayed with a $ prefix (e.g. $10FD80).
Binary coded decimal involves using hex values $0-$9 to represent decimal numbers. For example, the decimal number 20 would be encoded as hex $20.
Generally, processors don't agree on how data is stored internally. There are two competing formats, big endian (also referred to as "network byte order" in other docs) and little endian.
In little endian, the most significant byte is the first to be written, so a hex value of $12345678 is stored as $78 $56 $34 $12.
Luckily, the Neo-Geo doesn't use little endian; it uses big endian instead. A hex value of $12345678 will be stored in big endian as $12 $34 $56 $78.
The Neo-Geo was SNK (and ADK's) solution for arcade operators to switch out games easily. It ended up being quite popular, being released in 1989, with the last official game being released in 2004. In addition to the arcade machines (called "MVS" or "Multi Video System"), there was a home version produced. Later, a couple of CD versions of the system were released as well.
For the time, the Neo-Geo provided a lot of power.
The cart and CD systems have different sets of limitations. Unless you're trying to make the most detailed game ever, you probably won't run into them.
Both system types run into these limits:
Cart systems aren't limited by much, since pretty much everything is loaded from ROM. Cartridges also have the advantage of being able to use custom hardware. (Some examples are the link jacks and extra processor on League Bowling and Riding Hero, the CPLD on Metal Slug X, and the NEO-CMC, among others.)
CD systems are limited in a number of ways, since most data is loaded into RAM.
Getting around these limitations requires loading different files off of the disc. When switching a large amount of data, a loading screen should be used.
More technical information is found in later sections, closer to the program example.
At the heart of the Neo-Geo is the Motorola 68000 processor (also manufactured by third parties). The same processor (though sometimes a later revision) powers the Sega Genesis/Mega Drive, the Commodore Amiga (non-PPC versions), and many arcade games of the 1980s and 1990s. You might see the processor being referred to as "m68k" or "680x0" in various literature.
Though the 68000 is a 16-bit processor (defined by the external bus size), it is more than capable of handling 32-bit values.
On the 68000, the size of the data type you use plays an important role in the program's layout. The original 68000 is not capable of accessing words and longwords on "odd" addresses, so e.g. a word read (two bytes) from $102061 will cause an error. This is a very important thing to note, as your code may compile fine, but running it will cause a reset.
Name | Size |
---|---|
byte | 1 byte (8 bits) |
word | 2 bytes (16 bits) |
longword/long | 4 bytes (32 bits) |
quadword/quad | 8 bytes (64 bits, split across two registers) |
The 68000 family gives you access to 8 data registers, 7 address registers, the stack pointer, and a few others. All registers are 32-bit.
Register Name | Type |
---|---|
d0-d7 | Data |
a0-a6 | Address |
a7/sp | Stack Pointer (User) |
pc | Program Counter |
ccr | Condition Code Register |
sr | Status Register |
The 68000 has two modes of operation: User mode and Supervisor mode. The difference between the two modes is the instructions that are available for use. For Neo-Geo development, you're going to be using Supervisor mode, since the System ROM uses privileged instructions. It's easier to just set it and forget it, compared to using User mode and having to remember to switch back to supervisor before calling a system ROM routine, etc.
The 68K family is capable of servicing hardware interrupts. On the Neo-Geo, these are used for horizontal and vertical blanking. (The CD systems also seem to use the third interrupt for file loading?)
The interrupts are explained in more detail in the example section.
The 68000's instruction set is much too large to cover here, so you are suggested to pick up some extra documentation. The primary reference for the 680x0 family is the Motorola M68000 Family Programmer's Reference Manual, which is available for download from NXP's site. (See Further Reading.)
Other good sources are Internet source engines and possibly your local library. There's a lot of literature available for the 68000 family, but remember that you're only dealing with the base 68000 (and not any of the successors) without a FPU.
MarkeyJester's 68k Tutorial is a good resource for beginners.
In order to create the data necessary for a Neo-Geo game/utility/whatever, you're going to need some tools.
The assembler handles the job of taking your source code and spitting out the relevant data in binary format. There are many more assemblers out there than what is listed here.
vasm is a cross-platform assembler that supports a number of architectures, including 68000 and Z80. This tutorial uses vasm for assembly.
The GNU assembler ("as", "gas") is another option. Its default syntax for M68K leaves a bit to be desired, but there are options for getting around it.
AS is another cross-platform assembler that targets a number of architectures.
While a linker isn't exactly required, it helps a lot with bigger projects.
vlink is the recommended linker to use with vasm. It is the linker that would be used if this project was more than a simple "Hello World".
The GNU linker can also be used, if you have a toolchain that supports it.
Many tools for graphics exist, including converters, viewers, and editors. A lot of these tools are Windows-only.
NGFX is a tool by blastar for viewing and editing various Neo-Geo graphic formats. A public version has not yet been released.
A few graphics tools are included with DATLib. This is probably your best bet for easily creating Neo-Geo format graphics (until NGFX is released, anyways).
YY-CHR is a graphics editor for various game formats. While the original YY-CHR can run in Wine, the later YY-CHR.NET version does not.
NeoFixFormat is a YY-CHR.NET plugin that allows you to edit Neo-Geo fix tiles.
Converts 4BPP Sega Master System/Game Gear/Wonderswan Color graphics to a format the Neo-Geo can understand (after some manipulation; see ROMWak below).
If there's a weak point in the current Neo-Geo homebrew development scene, it's sound. Luckily, 2015 was the year of a few good sound-related tool releases.
Jeff Kurtz's Neo Sound Builder enables you to create ADPCM-A samples and handles placement and addresses for you.
Command line ADPCM-A encoder by freem.
Command line ADPCM-B encoder by ValleyBell and Fred/FRONT.
There are a few other tools that are extremely helpful to have around when developing for the Neo-Geo.
GNU Make is just one way of creating a system to build your project. Its lack of arithmetic commands (which would be useful for creating the C ROMs) may make you want to choose another option.
Originally created by Jeff Kurtz, ROMwak allows you to perform many simple binary manipulations (e.g. byteswapping, padding).
mkisofs is a part of the cdrtools suite. It creates an .iso image, which you can then burn and use on your Neo-Geo CD.
chdman is used to create .chd files from CD images. This is useful for when you want to test your Neo-Geo CD program on MAME before burning it to a disc.
Before we can begin creating the Hello World project, the tools will need to be installed and runnable.
You can use whatever tools you're most comfortable with, but this guide will be using vasm (68k target, Motorola syntax) as the assembler. Since the Hello World program is small, we don't really need a linker. The graphics are already supplied, so you don't have to mess with those if you don't want to.
The source code and binary for the simplest possible sound driver are also included. You do not need to have vasm (z80 target, oldstyle syntax) installed unless you want to rebuild the sound driver from source.
Setting up a project workspace is a matter of personal preference. For the most part, I like keeping items separated in directories, based on need.
My typical project workspace looks like this:
You are free to either use or ignore this; modify it to your liking, and so on.
As with most (if not all) 68k binaries, the first 256 ($100) bytes define various addresses for the machine to use. This is typically called the "vector" section. The vectors are slightly different depending on if you're targeting cart or CD systems. The values marked "(your choice)" are up to you. A typical define for these is $C00426, which will reset the system.
All values here are longwords (4 bytes each). (Only three bytes are shown for clarity, as the topmost byte will be $00 in all cases.)
Address | Name | Description | Suggested Default |
---|---|---|---|
$00 | Initial Supervisor Stack Pointer | Starting location of the supervisor stack. | $10F300 |
$04 | Initial Program Counter | The first address the program runs on boot. | $C00402 |
$08 | Bus Error | Used by the development BIOS to launch the built-in monitor. | $C00408 |
$0C | Address Error | $C0040E | |
$10 | Illegal Instruction | Run upon executing the ILLEGAL opcode. | $C00414 |
$14 | Divide by 0 | Computers can't divide by zero. | (your choice) |
$18 | CHK Instruction | Run upon executing the CHK opcode. | (your choice) |
$1C | TRAPV Instruction | Run upon executing the TRAPV opcode. | (your choice) |
$20 | Privilege Violation | Occurs when using privileged instructions (e.g. those meant for supervisor mode) in user mode. | $C0041A |
$24 | Trace | (Runs when the T bit of the stack register is set. ??) | $C00420 |
$28 | Line 1010 Emulator | Allows you to trap opcodes that start wth the bit pattern %1010. | (your choice) |
$2C | Line 1111 Emulator | Allows you to trap opcodes that start wth the bit pattern %1111. | (your choice) |
$30-$3B | Reserved | (not used for any specific purpose) | $C00426 |
$3C | Uninitialized Interrupt Vector | $C0042C | |
$40-$5F | Reserved | (not used for any specific purpose) | $C00426 |
$60 | Spurious Interrupt | $C00432 | |
$64 | Level 1 Interrupt | VBlank IRQ | (your choice) |
$68 | Level 2 Interrupt | HBlank IRQ | (your choice) |
$6C | Level 3 Interrupt | Typically unused | (your choice) |
$70 | Level 4 Interrupt | Typically unused | (your choice) |
$74 | Level 5 Interrupt | Typically unused | (your choice) |
$78 | Level 6 Interrupt | Typically unused | (your choice) |
$7C | Level 7 Interrupt | Typically unused | (your choice) |
$80-$BC | Traps | Typically unused. Puzzle Bobble seems to use some of them. | (your choice) |
$C0-$FF | Reserved | (not used for any specific purpose) | (your choice) |
Address | Name | Description | Suggested Default |
---|---|---|---|
$00 | Initial Supervisor Stack Pointer | Starting location of the supervisor stack. | $10F300 |
$04 | Initial Program Counter | The first address the program runs on boot. | $C00402 |
$08 | Bus Error | Used by the development BIOS to launch the built-in monitor. | $C00408 |
$0C | Address Error | $C0040E | |
$10 | Illegal Instruction | Run upon executing the ILLEGAL opcode. | $C00414 |
$14 | Divide by 0 | Computers can't divide by zero. | (your choice) |
$18 | CHK Instruction | Run upon executing the CHK opcode. | (your choice) |
$1C | TRAPV Instruction | Run upon executing the TRAPV opcode. | (your choice) |
$20 | Privilege Violation | Occurs when using privileged instructions (e.g. those meant for supervisor mode) in user mode. | $C0041A |
$24 | Trace | (Runs when the T bit of the stack register is set. ??) | $C00420 |
$28 | Line 1010 Emulator | Allows you to trap opcodes that start wth the bit pattern %1010. | (your choice) |
$2C | Line 1111 Emulator | Allows you to trap opcodes that start wth the bit pattern %1111. | (your choice) |
$30-$3B | Reserved | (not used for any specific purpose) | $C00426 |
$3C | Uninitialized Interrupt Vector | $C0042C | |
$40 | (CD-specific call?) | $C00522 | |
$44 | (CD-specific call?) | $C00528 | |
$48 | (CD-specific call?) | $C0052E | |
$4C | (CD-specific call?) | $C00534 | |
$50 | (CD-specific call?) | $C0053A | |
$54 | (CD-specific call?) | $C004F2 | |
$58 | (CD-specific call?) | $C004EC | |
$5C | (CD-specific call?) | $C004E6 | |
$60 | Spurious Interrupt | $C004E0 | |
$64 | Level 1 Interrupt | HBlank IRQ | (your choice) |
$68 | Level 2 Interrupt | VBlank IRQ | (your choice) |
$6C | Level 3 Interrupt | Used on CD systems for ???. | (your choice) |
$70 | Level 4 Interrupt | Typically unused | (your choice) |
$74 | Level 5 Interrupt | Typically unused | (your choice) |
$78 | Level 6 Interrupt | Typically unused | (your choice) |
$7C | Level 7 Interrupt | Typically unused | (your choice) |
$80-$BC | Traps | Typically unused. Puzzle Bobble seems to use some of them. | (your choice) |
$C0-$FF | Reserved | (not used for any specific purpose) | (your choice) |
After the vectors, there's a set of values that identify the binary as Neo-Geo compatible.
Address | Name | Length | Description |
---|---|---|---|
$0100 | "NEO-GEO" string | 7 bytes | |
$0107 | System Version | 1 byte | 0 on cart systems... 1 or 2 on Neo-Geo CD? |
$0108 | NGM/NGH Number | 2 bytes | The game's identifying number, used for memory card saves and MVS bookkeeping. |
$10A | Program Size | 4 bytes | The size of the program (in bytes). |
$10E | Backup RAM Pointer | 4 bytes | Points to a location in user RAM, used on MVS for saving backup data. (The first two bytes are used for debug dipswitches.) |
$112 | Game Save Size | 2 bytes | Size of the game's save size (in bytes). |
$114 | Eyecatch Flag | 1 byte | Determines how/if the BIOS plays the eyecatcher sequence. (0=handled by BIOS; 1=handled by game; 2=don't show) |
$115 | Eyecatch Sprite Bank | 1 byte | Defines the upper 8 bits of the tile number for the eyecatcher, if handled by the BIOS. |
$116 | Japanese Soft Dip address | 4 bytes | Pointer to the Japanese Soft Dips. |
$11A | USA Soft Dip address | 4 bytes | Pointer to the USA Soft Dips. |
$11E | European Soft Dip address | 4 bytes | Pointer to the European Soft Dips. |
$122 | Jump to USER routine | 6 bytes | jmp USER |
$128 | Jump to PLAYER_START routine | 6 bytes | jmp PLAYER_START |
$12E | Jump to DEMO_END routine | 6 bytes | jmp DEMO_END |
$134 | Jump to COIN_SOUND routine | 6 bytes | jmp COIN_SOUND |
$13A-$181 | (Typically unused?) | XX bytes | |
$182 | Security code location | 4 bytes | Pointer to security code data |
The software dipswitches are accessed via the operator's menu of the MVS. (They can also be accessed with the development BIOS; more on that later.)
Offset | Name | Size | Description | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
$00 | Game Name | 16 bytes | The game's name. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$10 | Timed Option 1 | 2 bytes | Timed option, BCD values. Maximum is $2959 (29 minutes, 59 seconds). Use $FFFF to disable. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$12 | Timed Option 2 | 2 bytes | Timed option, BCD values. Maximum is $2959 (29 minutes, 59 seconds). Use $FFFF to disable. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$14 | Counter Option 1 | 1 byte | A value between 00 and 99; $FFFF to disable. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$15 | Counter Option 2 | 1 byte | A value between 00 and 99, including "WITHOUT" and "INFINITE" options; $FFFF to disable. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$16 | Option Mapping | 10 bytes | Map the default value and number of choices for each option. Use $00 for unused slots. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$20 | Option Names and Choices | (variable) | Each string is 12 bytes long. |
Address | Short Name | Description | Read Value | Write Value | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
$300000 | REG_P1CNT | Player 1 Controller Data (active low; e.g. button pressed=0) |
|
(unknown) | ||||||||||||||||
$320000 | REG_SOUND | Sound Register | Read reply from Z80 | Send command to Z80 | ||||||||||||||||
$300001 | REG_DIPSW | Dipswitches and watchdog |
|
Kick Watchdog | ||||||||||||||||
$320001 | REG_STATUS_A | Status and Coin |
|
(unknown) | ||||||||||||||||
$340000 | REG_P2CNT | Player 2 Controller Data (active low; e.g. button pressed=0) |
|
(unknown) | ||||||||||||||||
$380000 | REG_STATUS_B | Start/Select buttons, etc. (active low; e.g. button pressed=0) |
|
(unknown) | ||||||||||||||||
$3A0001 | REG_NOSHADOW | Normal video output. | (invalid?) | byte | ||||||||||||||||
$3A0011 | REG_SHADOW | Darken video output. | (invalid?) | byte | ||||||||||||||||
$3A0003 | REG_SWAPBIOS | Use System ROM vector table. | (invalid?) | byte | ||||||||||||||||
$3A0013 | REG_SWAPROM | Use game's vector table. | (invalid?) | byte | ||||||||||||||||
$3A0005 | REG_CARDUNLOCK1 | Card unlock 1/2. | (invalid?) | byte | ||||||||||||||||
$3A0017 | REG_CARDUNLOCK2 | Card unlock 2/2. | (invalid?) | byte | ||||||||||||||||
$3A0015 | REG_CARDLOCK1 | Card lock 1/2. | (invalid?) | byte | ||||||||||||||||
$3A0007 | REG_CARDLOCK2 | Card lock 2/2. | (invalid?) | byte | ||||||||||||||||
$3A0009 | REG_CARD_REGSEL | Enables memory card register select mode. | (invalid?) | byte | ||||||||||||||||
$3A0019 | REG_CARD_NORMAL | Disables memory card register select mode. | (invalid?) | byte | ||||||||||||||||
$3A000B | REG_BIOSFIX | Use System ROM SFIX tiles and SM1/sound driver. | (invalid?) | byte | ||||||||||||||||
$3A001B | REG_GAMEFIX | Use game's Fix tiles and M1/sound driver. | (invalid?) | byte | ||||||||||||||||
$3A000D | REG_SRAMLOCK | Write-protect MVS backup RAM. | (invalid?) | byte | ||||||||||||||||
$3A001D | REG_SRAMUNLOCK | Un-protect MVS backup RAM. | (invalid?) | byte | ||||||||||||||||
$3A000F | REG_PALBANK1 | Use palette bank 1. | (invalid?) | byte | ||||||||||||||||
$3A001F | REG_PALBANK0 | Use palette bank 0. | (invalid?) | byte |
Special attention needs to be given to the watchdog. If it is not written to
every ~0.13 seconds, the system will reset. In order to kick the watchdog, write
any byte to the range $300000-$31FFFF. Typically, REG_DIPSW
is the
address written to.
The system ROM on the Neo-Geo uses a number of addresses in RAM. Some are for internal use only, while others are meant to be used by the programmer. The system ROM reserves the region from $10F300 to $10FFFF for its own purposes. CD systems use some RAM locations differently from cart systems.
These addresses are only meant for use with the official SNK System ROM; third party system ROM replacements may or may not use these addresses. Also, this is not a comprehensive list. The Neo-Geo Development Wiki has a page with various BIOS RAM locations if you wish to know more.
The system calls are outlined on the Neo-Geo Development Wiki. Not all system calls are included here.
Address | Name | Description |
---|---|---|
$C00438 | SYSTEM_INT1 | System ROM VBlank routine. |
$C0043E | SYSTEM_INT2 | Cart system VBlank routine. CD just rts es. |
$C00444 | SYSTEM_RETURN | Gives control back to the system ROM. jmp 'ed to at the end of the USER subroutine. |
$C0044A | SYSTEM_IO | Handles system input/output. Updates the system ROM's RAM values, among other things. |
$C004C2 | FIX_CLEAR | Clears the fix layer using tile $FF and draws a column of $20 tiles on each side of the screen. |
$C004C8 | LSP_1st | Initialize sprite hardware. |
The system ROM uses a lot of RAM locations. This list is nowhere near complete, as a complete understanding of the RAM isn't required at the beginning.
(todo: there are quite a few more locations to add here; determine which ones are ok)
Address | Short Name | Length | Description |
---|---|---|---|
$10FD80 | BIOS_SYSTEM_MODE | 1 byte | Determines which VBlank routine should run. $00=system ROM; $80=game |
$10FD82 | BIOS_MVS_FLAG | 1 byte | Software-defined identifier. $00=home system, $01=MVS. |
$10FD83 | BIOS_COUNTRY_CODE | 1 byte | Software-defined system region. $00=Japan, $01=USA, $02=Europe/Export |
$10FD96 | BIOS_P1CURRENT | 1 byte | Player 1's inputs from current frame. (0=not pressed, 1=pressed) |
$10FD97 | BIOS_P1CHANGE | 1 byte | Player 1 active-edge input. (0=not pressed, 1=pressed) |
$10FD98 | BIOS_P1REPEAT | 1 byte | Auto-repeat flags. |
$10FD9A | BIOS_P2CURRENT | 1 byte | Player 2's inputs from current frame. (0=not pressed, 1=pressed) |
$10FD9B | BIOS_P2CHANGE | 1 byte | Player 2 active-edge input. (0=not pressed, 1=pressed) |
$10FD9C | BIOS_P2REPEAT | 1 byte | Auto-repeat flags. |
$10FDAC | BIOS_STATCURNT | 1 byte | Start and Select inputs for current frame. (Select bits are 0 on MVS) |
$10FDAD | BIOS_STATCHANGE | 1 byte | Start and Select active-edge inputs. (Select bits are 0 on MVS) |
$10FDAE | BIOS_USER_REQUEST | 1 byte | Command for USER . |
$10FDAF | BIOS_USER_MODE | 1 byte | Current game status. (0:init/boot, 1:title/demo, 2:game) |
$10FDB0 | BIOS_CREDIT1_DEC | 1 byte (each) | Set the number of credits to decrement here, before calling CREDIT_CHECK and CREDIT_DOWN . $10FDB0-$10FDB3 for Credits 1-4. |
$10FDB4 | BIOS_START_FLAG | 1 byte | Player(s) starting the game on a PLAYER_START call. |
$10FDB6 | BIOS_PLAYER1_MODE | 1 byte (each) | Status Values: 0=No play, 1=Playing, 2=Continue display, 3=Game Over. $10FD86-$10FD89 for Players 1-4. |
$10FE80 | BIOS_DEVMODE | 1 byte | Determines if the system is in developer mode or not. |
In order to learn how to get graphics on the screen, you're going to need to know a little bit about the internals of the graphics system.
All access to the graphics hardware is done through memory-mapped registers. These registers lie in the $3C000x section. Most writes to the registers are meant to be words, though the IRQ acknowledgement register is an exception.
Address | Name | Description | Read | Write | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
$3C0000 | LSPC_ADDR | Used for setting the VRAM address to write to. | Read VRAM data (address unchanged) | Set VRAM address | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C0002 | LSPC_DATA | Used to write data to VRAM. | Read VRAM data (address unchanged) | Write VRAM data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C0004 | LSPC_INCR | Controls how many bytes should be added to the VRAM address after a write. | Read current value | Set new value (signed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C0006 | LSPC_MODE | LSPC status. |
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C0008 | LSPC_TIMER_HI | Timer control 1/2 | (Invalid) | Timer value, most significant bits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C000A | LSPC_TIMER_LO | Timer control 2/2 | (Invalid) | Timer value, least significant bits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C000C | LSPC_IRQ_ACK | IRQ acknowledgement register. Uses a byte instead of a word. | (Invalid) |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$3C000E | LSPC_TIMER_STOP | NTSC/PAL timer behavior | (Invalid) | If bit 0 is 1, timer counter is stopped for 32 raster lines in PAL mode. |
The VRAM in the Neo-Geo contains a map for the Fix data, and sections for sprite control. Not all VRAM addresses are mentioned here, as they are out of scope for a beginner's tutorial.
All values in VRAM are word length (two bytes), despite the addresses only incrementing by 1.
Address Range | Name | Description |
---|---|---|
$0000-$6FFF | Sprite Control Block 1 | Contains the tilemap, palette, auto-animation values, and flip values. |
$7000-$74FF | Fix Layer Map | Data contained on the Fix layer. |
$8000-$81FF | Sprite Control Block 2 | Handles horizontal and vertical shrinking values. |
$8200-$83FF | Sprite Control Block 3 | Controls the Y position, sprite size, and if this sprite is attached to the previous one. |
$8400-$85FF | Sprite Control Block 4 | X position |
The Fix Layer is a tile-based map that appears over all sprites. Therefore, it's typically useful for HUDs (and not much else). Tiles are mapped from top to bottom, left to right.
Data in the Fix Layer is mapped as follows:
Palette | Tile Index | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Sprites are used for everything else that needs to be displayed. A full sprite definition is split into four parts, making up the "Sprite Control Block".
SCB1 defines the tilemap for each sprite. Each sprite's entry in the SCB1 block takes up 2 words × 32 tiles.
Tile Index ($0xxxx) | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Palette | Tile Index ($x0000) | Auto-Anim (3) | Auto-Anim (2) | Vertical Flip | Horizontal Flip | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Contrary to popular belief, the Neo-Geo cannot zoom sprites. However, it can shrink them.
N/A | Horizontal | Vertical | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
The most complex of the sprite values, SCB3 controls the sprite's Y position, size, and if it's attached to the previous sprite (when "sticky bit" = 1). The Sprite Size value ranges from 0-33; a value of 33 makes the sprite 32 tiles high with looped borders (when shrinking).
Y Position (436-Y) | "Sticky Bit" | Sprite Size | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
SCB4 controls the X position. Unlike the Y position, it is mapped properly (e.g. a value of 0 will put the sprite at the leftmost part of the screen).
X Position | N/A | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
With all of this out of the way, we can finally start work on the Hello World program itself. Since it's easier to create the cart version, that will be the first target.
At this time, you should download the Hello World project so you can follow along.
The contents of the Hello World project are laid out as follows:
The first two things that need to be in the program ROM are the 68K vectors and the Neo-Geo header, both mentioned above.
Four routines were mentioned above in the Neo-Geo header section. In order to ensure proper system operation, these routines need to perform some tasks.
USER
The USER
routine needs to respond to a command sent by the system
ROM (stored in BIOS_USER_REQUEST
). USER is also the first port of
call for any Neo-Geo program, so a few things are initialized as well.
USER: move.b d0,REG_DIPSW ; kick watchdog lea $10F300,sp ; set stack pointer to $10F300 (BIOS_WORKRAM) move.w #0,LSPC_MODE ; Disable auto-animation and timer interrupts, set auto-anim speed to 0 frames move.w #7,LSPC_IRQ_ACK ; acknowledge all IRQs move.w #$2000,sr ; Enable VBlank interrupt, go Supervisor ; Handle user request moveq #0,d0 ; clear all bits of d0 move.b BIOS_USER_REQUEST,d0 ; put user request value into d0 lsl.b #2,d0 ; shift value left twice to get offset into tbl_UserRequestCommands lea tbl_UserRequestCommands,a0 ; load address of user commands movea.l (a0,d0),a0 ; get address from table and offset jsr (a0) ; jump to subroutine (typically ends with a jmp SYSTEM_RETURN) jmp SYSTEM_RETURN ; this is here just in case it doesn't...? ;------------------------------------------------------------------------------; ; tbl_UserRequestCommands ; Table that contains the addresses for each BIOS_USER_REQUEST command. tbl_UserRequestCommands: dc.l User_Initialize ; Command 0 (Initialize) dc.l SYSTEM_RETURN ; Command 1 (Custom Eyecatch, unused) dc.l User_Main ; Command 2 (Demo Game/Game) dc.l User_Main ; Command 3 (Title Display)
This routine is meant to initialize the backup RAM area (defined at $10E). This command is only called once on the MVS, when booting the game for the first time. (Other details are still todo...)
User_Initialize: ; this would be the place to initialize high scores and other such data in ; the backup RAM area (see cart/cd header, "pointer to backup RAM block"). ; in this demo we don't do anything, so just jump to SYSTEM_RETURN. jmp SYSTEM_RETURN
The "custom eyecatch" request allows you to change the standard Neo-Geo
boot screen, but only on the AES. The value of $114/Eyecatch Flag must be 1 for
this routine to be run. For now, it just jumps back to SYSTEM_RETURN
.
Games typically use this as their shared startup code. A lot of Neo-Geo System ROM variables will need to be juggled here in a normal game. For this demo, we're going to keep things simple and not worry about them. This command is described in more detail in the Code Structure section.
This is only called when forced start is enabled on MVS. The game's title
screen is displayed. If the select timer is enabled, you should print out the
number of seconds ($10FDDA/SELECT_TIMER
, in BCD) on the screen.
When time runs out, the game is started. There's no need to end with
SYSTEM_RETURN
. Everything else is the same as in command 2, so
we just point command 3 to the same routine as command 2.
PLAYER_START
When a player presses start or the compulsion timer expires on the title
screen, PLAYER_START
is called. We don't bother handling it in
this example...
PLAYER_START: ; In this demo, we don't handle the Start button, or coins, for that matter. rts
DEMO_END
DEMO_END
doesn't need to do much (it mainly saves MVS backup RAM
values), but it should end with an rts
instruction.
DEMO_END: rts ; we're not doing anything in this routine, so just exit.
COIN_SOUND
The only thing COIN_SOUND
needs to do is send a Z80 command to play
a coin noise. In this example, we don't even do that; we just exit.
COIN_SOUND: rts ; we're not doing anything in this routine, so just exit.
Every Neo-Geo game is expected to handle the horizontal and vertical blanking interrupts.
The horizontal blank will not be used in this demo, but we will need to answer
its interrupt in case it's run. This is done by writing 2 (%00000010; acknowledge HBlank)
to LSPC_IRQ_ACK
. Also, kick the watchdog, because defensive programming
is a good idea with a device that will reset the system.
IRQ2: move.w #2,LSPC_IRQ_ACK ; acknowledge interrupt #2 (HBlank) move.b d0,REG_DIPSW ; kick watchdog (prevent reset) rte
The VBlank, on the other hand, is one of the most important parts of any Neo-Geo program, as it's run once per frame. A few things are expected to happen in the VBlank.
BIOS_SYSTEM_MODE
. If the topmost bit
is not set, the game should jump to the system ROM's VBlank handler, SYSTEM_INT1
.LSPC_IRQ_ACK
.REG_DIPSW
to kick the watchdog.SYSTEM_IO
.rte
).VBlank: btst #7,BIOS_SYSTEM_MODE ; check if the System ROM wants to run its VBlank routine. bne.s .VBlank_game ; if not, run our VBlank. jmp SYSTEM_INT1 ; jump to system ROM's VBlank routine .VBlank_game: movem.l d0-d7/a0-a6,-(sp) ; save registers to the stack move.w #4,LSPC_IRQ_ACK ; acknowledge the VBlank interrupt move.b d0,REG_DIPSW ; kick the watchdog ; [Things to perform in VBlank] ; VBlank is where you should be doing sprite data updates (SCB writes) and ; palette RAM updates. However, this demo doesn't use either...yet. ; SNK also wants you to call SYSTEM_IO every 1/60th of a second. (probably 1/50th on PAL) ; This is pretty important, otherwise the RAM locations for input variables ; don't get updated (unless you do it yourself), among other things. jsr SYSTEM_IO ; Note: Other libraries may call this for you, so be aware when using them. .VBlank_end: move.b #0,flag_VBlank ; clear vblank flag so WaitVBlank knows to stop movem.l (sp)+,d0-d7/a0-a6 ; restore registers from the stack rte
A common routine to find in Neo-Geo games is a loop that waits for the VBlank
interrupt to run. The flag_VBlank
variable is an example of that.
It gets set to a nonzero value in a routine called WaitVBlank
,
which looks something like this:
WaitVBlank: move.b #1,flag_VBlank ; set vblank flag to 1. .WaitVBlank_loop: tst.b flag_VBlank ; check if vblank flag is 0. bne.s .WaitVBlank_loop ; if not zero, loop until it is. rts
IRQ 3 is only handled on CD systems. I'm not fully sure what it does yet, but like HBlank, we'll need to acknowledge the interrupt.
IRQ3: move.w #1,LSPC_IRQ_ACK ; acknowledge interrupt 3 move.b d0,REG_DIPSW ; kick watchdog rte
With the required routines out of the way, we can begin to worry about structuring our code. The end goal is to display "Hello World" on the screen, and the easiest way to do that is with the Fix Layer. Before we jump into writing the text, however, there are a few things we need to take care of.
In order for anything to display properly, you're going to need to set up
the palette RAM. The palette RAM spans the address range $400000-$401FFF, and
can be bankswitched (see Neo-Geo Hardware Registers).
Each entry in the palette is a word (two bytes), and has a special 6BPP format
that can be a pain to work with. For this tutorial, just pretend that the
values are $0RGB
, where R is red, G is green, and B is blue.
Each component ranges from $0 to $F.
Two positions in the palette RAM are important: the Reference Color, and
the Background Color. The Reference Color is at $400000
, and must
be set to $8000
for proper display. The Background Color is at
$401FFE
, and can be set to whatever value you'd like.
For this demo, there's only one other palette location that matters:
$400002
. This is the entry that defines the color of the text
displayed on the screen. By default, it's set to $0FFF, but you can change it
to see how it works.
The system ROM provides routines to set up the Fix layer and the Sprites.
FIX_CLEAR
clears the screen (aside from the left and right borders),
while LSP_1st
sets up the sprites.
User_Main: ; In a real Neo-Geo program, this would be handled differently, but this is ; a demo, so it's going to be simpler than expected. ; --perform initialization, part 1-- ; (Palette) ; The reference color (address $400000) must be set to the darkest possible ; color, which is $8000. (Black with the 'dark bit' set.) move.w #$8000,PALETTE_REFERENCE ; Set the background color ($401FFE) to a regular black ($0000). move.w #0,PALETTE_BACKDROP ; Finally, we need to set a color for the text to display. Set the first ; palette entry in the first palette row ($40xxxx) to white ($0FFF). move.w #$0FFF,PALETTES+2 ; (Fix Layer) ; The System ROM provides a command to set up the Fix layer, and it should ; typically be called at boot. jsr FIX_CLEAR ; jump to the FIX_CLEAR subroutine ; (Sprites) ; Sprites will need to be initialized as well. The System ROM provides a ; routine for this purpose as well. jsr LSP_1st ; jump to the LSP_1st subroutine ; --perform initialization, part 2-- jsr DrawHello_Manual ; draw "Hello World" string manually jsr DrawHello_Routine ; draw "Hello World" string with a routine ;------------------------------------------------------------------------------; MainLoop: ; this demo doesn't need to do anything, so infinite loop here. jmp MainLoop
Writing to the Fix Layer is relatively straightforward. First, you'll want to set the increment to 32 ($20), so the text displays horizontally. Next, you'll want to write the target address to the LSPC address register. Finally, write the combined palette and tile index to the VRAM.
The following code writes tile $041 (typically an "A" in the default Fix ROM) with palette 0 to $7026:
FixDraw_Example: move.w #$20,LSPC_INCR ; set LSPC increment to +1 horizontal move.w #$7026,LSPC_ADDR ; set LSPC address to $7026 move.w #$0041,LSPC_DATA ; write "A" from page 0 with palette 0
In order to introduce the basics of handling the VRAM, we'll manually write the "Hello World" string to the Fix layer. This involves sending multiple words to the LSPC data register.
DrawHello_Manual: move.w #$20,LSPC_INCR ; set LSPC increment to +1 horizontal move.w #$7066,LSPC_ADDR ; set LSPC address to $7066 move.w #$0348,LSPC_DATA ; write "H" from page 3 with palette 0 move.w #$0365,LSPC_DATA ; write "e" from page 3 with palette 0 move.w #$036C,LSPC_DATA ; write "l" from page 3 with palette 0 move.w #$036C,LSPC_DATA ; write "l" from page 3 with palette 0 move.w #$036F,LSPC_DATA ; write "o" from page 3 with palette 0 move.w #$0320,LSPC_DATA ; write " " from page 3 with palette 0 move.w #$0357,LSPC_DATA ; write "W" from page 3 with palette 0 move.w #$036F,LSPC_DATA ; write "o" from page 3 with palette 0 move.w #$0372,LSPC_DATA ; write "r" from page 3 with palette 0 move.w #$036C,LSPC_DATA ; write "l" from page 3 with palette 0 move.w #$0364,LSPC_DATA ; write "d" from page 3 with palette 0 rts
Manually writing the values to the Fix layer's VRAM is alright for small changes, but it gets old if you want to print multiple strings. By setting a few ground rules, we can print string data to the Fix layer using a subroutine.
$FF
.Much discussion could be had about these rules, but now is not the time. You're free to not follow them in your own productions, but for the sake of this example, using it will be easier than not.
;-----------------; ; str_HelloWorld ; $FF-terminated hello world string. str_HelloWorld: dc.b "Hello World",$FF ;-----------------; ; fix_PrintString ; Prints a string on the Fix layer. ; (Params) ; d0 - [word] Fix layer address ; d1 - [byte] Palette number and page number ; a0 - [long] Pointer to string to write fix_PrintString: move.w #$20,LSPC_INCR ; set LSPC increment to $20/32 (horizontal writing) move.w d0,LSPC_ADDR ; set up LSPC address from param in d0 moveq #0,d0 ; clear d0 so we can use it in the loop without issue. asl.w #8,d1 ; move byte from param in d1 to upper half of word .fix_PrintString_Loop: move.b (a0)+,d0 ; get character from string and increment pointer position cmpi.b #$FF,d0 ; check if this character is $FF (the terminator) beq.s .fix_PrintString_End ; if so, we're done with the string; exit the routine. ; normal execution: or.w d1,d0 ; OR with the palette and page number move.w d0,LSPC_DATA ; write tile to fix layer bra.s .fix_PrintString_Loop ; loop back for another character .fix_PrintString_End: rts ; exit routine ;-----------------; ; example usage: DrawHello_Routine: move.w #$708D,d0 ; fix layer address $708D moveq #$03,d1 ; Palette 0, Page 3 ; (moveq is used to clear out any garbage from the top bits, since it will be shifted later.) lea str_HelloWorld,a0 ; load pointer to string into a0 jsr fix_PrintString ; jump to the print string subroutine rts
The project is set up to be built using a Makefile with multiple targets. Required tools include vasm (targeting 68000 w/Motorola syntax) and GNU Make. vasm (targeting Z80 w/oldstyle syntax) is required to build the sound ROM.
(todo)
prg
TargetThe prg
target builds the program ROM (hello-p1.p1
) for cart systems.
cdprg
TargetThe prg
target builds the program ROM (HELLO.PRG
) for CD systems.
cart
TargetThe cart
target builds the entire program for cart systems.
cd
TargetThe cd
target produces a Neo-Geo CD version of the program, including an .ISO image.
chd
TargetThe chd
target produces a .chd file for use with MAME. It implies
making the cd
target.
Thanks to somewhat recent developments, it's now possible to test Neo-Geo
homebrew without requiring a recompile of MAME. After building the proper target
(either cart
or chd
), you'll need to let MAME know
a little bit about the ROMs/CD image. This involves editing files in the hash
directory.
Example hashes for the cart and CHD targets are included in the "hash" directory of the project distribution, so all you need to do is copy the data over to the proper xml file.
In order to test the cart versions (MVS/arcade, AES/home), you'll need to copy
the files in the _cart
directory to a new directory called hellotut
in your MAME ROMs directory. From there, you can run one of the two commands below,
assuming you're using the 64-bit versoin of MAME:
mame64 neogeo hellotut
mame64 aes hellotut
After creating the CHD target, copying the CHD file to the roms
directory, and editing the neocd.xml
to include the new entry,
you can test the tutorial with this command:
mame64 neocdz HelloTut
A note about the Neo-Geo CD hashes: The CHD itself stores the proper hash values
inside of the file (as opposed to being a hash of the entire file). In order to
get the proper values for the xml, you're going to need to run the program once
with the wrong hashes. MAME will tell you the proper values; edit those into
neocd.xml
and you'll be good to go.
This tutorial has only scratched the surface of developing for the Neo-Geo with the 68K. There's a lot of material out there worth reading for when you want to go further.