Full workflow for developing Sega Genesis (Mega Drive) games using SGDK (Sega Genesis Development Kit). Use this skill whenever the user is writing C code for the Genesis, working with SGDK APIs, planning VRAM budgets, setting up sprites or tilemaps, working with Tiled maps for Genesis, building a ROM, or asking about Genesis or Mega Drive hardware limits or constraints. Trigger this skill for any question involving rescomp .res files, SPR_init, VDP tile management, XGM/XGM2 audio, m68k game development, or anything about game development for the Sega Genesis or Mega Drive — even if the user doesn't say "SGDK" explicitly. Also trigger when the user asks why their sprites are flickering, their graphics look wrong, their game is running slowly, or they need help with the build system.
You are helping develop a Sega Genesis/Mega Drive game using SGDK (v2.x, modern APIs). The Genesis has strict hardware limits — not soft guidelines, hard physical limits baked into silicon. Every decision must respect them. When in doubt, use fewer resources.
The user's local SGDK install is required. The GDK environment variable must point to it (e.g. C:\sgdk on Windows). Build with:
%GDK_WIN%\bin\make -f %GDK_WIN%\makefile.gen
Output: out/rom.bin
| Resource | Hard limit | Notes |
|---|---|---|
| VRAM | 64 KB total | All tile graphics live here: sprites, backgrounds, fonts |
| Tile size | 32 bytes (8×8 px, 4bpp) | Hardware max: 2048 tiles if all VRAM were tiles |
| User tiles | ~1006 tiles (typical) | SGDK reserves ~1042 tiles for tables, font, etc. |
| Palettes | 4 × 16 colors | 64 colors total on screen; color index 0 = transparent for sprites |
| Screen (H40) | 320×224 px | Standard; H32 is 256×224 (40×28 or 32×28 tiles visible) |
| Hardware sprites | 80 max total | Per scan line limit: 20 sprites — exceed this and sprites on that line vanish |
| Max sprite size | 32×32 px (4×4 tiles) | Per hardware sprite |
| Background planes | 2 (Plane A, Plane B) | Each is a wrapping tilemap grid |
| Plane size options | 32×32, 32×64, 64×64, 32×128 tiles | Default is 64×32; 64×64 uses twice the VRAM |
| RAM | ~61 KB usable | Stack takes ~2.5 KB; be very conservative |
| DMA budget (NTSC) | 7,200 words/frame | Tile uploads per frame; overflow = tearing or dropped frames |
| ROM | Up to 4 MB standard | Bank switching required beyond 4 MB |
| CPU | Motorola 68000 @ 7.67 MHz | No FPU — always use fix16/fix32, never float/double |
The scan-line limit is easy to forget: 80 sprites total, but only 20 can appear on any single horizontal line. A large character built from multiple hardware sprites can consume several of those 20 slots on every line it spans.
myproject/
├── Makefile ← single line: include $(GDK)/makefile.gen
├── src/ ← .c, .s (68k asm), .s80 (Z80 asm)
├── res/ ← .res resource definition files
└── inc/ ← optional custom headers
Build profiles:
make -f makefile.gen → release build (optimized, out/rom.bin)make -f makefile.gen debug → debug build (symbols, no optimization, logs to emulator console)Entry point:
int main(u16 hard) // hard=1 on power-on, 0 on soft reset
{
// ...
while(TRUE) {
SYS_doVBlankProcess(); // always end your frame with this
}
return 0;
}
SGDK initializes RAM, VDP, palettes, font, and input before main() is called. You don't need to initialize these manually.
VRAM is the most commonly exhausted resource. Plan it before writing any code.
SGDK's default layout reserves approximately:
Verify your actual budget using bin/sizebnd.jar after each build — it reports exact VRAM usage.
| Asset | Tile count |
|---|---|
| 8×8 px image | 1 tile |
| 16×16 sprite frame | 4 tiles (2×2) |
| 32×32 sprite frame | 16 tiles (4×4) |
| 24×16 sprite frame | 6 tiles (3×2) |
| 256×128 tileset image | 512 tiles (worst case: all unique) |
| 256×256 tileset image | 1024 tiles (worst case: all unique) |
Unique tiles are what matter: a 256×256 background image can be as few as 10–50 unique tiles if designed with repetition. rescomp automatically deduplicates identical tiles. Use a tile-aware art tool (e.g. aseprite with tilemap mode) to check your unique tile count before importing.
The sprite engine (SPR_init) maintains a VRAM tile cache for sprite animation frames. It only keeps the currently displayed frames in VRAM — not all frames of all animations simultaneously. A 32×32 sprite with 20 animation frames uses 16 tiles of VRAM cache (one frame), not 320 tiles.
The default cache size is 128 tiles. For games with many large sprites, increase it:
SPR_initEx(256); // reserve 256 tiles for sprite engine cache
Compress large tilesets to save ROM space:
BEST — best compression ratio, slowest decompression (fine for static backgrounds loaded at level start)FAST — moderate compression, faster decompression (use for sprites and frequently-reloaded assets)NONE — no compression (use when decompression speed matters most)Never use BEST compression for sprites. The decompression overhead causes frame stutters. Use FAST at most; NONE is safest for animated sprites.
The resource compiler (rescomp) reads .res files and generates C arrays that you include in your code. Run it automatically via the build system.
SPRITE my_hero res/sprites/hero.png 2 3 NONE 5
// ^name ^image ^w ^h ^comp ^speed (ticks/frame)
// w/h are in TILES: 2=16px wide, 3=24px tall
Sprite sheet format: frames are read left-to-right. Each row of frames is one animation sequence. Row 0 = animation 0, row 1 = animation 1, etc.
[run_frame0][run_frame1][run_frame2][run_frame3] ← animation 0 (row 0)
[jump_frame0][jump_frame1][jump_frame2] ← animation 1 (row 1)
[idle_frame0][idle_frame1] ← animation 2 (row 2)
The image must be exactly (w_tiles × 8) × frame_count pixels per row, aligned to 8px boundaries.
For named animation constants, use ANIM blocks:
SPRITE my_hero res/sprites/hero.png 2 3 NONE 5
ANIM run 0 4 5 0 // name, start_frame, count, speed, loop_to_frame
ANIM jump 4 3 6 -1 // loop_to_frame -1 = play once (no loop)
ANIM idle 7 2 8 0
This generates constants: my_hero_ANIM_run, my_hero_ANIM_jump, my_hero_ANIM_idle.
ANIM parameter order: name start_frame frame_count speed loop_frame
start_frame — zero-based index into the full frame list across all rowsframe_count — how many frames in this animationspeed — ticks per frame (higher = slower)loop_frame — frame index to loop back to; -1 means don't loopTILESET bg_tiles res/images/bg.png BEST ALL
IMAGE bg_image res/images/bg.png BEST
TILESET extracts unique 8×8 tiles. IMAGE bundles tileset + tilemap + palette.
TILESET fg_tiles res/tilesets/stage1.png BEST ALL
MAP fg_map res/maps/stage1.tmx layer_foreground FAST FAST
TILESET must be declared before MAP. layer_foreground is the exact layer name in your TMX file (case-sensitive).
PALETTE hero_pal res/sprites/hero.png
PALETTE bg_pal res/images/bg.png
XGM2 bgm_level1 res/music/level1.vgm
SFX sfx_jump res/sfx/jump.wav 1
#include <genesis.h>
#include "resources.h" // auto-generated; contains extern declarations
The sprite engine handles VRAM tile cache, DMA uploads, animation state, and sprite attribute table management.
SPR_init(); // default: 128-tile VRAM cache for sprites
SPR_initEx(256); // larger cache for games with many/large sprites
Sprite *hero = SPR_addSprite(
&my_hero, // SpriteDefinition*
start_x, start_y, // initial position (pixels)
TILE_ATTR(PAL0, TRUE, FALSE, FALSE) // palette, priority, flipV, flipH
);
// Position
SPR_setPosition(hero, x, y); // absolute
SPR_moveSprite(hero, dx, dy); // relative
// Animation
SPR_setAnim(hero, my_hero_ANIM_run); // switch animation
SPR_setFrame(hero, 2); // jump to specific frame
SPR_setAnimAndFrame(hero, my_hero_ANIM_jump, 0); // set both at once
SPR_nextFrame(hero); // advance one frame manually
// Appearance
SPR_setHFlip(hero, TRUE); // mirror horizontally (face left)
SPR_setVFlip(hero, FALSE);
SPR_setVisibility(hero, VISIBLE);
// Cleanup
SPR_releaseSprite(hero);
Priority: TRUE = sprite draws over low-priority background tiles; FALSE = sprite draws behind high-priority tiles.
SPR_update(); // flush sprite state — call once per frame
SYS_doVBlankProcess(); // wait for VBlank, execute DMA queue, sync to 60Hz
Always call SPR_update() before SYS_doVBlankProcess().
Each SGDK Sprite object maps to 1–4 hardware sprites depending on tile dimensions:
Monitor usage at runtime: SPR_getUsedSpriteNum()
Scan-line budget: the stricter constraint for most games. A 32×32 sprite spans 4 tile rows — it consumes 1 slot on each of those 4 scan lines. Stack multiple sprites vertically and you'll hit the 20/line limit before hitting 80 total.
SPR_setCollisionType(hero, COLLISION_TYPE_BOX);
SPR_setCollisionType(enemy, COLLISION_TYPE_BOX);
if (SPR_checkCollision(hero, enemy)) {
// handle hit
}
// Load palette separately
PAL_setPalette(PAL1, bg_pal.data, CPU);
// Draw IMAGE to Plane B — uploads palette, tileset, and tilemap
VDP_drawImageEx(
BG_B, // plane
&bg_image, // Image*
TILE_ATTR_FULL(PAL1, FALSE, FALSE, FALSE, TILE_USER_INDEX),
0, 0, // tile x, tile y (top-left)
FALSE, // load palette (set TRUE to load palette from image)
TRUE // use CPU transfer (FALSE = DMA)
);
Or at lower level, with a separate tileset:
u16 tileIndex = TILE_USER_INDEX;
VDP_loadTileSet(&bg_tiles, tileIndex, DMA);
// then set the tilemap manually with VDP_setTileMapRect or similar
u16 tileIndex = TILE_USER_INDEX;
VDP_loadTileSet(&fg_tiles, tileIndex, DMA);
Map *map = MAP_create(
&fg_map, // MapDefinition*
BG_A, // plane
TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, tileIndex)
);
// CRITICAL: first MAP_scrollTo fills the entire plane at once (~8 KB of DMA)
// This exceeds the per-frame budget, so call SYS_doVBlankProcess immediately after
MAP_scrollTo(map, 0, 0);
SYS_doVBlankProcess(); // flush the initial fill before entering the game loop
// If you have two maps, flush between them too:
// MAP_scrollTo(mapB, 0, 0);
// SYS_doVBlankProcess();
// In the game loop:
MAP_scrollTo(map, cam_x, cam_y); // pixel coordinates
// When done (level end / transition):
MAP_release(map); // or MEM_free(map) — both free the heap allocation
.tsx.res must exactly match the Tiled layer name (case-sensitive)MAP resource declarationThe MAP engine streams content into a 64×32-tile plane (512×256 px) used as a circular buffer. Your map can be arbitrarily wide or tall — the engine overwrites columns/rows as the camera moves. Never scroll more than ~192 pixels in a single frame (the non-visible portion of the plane buffer), or you'll overwrite tiles before they render.
For maps taller than 256 px, use a 64×64 plane (doubles the tilemap VRAM cost to 8 KB):
VDP_setPlaneSize(64, 64, TRUE);
PAL_setPalette(PAL0, hero_pal.data, CPU); // sprite colors
PAL_setPalette(PAL1, bg_pal.data, CPU); // background colors
PAL_setPalette(PAL2, enemy_pal.data, CPU); // enemy/effects colors
PAL_setPalette(PAL3, hud_pal.data, CPU); // HUD/font
// Fade in all 64 colors over 20 frames (0=first color index, 63=last)
PAL_fadeIn(0, 63, combined_palette, 20, FALSE);
// Convert from 0-255 RGB to Genesis RGB333 format:
u16 vdp_color = RGB24_TO_VDPCOLOR(255, 128, 0);
The golden rule: sprites and tiles share the same 4 palette slots. Plan palette assignments across all assets together — one artist's spreadsheet before writing code saves hours of debugging.
// Callback style (event-driven, preferred):
void on_joy(u16 joy, u16 changed, u16 state) {
if (joy == JOY_1) {
if (changed & BUTTON_B && state & BUTTON_B) {
jump(); // B just pressed this frame
}
if (changed & BUTTON_START && !(state & BUTTON_START)) {
pause(); // START just released
}
}
}
JOY_setEventHandler(on_joy);
// Or polling style:
u16 pad = JOY_readJoypad(JOY_1);
if (pad & BUTTON_RIGHT) move_x += speed;
if (pad & BUTTON_LEFT) move_x -= speed;
if (pad & BUTTON_B) jump();
if (pad & BUTTON_START) pause_game();
Buttons: BUTTON_UP/DOWN/LEFT/RIGHT, BUTTON_A/B/C, BUTTON_START, BUTTON_X/Y/Z/MODE (6-button only)
JOY_update() is called automatically each VBlank — you don't need to call it manually.
The 68000 has no floating-point unit. Float operations compile to slow software routines that easily blow your 60Hz frame budget.
// fix16: range ±512, precision 1/64 (good for positions, velocities)
fix16 pos_x = FIX16(100.5);
fix16 speed = FIX16(2.5);
pos_x = pos_x + speed; // addition/subtraction: use + and - directly
pos_x = F16_mul(pos_x, FIX16(0.9)); // multiplication: MUST use F16_mul
pos_x = F16_div(pos_x, FIX16(2.0)); // division: MUST use F16_div
s16 screen_x = fix16ToInt(pos_x); // convert back to integer for screen coords
// fix32: range ±2M, precision 1/1024 (for large maps or precise physics)
fix32 world_x = FIX32(1234.5);
// Trigonometry
fix16 angle = FIX16(45.0); // in degrees
fix16 sin_a = F16_sin(angle);
fix16 cos_a = F16_cos(angle);
// Square root
fix16 len = F16_sqrt(FIX16(144.0)); // = FIX16(12.0)
Rule of thumb: use fix16 everywhere unless you need values outside ±512 or higher precision, then use fix32. The 68000 handles 16-bit math faster than 32-bit.
#include <genesis.h>
#include "resources.h"
int main(u16 hard) {
// hard=1 means power-on reset; can use to skip intro on soft reset
VDP_setScreenWidth320(); // H40 mode (320×224 px)
SPR_init();
PAL_setPalette(PAL0, hero_pal.data, CPU);
PAL_setPalette(PAL1, bg_pal.data, CPU);
// Load static background
VDP_drawImageEx(BG_B, &bg_image,
TILE_ATTR_FULL(PAL1, FALSE, FALSE, FALSE, TILE_USER_INDEX),
0, 0, FALSE, TRUE);
// Load tileset and create scrolling map
u16 tileIdx = TILE_USER_INDEX + bg_image.tileset->numTile;
VDP_loadTileSet(&fg_tiles, tileIdx, DMA);
Map *map = MAP_create(&fg_map, BG_A,
TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, tileIdx));
MAP_scrollTo(map, 0, 0);
SYS_doVBlankProcess(); // flush initial plane fill
Sprite *hero = SPR_addSprite(&my_hero, 100, 100,
TILE_ATTR(PAL0, TRUE, FALSE, FALSE));
fix16 cam_x = FIX16(0);
fix16 vel_x = FIX16(0);
JOY_setEventHandler(on_joy);
while (TRUE) {
// game logic
cam_x = cam_x + vel_x;
MAP_scrollTo(map, fix16ToInt(cam_x), 0);
SPR_update();
SYS_doVBlankProcess();
}
return 0;
}
Tile index stacking: when loading multiple tilesets, advance tileIdx by the number of tiles in the previous set so they don't overlap in VRAM.
XGM2 drives background music (from VGM files) and PCM sound effects simultaneously via the Z80.
XGM2_play(&bgm_level1); // start music (loops)
XGM2_stop(); // stop music
XGM2_pause(); // pause music
XGM2_resume(); // resume
XGM2_playPCMEx(&sfx_jump, SOUND_PCM_CH2, FALSE); // SFX on channel 2
// Channels: SOUND_PCM_CH1 (reserved by driver), CH2, CH3, CH4
KDebug (emulator console — no-op on hardware):
KDebug_Alert("reached checkpoint");
KDebug_alertNumber(my_value);
KLog("formatted %d", my_value); // like printf to emulator log
KLog_U1(value); // log unsigned 1-byte value
SYS_die("fatal: map null"); // halt with backtrace (debug build)
Recommended emulators:
Debug build: make -f makefile.gen debug — enables KDebug output and disables optimization.
Check DMA usage: DMA_getQueueSize() returns pending words. Watch for values near 7200 (NTSC limit).
Check sprite count: SPR_getUsedSpriteNum() at runtime.
| Symptom | Likely cause | Fix |
|---|---|---|
| Sprites flicker or vanish at specific Y positions | Hit 20-sprite scan-line limit | Check SPR_getUsedSpriteNum(); reduce vertical sprite stacking |
| All sprites vanish when count hits 80 | Hit total sprite limit | Reduce active sprites; cull off-screen sprites |
| Tiles flash or show garbage | VRAM overflow | Count unique tiles; check with sizebnd.jar |
| Screen tears horizontally | DMA budget exceeded | Reduce tile uploads per frame; check DMA_getQueueSize() |
| Colors wrong on background or sprites | Palette slot conflict | Audit which assets use which PAL0–PAL3 slot |
| Game runs at wrong speed or stutters on first frame | Missing SYS_doVBlankProcess() after first MAP_scrollTo | Add it immediately after first scroll call |
| Choppy movement | Using float/double in game loop | Switch to fix16 |
| Crash after many frames | Heap fragmentation | Allocate everything at level load time, not mid-loop |
| Sprites compress too slowly or cause stutters | Using BEST compression on sprite resources | Use FAST or NONE for sprites |
| Tiled objects missing | Used template objects in Tiled | Remove templates; place objects directly |
| Build error on VDP_setPalette | Function doesn't exist | Use PAL_setPalette() instead |
| Task | Function |
|---|---|
| Set 320px screen | VDP_setScreenWidth320() |
| Init sprite engine | SPR_init() / SPR_initEx(n_tiles) |
| Add sprite | SPR_addSprite(&def, x, y, TILE_ATTR(pal, pri, vf, hf)) |
| Move sprite | SPR_setPosition(spr, x, y) |
| Set animation | SPR_setAnim(spr, anim_index) |
| Set anim+frame | SPR_setAnimAndFrame(spr, anim, frame) |
| Flip sprite | SPR_setHFlip(spr, bool) |
| Check collision | SPR_checkCollision(a, b) |
| Update sprites | SPR_update() |
| Load tileset | VDP_loadTileSet(&ts, tileIndex, DMA) |
| Draw static bg | VDP_drawImageEx(plane, &img, attr, tx, ty, loadpal, cpu) |
| Create scroll map | MAP_create(&map, plane, attr) |
| Scroll map | MAP_scrollTo(map, px, py) |
| Free map | MAP_release(map) |
| Load palette | PAL_setPalette(PAL0..3, data, CPU) |
| Fade palette | PAL_fadeIn(first, last, data, steps, async) |
| Read joypad | JOY_readJoypad(JOY_1) |
| Register joy callback | JOY_setEventHandler(fn) |
| End frame | SYS_doVBlankProcess() |
| Play music | XGM2_play(&music) |
| Play SFX | XGM2_playPCMEx(&sfx, SOUND_PCM_CH2, FALSE) |
| fix16 from literal | FIX16(2.5) |
| fix16 to int | fix16ToInt(f) |
| fix16 multiply | F16_mul(a, b) |
| Debug print | KDebug_Alert("msg") / KLog("val %d", n) |
| Check sprite count | SPR_getUsedSpriteNum() |
For specific function signatures, struct fields, or constants, read the headers from the user's SGDK install ($GDK/inc/). Key files:
genesis.h (master include)sprite_eng.hvdp.h, vdp_tile.h, vdp_bg.hmap.hsnd/xgm2.hmaths.h$GDK/bin/rescomp.txt — authoritative syntax for all .res resource types$GDK/sample/ — sonic/ and platformer/ are especially complete