Wednesday, November 15, 2017

devkitSMS Programming Sample

In the previous post, we checked out devkitSMS Programming Setup. The devkitSMS provides tools and code to support homebrew development for the Sega Master System, SG-1000, and Sega Game Gear.

Using devkitSMS, it is possible to write game code using the C language rather than pure Z80 assembly. Therefore, we would now like to extend this knowledge and write more detailed programming sample.
Let's check it out!

Software
Follow all instructions from the previous post: this documents how to setup the pre-requisite software.
Note: ensure you have downloaded and installed the devkitSMS and Small Device C Compiler [SDCC].

Also download vgm2psg.exe required to convert VGM files to the Programmable Sound Generator [PSG].
# Clone PSGlib
cd C:\GitHub\sverx
git clone https://github.com/sverx/PSGlib.git

Demo
The devkitSMS programming sample is based on the Candy Kid Demo coding competition entry for 2017. All graphics, sprites, music, sound fx, coding logic etc. here is based on the devkitSMS tutorial by @sverx.

Create folder C:\CandyKidDemoSMS. Create the following sub-folders: asm, crt0, dev, gfx, lib, psg, utl
 Folder  File  Location
 asm  Opcodes.dat
 smsexamine.exe
 C:\smsexamine1_2a
 C:\smsexamine1_2a
 crt0  crt0_sms.rel  C:\Github\sverx\devkitSMS\crt0
 lib  SMSlib.h
 SMSlib.lib
 PSGlib.h
 PSGlib.rel
 C:\GitHub\sverx\devkitSMS\SMSlib\src
 C:\GitHub\sverx\devkitSMS\SMSlib
 C:\GitHub\sverx\devkitSMS\PSGlib\src
 C:\GitHub\sverx\devkitSMS\PSGlib
 utl  folder2c.exe
 ihx2sms.exe
 vgm2psg.exe
 C:\GitHub\sverx\devkitSMS\folder2c
 C:\GitHub\sverx\devkitSMS\ihx2sms
 C:\GitHub\sverx\PSGlib\tools
Note: assumes Opcodes.dat and smsexamine.exe downloaded from SMS Examine and extracted to C:\

Development
Launch Visual Studio 2008. File | New | Project... | Visual C++ | Win32 | Win32 Project
 Name:  Game
 Location:  C:\CandyKidDemoSMS\dev
 Create directory for solution  UNCHECKED
OK

 Application type:  Console application
 Additional options:  Empty project CHECKED
Finish

Graphics
Follow all instructions from the previous post to download and install BMP2Tile utility to convert graphics into Sega Master System format. Download gfxcomp_stm.dll STM compressed format to output directory.

Splash
Here is an example of background tiles that exhaust the full 256 x 192 resolution e.g. the Splash screen:

Choose "Tiles" tab. Ensure that "Remove duplicates" and "Planar tile output" are both checked.
Click Save button | File name splash (tiles).psgcompr | File type PS Gaiden (*.psgcompr)

Choose "Tilemap" tab. Leave "Use sprite palette" and "In front of sprites" options both unchecked.
Click Save button | File name splash (tilemap).stmcompr | File type Sverx's TileMap (*.stmcompr)

Choose "Palette" tab. Leave the "Output hex (SMS)" option checked for Sega Master System.
Click Save button | File name splash (palette).bin | File type Binary files (*.bin)
Here is basic code sample to render the splash screen in main.c with corresponding updated build.bat

main.c
#include "..\lib\SMSlib.h"
#include "gfx.h"

#define SPLASH_TILES 144

