Sunday, April 28, 2019

devkitSMS Programming Sample III

In 2017, we checked out devkitsms to setup productive development environment to build 8-bit retro video games for the Sega Master System. In 2018, we streamlined this build process to develop Simpsons Trivia.

However, as projects became larger they seemed longer to build. Plus it didn't seem possible to debug step through C source code. Look for opportunities to improve the process further as we build Platform Explorer! Let's check it out!

Game
Platform Explorer is an open source port of the XNA Platform starter kit for the Sega Master System. This video game was also an entry in the SMS Power! 2019 Coding Competition. Download source code here.

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].

Checklist
Here is a checklist of tasks to complete in order to try and improve the existing development environment:
  • Graphics changes and BMP2Tile update
  • Visual Studio 2015 upgrade and setup
  • Code file separation and build times
  • Various code architectural updates
  • Debug step through code support
  • Performance tweaks then polish

Graphics
Previous work on SMS graphics progressed from MS Paint to Gimp to use Indexed mode with max 16 colors to conform to the 4bpp (bits per pixel) constraints. However, this was only with 256x192 full screen images.

Now, we would like to save multiple individual image files, for example, for sprite animations and share the color palette such that it can be used across all relevant tiles. Therefore, here is how to import the palette:

Launch Gimp | Open an image you would like the palette shared. Image | Mode | Indexed to generate the 16 color palette (4bpp). Windows | Dockable dialogs | Palettes. Right click list and enter the following info:
Now there will new custom 4bpp 16-color palette at %USERPROFILE%\.gimp-2.8\palettes. Finally, extend previous process after Image | Mode | Indexed choose Colors | Map | Set Colormap... Palette: Platformer.

BMP2Tile
Upgrade to Version 0.43 for -fullpalette option to Output 16 colors rather than as many present in image.
bmp2tile.exe raw\back_tiles.bmp -savetiles "back_tiles (tiles).psgcompr" -noremovedupes -planar -tileoffset 0 -exit
bmp2tile.exe raw\back_tiles.bmp -savetilemap "back_tiles (tilemap).bin" -exit
bmp2tile.exe raw\back_tiles.bmp -savepalette "back_tiles (palette).bin" -fullpalette -exit

Visual Studio
In 2017, we were inspired by this suggestion to use Visual Studio as an IDE to better navigate files in larger projects and help automate the development build system. Now it is time to upgrade from VS2008 to 2015.

The motivation here assumes a point where it is difficult / impossible to use Visual Studio 2008 on Windows PC. Plus Visual Studio 2015 has a cool feature to easily Toggle Header / Code files for increased productivity. Replicate all instructions here: Setup External Tools to integrate the build process directly from within Visual Studio and connect "Run Batch File" command to Ctrl+1 hot key and automatically build and execute code!

Formatting
Formatting is important because formatted code makes it easier to read, understand, maintain and debug. Visual Studio 2015. Tools menu | Options... | Text Editor | C/C++ | Formatting | Spacing. Update settings:

Setup
Create folder C:\PlatformExplorerSMS. Create the following sub-folders: asm, crt0, dev, gfx, lib, psg, tmp. Note: tmp sub-folder contains dummy PSGlib.h and SMSlib.h header files used for debugging - more soon. IMPORTANT: more information on which files reside in sub-folders and why can be found at here and here.

Separation
As projects became larger they seemed longer to build. Traditionally, this has been because all the code has been spread across multiple header files and with only 1x main.c which forces a complete rebuild each time.

Therefore, in an attempt to improve build compile times, we'd like to separate interface from implementation code: keep header files lean with goal that static code in translation units need not be constantly recompiled!

Note: here object files would have to be version controlled and build script may change during development.

Timing
In order to test hypothesis that separating code improves compilation speed then record build script times:
:: Calculate the start timestamp
set _time=%time: =0%
set /a _hours=100%_time:~0,2%%%100,_min=100%_time:~3,2%%%100,_sec=100%_time:~6,2%%%100,_cs=%_time:~9,2%
set /a _started=_hours*60*60*100+_min*60*100+_sec*100+_cs

sdcc -c -mz80 --opt-code-speed --peep-file ..\peep-rules.txt --std-c99 _sms_manager.c
sdcc -c -mz80 --opt-code-speed --peep-file ..\peep-rules.txt --std-c99 _snd_manager.c
sdcc -c -mz80 --opt-code-speed --peep-file ..\peep-rules.txt --std-c99 global_manager.c
sdcc -c -mz80 --opt-code-speed --peep-file ..\peep-rules.txt --std-c99 debug_manager.c
sdcc -c -mz80 --opt-code-speed --peep-file ..\peep-rules.txt --std-c99 hack_manager.c
:: continue compiling remaining implementation [*.c] files

sdcc -c -mz80 --opt-code-speed --peep-file peep-rules.txt --std-c99 main.c

:: Calculate the difference in cSeconds
set _time=%time: =0%
set /a _hours=100%_time:~0,2%%%100,_min=100%_time:~3,2%%%100,_sec=100%_time:~6,2%%%100,_cs=%_time:~9,2%
set /a _duration=_hours*60*60*100+_min*60*100+_sec*100+_cs-_started

