diff --git a/.gitignore b/.gitignore index e16f44a1..66c03068 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ config.js chromedriver.log nightwatch.conf.js /src/docs/toolchains.md -LICENSES/ \ No newline at end of file +LICENSES/ +.vscode diff --git a/presets/gb/animstream.c b/presets/gb/animstream.c new file mode 100644 index 00000000..b48ae37d --- /dev/null +++ b/presets/gb/animstream.c @@ -0,0 +1,118 @@ +/* +Streaming animation: only the current frame lives in VRAM. + +A 32x32 pre-rendered ball (see ballframes.h, made by the book's +tools/prerender.py) has 16 animation frames of 16 tiles each: 256 tiles, +which is more than the sprite half of VRAM holds for *everything*. Games +with lots of big, smooth animation -- Donkey Kong Land, Shantae -- don't +keep every frame in VRAM. They keep only the frame being shown, and copy +the next one in from ROM as it's needed. + +To avoid showing a half-copied frame, sprite tiles are double-buffered: + + tiles 0..15 set A the sprites point at one set while we + tiles 16..31 set B copy the next frame into the other, then flip + +How fast is the copy? On a DMG the CPU copies 256 bytes with the usual +VRAM-safe loop. On a Game Boy Color, one general-purpose DMA (GDMA) +moves it in about one scanline while the CPU waits. copy_start and +copy_end record the scanline (LY) before and after, so you can compare: +run this on the "gb" and "gb.color" platforms and watch them in the +memory view. +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "gb/cgb.h" + +// DMA reads its source on 16-byte boundaries, so pin the frames to an +// aligned address in the upper ROM (this demo's code stays below it) +#define BALL_FRAMES_AT __at(0x7000) +#include "ballframes.h" + +#define SET_A 0 +#define SET_B 16 + +uint8_t copy_start, copy_end; // LY before and after the last copy + +// Copy one frame's 16 tiles into sprite tiles 'dest'..dest+15. +void load_frame(uint8_t frame, uint8_t dest) { + const uint8_t* src = ball_frames[frame]; + copy_start = LY_REG; + if (_cpu == CGB_TYPE) { + uint16_t s = (uint16_t)src; + uint16_t d = 0x8000 + dest * 16; + HDMA1_REG = s >> 8; // source, high/low + HDMA2_REG = s & 0xff; + HDMA3_REG = d >> 8; // destination in VRAM, high/low + HDMA4_REG = d & 0xff; + HDMA5_REG = BALL_TILES - 1; // bit 7 = 0: general-purpose, 16 blocks + // of 16 bytes; the CPU halts until done + } else { + set_sprite_data(dest, BALL_TILES, src); + } + copy_end = LY_REG; +} + +// point the 8 sprites (8x16 mode, 4 across x 2 down) at a tile set +void show_set(uint8_t base) { + uint8_t i; + for (i = 0; i < 8; i++) set_sprite_tile(i, base + i * 2); +} + +void place(uint8_t x, uint8_t y) { + uint8_t i; + for (i = 0; i < 8; i++) { + move_sprite(i, x + (i & 3) * 8, y + (i >> 2) * 16); + } +} + +/*{pal:555,n:4,bpw:16}*/ +const palette_color_t ball_pal[4] = { + 0x0000, 0x4B9F, 0x11DC, 0x088C // RGB(0,0,0), (31,28,18), (28,14,4), (12,4,2) +}; +/*{pal:555,n:4,bpw:16}*/ +const palette_color_t bg_pal[4] = { + 0x7F54, 0x2A8A, 0x1986, 0x0000 // RGB(20,26,31), (10,20,10), (6,12,6), (0,0,0) +}; + +void main(void) { + uint8_t frame = 0, tick = 0, cur = SET_A, x = 20; + int8_t dx = 1; + + DISPLAY_OFF; + BGP_REG = 0xE4; + // sprite colors 1..3 = white, light gray, dark gray (0 is transparent) + OBP0_REG = 0x90; + if (_cpu == CGB_TYPE) { + set_sprite_palette(0, 1, (palette_color_t*)ball_pal); + set_bkg_palette(0, 1, (palette_color_t*)bg_pal); + } + SPRITES_8x16; + load_frame(0, SET_A); + show_set(SET_A); + place(x, 70); + SHOW_BKG; + SHOW_SPRITES; + DISPLAY_ON; + + while (1) { + wait_vbl_done(); + // next frame every other video frame: 30 fps animation. Copy first, + // while we're still in VBlank: GDMA must not run while the PPU is + // drawing (mode 3), because VRAM writes are ignored then. + if (++tick & 1) { + uint8_t back = (cur == SET_A) ? SET_B : SET_A; + frame = (frame + (dx > 0 ? 1 : BALL_FRAMES - 1)) & (BALL_FRAMES - 1); + load_frame(frame, back); // copy into the hidden set... + show_set(back); // ...then flip (OAM updates next VBlank) + cur = back; + } + // roll back and forth + x += dx; + if (x < 10 || x > 130) dx = -dx; + place(x, 70); + } +} diff --git a/presets/gb/aputest.c b/presets/gb/aputest.c new file mode 100644 index 00000000..fdda008c --- /dev/null +++ b/presets/gb/aputest.c @@ -0,0 +1,208 @@ +/* +Generates random sounds in the Game Boy APU, printing the parameters +to the screen. Also shows an asterisk while each channel is +playing, i.e. while its length counter is active (NR52 bits 0-3). + +Port of presets/nes/aputest.c. The Game Boy has two pulse channels +(the first with a frequency sweep), a wave channel and a noise channel. +Only one or two channels are picked at a time so that all of their +parameters fit on the 20x18 tile screen. + +A = new sound, B or START = replay the same sound. +*/ + +#include +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "gbtext.h" + +typedef uint8_t byte; +typedef uint16_t word; + +// channel bits, same as NR52 status bits +#define CH_PULSE1 0x01 +#define CH_PULSE2 0x02 +#define CH_WAVE 0x04 +#define CH_NOISE 0x08 + +typedef struct APUParam { + byte chmask; + const char* name; + word valmask; + word valmin; +} APUParam; + +// parameter indices (order of APU_DEFS) +enum { + P1_FREQ, P1_DUTY, P1_VOL, P1_ENVPER, P1_LEN, P1_SWPPER, P1_SWPDN, P1_SWPSHF, + P2_FREQ, P2_DUTY, P2_VOL, P2_ENVPER, P2_LEN, + W_FREQ, W_LEVEL, W_LEN, + N_SHIFT, N_WIDTH, N_DIV, N_VOL, N_ENVPER, N_LEN, + APU_DEFCOUNT +}; + +const APUParam APU_DEFS[APU_DEFCOUNT] = { + {CH_PULSE1, "Pulse1 Freq", 0x7ff, 0x100 }, + {CH_PULSE1, "Pulse1 Duty", 0x03, 0 }, + {CH_PULSE1, "Pulse1 Vol", 0x0f, 1 }, + {CH_PULSE1, "Pulse1 EnvPer", 0x07, 0 }, + {CH_PULSE1, "Pulse1 Length", 0x3f, 0 }, + {CH_PULSE1, "Pulse1 SwpPer", 0x07, 0 }, + {CH_PULSE1, "Pulse1 SwpDown?",0x01, 0 }, + {CH_PULSE1, "Pulse1 SwpShf", 0x07, 0 }, + {CH_PULSE2, "Pulse2 Freq", 0x7ff, 0x100 }, + {CH_PULSE2, "Pulse2 Duty", 0x03, 0 }, + {CH_PULSE2, "Pulse2 Vol", 0x0f, 1 }, + {CH_PULSE2, "Pulse2 EnvPer", 0x07, 0 }, + {CH_PULSE2, "Pulse2 Length", 0x3f, 0 }, + {CH_WAVE, "Wave Freq", 0x7ff, 0x100 }, + {CH_WAVE, "Wave Level", 0x03, 1 }, + {CH_WAVE, "Wave Length", 0xff, 0 }, + {CH_NOISE, "Noise Shift", 0x0f, 0 }, + {CH_NOISE, "Noise 7-bit?", 0x01, 0 }, + {CH_NOISE, "Noise Divisor", 0x07, 0 }, + {CH_NOISE, "Noise Vol", 0x0f, 1 }, + {CH_NOISE, "Noise EnvPer", 0x07, 0 }, + {CH_NOISE, "Noise Length", 0x3f, 0 }, +}; + +/* 32-sample waveform, two 4-bit samples per byte. */ +const byte wave_pattern[16] = { + 0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef,0xfe,0xdc,0xba,0x98,0x76,0x54,0x32,0x10 +}; + +// 16-bit LCG +static word rnd = 0xCACE; +static word rand16(void) { + rnd = rnd * 17 + 53; + return rnd ^ (rnd >> 7); +} + +byte enmask; +word vals[APU_DEFCOUNT]; + +static byte popcount4(byte x) { + return (x & 1) + ((x >> 1) & 1) + ((x >> 2) & 1) + ((x >> 3) & 1); +} + +void random_sound(void) { + byte i; + // pick one or two channels + do { + enmask = rand16() & 15; + } while (enmask == 0 || popcount4(enmask) > 2); + for (i = 0; i < APU_DEFCOUNT; i++) { + const APUParam* p = &APU_DEFS[i]; + vals[i] = p->valmin + (rand16() & p->valmask); + if (vals[i] > p->valmask && p->valmask < 0x100) vals[i] = p->valmask; + if (vals[i] > 0x7ff) vals[i] = 0x7ff; + } +} + +// write decimal number, right-aligned in a field of 'width' chars +static void put_num(byte x, byte y, word v, byte width) { + char buf[6]; + byte i = width; + memset(buf, ' ', width); + do { + buf[--i] = '0' + (v % 10); + v /= 10; + } while (v && i); + set_bkg_tiles(x, y, width, 1, (uint8_t*)buf); +} + +// only rows of enabled channels are shown, in a compact list; +// row_ch[] remembers which channel each screen row belongs to +byte row_ch[16]; +byte row_count; + +void print_sound(void) { + byte i; + byte y = 0; + // clear the parameter area + for (i = 0; i < 16; i++) put_str(0, i, " "); + row_count = 0; + for (i = 0; i < APU_DEFCOUNT; i++) { + if ((enmask & APU_DEFS[i].chmask) && y < 16) { + put_str(0, y, APU_DEFS[i].name); + put_num(14, y, vals[i], 5); + row_ch[y] = APU_DEFS[i].chmask; + y++; + } + } + row_count = y; +} + +void play_sound(void) { + byte envdown = 0; // envelope always decays (bit 3 = 0) + NR51_REG = enmask | (enmask << 4); // route only the chosen channels + if (enmask & CH_PULSE1) { + NR10_REG = (vals[P1_SWPPER] << 4) | (vals[P1_SWPDN] << 3) | vals[P1_SWPSHF]; + NR11_REG = (vals[P1_DUTY] << 6) | vals[P1_LEN]; + NR12_REG = (vals[P1_VOL] << 4) | envdown | vals[P1_ENVPER]; + NR13_REG = vals[P1_FREQ] & 0xff; + NR14_REG = 0xc0 | (vals[P1_FREQ] >> 8); // trigger + length enable + } + if (enmask & CH_PULSE2) { + NR21_REG = (vals[P2_DUTY] << 6) | vals[P2_LEN]; + NR22_REG = (vals[P2_VOL] << 4) | envdown | vals[P2_ENVPER]; + NR23_REG = vals[P2_FREQ] & 0xff; + NR24_REG = 0xc0 | (vals[P2_FREQ] >> 8); + } + if (enmask & CH_WAVE) { + NR30_REG = 0x80; // wave DAC on + NR31_REG = vals[W_LEN]; + NR32_REG = vals[W_LEVEL] << 5; + NR33_REG = vals[W_FREQ] & 0xff; + NR34_REG = 0xc0 | (vals[W_FREQ] >> 8); + } + if (enmask & CH_NOISE) { + NR41_REG = vals[N_LEN]; + NR42_REG = (vals[N_VOL] << 4) | envdown | vals[N_ENVPER]; + NR43_REG = (vals[N_SHIFT] << 4) | (vals[N_WIDTH] << 3) | vals[N_DIV]; + NR44_REG = 0xc0; + } +} + +void print_status(void) { + byte i; + byte st = NR52_REG; + for (i = 0; i < row_count; i++) { + put_char(19, i, (st & row_ch[i]) ? '*' : ' '); + } +} + +void apu_init(void) { + byte i; + NR52_REG = 0x80; // sound on + NR50_REG = 0x77; // master volume + NR30_REG = 0x00; // wave DAC off while loading wave RAM + for (i = 0; i < 16; i++) _AUD3WAVERAM[i] = wave_pattern[i]; +} + +void main(void) { + byte keys; + DISPLAY_OFF; + BGP_REG = 0xE4; + font_init(); + put_str(0, 17, "A=New B/Start=Replay"); + SHOW_BKG; + DISPLAY_ON; + apu_init(); + random_sound(); + while (1) { + print_sound(); + play_sound(); + // wait for a key, updating the channel status each frame + do { + wait_vbl_done(); + print_status(); + rnd++; // stir the RNG while waiting + keys = joypad(); + while (joypad()) ; + } while (!(keys & (J_A | J_B | J_START))); + if (keys & J_A) random_sound(); + } +} diff --git a/presets/gb/ballframes.h b/presets/gb/ballframes.h new file mode 100644 index 00000000..90f9d990 --- /dev/null +++ b/presets/gb/ballframes.h @@ -0,0 +1,310 @@ +/* + ballframes.h -- generated by tools/prerender.py; do not edit. + + 16 frames of a 32x32 lit, striped sphere, each frame + 16 tiles in 8x16 sprite order (4 sprites across, 2 down). + + raw: 256 tiles = 4096 bytes + unique: 188 tiles = 3008 bytes (no blank tile) +*/ + +#define BALL_FRAMES 16 +#define BALL_TILES 16 + +// define BALL_FRAMES_AT (e.g. __at(0x7000)) before including to pin +// the data to an address -- DMA needs a 16-byte-aligned source +#ifndef BALL_FRAMES_AT +#define BALL_FRAMES_AT +#endif + +/*{w:32,h:32,bpp:1,count:16,brev:1,np:2,pofs:1,sl:4,wpimg:256,remap:[5,6,1,2,3,4,7,0,8,9,10,11]}*/ +const uint8_t BALL_FRAMES_AT ball_frames[BALL_FRAMES][BALL_TILES * 16] = { + { // frame 0 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x02,0x01,0x06,0x03,0x08,0x07,0x10,0x0F, + 0x30,0x0F,0x12,0x2F,0x20,0x1F,0x48,0x3F,0x60,0x1F,0x42,0x3F,0x20,0x5F,0x4A,0x3F, + 0x00,0x00,0x02,0x1F,0x80,0x7F,0x8A,0xFF,0x00,0xFF,0x22,0xFF,0x00,0xFF,0x80,0xFF, + 0x08,0xF7,0x14,0xEB,0x38,0xC7,0x10,0xEF,0x28,0xD7,0x02,0xFF,0x00,0xFF,0xAA,0xFF, + 0x00,0x00,0x40,0xB8,0xAB,0x55,0x50,0xAF,0xEA,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF,0xA8,0x57,0x40,0xBF,0xA0,0x5F,0x00,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0xC0,0x40,0x20,0xE0,0x10,0xF0,0x08,0xF8, + 0x84,0x7C,0x2C,0xFC,0x04,0xFC,0x2E,0xFE,0x04,0xFE,0x2E,0xFE,0x04,0xFE,0xAE,0xFE, + 0x20,0x5F,0x4A,0x3F,0x24,0x5F,0x0A,0x7F,0x31,0x5F,0x0A,0x3F,0x05,0x3F,0x2A,0x3F, + 0x05,0x1F,0x0B,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x55,0xFF,0xAE,0xFF, + 0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFF,0xFF,0x15,0x1F,0x00,0x00, + 0xA8,0x57,0x02,0xFF,0x00,0xFF,0x0A,0xFF,0x81,0x7F,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x15,0xFF,0xAA,0xFF,0x55,0xFF,0xAA,0xFF,0x55,0xFF,0xAB,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAE,0xFE,0x54,0xFE,0xAE,0xFE,0x54,0xFE,0xAC,0xFC,0x54,0xFC,0xAC,0xFC, + 0x50,0xF8,0xB0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 1 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x03,0x02,0x07,0x08,0x07,0x18,0x0F, + 0x20,0x1F,0x02,0x3F,0x20,0x1F,0x68,0x3F,0x40,0x3F,0x62,0x3F,0x00,0x7F,0x6A,0x3F, + 0x00,0x00,0x03,0x1E,0x81,0x7E,0x8B,0xFE,0x01,0xFE,0x23,0xFE,0x03,0xFC,0x81,0xFE, + 0x0B,0xF4,0x17,0xE8,0x3B,0xC4,0x13,0xEC,0x2A,0xD5,0x01,0xFE,0x02,0xFD,0xA9,0xFE, + 0x00,0x00,0x48,0xB8,0xA9,0x57,0x50,0xAF,0xEA,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF,0xA8,0x57,0x40,0xBF,0xA0,0x5F,0x00,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xC0,0x20,0xE0,0x10,0xF0,0x28,0xF8, + 0x94,0x7C,0x3C,0xFC,0x14,0xFC,0x2E,0xFE,0x04,0xFE,0x2E,0xFE,0x04,0xFE,0xAE,0xFE, + 0x00,0x7F,0x6A,0x3F,0x04,0x7F,0x2A,0x7F,0x11,0x7F,0x0A,0x3F,0x15,0x3F,0x2A,0x3F, + 0x05,0x1F,0x0B,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x02,0xFD,0xA8,0xFF,0x46,0xFD,0xA8,0xFF,0x10,0xFF,0xA8,0xFF,0x54,0xFF,0xAC,0xFF, + 0x54,0xFF,0xBA,0xFF,0x55,0xFF,0xFE,0xFF,0x55,0xFF,0xFE,0xFF,0x15,0x1F,0x00,0x00, + 0xA8,0x57,0x02,0xFF,0x00,0xFF,0x0A,0xFF,0x81,0x7F,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x15,0xFF,0xAA,0xFF,0x55,0xFF,0xAA,0xFF,0x55,0xFF,0xAB,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAE,0xFE,0x54,0xFE,0xAE,0xFE,0x54,0xFE,0xBC,0xFC,0x54,0xFC,0xBC,0xFC, + 0x50,0xF8,0xB0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 2 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x02,0x07,0x00,0x0F,0x08,0x1F, + 0x20,0x1F,0x22,0x3F,0x00,0x3F,0x68,0x3F,0x00,0x7F,0x22,0x7F,0x00,0x7F,0x2A,0x7F, + 0x00,0x00,0x03,0x1E,0x83,0x7C,0x89,0xFE,0x07,0xF8,0x25,0xFA,0x07,0xF8,0x85,0xFA, + 0x0F,0xF0,0x1F,0xE0,0x3F,0xC0,0x1F,0xE0,0x2E,0xD1,0x0D,0xF2,0x0A,0xF5,0xA5,0xFA, + 0x00,0x00,0x48,0xB8,0xAD,0x57,0x52,0xAF,0xEB,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF,0xA8,0x57,0x40,0xBF,0xA0,0x5F,0x00,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xC0,0xA0,0xE0,0x50,0xF0,0x28,0xF8, + 0x94,0x7C,0x3C,0xFC,0x14,0xFC,0x2E,0xFE,0x14,0xFE,0x3E,0xFE,0x14,0xFE,0xBE,0xFE, + 0x00,0x7F,0x2A,0x7F,0x44,0x7F,0x2A,0x7F,0x11,0x7F,0x2A,0x3F,0x15,0x3F,0x2A,0x3F, + 0x15,0x1F,0x0B,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x0A,0xF5,0xA4,0xFB,0x4A,0xF5,0xA0,0xFF,0x18,0xF7,0xA0,0xFF,0x50,0xFF,0xA0,0xFF, + 0x50,0xFF,0xBA,0xFF,0x55,0xFF,0xFA,0xFF,0x55,0xFF,0xFE,0xFF,0x15,0x1F,0x00,0x00, + 0xA8,0x57,0x02,0xFF,0x00,0xFF,0x0A,0xFF,0x81,0x7F,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x15,0xFF,0xAA,0xFF,0x55,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xBE,0xFE,0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xBC,0xFC,0x54,0xFC,0xBC,0xFC, + 0x50,0xF8,0xF0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 3 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x02,0x07,0x00,0x0F,0x08,0x1F, + 0x20,0x1F,0x22,0x3F,0x00,0x3F,0x28,0x7F,0x00,0x7F,0x22,0x7F,0x00,0x7F,0x2A,0x7F, + 0x00,0x00,0x01,0x1E,0x07,0xF8,0x85,0xFA,0x0F,0xF0,0x2D,0xF2,0x1F,0xE0,0x95,0xEA, + 0x1F,0xE0,0x1F,0xE0,0x3F,0xC0,0x3F,0xC0,0x3E,0xC1,0x3D,0xC2,0x3A,0xC5,0x95,0xEA, + 0x00,0x00,0x48,0xB8,0xAD,0x57,0x52,0xAF,0xE9,0x17,0x54,0xAB,0xAB,0x55,0x50,0xAF, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x50,0xAF,0xA8,0x57,0x40,0xBF,0xA0,0x5F,0x00,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xC0,0xA0,0xE0,0x50,0xF0,0xA8,0xF8, + 0x54,0xFC,0x3C,0xFC,0x54,0xFC,0x6E,0xFE,0x54,0xFE,0x3E,0xFE,0x54,0xFE,0xFE,0xFE, + 0x00,0x7F,0x2A,0x7F,0x44,0x7F,0x2A,0x7F,0x51,0x7F,0x2A,0x3F,0x15,0x3F,0x2A,0x3F, + 0x15,0x1F,0x0B,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x2A,0xD5,0x94,0xEB,0x6A,0xD5,0x80,0xFF,0x28,0xD7,0x80,0xFF,0x40,0xFF,0xA0,0xFF, + 0x40,0xFF,0xA2,0xFF,0x55,0xFF,0xFA,0xFF,0x55,0xFF,0xFA,0xFF,0x15,0x1F,0x00,0x00, + 0xA8,0x57,0x02,0xFF,0x00,0xFF,0x0A,0xFF,0x81,0x7F,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE,0x54,0xFE,0xFC,0xFC,0x54,0xFC,0xFC,0xFC, + 0x50,0xF8,0xF0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 4 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x02,0x07,0x00,0x0F,0x08,0x1F, + 0x00,0x3F,0x22,0x3F,0x00,0x3F,0x28,0x7F,0x00,0x7F,0x22,0x7F,0x00,0x7F,0x2A,0x7F, + 0x00,0x00,0x05,0x1A,0x0F,0xF0,0x95,0xEA,0x1F,0xE0,0x1D,0xE2,0x3F,0xC0,0xB5,0xCA, + 0x7F,0x80,0x7F,0x80,0x7F,0x80,0x7F,0x80,0xFE,0x01,0xFD,0x02,0xFA,0x05,0x55,0xAA, + 0x00,0x00,0x48,0xB8,0xA5,0x5F,0x52,0xAF,0xE9,0x17,0x56,0xAB,0xA9,0x57,0x52,0xAF, + 0xEB,0x15,0x54,0xAB,0xAB,0x55,0x50,0xAF,0xA8,0x57,0x40,0xBF,0xA0,0x5F,0x00,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xC0,0xA0,0xE0,0x50,0xF0,0xA8,0xF8, + 0x54,0xFC,0xBC,0xFC,0x54,0xFC,0xEE,0xFE,0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE, + 0x00,0x7F,0x2A,0x7F,0x44,0x7F,0x2A,0x7F,0x51,0x7F,0x2A,0x3F,0x15,0x3F,0x2A,0x3F, + 0x15,0x1F,0x0B,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x00,0xFF,0x28,0xD7,0x80,0xFF,0x20,0xDF,0x80,0xFF, + 0x40,0xFF,0xA2,0xFF,0x45,0xFF,0xEA,0xFF,0x55,0xFF,0xFA,0xFF,0x15,0x1F,0x00,0x00, + 0xA8,0x57,0x02,0xFF,0x00,0xFF,0x0A,0xFF,0x81,0x7F,0x2B,0xFF,0x05,0xFF,0xAB,0xFF, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xAF,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE,0x54,0xFE,0xFC,0xFC,0x54,0xFC,0xFC,0xFC, + 0x50,0xF8,0xF0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 5 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x02,0x07,0x00,0x0F,0x08,0x1F, + 0x00,0x3F,0x23,0x3E,0x01,0x3E,0x29,0x7E,0x01,0x7E,0x23,0x7E,0x01,0x7E,0x2B,0x7E, + 0x00,0x00,0x05,0x1A,0x1F,0xE0,0x95,0xEA,0x3F,0xC0,0x5D,0xA2,0x7F,0x80,0x75,0x8A, + 0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFE,0x01,0xFD,0x02,0xFA,0x05,0x55,0xAA, + 0x00,0x00,0x68,0xB8,0xA5,0x5F,0x5A,0xAF,0xE1,0x1F,0x5A,0xAF,0xAD,0x57,0x52,0xAF, + 0xE9,0x17,0x52,0xAF,0xA9,0x57,0x52,0xAF,0xA9,0x57,0x42,0xBF,0xA1,0x5F,0x02,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xC0,0xA0,0xE0,0x50,0xF0,0xA8,0xF8, + 0x54,0xFC,0xBC,0xFC,0x54,0xFC,0xEE,0xFE,0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE, + 0x00,0x7F,0x2B,0x7E,0x44,0x7F,0x2B,0x7E,0x50,0x7F,0x2A,0x3F,0x14,0x3F,0x2A,0x3F, + 0x15,0x1F,0x0B,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x00,0xFF,0xA8,0x57,0x00,0xFF,0x20,0xDF,0x00,0xFF, + 0x00,0xFF,0xA2,0xFF,0x05,0xFF,0xEA,0xFF,0x55,0xFF,0xEA,0xFF,0x15,0x1F,0x00,0x00, + 0xA9,0x57,0x03,0xFF,0x01,0xFF,0x0B,0xFF,0x81,0x7F,0x2B,0xFF,0x05,0xFF,0xAF,0xFF, + 0x15,0xFF,0xAF,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xBF,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE,0x54,0xFE,0xFC,0xFC,0x54,0xFC,0xF8,0xFC, + 0x50,0xF8,0xF0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 6 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x02,0x07,0x01,0x0E,0x09,0x1E, + 0x03,0x3C,0x21,0x3E,0x03,0x3C,0x29,0x7E,0x07,0x78,0x25,0x7A,0x03,0x7C,0x2D,0x7A, + 0x00,0x00,0x05,0x1A,0x1F,0xE0,0x95,0xEA,0x7F,0x80,0xDD,0x22,0xFF,0x00,0x75,0x8A, + 0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFE,0x01,0xFD,0x02,0xFA,0x05,0x55,0xAA, + 0x00,0x00,0x28,0xF8,0x84,0x7F,0x6A,0xBF,0xE1,0x1F,0x4A,0xBF,0xA5,0x5F,0x4A,0xBF, + 0xE1,0x1F,0x5A,0xAF,0xA5,0x5F,0x5A,0xAF,0xA1,0x5F,0x4A,0xBF,0xA5,0x5F,0x0A,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xC0,0xA0,0xE0,0x50,0xF0,0xA8,0xF8, + 0x54,0xFC,0xBC,0xFC,0x54,0xFC,0xEE,0xFE,0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE, + 0x06,0x79,0x2D,0x7A,0x42,0x7D,0x29,0x7E,0x52,0x7D,0x28,0x3F,0x16,0x3D,0x28,0x3F, + 0x14,0x1F,0x0A,0x0F,0x05,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x00,0xFF,0xA8,0x57,0x00,0xFF,0x20,0xDF,0x00,0xFF, + 0x00,0xFF,0x22,0xFF,0x05,0xFF,0xAA,0xFF,0x55,0xFF,0xEA,0xFF,0x15,0x1F,0x00,0x00, + 0xA5,0x5F,0x0B,0xFF,0x05,0xFF,0x0F,0xFF,0x85,0x7F,0x2B,0xFF,0x05,0xFF,0xAF,0xFF, + 0x15,0xFF,0xBF,0xFF,0x55,0xFF,0xBF,0xFF,0x55,0xFF,0xBE,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE,0x54,0xFE,0xFC,0xFC,0x54,0xFC,0xF8,0xFC, + 0x50,0xF8,0xF0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 7 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x03,0x06,0x03,0x0C,0x0B,0x1C, + 0x07,0x38,0x25,0x3A,0x07,0x38,0x25,0x7A,0x0F,0x70,0x25,0x7A,0x0B,0x74,0x25,0x7A, + 0x00,0x00,0x05,0x1A,0x3F,0xC0,0xD5,0xAA,0xFF,0x00,0xDD,0x22,0xFF,0x00,0x75,0x8A, + 0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFE,0x01,0xFD,0x02,0xFA,0x05,0x55,0xAA, + 0x00,0x00,0x28,0xF8,0x84,0x7F,0x2A,0xFF,0x81,0x7F,0x2A,0xFF,0x85,0x7F,0x6A,0xBF, + 0xC1,0x3F,0x6A,0xBF,0x85,0x7F,0x6A,0xBF,0x91,0x7F,0x6A,0xBF,0x85,0x7F,0x2A,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x40,0xC0,0xA0,0xE0,0x40,0xF0,0xA8,0xF8, + 0x54,0xFC,0xB8,0xFC,0x54,0xFC,0xEE,0xFE,0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE, + 0x0E,0x71,0x25,0x7A,0x4A,0x75,0x21,0x7E,0x5A,0x75,0x28,0x3F,0x12,0x3D,0x28,0x3F, + 0x14,0x1F,0x0A,0x0F,0x04,0x07,0x03,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x00,0xFF,0xA8,0x57,0x00,0xFF,0x20,0xDF,0x00,0xFF, + 0x00,0xFF,0x22,0xFF,0x05,0xFF,0xAA,0xFF,0x55,0xFF,0xEA,0xFF,0x15,0x1F,0x00,0x00, + 0x95,0x7F,0x2B,0xFF,0x15,0xFF,0x2F,0xFF,0x95,0x7F,0x3B,0xFF,0x15,0xFF,0xBF,0xFF, + 0x15,0xFF,0xBF,0xFF,0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFE,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBE,0xFE,0x54,0xFE,0xFE,0xFE,0x54,0xFE,0xF8,0xFC,0x54,0xFC,0xF8,0xFC, + 0x50,0xF8,0xE0,0xF0,0x40,0xE0,0xC0,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 8 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x02,0x01,0x06,0x07,0x08,0x0F,0x18, + 0x0F,0x30,0x2D,0x32,0x1F,0x20,0x35,0x6A,0x1F,0x60,0x35,0x6A,0x1B,0x64,0x35,0x6A, + 0x00,0x00,0x15,0x0A,0x7F,0x80,0x55,0xAA,0xFF,0x00,0xDD,0x22,0xFF,0x00,0x75,0x8A, + 0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFE,0x01,0xFD,0x02,0xFA,0x05,0x55,0xAA, + 0x00,0x00,0xA8,0xF8,0x04,0xFF,0xAA,0xFF,0x01,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x01,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0xC0,0xA0,0xE0,0x40,0xF0,0xA8,0xF8, + 0x54,0xFC,0xB8,0xFC,0x54,0xFC,0xEA,0xFE,0x54,0xFE,0xBA,0xFE,0x54,0xFE,0xFA,0xFE, + 0x0E,0x71,0x35,0x6A,0x4A,0x75,0x21,0x7E,0x4A,0x75,0x20,0x3F,0x12,0x3D,0x20,0x3F, + 0x10,0x1F,0x0A,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xEA,0x15,0x54,0xAB,0xAA,0x55,0x00,0xFF,0xA8,0x57,0x00,0xFF,0x20,0xDF,0x00,0xFF, + 0x00,0xFF,0x22,0xFF,0x05,0xFF,0xAA,0xFF,0x55,0xFF,0xAA,0xFF,0x15,0x1F,0x00,0x00, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF, + 0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFE,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBA,0xFE,0x54,0xFE,0xFA,0xFE,0x54,0xFE,0xF8,0xFC,0x54,0xFC,0xF8,0xFC, + 0x50,0xF8,0xE0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 9 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x05,0x02,0x07,0x08,0x07,0x18, + 0x1F,0x20,0x3D,0x22,0x1F,0x20,0x15,0x6A,0x3F,0x40,0x15,0x6A,0x3B,0x44,0x15,0x6A, + 0x00,0x00,0x14,0x0B,0x7E,0x81,0x54,0xAB,0xFE,0x01,0xDC,0x23,0xFC,0x03,0x74,0x8B, + 0xFC,0x03,0xFC,0x03,0xFC,0x03,0xFC,0x03,0xFC,0x03,0xFE,0x03,0xF8,0x07,0x56,0xAB, + 0x00,0x00,0xA0,0xF8,0x06,0xFD,0xAA,0xFF,0x01,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x01,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0xA0,0xE0,0x40,0xF0,0x88,0xF8, + 0x44,0xFC,0xA8,0xFC,0x44,0xFC,0xEA,0xFE,0x54,0xFE,0xBA,0xFE,0x54,0xFE,0xFA,0xFE, + 0x2E,0x51,0x15,0x6A,0x6A,0x55,0x01,0x7E,0x6A,0x55,0x20,0x3F,0x02,0x3D,0x20,0x3F, + 0x10,0x1F,0x0A,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xE9,0x17,0x56,0xAB,0xA9,0x57,0x02,0xFF,0xA9,0x57,0x02,0xFF,0x21,0xDF,0x02,0xFF, + 0x01,0xFF,0x23,0xFF,0x05,0xFF,0xAB,0xFF,0x55,0xFF,0xAB,0xFF,0x15,0x1F,0x00,0x00, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF, + 0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFE,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xBA,0xFE,0x54,0xFE,0xFA,0xFE,0x54,0xFE,0xE8,0xFC,0x54,0xFC,0xE8,0xFC, + 0x50,0xF8,0xE0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 10 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x05,0x02,0x0F,0x00,0x17,0x08, + 0x1F,0x20,0x1D,0x22,0x3F,0x00,0x15,0x6A,0x7F,0x00,0x55,0x2A,0x3B,0x44,0x55,0x2A, + 0x00,0x00,0x14,0x0B,0x7C,0x83,0x56,0xAB,0xF8,0x07,0xDA,0x27,0xF8,0x07,0x70,0x8F, + 0xF8,0x07,0xF4,0x0B,0xF8,0x07,0xF0,0x0F,0xF8,0x07,0xF2,0x0F,0xF0,0x0F,0x5A,0xAF, + 0x00,0x00,0xA0,0xF8,0x02,0xFD,0xA8,0xFF,0x00,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF, + 0x01,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0x20,0xE0,0x00,0xF0,0x88,0xF8, + 0x44,0xFC,0xA8,0xFC,0x44,0xFC,0xEA,0xFE,0x44,0xFE,0xAA,0xFE,0x44,0xFE,0xEA,0xFE, + 0x2E,0x51,0x55,0x2A,0x2A,0x55,0x01,0x7E,0x6A,0x55,0x00,0x3F,0x02,0x3D,0x20,0x3F, + 0x00,0x1F,0x0A,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xE1,0x1F,0x5A,0xAF,0xA5,0x5F,0x0A,0xFF,0xA1,0x5F,0x0A,0xFF,0x25,0xDF,0x0E,0xFF, + 0x05,0xFF,0x23,0xFF,0x05,0xFF,0xAF,0xFF,0x55,0xFF,0xAB,0xFF,0x15,0x1F,0x00,0x00, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF, + 0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xFE,0xFF,0x55,0xFF,0xFA,0xFF,0x50,0xF8,0x00,0x00, + 0x54,0xFE,0xAA,0xFE,0x54,0xFE,0xEA,0xFE,0x54,0xFE,0xE8,0xFC,0x54,0xFC,0xE8,0xFC, + 0x50,0xF8,0xA0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 11 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x05,0x02,0x0F,0x00,0x17,0x08, + 0x1F,0x20,0x1D,0x22,0x3F,0x00,0x55,0x2A,0x7F,0x00,0x55,0x2A,0x3B,0x44,0x55,0x2A, + 0x00,0x00,0x16,0x0B,0xF8,0x07,0x5A,0xAF,0xF0,0x0F,0xD2,0x2F,0xE0,0x1F,0x60,0x9F, + 0xE8,0x17,0xF4,0x0B,0xF8,0x07,0xD0,0x2F,0xE8,0x17,0xC2,0x3F,0xC0,0x3F,0x6A,0xBF, + 0x00,0x00,0xA0,0xF8,0x02,0xFD,0xA8,0xFF,0x02,0xFD,0x2A,0xFF,0x04,0xFF,0xAA,0xFF, + 0x01,0xFF,0x2A,0xFF,0x05,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0x20,0xE0,0x00,0xF0,0x08,0xF8, + 0x84,0x7C,0xA8,0xFC,0x04,0xFC,0xAA,0xFE,0x04,0xFE,0xAA,0xFE,0x04,0xFE,0xAA,0xFE, + 0x2E,0x51,0x55,0x2A,0x2A,0x55,0x01,0x7E,0x2A,0x55,0x00,0x3F,0x02,0x3D,0x20,0x3F, + 0x00,0x1F,0x0A,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0xC1,0x3F,0x6A,0xBF,0x85,0x7F,0x2A,0xFF,0x91,0x7F,0x2A,0xFF,0x35,0xDF,0x0E,0xFF, + 0x15,0xFF,0x3B,0xFF,0x05,0xFF,0xAF,0xFF,0x55,0xFF,0xAF,0xFF,0x15,0x1F,0x00,0x00, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF, + 0x55,0xFF,0xFE,0xFF,0x55,0xFF,0xFE,0xFF,0x55,0xFF,0xFA,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAA,0xFE,0x54,0xFE,0xAA,0xFE,0x54,0xFE,0xA8,0xFC,0x54,0xFC,0xA8,0xFC, + 0x50,0xF8,0xA0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 12 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x05,0x02,0x0F,0x00,0x17,0x08, + 0x3F,0x00,0x1D,0x22,0x3F,0x00,0x55,0x2A,0x7F,0x00,0x55,0x2A,0x3B,0x44,0x55,0x2A, + 0x00,0x00,0x12,0x0F,0xF0,0x0F,0x4A,0xBF,0xE0,0x1F,0xE2,0x3F,0xC0,0x3F,0x40,0xBF, + 0x88,0x77,0x94,0x6B,0xB8,0x47,0x90,0x6F,0x28,0xD7,0x02,0xFF,0x00,0xFF,0xAA,0xFF, + 0x00,0x00,0xA0,0xF8,0x0A,0xF5,0xA8,0xFF,0x02,0xFD,0x28,0xFF,0x06,0xFD,0xA8,0xFF, + 0x00,0xFF,0x2A,0xFF,0x04,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0x20,0xE0,0x00,0xF0,0x08,0xF8, + 0x84,0x7C,0x28,0xFC,0x04,0xFC,0x2A,0xFE,0x04,0xFE,0x2A,0xFE,0x04,0xFE,0xAA,0xFE, + 0x2E,0x51,0x55,0x2A,0x2A,0x55,0x01,0x7E,0x2A,0x55,0x00,0x3F,0x02,0x3D,0x20,0x3F, + 0x00,0x1F,0x0A,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF,0x91,0x7F,0x2A,0xFF,0x55,0xFF,0x2E,0xFF, + 0x15,0xFF,0x3B,0xFF,0x15,0xFF,0xBF,0xFF,0x55,0xFF,0xAF,0xFF,0x15,0x1F,0x00,0x00, + 0x15,0xFF,0xAB,0xFF,0x55,0xFF,0xAF,0xFF,0x55,0xFF,0xBA,0xFF,0x55,0xFF,0xFE,0xFF, + 0x55,0xFF,0xFE,0xFF,0x55,0xFF,0xFA,0xFF,0x55,0xFF,0xFA,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAA,0xFE,0x54,0xFE,0xAA,0xFE,0x54,0xFE,0xA8,0xFC,0x54,0xFC,0xA8,0xFC, + 0x50,0xF8,0xA0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 13 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x05,0x02,0x0F,0x00,0x17,0x08, + 0x3F,0x00,0x1C,0x23,0x3E,0x01,0x54,0x2B,0x7E,0x01,0x54,0x2B,0x3A,0x45,0x54,0x2B, + 0x00,0x00,0x12,0x0F,0xE0,0x1F,0x4A,0xBF,0xC0,0x3F,0xA2,0x7F,0x80,0x7F,0x80,0xFF, + 0x08,0xF7,0x14,0xEB,0x38,0xC7,0x10,0xEF,0x28,0xD7,0x02,0xFF,0x00,0xFF,0xAA,0xFF, + 0x00,0x00,0x80,0xF8,0x0A,0xF5,0xA0,0xFF,0x0A,0xF5,0x24,0xFB,0x02,0xFD,0xA8,0xFF, + 0x02,0xFD,0x2C,0xFB,0x06,0xFD,0xA8,0xFF,0x10,0xFF,0xA8,0xFF,0x44,0xFF,0xA8,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0x20,0xE0,0x00,0xF0,0x08,0xF8, + 0x84,0x7C,0x28,0xFC,0x04,0xFC,0x2A,0xFE,0x04,0xFE,0x2A,0xFE,0x04,0xFE,0xAA,0xFE, + 0x2E,0x51,0x54,0x2B,0x2A,0x55,0x00,0x7F,0x2B,0x55,0x00,0x3F,0x03,0x3D,0x20,0x3F, + 0x00,0x1F,0x0A,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x55,0xFF,0xAE,0xFF, + 0x55,0xFF,0x3B,0xFF,0x55,0xFF,0xBF,0xFF,0x55,0xFF,0xBF,0xFF,0x15,0x1F,0x00,0x00, + 0x14,0xFF,0xAA,0xFF,0x54,0xFF,0xAE,0xFF,0x55,0xFF,0xBA,0xFF,0x55,0xFF,0xFA,0xFF, + 0x55,0xFF,0xFA,0xFF,0x55,0xFF,0xFA,0xFF,0x55,0xFF,0xEA,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAA,0xFE,0x54,0xFE,0xAA,0xFE,0x54,0xFE,0xA8,0xFC,0x54,0xFC,0xAC,0xFC, + 0x50,0xF8,0xA0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 14 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x05,0x02,0x0E,0x01,0x16,0x09, + 0x3C,0x03,0x1E,0x23,0x3C,0x03,0x54,0x2B,0x78,0x07,0x52,0x2F,0x38,0x47,0x52,0x2F, + 0x00,0x00,0x12,0x0F,0xE0,0x1F,0x4A,0xBF,0x80,0x7F,0x22,0xFF,0x00,0xFF,0x80,0xFF, + 0x08,0xF7,0x14,0xEB,0x38,0xC7,0x10,0xEF,0x28,0xD7,0x02,0xFF,0x00,0xFF,0xAA,0xFF, + 0x00,0x00,0xC0,0xB8,0x2B,0xD5,0x90,0xEF,0x0A,0xF5,0x34,0xEB,0x0A,0xF5,0xB0,0xEF, + 0x0A,0xF5,0x24,0xFB,0x0A,0xF5,0xA0,0xFF,0x18,0xF7,0xA0,0xFF,0x40,0xFF,0xA0,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x40,0x20,0xE0,0x00,0xF0,0x08,0xF8, + 0x84,0x7C,0x28,0xFC,0x04,0xFC,0x2A,0xFE,0x04,0xFE,0x2A,0xFE,0x04,0xFE,0xAA,0xFE, + 0x28,0x57,0x52,0x2F,0x2C,0x57,0x02,0x7F,0x29,0x57,0x02,0x3F,0x01,0x3F,0x22,0x3F, + 0x01,0x1F,0x0B,0x0F,0x04,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x55,0xFF,0xAE,0xFF, + 0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xBF,0xFF,0x15,0x1F,0x00,0x00, + 0x18,0xF7,0xA2,0xFF,0x50,0xFF,0xAA,0xFF,0x51,0xFF,0xBA,0xFF,0x55,0xFF,0xFA,0xFF, + 0x55,0xFF,0xEA,0xFF,0x55,0xFF,0xEA,0xFF,0x55,0xFF,0xEB,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAA,0xFE,0x54,0xFE,0xAA,0xFE,0x54,0xFE,0xA8,0xFC,0x54,0xFC,0xAC,0xFC, + 0x50,0xF8,0xA0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, + { // frame 15 + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x04,0x03,0x0C,0x03,0x14,0x0B, + 0x38,0x07,0x1A,0x27,0x38,0x07,0x58,0x2F,0x70,0x0F,0x52,0x2F,0x30,0x4F,0x5A,0x2F, + 0x00,0x00,0x12,0x0F,0xC0,0x3F,0x0A,0xFF,0x00,0xFF,0x22,0xFF,0x00,0xFF,0x80,0xFF, + 0x08,0xF7,0x14,0xEB,0x38,0xC7,0x10,0xEF,0x28,0xD7,0x02,0xFF,0x00,0xFF,0xAA,0xFF, + 0x00,0x00,0xC0,0xB8,0x2B,0xD5,0xD0,0xAF,0x6A,0x95,0x54,0xAB,0x2A,0xD5,0x90,0xEF, + 0x2A,0xD5,0x14,0xEB,0x2A,0xD5,0x90,0xEF,0x28,0xD7,0x80,0xFF,0x60,0xDF,0x80,0xFF, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x40,0x20,0xE0,0x10,0xF0,0x08,0xF8, + 0x84,0x7C,0x2C,0xFC,0x04,0xFC,0x2A,0xFE,0x04,0xFE,0x2A,0xFE,0x04,0xFE,0xAA,0xFE, + 0x20,0x5F,0x5A,0x2F,0x24,0x5F,0x0A,0x7F,0x21,0x5F,0x02,0x3F,0x05,0x3F,0x22,0x3F, + 0x01,0x1F,0x0B,0x0F,0x05,0x07,0x02,0x03,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0xFF,0xAA,0xFF,0x45,0xFF,0xAA,0xFF,0x11,0xFF,0xAA,0xFF,0x55,0xFF,0xAE,0xFF, + 0x55,0xFF,0xBB,0xFF,0x55,0xFF,0xFF,0xFF,0x55,0xFF,0xBF,0xFF,0x15,0x1F,0x00,0x00, + 0x28,0xD7,0x82,0xFF,0x40,0xFF,0x8A,0xFF,0x41,0xFF,0xAA,0xFF,0x45,0xFF,0xEA,0xFF, + 0x55,0xFF,0xEA,0xFF,0x55,0xFF,0xAA,0xFF,0x55,0xFF,0xAB,0xFF,0x50,0xF8,0x00,0x00, + 0x14,0xFE,0xAA,0xFE,0x54,0xFE,0xAA,0xFE,0x54,0xFE,0xAC,0xFC,0x54,0xFC,0xAC,0xFC, + 0x50,0xF8,0xB0,0xF0,0x40,0xE0,0x80,0xC0,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00, + }, +}; diff --git a/presets/gb/detect.c b/presets/gb/detect.c new file mode 100644 index 00000000..c7de9e93 --- /dev/null +++ b/presets/gb/detect.c @@ -0,0 +1,91 @@ +/* +Which Game Boy is this? + +The boot ROM leaves a fingerprint in the CPU registers when it jumps to +the cartridge at $0100: + + A = $01 original Game Boy (DMG) or Super Game Boy + A = $FF Game Boy Pocket / Light, or Super Game Boy 2 + A = $11 Game Boy Color -- or Game Boy Advance, in which case + bit 0 of B is also set + +GBDK's startup code (crt0) saves A in _cpu and, on color hardware, the B +bit in _is_GBA, before anything else can overwrite them. The Super Game +Boy can't be told apart by registers; the game has to send it a command +packet and see whether it answers (sgb_check()). + +What games did with this: +- Shantae (2002) brightens its palettes on a GBA, whose unlit screen made + GBC colors look muddy, and unlocks a bonus transformation there. +- Many DMG games (Donkey Kong Land, Kirby's Dream Land 2) add a border + and colors when they find a Super Game Boy. +- "Dual-mode" cartridges use the color palettes only if _cpu == CGB_TYPE. +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "gb/cgb.h" +#include "gb/sgb.h" +#include "gbtext.h" + +// a normal palette, and a brighter one for the GBA's darker screen. +// Stored as 15-bit CGB words; the RGB() values are in the comments. +/*{pal:555,n:4,bpw:16}*/ +const palette_color_t pal_gbc[4] = { + 0x639C, 0x2214, 0x110C, 0x0842 // RGB(28,28,24), (20,16,8), (12,8,4), (2,2,2) +}; +/*{pal:555,n:4,bpw:16}*/ +const palette_color_t pal_gba[4] = { + 0x73FF, 0x3B1C, 0x21D4, 0x1084 // RGB(31,31,28), (28,24,14), (20,14,8), (4,4,4) +}; + +void put_hex(uint8_t x, uint8_t y, uint8_t v) { + char s[3]; + const char* hex = "0123456789ABCDEF"; + s[0] = hex[v >> 4]; + s[1] = hex[v & 15]; + s[2] = 0; + put_str(x, y, s); +} + +void main(void) { + uint8_t is_sgb; + DISPLAY_OFF; + BGP_REG = 0xE4; + font_init(); + fill_bkg_rect(0, 0, 20, 18, ' '); + SHOW_BKG; + DISPLAY_ON; + + // the SGB needs a few frames after power-on before it listens + wait_vbl_done(); wait_vbl_done(); wait_vbl_done(); wait_vbl_done(); + is_sgb = sgb_check(); + + put_str(2, 1, "WHICH GAME BOY?"); + put_str(2, 4, "_CPU = $"); + put_hex(13, 4, _cpu); + put_str(2, 5, "_IS_GBA = $"); + put_hex(13, 5, _is_GBA); + put_str(2, 6, "SGB = "); + put_str(12, 6, is_sgb ? "YES" : "NO"); + + put_str(2, 9, "RUNNING ON:"); + if (_cpu == CGB_TYPE && _is_GBA == GBA_DETECTED) { + put_str(3, 11, "GAME BOY ADVANCE"); + set_bkg_palette(0, 1, (palette_color_t*)pal_gba); + put_str(3, 12, "(BRIGHT PALETTE)"); + } else if (_cpu == CGB_TYPE) { + put_str(3, 11, "GAME BOY COLOR"); + set_bkg_palette(0, 1, (palette_color_t*)pal_gbc); + } else if (is_sgb) { + put_str(3, 11, "SUPER GAME BOY"); + } else if (_cpu == MGB_TYPE) { + put_str(3, 11, "GAME BOY POCKET"); + } else { + put_str(3, 11, "GAME BOY (DMG)"); + } + + while (1) wait_vbl_done(); +} diff --git a/presets/gb/flicker.c b/presets/gb/flicker.c new file mode 100644 index 00000000..de73feb4 --- /dev/null +++ b/presets/gb/flicker.c @@ -0,0 +1,100 @@ +/* +Flicker, on purpose: transparency and sprite cycling. + +1. Transparency. The Game Boy has no blending hardware, but the original + DMG screen is slow: a pixel takes several frames to fully change. So a + sprite shown on every *other* frame (30 Hz) doesn't blink -- it + smears into a half-tone ghost over the background. Chikyu Kaiho Gun + ZAS (T&E Soft, 1992) used this for see-through clouds and effects, + and Link's Awakening DX (1998) for see-through portraits. On a fast + modern screen or an emulator without frame blending, it just flickers. + +2. Sprite cycling. The PPU draws at most 10 sprites per scanline: the + first 10 in OAM order whose rows overlap it. Here 16 balls share one + row, so 6 would simply vanish. Rotating which ball goes in which OAM + slot every frame spreads the loss around: every ball is visible + 10 frames out of 16, and the eye sees them all, flickering. +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" + +/*{w:8,h:8,bpp:1,count:5,brev:1,np:2,pofs:1,sl:2}*/ +const uint8_t sprite_tiles[] = { + // 0: ball + 0x3C,0x3C, 0x42,0x7E, 0x81,0xFF, 0x85,0xFB, 0x85,0xFB, 0x81,0xFF, 0x42,0x7E, 0x3C,0x3C, + // 1-4: 16-by-16 ghost, solid color 3 (the flicker makes it look gray) + 0x07,0x07, 0x1F,0x1F, 0x3F,0x3F, 0x7F,0x7F, 0x73,0x73, 0xF3,0xF3, 0xFF,0xFF, 0xFF,0xFF, + 0xE0,0xE0, 0xF8,0xF8, 0xFC,0xFC, 0xFE,0xFE, 0xCE,0xCE, 0xCF,0xCF, 0xFF,0xFF, 0xFF,0xFF, + 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xDB,0xDB, 0x89,0x89, + 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xDB,0xDB, 0x91,0x91, +}; + +// background: a checkerboard, so the ghost's transparency is visible +/*{w:8,h:8,bpp:1,count:2,brev:1,np:2,pofs:1,sl:2}*/ +const uint8_t bg_tiles[] = { + 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, + 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, +}; + +#define NBALLS 16 +#define GHOST_OAM 36 // OAM slots 36..39 for the ghost + +uint8_t ball_x[NBALLS]; + +void main(void) { + uint8_t i, x, y, frame = 0, rot = 0; + uint8_t row[32]; + uint8_t gx = 40; + int8_t gdx = 1; + + DISPLAY_OFF; + BGP_REG = 0xE4; + OBP0_REG = 0xE4; + set_bkg_data(0, 2, bg_tiles); + for (y = 0; y < 18; y++) { + for (x = 0; x < 32; x++) row[x] = ((x >> 1) ^ (y >> 1)) & 1; + set_bkg_tiles(0, y, 32, 1, row); + } + set_sprite_data(0, 5, sprite_tiles); + for (i = 0; i < NBALLS; i++) { + ball_x[i] = 12 + i * 9; // 16 balls across 144 pixels + set_sprite_tile(i, 0); + } + set_sprite_tile(GHOST_OAM+0, 1); + set_sprite_tile(GHOST_OAM+1, 2); + set_sprite_tile(GHOST_OAM+2, 3); + set_sprite_tile(GHOST_OAM+3, 4); + SHOW_BKG; + SHOW_SPRITES; + DISPLAY_ON; + + while (1) { + wait_vbl_done(); + frame++; + + // --- sprite cycling: ball (i + rot) goes in OAM slot i --- + rot = (rot + 7) & (NBALLS - 1); // step by 7: coprime with 16, so + // every ball gets every slot + for (i = 0; i < NBALLS; i++) { + uint8_t b = (i + rot) & (NBALLS - 1); + move_sprite(i, ball_x[b], 40); + if (++ball_x[b] >= 12 + 144) ball_x[b] = 12; // wrap on screen + } + + // --- 30 Hz ghost: visible on even frames only --- + gx += gdx; + if (gx < 16 || gx > 150) gdx = -gdx; + if (frame & 1) { + // hide by moving off the top of the screen (Y = 0) + for (i = 0; i < 4; i++) move_sprite(GHOST_OAM + i, 0, 0); + } else { + move_sprite(GHOST_OAM+0, gx, 96); + move_sprite(GHOST_OAM+1, gx + 8, 96); + move_sprite(GHOST_OAM+2, gx, 104); + move_sprite(GHOST_OAM+3, gx + 8, 104); + } + } +} diff --git a/presets/gb/gb3d.h b/presets/gb/gb3d.h new file mode 100644 index 00000000..2bcc3422 --- /dev/null +++ b/presets/gb/gb3d.h @@ -0,0 +1,140 @@ +/* +Shared 3D math for the tile-framebuffer demos (wireframe.c, polyfill.c). + +Everything here avoids the SM83's missing multiply and divide: +- sin_tab: a 256-step sine table, so an angle is one byte +- mul8(): 8x8 signed multiply from a quarter-square table +- recip[]: 24576/z, so perspective is a multiply instead of a divide +- transform(): rotates and projects the 8 corners of a cube +*/ + +#ifndef _GB3D_H +#define _GB3D_H + +#include +#include "tilebuf.h" + +// sin(a) * 127, a = 0..255 is one full turn +const int8_t sin_tab[256] = { + 0,3,6,9,12,16,19,22,25,28,31,34,37,40,43,46, + 49,51,54,57,60,63,65,68,71,73,76,78,81,83,85,88, + 90,92,94,96,98,100,102,104,106,107,109,111,112,113,115,116, + 117,118,120,121,122,122,123,124,125,125,126,126,126,127,127,127, + 127,127,127,127,126,126,126,125,125,124,123,122,122,121,120,118, + 117,116,115,113,112,111,109,107,106,104,102,100,98,96,94,92, + 90,88,85,83,81,78,76,73,71,68,65,63,60,57,54,51, + 49,46,43,40,37,34,31,28,25,22,19,16,12,9,6,3, + 0,-3,-6,-9,-12,-16,-19,-22,-25,-28,-31,-34,-37,-40,-43,-46, + -49,-51,-54,-57,-60,-63,-65,-68,-71,-73,-76,-78,-81,-83,-85,-88, + -90,-92,-94,-96,-98,-100,-102,-104,-106,-107,-109,-111,-112,-113,-115,-116, + -117,-118,-120,-121,-122,-122,-123,-124,-125,-125,-126,-126,-126,-127,-127,-127, + -127,-127,-127,-127,-126,-126,-126,-125,-125,-124,-123,-122,-122,-121,-120,-118, + -117,-116,-115,-113,-112,-111,-109,-107,-106,-104,-102,-100,-98,-96,-94,-92, + -90,-88,-85,-83,-81,-78,-76,-73,-71,-68,-65,-63,-60,-57,-54,-51, + -49,-46,-43,-40,-37,-34,-31,-28,-25,-22,-19,-16,-12,-9,-6,-3, +}; + +// quarter squares: sq4[n+256] = floor(n*n/4), n = -256..255 +const uint16_t sq4[512] = { + 16384,16256,16129,16002,15876,15750,15625,15500,15376,15252,15129,15006, + 14884,14762,14641,14520,14400,14280,14161,14042,13924,13806,13689,13572, + 13456,13340,13225,13110,12996,12882,12769,12656,12544,12432,12321,12210, + 12100,11990,11881,11772,11664,11556,11449,11342,11236,11130,11025,10920, + 10816,10712,10609,10506,10404,10302,10201,10100,10000,9900,9801,9702, + 9604,9506,9409,9312,9216,9120,9025,8930,8836,8742,8649,8556, + 8464,8372,8281,8190,8100,8010,7921,7832,7744,7656,7569,7482, + 7396,7310,7225,7140,7056,6972,6889,6806,6724,6642,6561,6480, + 6400,6320,6241,6162,6084,6006,5929,5852,5776,5700,5625,5550, + 5476,5402,5329,5256,5184,5112,5041,4970,4900,4830,4761,4692, + 4624,4556,4489,4422,4356,4290,4225,4160,4096,4032,3969,3906, + 3844,3782,3721,3660,3600,3540,3481,3422,3364,3306,3249,3192, + 3136,3080,3025,2970,2916,2862,2809,2756,2704,2652,2601,2550, + 2500,2450,2401,2352,2304,2256,2209,2162,2116,2070,2025,1980, + 1936,1892,1849,1806,1764,1722,1681,1640,1600,1560,1521,1482, + 1444,1406,1369,1332,1296,1260,1225,1190,1156,1122,1089,1056, + 1024,992,961,930,900,870,841,812,784,756,729,702, + 676,650,625,600,576,552,529,506,484,462,441,420, + 400,380,361,342,324,306,289,272,256,240,225,210, + 196,182,169,156,144,132,121,110,100,90,81,72, + 64,56,49,42,36,30,25,20,16,12,9,6, + 4,2,1,0,0,0,1,2,4,6,9,12, + 16,20,25,30,36,42,49,56,64,72,81,90, + 100,110,121,132,144,156,169,182,196,210,225,240, + 256,272,289,306,324,342,361,380,400,420,441,462, + 484,506,529,552,576,600,625,650,676,702,729,756, + 784,812,841,870,900,930,961,992,1024,1056,1089,1122, + 1156,1190,1225,1260,1296,1332,1369,1406,1444,1482,1521,1560, + 1600,1640,1681,1722,1764,1806,1849,1892,1936,1980,2025,2070, + 2116,2162,2209,2256,2304,2352,2401,2450,2500,2550,2601,2652, + 2704,2756,2809,2862,2916,2970,3025,3080,3136,3192,3249,3306, + 3364,3422,3481,3540,3600,3660,3721,3782,3844,3906,3969,4032, + 4096,4160,4225,4290,4356,4422,4489,4556,4624,4692,4761,4830, + 4900,4970,5041,5112,5184,5256,5329,5402,5476,5550,5625,5700, + 5776,5852,5929,6006,6084,6162,6241,6320,6400,6480,6561,6642, + 6724,6806,6889,6972,7056,7140,7225,7310,7396,7482,7569,7656, + 7744,7832,7921,8010,8100,8190,8281,8372,8464,8556,8649,8742, + 8836,8930,9025,9120,9216,9312,9409,9506,9604,9702,9801,9900, + 10000,10100,10201,10302,10404,10506,10609,10712,10816,10920,11025,11130, + 11236,11342,11449,11556,11664,11772,11881,11990,12100,12210,12321,12432, + 12544,12656,12769,12882,12996,13110,13225,13340,13456,13572,13689,13806, + 13924,14042,14161,14280,14400,14520,14641,14762,14884,15006,15129,15252, + 15376,15500,15625,15750,15876,16002,16129,16256, +}; + +// Quarter-square multiplication: +// a*b = ((a+b)^2 - (a-b)^2) / 4 +// With a table of floor(n^2/4), an 8x8 signed multiply costs two table +// lookups and a subtraction. (The floors cancel: a+b and a-b are always +// both even or both odd.) +int16_t mul8(int8_t a, int8_t b) { + return sq4[256 + a + b] - sq4[256 + a - b]; +} + +// Perspective divide by table: recip[z] = 24576 / z, built once at boot +// with the slow library divide. At run time x/z becomes x * recip[z]. +uint16_t recip[256]; + +#define ZDIST 160 // distance from eye to cube center +#define CX (TB_W/2) +#define CY (TB_H/2) + +// cube corners +const int8_t cube[8][3] = { + {-32,-32,-32}, { 32,-32,-32}, { 32, 32,-32}, {-32, 32,-32}, + {-32,-32, 32}, { 32,-32, 32}, { 32, 32, 32}, {-32, 32, 32}, +}; +// 12 edges as pairs of corner indices +const uint8_t edges[12][2] = { + {0,1},{1,2},{2,3},{3,0}, {4,5},{5,6},{6,7},{7,4}, {0,4},{1,5},{2,6},{3,7}, +}; + +uint8_t sx[8], sy[8]; // projected corners + +void transform(uint8_t ay, uint8_t ax) { + int8_t sa = sin_tab[ay], ca = sin_tab[(uint8_t)(ay + 64)]; + int8_t sb = sin_tab[ax], cb = sin_tab[(uint8_t)(ax + 64)]; + uint8_t i; + for (i = 0; i < 8; i++) { + int8_t x = cube[i][0], y = cube[i][1], z = cube[i][2]; + int8_t x1, z1, y2, z2; + uint8_t zz; + // rotate around Y (results scaled back down by 127 ~ >>7) + x1 = (mul8(x, ca) - mul8(z, sa)) >> 7; + z1 = (mul8(x, sa) + mul8(z, ca)) >> 7; + // rotate around X + y2 = (mul8(y, cb) - mul8(z1, sb)) >> 7; + z2 = (mul8(y, sb) + mul8(z1, cb)) >> 7; + // project: screen = center + x * (96 / z) + zz = ZDIST + z2; + sx[i] = CX + (int8_t)(((int16_t)x1 * (int16_t)recip[zz]) >> 8); + sy[i] = CY + (int8_t)(((int16_t)y2 * (int16_t)recip[zz]) >> 8); + } +} + +// call once at startup: fill recip[] using the (slow) library divide +void gb3d_init(void) { + uint16_t z; + for (z = 1; z < 256; z++) recip[z] = 24576U / z; +} + +#endif diff --git a/presets/gb/parallax.c b/presets/gb/parallax.c new file mode 100644 index 00000000..0a07104a --- /dev/null +++ b/presets/gb/parallax.c @@ -0,0 +1,170 @@ +/* +Parallax scrolling with one background layer. + +The Game Boy has one scrolling background (plus the non-scrolling-ish +Window), so there is no second "far" layer to scroll at a different +speed. Games fake one in two ways, and this demo does both: + +1. Raster splits. The screen is cut into horizontal bands. An LY==LYC + interrupt fires just before each band starts and rewrites SCX, so + each band scrolls at its own speed: clouds at 1/8, mountains at 1/4, + trees at 1/2, the ground at full speed. + +2. Tile animation. The water band at the bottom never scrolls (SCX = 0 + there), but its one tile's pixels are rotated a bit each frame, so the + whole band appears to move. This works anywhere -- even in the middle + of a band scrolling at another speed -- because every copy of a tile + on screen changes at once. The cost: the rotated tile can't be used + anywhere else. + +Chikyu Kaiho Gun ZAS (T&E Soft, 1992) and Shantae (WayForward, 2002) are +both known for parallax on this hardware; this demo shows the general +technique, not their code. +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" + +// --------------------------------------------------------------------- +// tiles: each row is two bytes, low bit-plane then high bit-plane + +#define T_SKY 0 +#define T_CLOUDL 1 +#define T_CLOUD 2 +#define T_CLOUDR 3 +#define T_MTNL 4 +#define T_MTNR 5 +#define T_MTN 6 +#define T_TREE 7 +#define T_TRUNK 8 +#define T_GRASS 9 +#define T_DIRT 10 +#define T_WATER 11 + +/*{w:8,h:8,bpp:1,count:12,brev:1,np:2,pofs:1,sl:2}*/ +const uint8_t tiles[] = { + // T_SKY: color 0 + 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, + // T_CLOUDL + 0x00,0x00, 0x07,0x00, 0x1F,0x00, 0x3F,0x00, 0x7F,0x00, 0xFF,0x00, 0x7F,0x00, 0x00,0x00, + // T_CLOUD + 0x3C,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0x00,0x00, + // T_CLOUDR + 0x00,0x00, 0xE0,0x00, 0xF8,0x00, 0xFC,0x00, 0xFE,0x00, 0xFF,0x00, 0xFE,0x00, 0x00,0x00, + // T_MTNL: '/' edge, color 2 below + 0x00,0x01, 0x00,0x03, 0x00,0x07, 0x00,0x0F, 0x00,0x1F, 0x00,0x3F, 0x00,0x7F, 0x00,0xFF, + // T_MTNR: '\' edge + 0x00,0x80, 0x00,0xC0, 0x00,0xE0, 0x00,0xF0, 0x00,0xF8, 0x00,0xFC, 0x00,0xFE, 0x00,0xFF, + // T_MTN: solid color 2, a little snow-line texture + 0x00,0xFF, 0x00,0xFF, 0x22,0xFF, 0x00,0xFF, 0x00,0xFF, 0x88,0xFF, 0x00,0xFF, 0x00,0xFF, + // T_TREE: round top, color 3 + 0x3C,0x3C, 0x7E,0x7E, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0x7E,0x7E, 0x3C,0x3C, + // T_TRUNK + 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x3C,0x3C, 0xFF,0xFF, + // T_GRASS: color 1 with blades of color 3 + 0x00,0x00, 0x44,0x44, 0xEE,0xEE, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, + // T_DIRT: color 2 with color 3 pebbles + 0x00,0xFF, 0x40,0xFF, 0x00,0xFF, 0x04,0xFF, 0x00,0xFF, 0x20,0xFF, 0x00,0xFF, 0x02,0xFF, + // T_WATER: waves (animated by rotating each row's bits) + 0x00,0xFF, 0x00,0xFF, 0xC3,0x3C, 0xFF,0x00, 0x00,0xFF, 0x00,0xFF, 0x3C,0xC3, 0xFF,0x00, +}; + +// --------------------------------------------------------------------- +// the map: an 8-tile-wide pattern repeated across all 32 columns, so the +// 256-pixel background wraps seamlessly as SCX rolls over + +const uint8_t pattern[18][8] = { + { T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_SKY }, + { T_SKY,T_CLOUDL,T_CLOUD,T_CLOUDR,T_SKY,T_SKY,T_SKY,T_SKY }, + { T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_CLOUDL,T_CLOUDR,T_SKY }, + { T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_SKY,T_SKY }, + { T_SKY,T_SKY,T_SKY,T_MTNL,T_MTNR,T_SKY,T_SKY,T_SKY }, + { T_SKY,T_SKY,T_MTNL,T_MTN,T_MTN,T_MTNR,T_SKY,T_SKY }, + { T_SKY,T_MTNL,T_MTN,T_MTN,T_MTN,T_MTN,T_MTNR,T_SKY }, + { T_MTNL,T_MTN,T_MTN,T_MTN,T_MTN,T_MTN,T_MTN,T_MTNR }, + { T_SKY,T_TREE,T_SKY,T_SKY,T_TREE,T_SKY,T_TREE,T_SKY }, + { T_SKY,T_TRUNK,T_SKY,T_SKY,T_TRUNK,T_SKY,T_TRUNK,T_SKY }, + { T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS }, + { T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT }, + { T_TREE,T_SKY,T_SKY,T_TREE,T_SKY,T_SKY,T_SKY,T_SKY }, + { T_TRUNK,T_SKY,T_SKY,T_TRUNK,T_SKY,T_SKY,T_SKY,T_SKY }, + { T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS,T_GRASS }, + { T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT,T_DIRT }, + { T_WATER,T_WATER,T_WATER,T_WATER,T_WATER,T_WATER,T_WATER,T_WATER }, + { T_WATER,T_WATER,T_WATER,T_WATER,T_WATER,T_WATER,T_WATER,T_WATER }, +}; + +// --------------------------------------------------------------------- +// raster bands + +#define NBANDS 5 +// LYC value for each band: the line *before* the band starts, so the +// handler has time to wait for HBlank and write SCX before it's drawn +const uint8_t band_lyc[NBANDS] = { 0, 31, 63, 95, 127 }; +uint8_t band_scx[NBANDS]; // SCX for each band, set by main() +volatile uint8_t band; // band the raster is in + +void vbl_isr(void) { + // top of the frame: band 0 and arm the first split + band = 0; + SCX_REG = band_scx[0]; + LYC_REG = band_lyc[1]; +} + +void lcd_isr(void) { + // we're at the start of the line before the next band; wait for + // HBlank so the change doesn't land in the middle of a visible line + while (STAT_REG & 3) ; + band++; + SCX_REG = band_scx[band]; + // arm the next split, or park LYC below the screen + LYC_REG = (band < NBANDS-1) ? band_lyc[band+1] : 200; +} + +void main(void) { + uint8_t x, y, i; + uint16_t pos = 0; + uint8_t water[16]; + + DISPLAY_OFF; + BGP_REG = 0xE4; + set_bkg_data(0, sizeof(tiles)/16, tiles); + for (y = 0; y < 18; y++) { + for (x = 0; x < 32; x += 8) { + set_bkg_tiles(x, y, 8, 1, pattern[y]); + } + } + for (i = 0; i < 16; i++) water[i] = tiles[T_WATER*16 + i]; + + STAT_REG = STATF_LYC; + LYC_REG = band_lyc[1]; + disable_interrupts(); + add_VBL(vbl_isr); + add_LCD(lcd_isr); + enable_interrupts(); + set_interrupts(VBL_IFLAG | LCD_IFLAG); + SHOW_BKG; + DISPLAY_ON; + + while (1) { + wait_vbl_done(); + // 1. new scroll positions for next frame + pos++; + band_scx[0] = pos >> 3; // clouds + band_scx[1] = pos >> 2; // mountains + band_scx[2] = pos >> 1; // far trees + band_scx[3] = pos; // near ground + band_scx[4] = 0; // water: fixed, animated below + // 2. animate the water tile: rotate every row one pixel left + // (every other frame, so it moves at half speed) + if (pos & 1) { + for (i = 0; i < 16; i++) { + uint8_t b = water[i]; + water[i] = (b << 1) | (b >> 7); + } + set_bkg_data(T_WATER, 1, water); + } + } +} diff --git a/presets/gb/polyfill.c b/presets/gb/polyfill.c new file mode 100644 index 00000000..548bc69b --- /dev/null +++ b/presets/gb/polyfill.c @@ -0,0 +1,117 @@ +/* +Filled, dithered polygons in a tile framebuffer, in the style of +Race Drivin' (Argonaut, 1993). + +The same spinning cube as wireframe.c, but solid: + +1. back-face culling: a face whose corners appear clockwise on screen is + facing away, so it is skipped -- on a convex object that is all the + hidden-surface removal we need +2. each visible face is scan-converted into a table of left/right X + coordinates, one pair per pixel row +3. each row is filled a *byte* at a time: a masked partial byte at each + end, and plain writes for the 8-pixel runs in between +4. shading: 4 gray levels plus checkerboard dithers between neighbors + give 7 shades; the more a face points at the viewer, the lighter it is +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "tilebuf.h" +#include "gb3d.h" + +// the six faces, corners listed in the same winding order +const uint8_t faces[6][4] = { + {0,1,2,3}, {5,4,7,6}, {4,0,3,7}, {1,5,6,2}, {4,5,1,0}, {3,2,6,7}, +}; + +// --------------------------------------------------------------------- +// scan conversion + +uint8_t xleft[TB_H], xright[TB_H]; +uint8_t ytop, ybot; + +// Walk one edge with an integer error term (no divide), recording the +// X at every row into both the left and right tables: the smaller X +// ends up in xleft[], the larger in xright[]. +void edge(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { + uint8_t dx, dy, y, x; + int8_t xdir; + int16_t err; + if (y0 > y1) { + y = x0; x0 = x1; x1 = y; + y = y0; y0 = y1; y1 = y; + } + if (y0 < ytop) ytop = y0; + if (y1 > ybot) ybot = y1; + dy = y1 - y0; + if (x1 >= x0) { dx = x1 - x0; xdir = 1; } + else { dx = x0 - x1; xdir = -1; } + x = x0; + err = dy >> 1; + for (y = y0; ; y++) { + if (x < xleft[y]) xleft[y] = x; + if (x > xright[y]) xright[y] = x; + if (y == y1) break; + // advance X until the error term says it's time for the next row + err -= dx; + while (err < 0) { + err += dy; + x += xdir; + if (x < xleft[y]) xleft[y] = x; + if (x > xright[y]) xright[y] = x; + } + } +} + +void fill_face(const uint8_t* f, const Shade* s) { + uint8_t y; + ytop = TB_H - 1; ybot = 0; + for (y = 0; y < TB_H; y++) { xleft[y] = 255; xright[y] = 0; } + edge(sx[f[0]], sy[f[0]], sx[f[1]], sy[f[1]]); + edge(sx[f[1]], sy[f[1]], sx[f[2]], sy[f[2]]); + edge(sx[f[2]], sy[f[2]], sx[f[3]], sy[f[3]]); + edge(sx[f[3]], sy[f[3]], sx[f[0]], sy[f[0]]); + for (y = ytop; y <= ybot; y++) { + span(y, xleft[y], xright[y], s); + } +} + +// Twice the signed screen area of triangle (a,b,c): positive when the +// corners run counter-clockwise, i.e. the face points toward us. Its +// size also says how squarely the face points at us, which makes a +// free light source "behind the camera". +int16_t facing(const uint8_t* f) { + int16_t ax = sx[f[1]] - sx[f[0]], ay = sy[f[1]] - sy[f[0]]; + int16_t bx = sx[f[2]] - sx[f[0]], by = sy[f[2]] - sy[f[0]]; + return ax * by - ay * bx; +} + +void main(void) { + uint8_t ay = 0, ax = 0, i, y; + DISPLAY_OFF; + BGP_REG = 0xE4; + gb3d_init(); + tb_init(); + DISPLAY_ON; + while (1) { + // ground: dithered lower half, like a road-racer horizon + for (y = TB_H/2 + 20; y < TB_H; y++) span(y, 0, TB_W - 1, &shades[1]); + transform(ay, ax); + for (i = 0; i < 6; i++) { + int16_t area = facing(faces[i]); + if (area > 0) { + // bigger area = facing us more squarely = lighter shade + // (faces use shades 3..6 so they never match the ground's shade 1) + int16_t d = area / 600; + uint8_t shade = (d >= 3) ? 3 : 6 - d; + fill_face(faces[i], &shades[shade]); + } + } + tb_blit(); + ay += 3; + ax += 2; + } +} diff --git a/presets/gb/raster.c b/presets/gb/raster.c index 2bfd3bc3..134826ea 100644 --- a/presets/gb/raster.c +++ b/presets/gb/raster.c @@ -42,7 +42,8 @@ void lcd_handler(void) NONBANKED { uint8_t ly = LY_REG; if (ly < 143) { // offset this line and arm the interrupt for the next one - SCX_REG = 248 + (int8_t)wave[(ly + phase) & 31]; + // (ly advances 8 lines per interrupt, so ly >> 3 steps one table entry) + SCX_REG = 248 + (int8_t)wave[((ly >> 3) + phase) & 31]; LYC_REG = ly + 8; } else { // last line: reset so the next frame starts at the top diff --git a/presets/gb/raycast.c b/presets/gb/raycast.c new file mode 100644 index 00000000..2410f20c --- /dev/null +++ b/presets/gb/raycast.c @@ -0,0 +1,265 @@ +/* +A grid raycaster in a tile framebuffer, in the spirit of Faceball 2000 +(Xanth Software F/X, 1991) and Wolfenstein 3D. + +The world is a 16x16 grid of cells. For each 4-pixel-wide column of the +144x112 viewport we cast one ray from the player through the grid, one +cell boundary at a time (the "DDA" walk), until it hits a wall. The +distance to the wall sets the height of that column's wall slice. + +Why this suits the Game Boy: +- only 36 rays per frame, and each ray visits a few cells +- walls are vertical spans centered on the horizon, so every pixel is + written exactly once -- no sorting, no overdraw, no buffer clear needed +- flat shading: walls facing east/west get one shade, north/south + another -- the same cheap trick Faceball and Wolfenstein use + +Controls: left/right to turn, up/down to move. With no input, the +player slowly turns in place. +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "tilebuf.h" +#include "gb3d.h" + +#define NCOLS (TB_W/4) // 36 rays, 4 pixels wide each + +const uint8_t map[16][16] = { + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,1,1,0,0,0,0,0,1,1,0,0,0,1}, + {1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1}, + {1,0,0,1,1,0,0,0,0,0,1,1,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, +}; + +// player position in 8.8 fixed point (high byte = map cell) +uint16_t px = 0x0880, py = 0x0880; +uint8_t pa = 0; // facing angle, 256 = full turn + +// camera-plane offset for each column, -80..+80 (a ~64 degree view) +int8_t camx[NCOLS]; + +// inv[r] = 32512 / r: the distance along a ray between two grid lines, +// in 8.8 cells, for a ray whose X (or Y) component is r/127 +uint16_t inv[256]; + +// wall half-height for each distance: htab[d >> 3] = (TB_H/2) / d, +// with d in 8.8 cells -- a table instead of a divide per column +uint8_t htab[512]; + +// wall slice for each column, computed by cast() +uint8_t wall_h[NCOLS]; // half-height, 0..TB_H/2 +uint8_t wall_side[NCOLS]; // 0 = east/west face, 1 = north/south face + +// distance to the first grid line = fraction of a cell * ddx. +// ddx is 8.8 and frac is 0..256, so pre-shift both to stay in 16 bits. +uint16_t first_side(uint16_t frac, uint16_t dd) { + if (dd >= 0x1000) return 0xffff; // ray nearly parallel: never + return ((dd >> 4) * frac) >> 4; +} + +void cast(void) { + int8_t dirx = sin_tab[(uint8_t)(pa + 64)]; // cos(pa) + int8_t diry = sin_tab[pa]; // sin(pa) + uint8_t col; + for (col = 0; col < NCOLS; col++) { + // ray direction = view direction + camera plane * camx + int16_t rx = dirx - (mul8(diry, camx[col]) >> 7); + int16_t ry = diry + (mul8(dirx, camx[col]) >> 7); + uint8_t mx = px >> 8, my = py >> 8; + uint8_t arx = rx < 0 ? -rx : rx, ary = ry < 0 ? -ry : ry; + uint16_t ddx = arx ? inv[arx] : 0xffff; + uint16_t ddy = ary ? inv[ary] : 0xffff; + uint16_t sdx, sdy, dist; + int8_t stepx, stepy; + uint8_t side; + // distance to the first vertical and horizontal grid lines + if (rx < 0) { stepx = -1; sdx = first_side(px & 0xff, ddx); } + else { stepx = 1; sdx = first_side(256 - (px & 0xff), ddx); } + if (ry < 0) { stepy = -1; sdy = first_side(py & 0xff, ddy); } + else { stepy = 1; sdy = first_side(256 - (py & 0xff), ddy); } + // DDA: step to whichever grid line is nearer, until we hit a wall + while (1) { + if (sdx < sdy) { sdx += ddx; mx += stepx; side = 0; } + else { sdy += ddy; my += stepy; side = 1; } + if (map[my][mx]) break; + } + // perpendicular distance (no fish-eye) in 8.8 cells + dist = side ? sdy - ddy : sdx - ddx; + dist >>= 3; + wall_h[col] = dist < 512 ? htab[dist] : 0; + wall_side[col] = side; + } +} + +// --------------------------------------------------------------------- +// drawing +// +// Two 4-pixel columns share each byte, so drawing one column at a time +// means a read-modify-write for every row -- far too slow in C. Instead +// we draw columns in *pairs*, top to bottom, writing whole bytes. Because +// walls are centered on the horizon, each pair splits into at most five +// bands where the byte pattern doesn't change: +// +// ceiling | taller wall + ceiling | both walls | taller wall + floor | floor +// +// so we compute one pattern per band and blast it down the rows. + +uint8_t *g_p; // current framebuffer byte +uint8_t g_y; // current row +uint8_t g_n; // rows to fill +uint8_t ple, phe, plo, pho; // pattern: even-row lo/hi, odd-row lo/hi + +// Write g_n rows of the current pattern, starting at g_p / row g_y. +// +// This is the innermost loop of the renderer (18 pairs x 112 rows = +// 2016 rows per frame), so it's in assembly: about 20 cycles per row, +// versus about 120 for the same loop compiled from C. +void fill_rows(void) __naked { +__asm + ld a, (_g_n) + or a, a + ret z + ld b, a ; B = row count + ld a, (_g_p) + ld l, a + ld a, (_g_p+1) + ld h, a ; HL = framebuffer pointer + ld a, (_g_y) + ld c, a ; C = row number + ld a, (_ple) + ld d, a + ld a, (_phe) + ld e, a ; DE = even-row pattern +1$: + bit 0, c + jr nz, 2$ + ld a, d ; even row + ld (hl+), a + ld a, e + ld (hl+), a + jr 3$ +2$: + ld a, (_plo) ; odd row + ld (hl+), a + ld a, (_pho) + ld (hl+), a +3$: + ld a, c + inc c + and a, #7 + cp a, #7 + jr nz, 4$ + ; last row of a tile: skip to the next row of tiles + ; (+288 bytes per tile row, minus the 16 we walked through) + ld a, l + add a, #<(288-16) + ld l, a + ld a, h + adc a, #>(288-16) + ld h, a +4$: + dec b + jr nz, 1$ + ld a, l + ld (_g_p), a + ld a, h + ld (_g_p+1), a + ld a, c + ld (_g_y), a + ret +__endasm; +} + +#define CEIL 0 // shades[] index for ceiling +#define FLOOR 1 // shades[] index for floor +#define WALL_EW 6 // east/west walls +#define WALL_NS 4 // north/south walls + +// pattern bytes for a left/right nibble pair of two shades +void pair_pattern(uint8_t sl, uint8_t sr) { + const Shade* a = &shades[sl]; + const Shade* b = &shades[sr]; + ple = (a->lo0 & 0xf0) | (b->lo0 & 0x0f); + phe = (a->hi0 & 0xf0) | (b->hi0 & 0x0f); + plo = (a->lo1 & 0xf0) | (b->lo1 & 0x0f); + pho = (a->hi1 & 0xf0) | (b->hi1 & 0x0f); +} + +void draw_pair(uint8_t pair) { + uint8_t c = pair << 1; + uint8_t hl = wall_h[c], hr = wall_h[c+1]; + uint8_t wl = wall_side[c] ? WALL_NS : WALL_EW; + uint8_t wr = wall_side[c+1] ? WALL_NS : WALL_EW; + uint8_t hs = hl < hr ? hl : hr; // shorter wall + uint8_t ht = hl < hr ? hr : hl; // taller wall + g_p = tb_buf + (pair << 4); + g_y = 0; + // 1. ceiling over both + pair_pattern(CEIL, CEIL); + g_n = TB_H/2 - ht; fill_rows(); + // 2. taller wall beside ceiling + if (hl > hr) pair_pattern(wl, CEIL); else pair_pattern(CEIL, wr); + g_n = ht - hs; fill_rows(); + // 3. both walls, across the horizon + pair_pattern(wl, wr); + g_n = hs * 2; fill_rows(); + // 4. taller wall beside floor + if (hl > hr) pair_pattern(wl, FLOOR); else pair_pattern(FLOOR, wr); + g_n = ht - hs; fill_rows(); + // 5. floor under both + pair_pattern(FLOOR, FLOOR); + g_n = TB_H/2 - ht; fill_rows(); +} + +void render(void) { + uint8_t pair; + for (pair = 0; pair < NCOLS/2; pair++) draw_pair(pair); +} + +void move(int8_t speed) { + // move along the facing direction, if the destination cell is open + uint16_t nx = px + (((int16_t)sin_tab[(uint8_t)(pa + 64)] * speed) >> 5); + uint16_t ny = py + (((int16_t)sin_tab[pa] * speed) >> 5); + if (!map[ny >> 8][nx >> 8]) { px = nx; py = ny; } +} + +void main(void) { + uint8_t i; + uint16_t d; + DISPLAY_OFF; + BGP_REG = 0xE4; + for (i = 1; i != 0; i++) inv[i] = 32512U / i; + for (d = 0; d < 512; d++) { + uint16_t h = d ? (TB_H/2 * 32U) / d : 255; // (TB_H/2) / (d*8/256) + htab[d] = h > TB_H/2 ? TB_H/2 : h; + } + for (i = 0; i < NCOLS; i++) camx[i] = (int8_t)(((int16_t)i * 160) / (NCOLS - 1) - 80); + tb_init(); + DISPLAY_ON; + while (1) { + uint8_t j = joypad(); + if (j & J_LEFT) pa -= 4; + else if (j & J_RIGHT) pa += 4; + else if (!j) pa += 2; + if (j & J_UP) move(4); + if (j & J_DOWN) move(-4); + cast(); + render(); + tb_blit(); + } +} diff --git a/presets/gb/savegame.c b/presets/gb/savegame.c new file mode 100644 index 00000000..64a4cdec --- /dev/null +++ b/presets/gb/savegame.c @@ -0,0 +1,156 @@ +/* +Battery-backed save RAM for Game Boy. + +The cartridge header declares MBC5 + RAM + battery, with 8 KB of RAM +mapped at 0xA000-0xBFFF. The RAM is switched off at power-up; we +write 0x0A to 0x0000 to enable it and anything else to disable it. + +The save is a small record with a signature and a checksum, so that +a fresh cartridge (or one whose battery has died) is not mistaken +for a real save: + + 0xA000 "GBSV" signature + 0xA004 boots how many times the game has been started + 0xA006 best best score + 0xA008 check sum of the bytes above, plus 0x5A + +Controls: A adds a point, Start saves the score if it's a new best, +B erases the save. Reset the emulator (or switch a real console off +and on) and the numbers come back. +*/ + +//#symbol ld GB_MAPPER=0x1B +//#symbol ld GB_RAM_SIZE=0x02 + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "gbtext.h" + +typedef struct { + char magic[4]; + uint16_t boots; + uint16_t best; + uint8_t check; +} Save; + +#define SAVE ((volatile Save*)0xA000) + +#define ENABLE_SAVE_RAM (*(volatile uint8_t*)0x0000 = 0x0A) +#define DISABLE_SAVE_RAM (*(volatile uint8_t*)0x0000 = 0x00) + +// sum of every byte of the record except the checksum itself +static uint8_t save_sum(void) { + volatile uint8_t* p = (volatile uint8_t*)SAVE; + uint8_t i, sum = 0x5A; + for (i = 0; i < sizeof(Save) - 1; i++) sum += p[i]; + return sum; +} + +static uint8_t save_valid(void) { + return SAVE->magic[0] == 'G' && SAVE->magic[1] == 'B' && + SAVE->magic[2] == 'S' && SAVE->magic[3] == 'V' && + SAVE->check == save_sum(); +} + +// RAM must be enabled by the caller +static void save_reset(void) { + SAVE->magic[0] = 'G'; SAVE->magic[1] = 'B'; + SAVE->magic[2] = 'S'; SAVE->magic[3] = 'V'; + SAVE->boots = 0; + SAVE->best = 0; + SAVE->check = save_sum(); +} + +// format a number as five digits (the division is slow, so +// we do this while the screen is drawing, not during VBlank) +static void fmt_num(char* buf, uint16_t v) { + uint8_t i; + for (i = 5; i > 0; i--) { + buf[i - 1] = '0' + v % 10; + v /= 10; + } + buf[5] = '\0'; +} + +void main(void) { + char bootbuf[6], bestbuf[6], scorebuf[6]; + const char* msg; + uint16_t score = 0, best; + uint8_t keys, prev = 0, fresh, dirty; + + DISPLAY_OFF; + BGP_REG = 0xE4; + font_init(); + + // load the save, or start a new one + ENABLE_SAVE_RAM; + fresh = !save_valid(); + if (fresh) save_reset(); + SAVE->boots++; + SAVE->check = save_sum(); + fmt_num(bootbuf, SAVE->boots); + best = SAVE->best; + DISABLE_SAVE_RAM; + fmt_num(bestbuf, best); + fmt_num(scorebuf, score); + + put_str(5, 2, "SAVE RAM"); + put_str(2, 5, "BOOTS"); + put_str(2, 7, "BEST"); + put_str(2, 9, "SCORE"); + put_str(2, 12, fresh ? "NEW SAVE" : "SAVE LOADED"); + put_str(2, 15, "A ADD START SAVE"); + put_str(2, 16, "B ERASE"); + put_str(9, 5, bootbuf); + put_str(9, 7, bestbuf); + put_str(9, 9, scorebuf); + + SHOW_BKG; + DISPLAY_ON; + + while (1) { + keys = joypad(); + dirty = 0; + msg = 0; + + if ((keys & J_A) && !(prev & J_A)) { + score++; + dirty = 1; + } + + if ((keys & J_START) && !(prev & J_START) && score > best) { + best = score; + ENABLE_SAVE_RAM; + SAVE->best = best; + SAVE->check = save_sum(); + DISABLE_SAVE_RAM; + msg = "SAVED "; + dirty = 1; + } + + if ((keys & J_B) && !(prev & J_B)) { + ENABLE_SAVE_RAM; + save_reset(); + DISABLE_SAVE_RAM; + best = 0; + score = 0; + msg = "ERASED "; + dirty = 1; + } + prev = keys; + + if (dirty) { + fmt_num(bestbuf, best); + fmt_num(scorebuf, score); + } + + wait_vbl_done(); + if (dirty) { + put_str(9, 7, bestbuf); + put_str(9, 9, scorebuf); + if (msg) put_str(2, 12, msg); + } + } +} diff --git a/presets/gb/sm83.c b/presets/gb/sm83.c new file mode 100644 index 00000000..f303606d --- /dev/null +++ b/presets/gb/sm83.c @@ -0,0 +1,106 @@ +/* +Assembly language on the SM83. + +The same loop -- add up N bytes -- written twice: once in C, and once +by hand as a __naked function. The timer counts how long each version +takes, so we can see what the compiler's version costs. + +SDCC passes arguments on the stack (the first at SP+2 once the return +address is pushed), and an 8-bit result comes back in E. A __naked +function gets no prologue or epilogue at all, so the hand-written +version has to end with its own ret. + +TAC = 5 makes TIMA tick at 262144 Hz: once every 16 clocks, which is +4 machine cycles. TIMA is only 8 bits wide, so keep the runs short +enough that it can't wrap (256 ticks = 1024 machine cycles). +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "gbtext.h" + +#define COUNT 16 + +uint8_t sum_c(const uint8_t *p, uint8_t n) { + uint8_t s = 0; + while (n--) s += *p++; + return s; +} + +uint8_t sum_asm(const uint8_t *p, uint8_t n) __naked { + p; n; // (silences unused-argument warnings) + __asm + ldhl sp,#2 ; HL -> first argument + ld a,(hl+) + ld e,a + ld a,(hl+) + ld d,a ; DE = p + ld b,(hl) ; B = n + ld h,d + ld l,e ; HL = p + xor a,a ; A = 0 + inc b ; enter the loop at the test, so n = 0 works + jr 2$ +1$: add a,(hl) ; A += *p + inc hl ; p++ +2$: dec b + jr nz,1$ + ld e,a ; the result goes in E + ret + __endasm; +} + +uint8_t buf[COUNT]; + +// clear the timer, run one call, and return the ticks it took +#define TIME(var, expr) \ + do { TIMA_REG = 0; sink = (expr); var = TIMA_REG; } while (0) + +volatile uint8_t sink; + +static void put_dec(uint8_t x, uint8_t y, uint16_t v) { + char s[6]; + uint8_t i = 5; + s[5] = '\0'; + do { s[--i] = '0' + v % 10; v /= 10; } while (v); + put_str(x, y, s + i); +} + +void main(void) { + uint8_t i, c0, c1, a0, a1, same; + + DISPLAY_OFF; + BGP_REG = 0xE4; + font_init(); + + for (i = 0; i < COUNT; i++) buf[i] = i * 3; + TMA_REG = 0; + TAC_REG = 0x05; // enabled, 262144 Hz + + TIME(c0, sum_c(buf, 0)); + TIME(c1, sum_c(buf, COUNT)); + TIME(a0, sum_asm(buf, 0)); + TIME(a1, sum_asm(buf, COUNT)); + same = sum_c(buf, COUNT) == sum_asm(buf, COUNT); + + put_str(2, 1, "C VS ASSEMBLY"); + put_str(1, 4, "SAME ANSWER"); + put_str(14, 4, same ? "YES" : "NO"); + put_str(1, 7, "C "); + put_str(1, 8, "ASM"); + put_str(6, 6, "TICKS"); + put_dec(7, 7, c1 - c0); + put_dec(7, 8, a1 - a0); + put_str(1, 11, "CYCLES PER BYTE"); + // one tick is 4 machine cycles (rounded; TIMA can be off by one tick) + put_str(1, 12, "C "); + put_str(1, 13, "ASM"); + put_dec(7, 12, ((uint16_t)(c1 - c0) * 4 + COUNT / 2) / COUNT); + put_dec(7, 13, ((uint16_t)(a1 - a0) * 4 + COUNT / 2) / COUNT); + + SHOW_BKG; + DISPLAY_ON; + while (1) wait_vbl_done(); +} diff --git a/presets/gb/tilebuf.h b/presets/gb/tilebuf.h new file mode 100644 index 00000000..d7f5c36e --- /dev/null +++ b/presets/gb/tilebuf.h @@ -0,0 +1,260 @@ +/* +A software framebuffer made of background tiles. + +The Game Boy has no bitmap mode, but if every cell in a rectangle of the +tile map points at a *different* tile, then each pixel in that rectangle +belongs to exactly one bit of one tile -- and the tiles become a bitmap. + +This header uses the layout from Faceball 2000 (1991): + +- The viewport is 18x14 tiles (144x112 pixels): 252 unique tiles. +- The background uses the $8800 "signed" tile-data mode (LCDC bit 4 = 0). + In that mode tile numbers $80..$FF live at $8800..$8FFF and $00..$7F at + $9000..$97FF, so counting upward from tile $84 and wrapping past $FF + gives one *linear* run of VRAM: $8840..$983F. One copy loop fills it. +- Tiles $80..$83 are left over for the border around the viewport. +- The CPU draws into a 4032-byte buffer in WRAM (tb_buf), which has the + same tile-by-tile layout as VRAM, then tb_blit() copies it to VRAM + eight bytes per scanline, during HBlank, while the screen is on. + +The price is VRAM bandwidth: 4032 bytes / 8 per line = 504 scanlines, +about 3.3 frames just to copy one picture. That is why 3D games on the +original Game Boy ran at single-digit frame rates. +*/ + +#ifndef _TILEBUF_H +#define _TILEBUF_H + +#include + +#define TB_COLS 18 // viewport width in tiles +#define TB_ROWS 14 // viewport height in tiles +#define TB_W (TB_COLS*8) // 144 pixels +#define TB_H (TB_ROWS*8) // 112 pixels +#define TB_ROWBYTES (TB_COLS*16) // 288 bytes per row of tiles +#define TB_BYTES (TB_ROWS*TB_ROWBYTES) // 4032 +#define TB_FIRST_TILE 0x84 // first viewport tile number +#define TB_VRAM ((uint8_t*)0x8840) // tile $84 in $8800 mode +#define TB_MAPX 1 // viewport position on the tile map +#define TB_MAPY 1 + +// the framebuffer, in WRAM, laid out exactly like the VRAM tiles +uint8_t tb_buf[TB_BYTES]; + +// address of pixel row y, column 0 -- Faceball keeps this same +// table at $0300 in its ROM ("FrameBufferRAMOffsets") +uint8_t* tb_rowaddr[TB_H]; + +// bit mask for each pixel column within a byte +const uint8_t tb_bit[8] = { 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01 }; + +// --------------------------------------------------------------------- +// setup + +// Border tiles: $80 blank, $81 horizontal edge, $82 vertical edge. +/*{w:8,h:8,bpp:1,count:3,brev:1,np:2,pofs:1,sl:2}*/ +static const uint8_t tb_border_tiles[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +}; + +void tb_init(void) { + uint8_t x, y, t; + uint8_t row[TB_COLS]; + // build the row-address table + for (y = 0; y < TB_H; y++) { + tb_rowaddr[y] = tb_buf + (y >> 3) * TB_ROWBYTES + (y & 7) * 2; + } + // border tiles live in the $8800 half, tile numbers $80..$82 + set_bkg_data(0x80, 3, tb_border_tiles); + fill_bkg_rect(0, 0, 20, 18, 0x80); + fill_bkg_rect(TB_MAPX, 0, TB_COLS, 1, 0x81); + fill_bkg_rect(0, TB_MAPY, 1, TB_ROWS, 0x82); + // number the viewport cells $84, $85, ... $FF, $00, ... $7F + t = TB_FIRST_TILE; + for (y = 0; y < TB_ROWS; y++) { + for (x = 0; x < TB_COLS; x++) row[x] = t++; + set_bkg_tiles(TB_MAPX, TB_MAPY + y, TB_COLS, 1, row); + } + // clear buffer and VRAM + for (x = 0; x < TB_ROWS; x++) { + uint8_t* p = tb_buf + x * TB_ROWBYTES; + uint16_t i; + for (i = 0; i < TB_ROWBYTES; i++) p[i] = 0; + } + // $8800 addressing: LCDC bit 4 clear + LCDC_REG = LCDCF_ON | LCDCF_BG8800 | LCDCF_BG9800 | LCDCF_BGON; +} + +// --------------------------------------------------------------------- +// the blitter + +// Interrupts need a real stack while tb_blit() is borrowing SP, +// so we give them this one (Faceball uses $C9A1). +static uint8_t tb_isr_stack[96]; +static uint16_t tb_saved_sp; + +/* +Copy tb_buf to VRAM, 8 bytes per scanline, and clear tb_buf behind us. + +This is a transcription of Faceball 2000's UpdateFrameBuffer routine +($13F0 in the ROM). The trick is the stack pointer: POP BC reads two +bytes and advances the pointer in 3 cycles, the fastest way to read +memory on the SM83. So SP is pointed at the buffer and the loop pops +its way through it: + + 1. POP the first two bytes before we wait (saves time in HBlank) + 2. wait until STAT bit 1 is set (mode 2/3: the PPU is drawing) + 3. wait until STAT bit 1 is clear (mode 0: HBlank -- VRAM is free) + 4. write 8 bytes to VRAM. Mode 0 plus the following mode 2 (VRAM is + only locked in mode 3) gives at least ~40 M-cycles, enough for 8. + 5. park SP on a safe stack and enable interrupts while the PPU draws + the next line, so the VBlank handler/music can still run + 6. PUSH zeros over the 8 bytes we just copied -- clearing the buffer + for the next frame costs almost nothing + +Note that in VBlank (mode 1) STAT bit 1 is 0, so step 2 waits out all +of VBlank: this routine only uses the 144 visible lines per frame. +*/ +void tb_blit(void) __naked { +__asm + ld (_tb_saved_sp), sp + ld hl, #0x8840 ; VRAM destination (tile $84) + ld de, #504 ; 4032 / 8 lines + di + ld sp, #_tb_buf +1$: + pop bc ; bytes 0,1 +2$: + ldh a, (0x41) ; STAT + and a, #0x02 + jr z, 2$ ; wait for mode 2/3 +3$: + ldh a, (0x41) + and a, #0x02 + jr nz, 3$ ; wait for mode 0 (HBlank) + ld a, c + ld (hl+), a + ld a, b + ld (hl+), a + pop bc ; bytes 2,3 + ld a, c + ld (hl+), a + ld a, b + ld (hl+), a + pop bc ; bytes 4,5 + ld a, c + ld (hl+), a + ld a, b + ld (hl+), a + pop bc ; bytes 6,7 + ld a, c + ld (hl+), a + ld a, b + ld (hl+), a + ; swap SP to the ISR stack, let interrupts in + ld c, l + ld b, h ; BC = VRAM pointer + ldhl sp, #0 ; HL = buffer pointer + ld sp, #_tb_isr_stack+96 + ei +4$: + ldh a, (0x41) + and a, #0x02 + jr z, 4$ ; wait until HBlank is over + di + ld sp, hl + ld l, c + ld h, b ; HL = VRAM pointer again + ld bc, #0 + push bc ; clear the 8 bytes we just popped + push bc + push bc + push bc + add sp, #8 + dec de + ld a, d + or a, e + jr nz, 1$ + ; restore the C stack + ld a, (_tb_saved_sp) + ld l, a + ld a, (_tb_saved_sp+1) + ld h, a + ld sp, hl + ei + ret +__endasm; +} + +// --------------------------------------------------------------------- +// drawing +// +// Each pixel row of a tile is two bytes: the low bit-plane, then the high +// bit-plane. Color c (0..3) sets bit (c & 1) in the first and (c & 2) in +// the second. So one pixel = two read-modify-writes, but eight pixels of +// the same color = two plain writes. Fast code draws bytes, not pixels. + +uint8_t tb_color = 3; + +void tb_pset(uint8_t x, uint8_t y) { + uint8_t* p = tb_rowaddr[y] + ((x & 0xf8) << 1); + uint8_t m = tb_bit[x & 7]; + if (tb_color & 1) p[0] |= m; else p[0] &= ~m; + if (tb_color & 2) p[1] |= m; else p[1] &= ~m; +} + +// --------------------------------------------------------------------- +// shades +// +// Shade 0,2,4,6 are the solid colors 0..3. Odd shades are a checkerboard +// of the two neighbors. Each shade is described by its two bit-plane +// bytes on even rows and on odd rows. + +typedef struct { uint8_t lo0, hi0, lo1, hi1; } Shade; + +const Shade shades[7] = { + // lo0 hi0 lo1 hi1 + { 0x00, 0x00, 0x00, 0x00 }, // color 0 + { 0xAA, 0x00, 0x55, 0x00 }, // 0/1 checker + { 0xFF, 0x00, 0xFF, 0x00 }, // color 1 + { 0x55, 0xAA, 0xAA, 0x55 }, // 1/2 checker + { 0x00, 0xFF, 0x00, 0xFF }, // color 2 + { 0xAA, 0xFF, 0x55, 0xFF }, // 2/3 checker + { 0xFF, 0xFF, 0xFF, 0xFF }, // color 3 +}; + +// left mask for x&7, right mask for x&7 +const uint8_t lmask[8] = { 0xFF,0x7F,0x3F,0x1F,0x0F,0x07,0x03,0x01 }; +const uint8_t rmask[8] = { 0x80,0xC0,0xE0,0xF0,0xF8,0xFC,0xFE,0xFF }; + +// Fill pixels xl..xr (inclusive) of row y with a shade. +void span(uint8_t y, uint8_t xl, uint8_t xr, const Shade* s) { + uint8_t *p = tb_rowaddr[y] + ((xl & 0xf8) << 1); + uint8_t lo = (y & 1) ? s->lo1 : s->lo0; + uint8_t hi = (y & 1) ? s->hi1 : s->hi0; + uint8_t m = lmask[xl & 7]; + uint8_t n = (xr >> 3) - (xl >> 3); // number of byte boundaries crossed + if (n == 0) { + m &= rmask[xr & 7]; // span starts and ends in one byte + } else { + // left partial byte + p[0] = (p[0] & ~m) | (lo & m); + p[1] = (p[1] & ~m) | (hi & m); + p += 16; + // whole bytes: no masking, two writes per 8 pixels + while (--n) { + p[0] = lo; + p[1] = hi; + p += 16; + } + m = rmask[xr & 7]; + } + // right (or only) partial byte + p[0] = (p[0] & ~m) | (lo & m); + p[1] = (p[1] & ~m) | (hi & m); +} + +#endif diff --git a/presets/gb/wireframe.c b/presets/gb/wireframe.c new file mode 100644 index 00000000..c60d8c98 --- /dev/null +++ b/presets/gb/wireframe.c @@ -0,0 +1,94 @@ +/* +Wireframe 3D in a tile framebuffer, in the style of X (Argonaut, 1992). + +A cube spins in the 144x112 viewport from tilebuf.h. Each frame: + +1. rotate the 8 corners around two axes -- 8-bit fixed point, using a + quarter-square table because the SM83 has no multiply instruction +2. project them with a reciprocal table instead of dividing by Z +3. draw the 12 edges with Bresenham's line algorithm, stepping a VRAM-style + pointer and bit mask instead of recomputing each pixel's address +4. tb_blit() copies the finished picture to VRAM (and clears the buffer) +*/ + +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" +#include "tilebuf.h" +#include "gb3d.h" + +// Bresenham's line, walking a framebuffer pointer and a bit mask. +// Moving right one pixel = shift the mask; every 8 pixels, step 16 bytes +// to the next tile. Moving down one pixel = 2 bytes, except every 8 rows, +// where we jump to the next row of tiles. +void line(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { + uint8_t *p; + uint8_t mask, dx, dy, n, y; + int16_t err; + int8_t xdir; + if (y0 > y1) { // always draw downward + n = x0; x0 = x1; x1 = n; + n = y0; y0 = y1; y1 = n; + } + dy = y1 - y0; + if (x1 >= x0) { dx = x1 - x0; xdir = 1; } + else { dx = x0 - x1; xdir = -1; } + p = tb_rowaddr[y0] + ((x0 & 0xf8) << 1); + mask = tb_bit[x0 & 7]; + y = y0; + if (dx >= dy) { // mostly horizontal + err = dx >> 1; + for (n = dx + 1; n; n--) { + p[0] |= mask; p[1] |= mask; // color 3 + if (xdir > 0) { + mask >>= 1; + if (!mask) { mask = 0x80; p += 16; } + } else { + mask <<= 1; + if (!mask) { mask = 0x01; p -= 16; } + } + err -= dy; + if (err < 0) { + err += dx; + p += ((++y & 7) == 0) ? (TB_ROWBYTES - 14) : 2; + } + } + } else { // mostly vertical + err = dy >> 1; + for (n = dy + 1; n; n--) { + p[0] |= mask; p[1] |= mask; + p += ((++y & 7) == 0) ? (TB_ROWBYTES - 14) : 2; + err -= dx; + if (err < 0) { + err += dy; + if (xdir > 0) { + mask >>= 1; + if (!mask) { mask = 0x80; p += 16; } + } else { + mask <<= 1; + if (!mask) { mask = 0x01; p -= 16; } + } + } + } + } +} + +void main(void) { + uint8_t ay = 0, ax = 0, i; + DISPLAY_OFF; + BGP_REG = 0xE4; + gb3d_init(); + tb_init(); + DISPLAY_ON; + while (1) { + transform(ay, ax); + for (i = 0; i < 12; i++) { + uint8_t a = edges[i][0], b = edges[i][1]; + line(sx[a], sy[a], sx[b], sy[b]); + } + tb_blit(); // ~3.3 frames: copy to VRAM and clear the buffer + ay += 3; + ax += 2; + } +} diff --git a/src/platform/gb.ts b/src/platform/gb.ts index a9298ae0..379dfeed 100644 --- a/src/platform/gb.ts +++ b/src/platform/gb.ts @@ -16,19 +16,29 @@ const GB_PRESETS: Preset[] = [ { id: 'large_map.c', name: 'Large Scrolling Map' }, { id: 'window.c', name: 'Window Status Bar' }, { id: 'galaxy.c', name: 'Window + Sprites' }, - { id: 'raster.c', name: 'STAT/LYC Raster Effects' }, { id: 'interrupts.c', name: 'VBlank + Timer Interrupts' }, { id: 'vram.c', name: 'VRAM/OAM Access' }, + { id: 'detect.c', name: 'Detect Hardware' }, { id: 'gbc.c', name: 'Game Boy Color' }, { id: 'gbdecompress.c', name: 'Decompression' }, { id: 'music.c', name: 'Music Player' }, + { id: 'aputest.c', name: 'Sound Tester' }, { id: 'clock.c', name: 'MBC3 Real-Time Clock' }, + { id: 'savegame.c', name: 'Battery Save RAM' }, { id: 'banking.c', name: 'ROM Bank Switching' }, + { id: 'flicker.c', name: 'Flicker Transparency', category: 'Advanced Graphics' }, + { id: 'parallax.c', name: 'Parallax Scrolling' }, + { id: 'animstream.c', name: 'Streaming Animation' }, + { id: 'raster.c', name: 'STAT/LYC Raster Effects' }, + { id: 'wireframe.c', name: 'Wireframe 3D' }, + { id: 'polyfill.c', name: 'Filled Polygons' }, + { id: 'raycast.c', name: 'Raycasting 3D' }, { id: 'siegegame.c', name: 'Siege Game', category: 'Games' }, { id: 'climber.c', name: 'Climber Game' }, { id: 'chase.c', name: "Shiru's Chase Game" }, { id: 'pakupaku.c', name: 'Paku Paku' }, - { id: 'hello.sgb', name: 'Hello World (ASM)', category: 'Assembler' }, + { id: 'sm83.c', name: 'C vs. Assembly', category: 'Assembler' }, + { id: 'hello.sgb', name: 'Hello World (ASM)' }, { id: 'main.wiz', name: 'Snake Game (Wiz)' }, ];