void engine_content_manager_splash()
{
  SMS_loadPSGaidencompressedTiles(splash__tiles__psgcompr, SPLASH_TILES);
  SMS_loadSTMcompressedTileMap(0, 0, splash__tilemap__stmcompr);
  SMS_loadBGPalette(splash__palette__bin);
}
void main (void)
{
  engine_content_manager_splash();
  SMS_displayOn();
  for (;;)
  {
    SMS_waitForVBlank();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

build.bat
@echo off
..\utl\folder2c ..\gfx gfx

sdcc -c -mz80 gfx.c
sdcc -c -mz80 main.c
sdcc -o output.ihx -mz80 --data-loc 0xC000 --no-std-crt0 ..\crt0\crt0_sms.rel main.rel ..\lib\SMSlib.lib gfx.rel

..\utl\ihx2sms output.ihx output.sms
output.sms


Background
Repeat the process for background tiles e.g. Font however save tilemap as uncompressed binary format [bin] to access data randomly. Save tiles in PS Gaiden compressed format + use same palette as above.
 Tiles  font (tiles).psgcompr  PS Gaiden (*.psgcompr)
 Tilemap  font (tilemap).bin  Raw (uncompressed) binary (*.bin)
 Palette  font (palette).bin  Raw (uncompressed) binary (*.bin)

main.c
#include "..\lib\SMSlib.h"
#include "gfx.h"

#define FONT_TILES  16
#define TEXT_ROOT  33  // 33 is "!" in ASCII.

void engine_content_manager_load()
{
  SMS_loadPSGaidencompressedTiles(font__tiles__psgcompr, FONT_TILES);
  SMS_loadBGPalette(font__palette__bin);
}
void engine_font_manager_draw_text(unsigned char* text, unsigned char x, unsigned char y)
{
  const unsigned int *pnt = font__tilemap__bin;
  unsigned char idx=0;
  while ('\0' != text[idx])
  {
    signed char tile = text[idx] - TEXT_ROOT;
    SMS_setNextTileatXY(x++, y);
    SMS_setTile(*pnt + tile);
    idx++;
  }
}
void main (void)
{
  char *text = "HELLO";
  engine_content_manager_load();
  engine_font_manager_draw_text(text, 0, 0);

  SMS_displayOn();
  for (;;)
  {
    SMS_waitForVBlank();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Sprites
Repeat the process for sprite tiles however no need to save the tilemap file. Also, introduce sprite code:
 Tiles  sprites (tiles).psgcompr  PS Gaiden (*.psgcompr)
 Palette  sprites (palette).bin  Raw (uncompressed) binary (*.bin)

main.c
#include <stdbool.h>
#include "..\lib\SMSlib.h"
#include "gfx.h"

#define SPRITE_TILES  80
#define KID_BASE_TILE  SPRITE_TILES + 0

void engine_content_manager_load()
{
  SMS_loadPSGaidencompressedTiles(sprites__tiles__psgcompr, SPRITE_TILES);
  SMS_loadSpritePalette(sprites__palette__bin);
}
void engine_sprite_manager_draw(unsigned char x, unsigned char y, unsigned char tile)
{
  SMS_addSprite(x+0, y+0, tile+0);
  SMS_addSprite(x+8, y+0, tile+1);
  SMS_addSprite(x+0, y+8, tile+8);
  SMS_addSprite(x+8, y+8, tile+9);
}
void main (void)
{
  unsigned char kidX = 32;
  unsigned char kidY = 32;
  unsigned char kidColor = 0;
  unsigned char kidFrame = 0;
  unsigned char kidTile = KID_BASE_TILE + ((kidColor * 2) + kidFrame) * 2;

  SMS_setSpriteMode(SPRITEMODE_NORMAL);
  SMS_useFirstHalfTilesforSprites(true);
  engine_content_manager_load();

  SMS_displayOn();
  for (;;)
  {
    SMS_initSprites();
    engine_sprite_manager_draw(kidX, kidY, kidTile);
    SMS_finalizeSprites();
    SMS_waitForVBlank();
    SMS_copySpritestoSAT();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Music
Download Mod2PSG2 music tracker to record music [FX] and convert output VGM files to PSG file format. File | Export module | VGM... | C:\CandyKidDemoSMS\psg\raw\music.vgm | Save
Now convert to PSG file format using vgm2psg.exe utility downloaded previously:

Start | run | cmd | cd C:\CandyKidDemoSMS
utl\vgm2psg.exe psg\raw\music.vgm psg\music.psg
Here is basic code sample to play the background music in main.c with corresponding updated build.bat
main.c
#include "..\lib\SMSlib.h"
#include "..\lib\PSGlib.h"
#include "gfx.h"
#include "psg.h"

#define MUSIC_PSG   music_psg

void main (void)
{
  PSGPlayNoRepeat(MUSIC_PSG);
  SMS_displayOn();
  for (;;)
  {
    SMS_waitForVBlank();
    PSGFrame();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

build.bat
@echo off
..\utl\folder2c ..\gfx gfx
..\utl\folder2c ..\psg psg

sdcc -c -mz80 gfx.c
sdcc -c -mz80 psg.c
sdcc -c -mz80 main.c
sdcc -o output.ihx -mz80 --data-loc 0xC000 --no-std-crt0 ..\crt0\crt0_sms.rel main.rel ..\lib\SMSlib.lib ..\lib\PSGlib.rel gfx.rel psg.rel

..\utl\ihx2sms output.ihx output.sms
output.sms

Sound
Repeat the process for sound effects however export VGM file to channel 2 and play on SFX_CHANNEL2:

Start | run | cmd | cd C:\CandyKidDemoSMS
utl\vgm2psg.exe psg\raw\sound.vgm psg\sound.psg 2 main.c
#include "..\lib\SMSlib.h"
#include "..\lib\PSGlib.h"
#include "gfx.h"
#include "psg.h"

#define SOUND_PSG   sound_psg

void main (void)
{
  PSGSFXPlay(SOUND_PSG, SFX_CHANNEL2);
  SMS_displayOn();
  for (;;)
  {
    SMS_waitForVBlank();
    PSGSFXFrame();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Noise
Repeat the process for noise effects however export VGM file to channel 3 and play on SFX_CHANNEL3:

Start | run | cmd | cd C:\CandyKidDemoSMS
utl\vgm2psg.exe psg\raw\noise.vgm psg\noise.psg 3 main.c
#include "..\lib\SMSlib.h"
#include "..\lib\PSGlib.h"
#include "gfx.h"
#include "psg.h"

#define NOISE_PSG   noise_psg

void main (void)
{
  PSGSFXPlay(NOISE_PSG, SFX_CHANNEL3);
  SMS_displayOn();
  for (;;)
  {
    SMS_waitForVBlank();
    PSGSFXFrame();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Input
Input detection is straightforward although it seems joypad variables must be static so to persist values:

main.c
#include "..\lib\SMSlib.h"
void update(const unsigned int curr_joypad1, const unsigned int prev_joypad1)
{
  if (curr_joypad1 & PORT_A_KEY_1 && !(prev_joypad1 & PORT_A_KEY_1))
  {
    SMS_setSpritePaletteColor(0, RGB(2,2,2));
  }
}
void main (void)
{
  // Must be static to persist values!
  static unsigned int curr_joypad1 = 0;
  static unsigned int prev_joypad1 = 0;

  SMS_setSpritePaletteColor(0, RGB(3,3,3));
  SMS_displayOn();
  for (;;)
  {
    curr_joypad1 = SMS_getKeysStatus();
    update(curr_joypad1, prev_joypad1);
    SMS_waitForVBlank();
    prev_joypad1 = curr_joypad1;
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Pause
Many homebrew games do not query pause button although I believe this is good programming practice!

main.c
#include <stdbool.h>
#include "..\lib\SMSlib.h"
#include "..\lib\PSGlib.h"

bool global_pause;
void main (void)
{
  global_pause = false;
  SMS_setSpritePaletteColor(0, RGB(3,3,3));
  SMS_displayOn();
  for (;;)
  {
    if (SMS_queryPauseRequested())
    {
      SMS_resetPauseRequest();
      global_pause = !global_pause;
      if (global_pause)
      {
        SMS_setSpritePaletteColor(0, RGB(1,1,1));
        PSGSilenceChannels();
      }
      else
      {
        SMS_setSpritePaletteColor(0, RGB(3,3,3));
        PSGRestoreVolumes();
      }
    }
    if (global_pause)
    {
      continue;
    }

    SMS_waitForVBlank();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Assembler
Ultimately the goal would be to write game code in Z80 assembly! However, one useful way to slowly transition from C language to Z80 assembly would be to wrap inline assembly blocks via C functions.

Let's integrate the Z80 assembly code inline to clear VRAM from Maxim Hello World example directly:

main.c
#include "..\lib\SMSlib.h"
__sfr __at 0xBF VDPControlPort;
__sfr __at 0xBE VDPDataPort;

void engine_asm_manager_clear_VRAM()
{
  __asm
    ld a,#0x00
    out (_VDPControlPort),a
    ld a,#0x40
    out (_VDPControlPort),a
    ld bc, #0x4000
ClearVRAMLoop:
    ld a,#0x00
    out (_VDPDataPort),a
    dec bc
    ld a,b
    or c
    jp nz,ClearVRAMLoop
  __endasm;
}

void main (void)
{
  engine_asm_manager_clear_VRAM();
  SMS_displayOn();
  for (;;)
  {
    SMS_waitForVBlank();
  }
}

SMS_EMBED_SEGA_ROM_HEADER(9999, 0);
SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");

Summary
Armed with all this knowledge, we are now in an excellent position to build complete video games for the Sega Master System that will be able to run on real hardware but now without any VDP graphics glitches!