:: Populate variables for rendering (100+ needed for padding)
set /a _hours=_duration/60/60/100,_min=100+_duration/60/100%%60,_sec=100+(_duration/100%%60%%60),_cs=100+_duration%%100

echo.
echo Time taken: %_sec:~-2%.%_cs:~-2% secs
echo

Coding
The following coding changes + enhancements have been made throughout Platform Explorer development:

devkitSMS
The original SMSlib.h and PSGlib.h header files do not seem to have include guard clauses which means they can't be included more than once so let's create wrappers for them. In fact, this is beneficial for debugging.
// _sms_manager.h
#ifndef _SMS_MANAGER_H_
#define _SMS_MANAGER_H_

void devkit_SMS_init();
void devkit_SMS_displayOn();
void devkit_SMS_displayOff();
void devkit_SMS_mapROMBank( unsigned char n );
// ...

// input
unsigned int devkit_SMS_getKeysStatus();
unsigned int devkit_PORT_A_KEY_UP();
unsigned int devkit_PORT_A_KEY_DOWN();
// ...
unsigned int devkit_PORT_A_KEY_1();
unsigned int devkit_PORT_A_KEY_2();

// #defines
unsigned char devkit_SPRITEMODE_NORMAL();
unsigned int devkit_VDPFEATURE_HIDEFIRSTCOL();

#endif//_SMS_MANAGER_H_
Note: repeat this process for PSGlib.h. Also, don't forget to upgrade PSGlib to include PSGResume() API.

Screen
As projects get larger there will be more screens. Therefore, refactor and channel through screen manager:
// screen_manager.c
#include "screen_manager.h"
#include "global_manager.h"
#include "enum_manager.h"

// Screens
#include "splash_screen.h"
#include "intro_screen.h"
// ...
#include "over_screen.h"

static void( *load_method[ MAX_SCREEENS ] )( );
static void( *update_method[ MAX_SCREEENS ] )( unsigned char *screen_type );

static unsigned char curr_screen_type;
static unsigned char next_screen_type;

void engine_screen_manager_init( unsigned char open_screen_type )
{
  next_screen_type = open_screen_type;
  curr_screen_type = screen_type_none;

  // Set load methods.
  load_method[ screen_type_splash ] = screen_splash_screen_load;
  load_method[ screen_type_intro ] = screen_intro_screen_load;
  // ...
}

void engine_screen_manager_update()
{
  if( curr_screen_type != next_screen_type )
  {
    curr_screen_type = next_screen_type;
    load_method[ curr_screen_type ]();
  }

  update_method[ curr_screen_type ]( &next_screen_type );
}

Input
Remove previous + current input state that was injected into methods and channel through input manager:
// input_manager.c
#include "input_manager.h"
#include "_sms_manager.h"

// Must be static to persist values!
static unsigned int curr_joypad1 = 0;
static unsigned int prev_joypad1 = 0;

// Private helper methods.
static unsigned char engine_input_manager_hold( unsigned int data );
static unsigned char engine_input_manager_move( unsigned int data );

// Public method.
void engine_input_manager_update()
{
  prev_joypad1 = curr_joypad1;
  curr_joypad1 = devkit_SMS_getKeysStatus();
}

// main.c
for (;;)
{
  if( devkit_SMS_queryPauseRequested() )
  {
    // ...
  }

  devkit_SMS_initSprites();
  engine_input_manager_update();
  engine_screen_manager_update();

  devkit_SMS_finalizeSprites();
  devkit_SMS_waitForVBlank();
  devkit_SMS_copySpritestoSAT();

  devkit_PSGFrame();
  devkit_PSGSFXFrame();
}

Banks
In 2017, Astro Force by eruiz00 gave a great example on how to reference bank information generated from bmp2tile and folder2c in code to lookup data stored in arrays, for example, to load banked sprite animation:
// anim_object.h
extern const unsigned char *player_anim_data[];
extern const unsigned char player_anim_bank[];

//anim_object.c
#include "anim_object.h"
#include "..\banks\bank2.h"

const unsigned char *player_anim_data[] =
{
  player_idle__tiles__psgcompr,
  player_run_left_01__tiles__psgcompr,
  player_run_left_02__tiles__psgcompr,
  // ...
  player_run_rght_04__tiles__psgcompr,
  player_run_rght_05__tiles__psgcompr,
};
const unsigned char player_anim_bank[] =
{
  player_idle__tiles__psgcompr_bank,
  player_run_left_01__tiles__psgcompr_bank,
  player_run_left_02__tiles__psgcompr_bank,
  // ...
  player_run_rght_04__tiles__psgcompr_bank,
  player_run_rght_05__tiles__psgcompr_bank,
};

// anim_manager.c
static void player_load( unsigned char index, unsigned int tile )
{
  const unsigned char *data = ( const unsigned char * ) player_anim_data[ index ];
  const unsigned char bank = ( const unsigned char ) player_anim_bank[ index ];

  devkit_SMS_mapROMBank( bank );
  devkit_SMS_loadPSGaidencompressedTiles( data, tile );
}

Levels
In 2018, Duckslayer Adventures by haroldoop gave a great example of how levels could be stored externally in text files then converted into byte arrays using folder2c. The byte arrays are then interpreted for drawing:
 KEY  VALUE
 .  Blank
 #  Block
 @  Platform
 $  Optional
 1  Player
 X  Exit
 G  Gem
 P  Power
  // Level0101
  ................
  .P..............
  .@@...........@@
  ..........A.....
  ..G...$@@@@....G
  .@@...........@@
  ................
  .G............G.
  .@@...........@@
  .@@.1.......X.@@
  .####$$$$$$#####
  ..###$$$$$$####.

Structs
Finally, as seen in many code samples from haroldoop, structs are used extensively to store data especially data accessed globally throughout the code base. Platform Explorer uses this idea coupled with "manager":
// hack_object.h
typedef struct tag_struct_hack_object
{
  unsigned char hack_delayspeed;
  unsigned char hack_invincible;
  unsigned char hack_difficulty;
  unsigned char hack_game_speed;

} struct_hack_object;

 
// hack_manager.h
#include "hack_object.h"

void engine_hack_manager_init();
extern struct_hack_object global_hack_object;


// hack_manager.c
#include "hack_manager.h"
#include "global_manager.h"
#include "enum_manager.h"

#define PEEK( addr)        (* ( unsigned char *)( addr ) )
#define POKE( addr, data ) (* ( unsigned char *)( addr ) = ( data ) )

#define HACKER_START       0x0050

// Global variable.
struct_hack_object global_hack_object;

void engine_hack_manager_init()
{
  struct_hack_object *ho = &global_hack_object;
  ho->hack_delayspeed = 0;
  ho->hack_invincible = 0;
  ho->hack_difficulty = 0;
  ho->hack_game_speed = 0;

#ifndef _CONSOLE
  ho->hack_delayspeed = PEEK( HACKER_START - 1 );
  ho->hack_invincible = PEEK( HACKER_START + 0 );
  ho->hack_difficulty = PEEK( HACKER_START + 1 );
  ho->hack_game_speed = PEEK( HACKER_START + 2 );
#endif
}

Debugging
Visual Studio is useful IDE for navigating a large code base. However, as the game code currently targets SDCC compiler it does not seem possible to debug step through the devkitSMS specific API source calls.

Nonetheless, leverage the _CONSOLE conditional compilation statement and implement the following code. Here, it is possible to debug step through all ANSI-C code at least trusting devkitSMS code works correctly.
// _sms_manager.c
#include "_sms_manager.h"
#include "info_manager.h"
#include <stdbool.h>

#ifdef _CONSOLE
#include "..\..\tmp\SMSlib.h"
#else
#include "..\..\lib\SMSlib.h"
#endif

void devkit_SMS_init()
{
  SMS_init();
}
void devkit_SMS_displayOn()
{
  SMS_displayOn();
}
void devkit_SMS_displayOff()
{
  SMS_displayOff();
}
void devkit_SMS_mapROMBank( unsigned char n )
{
  SMS_mapROMBank( n );
}
// ...

// Sega header.
#ifdef _CONSOLE
#else
  SMS_EMBED_SEGA_ROM_HEADER( productCode, revision );
  SMS_EMBED_SDSC_HEADER( verMaj, verMin, dateYear, dateMonth, dateDay, author, name, descr );
#endif
Remember: tmp sub-folder contains dummy PSGlib.h and SMSlib.h header files that facilitate debugging.

Performance
In order to process collision detections in Platformers, it is commonplace to take the absolute value of the distance of two points + use for comparison. Unfortunately, there was noticeable delay with this function:
int abs( int v )
{
  return v * ( ( v < 0 ) * ( -1 ) + ( v > 0 ) );
}
After examining code this function invoked multiple times per frame it was clear that abs() could be replaced with simple if statement. Being able to debug step through C code helped ensure this still worked as before!

Polish
Also, as projects become larger, more content is required. Therefore, to ensure polish throughout the game it was critical that the VRAM was cleared properly between large graphic content loads to avoid any glitches.
// intro_screen.c
void screen_intro_screen_load()
{
  // Unload VRAM each begin to remove any unwanted graphics glitches...!
  devkit_SMS_displayOff();
  engine_asm_manager_clear_VRAM();
  engine_text_manager_clear_all();

  engine_content_manager_load_back_tiles();
  engine_content_manager_load_sprites();
  engine_content_manager_load_title();
  // ...
  devkit_SMS_displayOn();
}
Deployment
Finally, a big shout out to Calindro who helped deploy updated versions of Platform Explorer during the competition + for outlining is always better to stick with Power of 2 ROM sizes for compatibility reasons.

Summary
To summarize, once again the SMS Power! community has been awesome and by members sharing their code this has helped immensely with Platform Explorer doing so well in the 2019 Coding Competition J