From 7548f21a110dd4928bc383be62edf949f35fb0cb Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sat, 21 Sep 2019 14:37:54 +0300 Subject: [PATCH] Now with actual content! Fixes #1 --- .gitignore | 1 + LICENSE | 21 ++ Makefile | 59 +++++ README.md | 44 +++- encoder.c | 513 ++++++++++++++++++++++++++++++++++++++++++ gbhw.asm | 111 +++++++++ video.asm | 649 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1395 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 encoder.c create mode 100644 gbhw.asm create mode 100644 video.asm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53752db --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +output diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94966be --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2019 Lior Halphon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7d3146d --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +# Quality: Lower is better. 0 is lossless as far as the format allows. +QUALITY ?= 4 + +ifneq ($(MAKECMDGOALS),clean) +ifeq ($(SOURCE),) +$(error Missing video source. Use "make SOURCE=...") +endif +endif + +OUT := output/$(basename $(notdir $(SOURCE))) +$(shell mkdir -p $(OUT)) +CC ?= clang +FFMPEG := ffmpeg -loglevel warning -stats -hide_banner + +TITLE = "\033[1m\033[36m" +TITLE_END = "\033[0m" + +$(OUT)/$(basename $(notdir $(SOURCE))).gbc: output/video.gbc $(OUT)/video.bin + @echo $(TITLE)Creating ROM...$(TITLE_END) + cat $^ > $@ + rgbfix -Cv -t "GBVP2" -m 25 $@ + cp output/video.sym $(basename $@).sym + +output/video.gbc: video.asm + @echo $(TITLE)Compiling player...$(TITLE_END) + rgbasm -o $@.o $< + rgblink -o $@ -m output/video.map -n output/video.sym $@.o + +$(OUT)/video.bin: output/encoder $(OUT)/frames $(OUT)/audio.raw + @echo $(TITLE)Encoding video...$(TITLE_END) + find $(OUT)/frames | sort | \ + output/encoder $(shell ffmpeg -i $(SOURCE) 2>&1 | sed -n "s/.*, \(.*\) fp.*/\1/p") \ + $(QUALITY) \ + $(OUT)/audio.raw \ + $@ \ + +output/encoder: encoder.c + @echo $(TITLE)Compiling encoder...$(TITLE_END) + $(CC) -g -Ofast -std=c11 -Werror -Wall -o $@ $^ + +$(OUT)/audio.raw: $(SOURCE) + @echo $(TITLE)Converting audio...$(TITLE_END) + $(eval GAIN := 0$(shell ffmpeg -i $^ -filter:a volumedetect -f null /dev/null 2>&1 | sed -n "s/.*max_volume: -\(.*\) dB/\1/p")) + $(FFMPEG) -i $^ -f u8 -acodec pcm_u8 -ar 9198 -filter:a "volume=$(GAIN)dB" $@ + +$(OUT)/frames: $(OUT)/video.mp4 + @echo $(TITLE)Extracting frames...$(TITLE_END) + -@rm -rf $@ + mkdir -p $@ + $(FFMPEG) -i $^ -coder "raw" $@/%05d.tga + +$(OUT)/video.mp4: $(SOURCE) + @echo $(TITLE)Resizing video...$(TITLE_END) + $(FFMPEG) -i $^ -c:v rawvideo -vf scale=-2:144 $@.tmp.mp4 + $(FFMPEG) -i $@.tmp.mp4 -c:v rawvideo -filter:v "crop=160:144" $@ + rm $@.tmp.mp4 + +clean: + rm -rf output \ No newline at end of file diff --git a/README.md b/README.md index f665180..ac6572e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,44 @@ # GBVideoPlayer2 -A new version of GBVideoPlayer with higher resolution, 3-bit stereo PCM audio and video compression +A new version of GBVideoPlayer with higher resolution, 3-bit stereo PCM audio and video compression. -## Wen Eta? +Version 2 increases the horizontal resolution by up to 4, replaces the chiptune music with ~9KHz, 3-bit PCM audio, introduces simple video compression with configurable quality settings, and uses a faster and easier to use encoding routines that can directly re-encode FFMPEG-compatible video to GBVP2 format. -The source code for the player and encoder will be uploaded here soon. Meanwhile, you can grab a compiled ROM from the [releases page](https://github.com/LIJI32/GBVideoPlayer2/releases) or [watch the video](https://youtu.be/iDd_aqpLf5Q) +## Examples + +You can grab compiled example ROMs from the [releases page](https://github.com/LIJI32/GBVideoPlayer2/releases) or [watch a video](https://youtu.be/iDd_aqpLf5Q) + +## Requirements + +For playing a GBVP2 video ROM on hardware, an MBC5-compatible flash cart is required. Remember that your cartridge's capacity must be big enough for your ROM – if your ROM is over 4MBs, you will need a cartridge that can store a 8MB ROM. (Note: the common EMS 64M flash carts can only store two 4MB (32 megabits) ROMs, not a single 8MB (64 megabits) ROM) + +For playing a GBVP2 video ROM on an emulator, you must use an accurate Game Boy Color emulator, such as recent versions of [SameBoy](https://sameboy.github.io) or BGB. GBVP2 will not work on inaccurate emulators, such as VisualBoyAdvance or GameBoy Online. + +For encoding and building a video ROM, you will need a Make, a C compiler (Clang recommended), [rgbds](https://github.com/bentley/rgbds/releases/), and a recent version of FFMPEG. + +## Format Specifications + +* Irregular horizontal resolution, from effectively 120 pixels to 160 pixels wide, stretched to fill the 160 pixels wide Game Boy screen +* Vertical resolution of 144 pixels, same as the Game Boy screen +* Effectively up to 528 different colors per frame +* Stereo PCM audio at 9198 Hz and 3-bits per channel +* A frame rate of 29.86 frames per second +* In-frame compression of successive similar rows of pixels, customizable compression quality +* Can repeat a frame up to 255 times to avoid re-encoding highly similar frames + +## Building a ROM + +You can encode and build a ROM simply by running `make`: + +``` +make SOURCE=/path/to/my_video.mp4 +``` + +`SOURCE` may be any file compatible with your copy of FFMPEG. Your output will be at `output/my_video/my_video.gbc`. + +Optionally, you may specify `QUALITY` to reduce the file size or, alternatively, improve video quality: + +``` +make SOURCE=/path/to/my_video.mp4 QUALITY=8 +``` + +`QUALITY` can be any non-negative integer. The higher the value, the more aggressive the compression. A value of 0 performs no lossy compression after converting a frame to the player's format. The default value is 4. \ No newline at end of file diff --git a/encoder.c b/encoder.c new file mode 100644 index 0000000..786ca0b --- /dev/null +++ b/encoder.c @@ -0,0 +1,513 @@ +#include +#include +#include +#include + +#define SCREEN_SIZE (160 * 144) +#define PIXELS (SCREEN_SIZE * 2) +#define SCY_DATA_SIZE (PIXELS / 8) +#define PALETTE_SIZE (8 * 4) +#define DIFF_THRESHOLD 0x8000 // Todo: make this an argument? + +unsigned compressed_size = 0; +unsigned uncompressed_size = 0; +static const double GB_FPS = (1024 * 1024 * 4) / (70224.0); + +typedef struct { + uint8_t b, g, r; +} color_t; +#define COLOR_INDEX(PAL, IND) ((PAL) * 4 + (IND)) + +#define LEFT_COMBINATIONS(LEFT, FIRST_RIGHT) \ +LEFT, LEFT, LEFT, FIRST_RIGHT + 0, FIRST_RIGHT + 0 , FIRST_RIGHT + 0 , FIRST_RIGHT + 0 , FIRST_RIGHT + 0 , \ +LEFT, LEFT, LEFT, FIRST_RIGHT + 1, FIRST_RIGHT + 1 , FIRST_RIGHT + 1 , FIRST_RIGHT + 1 , FIRST_RIGHT + 1 , \ +LEFT, LEFT, LEFT, FIRST_RIGHT + 2, FIRST_RIGHT + 2 , FIRST_RIGHT + 2 , FIRST_RIGHT + 2 , FIRST_RIGHT + 2 , \ +LEFT, LEFT, LEFT, FIRST_RIGHT + 3, FIRST_RIGHT + 3 , FIRST_RIGHT + 3 , FIRST_RIGHT + 3 , FIRST_RIGHT + 3 , \ + +#define LEFT_COMBINATIONS_FOR_PALETTE(PAL) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 0), COLOR_INDEX(PAL, 0)) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 1), COLOR_INDEX(PAL, 0)) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 2), COLOR_INDEX(PAL, 0)) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 3), COLOR_INDEX(PAL, 0)) \ + +#define RIGHT_COMBINATION(LEFT, RIGHT) \ +LEFT, LEFT, \ +(LEFT) == (RIGHT) ? (LEFT) ^ 1 : LEFT, (LEFT) == (RIGHT) ? (LEFT) ^ 1 : LEFT, \ +(LEFT) == (RIGHT) ? (LEFT) ^ 2 : LEFT, (LEFT) == (RIGHT) ? (LEFT) ^ 2 : RIGHT, \ +(LEFT) == (RIGHT) ? (LEFT) ^ 3 : RIGHT , (LEFT) == (RIGHT) ? (LEFT) ^ 3 : RIGHT, \ + +#define RIGHT_COMBINATIONS(LEFT, FIRST_RIGHT) \ +RIGHT_COMBINATION(LEFT, FIRST_RIGHT + 0) \ +RIGHT_COMBINATION(LEFT, FIRST_RIGHT + 1) \ +RIGHT_COMBINATION(LEFT, FIRST_RIGHT + 2) \ +RIGHT_COMBINATION(LEFT, FIRST_RIGHT + 3) \ + +#define RIGHT_COMBINATIONS_FOR_PALETTE(PAL) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 0), COLOR_INDEX(PAL, 0)) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 1), COLOR_INDEX(PAL, 0)) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 2), COLOR_INDEX(PAL, 0)) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 3), COLOR_INDEX(PAL, 0)) \ + +/* + + // Alternative encoding, can't be configured right now + +#define LEFT_COMBINATIONS(LEFT, FIRST_RIGHT) \ +LEFT, LEFT, FIRST_RIGHT + 0, FIRST_RIGHT + 0 , LEFT, LEFT, FIRST_RIGHT + 0 , FIRST_RIGHT + 0 , \ +LEFT, LEFT, FIRST_RIGHT + 1, FIRST_RIGHT + 1 , LEFT, LEFT, FIRST_RIGHT + 1 , FIRST_RIGHT + 1 , \ +LEFT, LEFT, FIRST_RIGHT + 2, FIRST_RIGHT + 2 , LEFT, LEFT, FIRST_RIGHT + 2 , FIRST_RIGHT + 2 , \ +LEFT, LEFT, FIRST_RIGHT + 3, FIRST_RIGHT + 3 , LEFT, LEFT, FIRST_RIGHT + 3 , FIRST_RIGHT + 3 , \ + +#define LEFT_COMBINATIONS_FOR_PALETTE(PAL) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 0), COLOR_INDEX(PAL, 0)) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 1), COLOR_INDEX(PAL, 0)) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 2), COLOR_INDEX(PAL, 0)) \ +LEFT_COMBINATIONS(COLOR_INDEX(PAL, 3), COLOR_INDEX(PAL, 0)) \ + +#define RIGHT_COMBINATIONS(LEFT, FIRST_RIGHT) \ +LEFT, LEFT, FIRST_RIGHT + 0, FIRST_RIGHT + 0 , LEFT ^ 3, LEFT ^ 3, (FIRST_RIGHT + 0) ^ 3 , (FIRST_RIGHT + 0) ^ 3 , \ +LEFT, LEFT, FIRST_RIGHT + 1, FIRST_RIGHT + 1 , LEFT ^ 3, LEFT ^ 3, (FIRST_RIGHT + 1) ^ 3 , (FIRST_RIGHT + 1) ^ 3 , \ +LEFT, LEFT, FIRST_RIGHT + 2, FIRST_RIGHT + 2 , LEFT ^ 3, LEFT ^ 3, (FIRST_RIGHT + 2) ^ 3 , (FIRST_RIGHT + 2) ^ 3 , \ +LEFT, LEFT, FIRST_RIGHT + 3, FIRST_RIGHT + 3 , LEFT ^ 3, LEFT ^ 3, (FIRST_RIGHT + 3) ^ 3 , (FIRST_RIGHT + 3) ^ 3 , \ + +#define RIGHT_COMBINATIONS_FOR_PALETTE(PAL) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 0), COLOR_INDEX(PAL, 0)) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 1), COLOR_INDEX(PAL, 0)) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 2), COLOR_INDEX(PAL, 0)) \ +RIGHT_COMBINATIONS(COLOR_INDEX(PAL, 3), COLOR_INDEX(PAL, 0)) \ + */ + +const static uint8_t combinations[256 * 8] = +{ + LEFT_COMBINATIONS_FOR_PALETTE(7) + RIGHT_COMBINATIONS_FOR_PALETTE(7) + LEFT_COMBINATIONS_FOR_PALETTE(6) + RIGHT_COMBINATIONS_FOR_PALETTE(6) + LEFT_COMBINATIONS_FOR_PALETTE(5) + RIGHT_COMBINATIONS_FOR_PALETTE(5) + LEFT_COMBINATIONS_FOR_PALETTE(4) + RIGHT_COMBINATIONS_FOR_PALETTE(4) + LEFT_COMBINATIONS_FOR_PALETTE(3) + RIGHT_COMBINATIONS_FOR_PALETTE(3) + LEFT_COMBINATIONS_FOR_PALETTE(2) + RIGHT_COMBINATIONS_FOR_PALETTE(2) + LEFT_COMBINATIONS_FOR_PALETTE(1) + RIGHT_COMBINATIONS_FOR_PALETTE(1) + LEFT_COMBINATIONS_FOR_PALETTE(0) + RIGHT_COMBINATIONS_FOR_PALETTE(0) +}; + +/* RGB2 is rounded to 5bpc, RGB1 remains as is. */ +static unsigned rounded_color_diff(signed r1, signed g1, signed b1, signed r2, signed g2, signed b2) +{ + r2 &= 0xF8; + r2 |= r2 >> 5; + + g2 &= 0xF8; + g2 |= g2 >> 5; + + b2 &= 0xF8; + b2 |= b2 >> 5; + + return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2); +} + +static unsigned score_for_combination(const uint8_t *r, const uint8_t *g, const uint8_t *b, const color_t *palette, uint8_t combination) +{ + unsigned score = 0; + for (unsigned x = 0; x < 8; x++) { + unsigned color_index = combinations[combination * 8 + x]; + unsigned diff = rounded_color_diff(r[x], g[x], b[x], + palette[color_index].r, palette[color_index].g, palette[color_index].b); + score += diff; + } + + return score; +} + +static unsigned best_combination_for_pixels(const uint8_t *r, const uint8_t *g, const uint8_t *b, const color_t *palette, unsigned *score) +{ + unsigned best = -1; + unsigned best_index = -1; + for (unsigned combination = 0; combination < 256; combination++) { + unsigned current_score = score_for_combination(r, g, b, palette, combination); + if (current_score < best) { + best = current_score; + best_index = combination; + } + } + + if (score) *score = best; + + return best_index; +} + +static bool optimize_palette_step(const uint8_t *r, const uint8_t *g, const uint8_t *b, color_t *palette, unsigned old_score, unsigned *new_score) +{ + unsigned r_sum[PALETTE_SIZE] = {0,}; + unsigned g_sum[PALETTE_SIZE] = {0,}; + unsigned b_sum[PALETTE_SIZE] = {0,}; + unsigned count[PALETTE_SIZE] = {0,}; + unsigned combination_score = 0; + unsigned score = 0; + color_t old_palette[PALETTE_SIZE]; + memcpy(&old_palette, palette, sizeof(old_palette)); + setbuf(stdout, NULL); + for (unsigned i = SCY_DATA_SIZE; i--;) { + const uint8_t *combination = combinations + (best_combination_for_pixels(r, g, b, palette, &combination_score)) * 8; + score += combination_score; + for (unsigned x = 8; x--;) { + unsigned color_index = *(combination++); + r_sum[color_index] += *(r++); + g_sum[color_index] += *(g++); + b_sum[color_index] += *(b++); + count[color_index] ++; + } + } + + + for (unsigned i = 0; i < PALETTE_SIZE; i++) { + if (count[i]) { + palette[i].r = r_sum[i] / count[i]; + palette[i].g = g_sum[i] / count[i]; + palette[i].b = b_sum[i] / count[i]; + } + else { + palette[i].r = rand(); + palette[i].g = rand(); + palette[i].b = rand(); + } + } + + if (old_score <= score) { + memcpy(palette, &old_palette, sizeof(old_palette)); + return false; + } + *new_score = score; + return true; +} + +__attribute__((unused)) static void dump_tga_paletted(const char *output, const uint8_t *r, const uint8_t *g, const uint8_t *b, color_t *palette) +{ + static const uint8_t header[] = { + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x18, 0x20, + }; + FILE *out = fopen(output, "wb"); + fwrite(header, sizeof(header), 1, out); + for (unsigned i = SCY_DATA_SIZE; i--;) { + const uint8_t *combination = combinations + (best_combination_for_pixels(r, g, b, palette, NULL)) * 8; + + for (unsigned x = 8; x--;) { + fwrite(palette + *(combination++), 3, 1, out); + } + r += 8; + g += 8; + b += 8; + } + fclose(out); +} + +void round_palette(color_t *palette) +{ + for (unsigned i = PALETTE_SIZE; i--;) { + palette->r &= 0xF8; + palette->r |= palette->r >> 5; + + palette->g &= 0xF8; + palette->g |= palette->g >> 5; + + palette->b &= 0xF8; + palette->b |= palette->b >> 5; + + palette++; + } +} + +static uint8_t output[1024 * 1024 * 8 + 0x4000]; +size_t pos = 0x4001; +size_t frame_count_pos = 0; +/* Quality: 0 is lossless (as far as format allows), increasing value reduces quality */ +static void encode_image(const uint8_t *r, const uint8_t *g, const uint8_t *b, const color_t *palette, unsigned quality, unsigned count) +{ + if (count > 255) { + encode_image(r, g, b, palette, quality, count - 255); + count = 255; + } + static uint8_t previous_line_buffer[20] = {0,}; + uint8_t line_buffer[20] = {}; + uint8_t lossy_line_buffer[20] = {}; + quality += 8; + + /* Frames are not allowed to start in bank 0xFF */ + if (pos / 0x4000 == 0xFF) { + pos &= ~0x3fff; + pos += 0x4000; + printf("\n\033[1m\033[33mWarning: This video exceeds 256 banks, it might not be compatible with all flash carts\033[0m\n"); + } + + frame_count_pos = pos; + output[pos++] = (uint8_t) count; + + if ((pos & 0x3fff) >= 0x4000 - 33) { + output[pos] = 0xFF; // Needs bank switch + pos &= ~0x3fff; + pos += 0x4000; + } + for (unsigned i = 0; i < PALETTE_SIZE / 2; i++) { + uint16_t color = (palette[i].r >> 3) | ((palette[i].g >> 3) << 5) | ((palette[i].b >> 3) << 10); + output[pos++] = color >> 8; + output[pos++] = color; + } + + if ((pos & 0x3fff) >= 0x4000 - 33) { + output[pos] = 0xFF; // Needs bank switch + pos &= ~0x3fff; + pos += 0x4000; + } + for (unsigned i = PALETTE_SIZE / 2; i < PALETTE_SIZE; i++) { + uint16_t color = (palette[i].r >> 3) | ((palette[i].g >> 3) << 5) | ((palette[i].b >> 3) << 10); + output[pos++] = color >> 8; + output[pos++] = color; + } + + for (unsigned y = 0; y < 288; y++) { + unsigned ndiffs = 0; + for (unsigned j = 0; j < 20; j++) { + unsigned score; + uint8_t combination = (best_combination_for_pixels(r, g, b, palette, &score)); + line_buffer[j] = combination; + lossy_line_buffer[j] = combination; + if (previous_line_buffer[j] != combination) { + unsigned lossy_score = score_for_combination(r, g, b, palette, previous_line_buffer[j]); + if (lossy_score > score * quality / 8) { + ndiffs++; + } + else { + lossy_line_buffer[j] = previous_line_buffer[j]; + } + } + + r += 8; + g += 8; + b += 8; + } + + if (y == 0) { + /* Using compression for the first line in the first subframe has some unpleast artifacts if the palette + greatly changed from the previous frame */ + ndiffs = 20; + } + + static const unsigned max_diffs = 3; + + if ((pos & 0x3fff) < 0x4000 - 22) { + /* Sadly, we only support up to 3 diffs, because 4 diffs and more take too much CPU cycles to copy */ + if (ndiffs > max_diffs) { + output[pos++] = 0; // Uncompressed, no bank switch + } + else { + output[pos++] = (((9 - ndiffs) * 3 + 1) << 1); // Compressed + } + } + else { + output[pos] = 1; // Uncompressed, Bank switch + pos &= ~0x3fff; + pos += 0x4000; + ndiffs = 20; + } + + if (ndiffs > max_diffs) { + memcpy(previous_line_buffer, line_buffer, 20); + memcpy(output + pos, line_buffer, 20); + for (unsigned i = 20; i--;) { + output[pos++] -= (y % 144); + } + } + else { + unsigned test = 0; + for (unsigned i = 0; i < 20; i++) { + if (previous_line_buffer[i] != lossy_line_buffer[i]) { + output[pos++] = lossy_line_buffer[i] - (y % 144) + 1; + output[pos++] = i + 0xc1; + test++; + } + } + memcpy(previous_line_buffer, lossy_line_buffer, 20); + if (test != ndiffs) { + printf("WAT %d != %d\n", test, ndiffs); + exit(1); + } + } + } + + if (count != 1 && pos / 0x4000 == 0xFF) { + /* The last frame in the first 256 banks must not repeat. Encode the frame once again in the next bank. */ + + output[frame_count_pos] = 1; + encode_image(r - 46080, g - 46080, b - 46080, palette, quality, count - 1); + } +} + +static uint8_t quantize(signed sample) +{ + if (sample < 18) return 0; + if (sample < 55) return 1; + if (sample < 91) return 2; + if (sample < 130) return 3; + if (sample < 164) return 4; + if (sample < 200) return 5; + if (sample < 237) return 6; + return 7; +} + +int main(int argc, const char * argv[]) +{ + + if (argc != 5) { + printf("Usage: %s frame_rate quality audio_file.pcm output.bin < frame_list.txt\n", argv[0]); + return -1; + } + + double source_fps = atof(argv[1]); + unsigned quality = atoi(argv[2]); + + uint8_t r[PIXELS], g[PIXELS], b[PIXELS], rgb[SCREEN_SIZE * 3]; + color_t palette[PALETTE_SIZE] = {{0,},}; + color_t rounded_palette[PALETTE_SIZE] = {{0,},}; + bool palette_inited = false; + unsigned id = 0; + double frame_multiplier = GB_FPS / 2 / source_fps; + double fps_tracking = 0; + + FILE *audiof = fopen(argv[3], "rb"); + if (!audiof) { + perror("Failed to load PCM file"); + return 1; + } + + memset(output, 0xFF, sizeof(output)); + + bool done = false; + while (!done) { + for (unsigned i = 154; i--; ) { + uint8_t left, right; + if (fread(&left, 1, 1, audiof) != 1 || fread(&right, 1, 1, audiof) != 1) { + left = right = 0x80; + done = true; + } + + output[pos++] = quantize(left) | (quantize(right) << 4); + } + if ((pos & 0x3FFF) + 154 > 0x4000) { + pos &= ~0x3fff; + pos += 0x4000; + } + } + + pos--; + pos &= ~0x3fff; + pos += 0x4000; + output[0x4000] = pos / 0x4000; + fclose(audiof); + char filepath[1024]; + printf("GBVP2 Encoder! \n"); + unsigned long diff = 0; + while (true) { + bool truncate = false; + if (pos / 0x4000 == 0x1FF) { + printf("\n\033[1m\033[31mError: This video exceeds 512 banks. Try reducing the quality by setting 'QUALITY' to a higher value.\033[0m\n"); + truncate = true; + } + if (scanf("%1023s", filepath) != 1 || truncate) { + /* Frames are not allowed to start in bank 0xFF */ + if (((pos / 0x4000) & 0xFF) == 0xFF) { + pos &= ~0x3fff; + pos += 0x4000; + printf("\n\033[1m\033[33mWarning: This video exceeds 256 banks, it might not be compatible with all flash carts\033[0m\n"); + } + /* Add a restart marker */ + output[pos++] = 0; + FILE *f = fopen(argv[4], "wb"); + if (!f) { + perror("Failed to write output file"); + return 1; + } + pos += 0x3fff; + pos &= ~0x3fff; + fwrite(output + 0x4000, pos - 0x4000, 1, f); + fclose(f); + printf("\n"); + return truncate; + } + + fps_tracking += frame_multiplier; + unsigned frame_length = (unsigned) fps_tracking; + if (frame_length == 0) { + continue; + } + fps_tracking -= frame_length; + FILE *in = fopen(filepath, "rb"); + fseek(in, 18, SEEK_SET); // Skip header + /* Deinterleave the image (easier to process) while computing diff */ + fread(rgb, sizeof(rgb), 1, in); + for (unsigned i = 0; i < SCREEN_SIZE; i++) { + diff += abs(b[i] - rgb[i * 3]); + diff += abs(g[i] - rgb[i * 3 + 1]); + diff += abs(r[i] - rgb[i * 3 + 2]); + b[i] = rgb[i * 3]; + g[i] = rgb[i * 3 + 1]; + r[i] = rgb[i * 3 + 2]; + } + fclose(in); + if (id != 0 && diff < DIFF_THRESHOLD && (unsigned)output[frame_count_pos] + frame_length < 0xFF && + (frame_count_pos / 0x4000 != 0xFE)) { + output[frame_count_pos] += frame_length; + id++; + continue; + } + else { + diff = 0; + } + + /* Create shifted image for the blending effect */ + + memcpy(r + SCREEN_SIZE + 3, r, SCREEN_SIZE - 3); + memcpy(g + SCREEN_SIZE + 3, g, SCREEN_SIZE - 3); + memcpy(b + SCREEN_SIZE + 3, b, SCREEN_SIZE - 3); + + for (unsigned y = 0; y < 144; y++) { + r[y * 160 + SCREEN_SIZE] = r[y * 160 + SCREEN_SIZE + 1] = r[y * 160 + SCREEN_SIZE + 2] = r[y * 160 + SCREEN_SIZE + 3]; + g[y * 160 + SCREEN_SIZE] = g[y * 160 + SCREEN_SIZE + 1] = g[y * 160 + SCREEN_SIZE + 2] = g[y * 160 + SCREEN_SIZE + 3]; + b[y * 160 + SCREEN_SIZE] = b[y * 160 + SCREEN_SIZE + 1] = b[y * 160 + SCREEN_SIZE + 2] = b[y * 160 + SCREEN_SIZE + 3]; + } + + if (!palette_inited) { + for (unsigned i = 0; i < PALETTE_SIZE; i++) { + unsigned pixel = rand() % PIXELS; + palette[i].r = r[pixel]; + palette[i].g = g[pixel]; + palette[i].b = b[pixel]; + } + palette_inited = true; + } + + unsigned i = 128; + unsigned score = -1; + for (; i--;) { + if (!optimize_palette_step(r, g, b, palette, score, &score)) break; + } + + + memcpy(rounded_palette, palette, sizeof(palette)); + round_palette(rounded_palette); + /* + // For debugging + sprintf(filepath, "debug/%04u.tga", id); + dump_tga_paletted(filepath, r, g, b, palette); + */ + printf("\rEncoding frame %d ", id); + encode_image(r, g, b, palette, quality, frame_length); + id++; + } +} diff --git a/gbhw.asm b/gbhw.asm new file mode 100644 index 0000000..a2156c5 --- /dev/null +++ b/gbhw.asm @@ -0,0 +1,111 @@ +; Graciously aped from http://nocash.emubase.de/pandocs.htm . + +; MBC3 +MBC3SRamEnable EQU $0000 +MBC3RomBank EQU $2000 +MBC3SRamBank EQU $4000 +MBC3LatchClock EQU $6000 +MBC3RTC EQU $a000 + +SRAM_DISABLE EQU $00 +SRAM_ENABLE EQU $0a + +NUM_SRAM_BANKS EQU 4 + +RTC_S EQU $08 ; Seconds 0-59 (0-3Bh) +RTC_M EQU $09 ; Minutes 0-59 (0-3Bh) +RTC_H EQU $0a ; Hours 0-23 (0-17h) +RTC_DL EQU $0b ; Lower 8 bits of Day Counter (0-FFh) +RTC_DH EQU $0c ; Upper 1 bit of Day Counter, Carry Bit, Halt Flag + ; Bit 0 Most significant bit of Day Counter (Bit 8) + ; Bit 6 Halt (0=Active, 1=Stop Timer) + ; Bit 7 Day Counter Carry Bit (1=Counter Overflow) + +; interrupt flags +VBLANK EQU 0 +LCD_STAT EQU 1 +TIMER EQU 2 +SERIAL EQU 3 +JOYPAD EQU 4 + +; OAM attribute flags +OAM_PALETTE EQU %111 +OAM_TILE_BANK EQU 3 +OAM_OBP_NUM EQU 4 ; Non CGB Mode Only +OAM_X_FLIP EQU 5 +OAM_Y_FLIP EQU 6 +OAM_PRIORITY EQU 7 ; 0: OBJ above BG, 1: OBJ behind BG (colors 1-3) + + +; Hardware registers +rJOYP EQU $ff00 ; Joypad (R/W) +rSB EQU $ff01 ; Serial transfer data (R/W) +rSC EQU $ff02 ; Serial Transfer Control (R/W) +rSC_ON EQU 7 +rSC_CGB EQU 1 +rSC_CLOCK EQU 0 +rDIV EQU $ff04 ; Divider Register (R/W) +rTIMA EQU $ff05 ; Timer counter (R/W) +rTMA EQU $ff06 ; Timer Modulo (R/W) +rTAC EQU $ff07 ; Timer Control (R/W) +rTAC_ON EQU 2 +rTAC_4096_HZ EQU 0 +rTAC_262144_HZ EQU 1 +rTAC_65536_HZ EQU 2 +rTAC_16384_HZ EQU 3 +rIF EQU $ff0f ; Interrupt Flag (R/W) +rNR10 EQU $ff10 ; Channel 1 Sweep register (R/W) +rNR11 EQU $ff11 ; Channel 1 Sound length/Wave pattern duty (R/W) +rNR12 EQU $ff12 ; Channel 1 Volume Envelope (R/W) +rNR13 EQU $ff13 ; Channel 1 Frequency lo (Write Only) +rNR14 EQU $ff14 ; Channel 1 Frequency hi (R/W) +rNR21 EQU $ff16 ; Channel 2 Sound Length/Wave Pattern Duty (R/W) +rNR22 EQU $ff17 ; Channel 2 Volume Envelope (R/W) +rNR23 EQU $ff18 ; Channel 2 Frequency lo data (W) +rNR24 EQU $ff19 ; Channel 2 Frequency hi data (R/W) +rNR30 EQU $ff1a ; Channel 3 Sound on/off (R/W) +rNR31 EQU $ff1b ; Channel 3 Sound Length +rNR32 EQU $ff1c ; Channel 3 Select output level (R/W) +rNR33 EQU $ff1d ; Channel 3 Frequency's lower data (W) +rNR34 EQU $ff1e ; Channel 3 Frequency's higher data (R/W) +rNR41 EQU $ff20 ; Channel 4 Sound Length (R/W) +rNR42 EQU $ff21 ; Channel 4 Volume Envelope (R/W) +rNR43 EQU $ff22 ; Channel 4 Polynomial Counter (R/W) +rNR44 EQU $ff23 ; Channel 4 Counter/consecutive; Inital (R/W) +rNR50 EQU $ff24 ; Channel control / ON-OFF / Volume (R/W) +rNR51 EQU $ff25 ; Selection of Sound output terminal (R/W) +rNR52 EQU $ff26 ; Sound on/off +rLCDC EQU $ff40 ; LCD Control (R/W) +rSTAT EQU $ff41 ; LCDC Status (R/W) +rSCY EQU $ff42 ; Scroll Y (R/W) +rSCX EQU $ff43 ; Scroll X (R/W) +rLY EQU $ff44 ; LCDC Y-Coordinate (R) +rLYC EQU $ff45 ; LY Compare (R/W) +rDMA EQU $ff46 ; DMA Transfer and Start Address (W) +rBGP EQU $ff47 ; BG Palette Data (R/W) - Non CGB Mode Only +rOBP0 EQU $ff48 ; Object Palette 0 Data (R/W) - Non CGB Mode Only +rOBP1 EQU $ff49 ; Object Palette 1 Data (R/W) - Non CGB Mode Only +rWY EQU $ff4a ; Window Y Position (R/W) +rWX EQU $ff4b ; Window X Position minus 7 (R/W) +rKEY1 EQU $ff4d ; CGB Mode Only - Prepare Speed Switch +rVBK EQU $ff4f ; CGB Mode Only - VRAM Bank +rHDMA1 EQU $ff51 ; CGB Mode Only - New DMA Source, High +rHDMA2 EQU $ff52 ; CGB Mode Only - New DMA Source, Low +rHDMA3 EQU $ff53 ; CGB Mode Only - New DMA Destination, High +rHDMA4 EQU $ff54 ; CGB Mode Only - New DMA Destination, Low +rHDMA5 EQU $ff55 ; CGB Mode Only - New DMA Length/Mode/Start +rRP EQU $ff56 ; CGB Mode Only - Infrared Communications Port +rBGPI EQU $ff68 ; CGB Mode Only - Background Palette Index +rBGPD EQU $ff69 ; CGB Mode Only - Background Palette Data +rOBPI EQU $ff6a ; CGB Mode Only - Sprite Palette Index +rOBPD EQU $ff6b ; CGB Mode Only - Sprite Palette Data +rUNKNOWN1 EQU $ff6c ; (FEh) Bit 0 (Read/Write) - CGB Mode Only +rSVBK EQU $ff70 ; CGB Mode Only - WRAM Bank +rUNKNOWN2 EQU $ff72 ; (00h) - Bit 0-7 (Read/Write) +rUNKNOWN3 EQU $ff73 ; (00h) - Bit 0-7 (Read/Write) +rUNKNOWN4 EQU $ff74 ; (00h) - Bit 0-7 (Read/Write) - CGB Mode Only +rUNKNOWN5 EQU $ff75 ; (8Fh) - Bit 4-6 (Read/Write) +rUNKNOWN6 EQU $ff76 ; (00h) - Always 00h (Read Only) +rUNKNOWN7 EQU $ff77 ; (00h) - Always 00h (Read Only) +rIE EQU $ffff ; Interrupt Enable (R/W) + diff --git a/video.asm b/video.asm new file mode 100644 index 0000000..e2ca248 --- /dev/null +++ b/video.asm @@ -0,0 +1,649 @@ +INCLUDE "gbhw.asm" + +MBC5_bank_low EQU $2000 +MBC5_bank_high EQU $3000 + +audio_buffer EQU $c100 +line_buffer EQU ((audio_buffer >> 8) * $101) + +current_line EQU $94 ; For backup, current_line is stored in SP +current_bank EQU $96 ; For backup, current_bank is stored in b (Except for the 9th bit) + + +frame_repeat EQU $a0 +; Address to jump to on repeat +repeat_bank EQU $a1 +repeat_line EQU $a3 + +audio_bank EQU $a5 ; Since audio isn't that big, we use only one byte for bank number +audio_address EQU $a6 +first_sample_backup EQU $a8 + +compression_jr EQU $fd + + +SECTION "Player", ROM0[0] +Bank0: +; Write changed values (Up to 9) compared to the previous line +rept 9 + pop de + ld l, d + ld [hl], e +endr +; Decrease all values by 1 + ld l, h + dec [hl] +rept 19 + inc l + dec [hl] +endr + +WaitForInterrupt:: + ; Wait for interrupt + halt + xor a + ldh [rIF], a + + ; Update PCM sample + ld h, audio_buffer >> 8 + ldh a, [rLY] + ld l, a + cp 143 + ld a, [hl] + ldh [rNR50], a + + ; Render the line by modifying SCY + ld l, h ; hl now points to line_buffer +rept 20 + ld a, [hli] + ld [c], a ; c = rSCY +endr + jr z, _VBlank + ld l, h ; Due to how line_buffer is defined, hl now points to line_buffer + +; The main loop. HL will point to either a frame start, or a row start +Main:: + ; When entering Main, the following is assumed: + ; - c is rSCY + ; - b is the lowest 8 bits of bank + ; - sp points to current video position in ROM1 + ; - hl = line_buffer + + ; Read the first byte at sp + pop de + dec sp + ; Valid values for e: + ; $00 - Uncompressed, no new bank + ; $01 - Uncompressed, data starts at $4000 on new bank + ; $02 - Compressed, 9 changes + ; $08 - Compressed, 8 changes + ; $0e - Compressed, 7 changes + ; $14 - Compressed, 6 changes + ; $1a - Compressed, 5 changes + ; $20 - Compressed, 4 changes + ; $26 - Compressed, 3 changes + ; $2c - Compressed, 2 changes + ; $32 - Compressed, 1 changes + ; $38 - Compressed, no changes + sra e + jr nz, Compressed ; If neither 0 or 1, means line is compressed + jr nc, NoNewBank; ; If zero, means no bank switch needed + ; If 1, means bank switch required + + ; b is the current bank + inc b + ld h, MBC5_bank_low >> 8 + ld [hl], b ; Write to $20xx to bank switch + ld sp, $4000 ; Reset SP + ld h, l ; Due to how line_buffer is defined, hl now points to line_buffer + + +NoNewBank:: + ; Copy the current line (20 bytes) to line_buffer +rept 10 + pop de + ld a, e + ld [hli], a + ld a, d + ld [hli], a +endr + jr WaitForInterrupt + +_VBlank:: + jp VBlank +Compressed:: + ld a, e + ldh [compression_jr + 1], a + jp $ff00 + compression_jr +; Header +ds $100 - (@ - Bank0) + +_Start:: + di + jp Start + +; Start +ds $150 - (@ - Bank0) + +Start:: + ; Increase the CPU speed from 4MHz to 8MHz + ld a, 1 + ldh [rKEY1], a + stop + + ; Init the stack for the initialization routines + ld sp, $fffe + + ; Other inits + call InitAPU + call LCDOff + call LoadGraphics + call CreateMap + call CreateAttributeMap + + ; Start Playing + jp StartPlayback + +WaitVBlank:: + ldh a, [rLY] + cp 144 + jr nz, WaitVBlank + ret + +InitAPU:: + ; Reset the APU + xor a + ldh [rNR52], a + ld a, $80 + ldh [rNR52], a + ; Turn all DACs on + ldh [rNR12], a + ldh [rNR22], a + ldh [rNR30], a + ldh [rNR42], a + ; Put all channels on both left and right + ld a, $FF + ldh [rNR51], a + ret + +LCDOff:: + call WaitVBlank + ldh a, [rLCDC] + and $7F + ldh [rLCDC], a + ret + +LoadGraphics:: + ld de, $8000 + ld b, PixelStructureEnd - PixelStructure + ld hl, PixelStructure +.loop + ld a, [hli] + ld [de], a + inc de + dec b + jr nz, .loop + ret + +CreateMap:: + ld hl, $9800 + ld a, 0 + ld c, 32 + xor a +.loopY + ld b, 32 +.loopX + ld [hli], a + dec b + jr nz, .loopX + inc a + and 3 + dec c + jr nz, .loopY + ret + +CreateAttributeMap:: + ld a, 1 + ldh [rVBK], a + ld hl, $9800 + ld a, 7 + ld c, 32 +.loopY + ld b, 32 +.loopX + ld [hli], a + dec b + jr nz, .loopX + dec c + ld a, c + dec a + rra + rra + and 7 + jr nz, .loopY + ret + +PixelStructure:: + db %00000000 + db %00000000 + db %00011111 + db %00000000 + db %00000000 + db %00011111 + db %00011111 + db %00011111 + + db %11100000 + db %00000000 + db %11111111 + db %00000000 + db %11100000 + db %00011111 + db %11111111 + db %00011111 + + db %00000000 + db %11100000 + db %00011111 + db %11100000 + db %00000000 + db %11111111 + db %00011111 + db %11111111 + + db %11100000 + db %11100000 + db %11111111 + db %11100000 + db %11100000 + db %11111111 + db %11111111 + db %11111111 + + db %00110011 + db %00001111 + db %00000111 + db %00000000 + db %00000000 + db %00000111 + db %00000111 + db %00000111 + + db %11111000 + db %00000000 + db %11001100 + db %00001111 + db %11111000 + db %00000111 + db %11111111 + db %00000111 + + db %00000000 + db %11111000 + db %00000111 + db %11111000 + db %00110011 + db %11110000 + db %00000111 + db %11111111 + + db %11111000 + db %11111000 + db %11111111 + db %11111000 + db %11111000 + db %11111111 + db %11001100 + db %11110000 + +PixelStructureEnd:: + +StartPlayback: + + ; Enable timer interrupt + ld a, 4 + ldh [rIE], a + + ; Set SCX to 4. When HandleFrameRepeatAndUnshiftFramebuffer runs, + ; it will set it back to 0 and consider this (HW) frame as the start + ; of a (video) frame. + ldh [rSCX], a + + ; Set up the compression JR instruction + ld a, $18 + ldh [compression_jr], a + + ; Init variables + xor a + ldh [current_bank + 1], a + + ld hl, audio_buffer + ld c, 154 +.loop + ld [hli], a + dec c + jr nz, .loop + + ld [$3000], a + + ; Audio starts at 1:4001 + inc a + ldh [audio_bank], a + ldh [audio_address], a + ldh [frame_repeat], a ; Set frame repeat to 1 so the VBlank functions load + ; a new frame + + ld [$2000], a + + ld a, $40 + ldh [audio_address + 1], a + + ; Fill line_buffer with $101 - 144. When HandleFrameRepeatAndUnshiftFramebuffer + ; runs, it will add 144 to all bytes, setting them to the expected initial + ; value of $01. (Note: the encoder currently does not make use of this property) + ld a, $101 - 144 + ld c, 20 + ld l, h ; hl = line_buffer +.loop2 + ld [hli], a + dec c + jr nz, .loop2 + + ld c, rSCY & $FF ; C is assumed to be SCY when entring Main + ; Audio comes first, first byte at ROM1 is the first video bank + ld a, [$4000] + ld b, a ; B is assumed to be the current bank + ld [$2000], a + ld sp, $4000 ; Point SP to video start + ld hl, line_buffer + + ; Enable LCD + ldh a, [rLCDC] + or $80 + ldh [rLCDC], a + +.lyloop + ldh a, [rLY] + cp 143 + jr nz, .lyloop + + ; Exactly 2 NOPs are needed to sync properly on both CGB-D and newer and CGB-C and older + nop + nop + ; Configure timer + ld [rDIV], a ; Synchronize DIV + ld a, $100-(912 / 16) ; Configure modulo so we tick every 912 clocks (The length of a scanline in double speed mode) + ldh [rTMA], a + ld a, $c8 ; Configure the initial value of TIMA so we overflow at the right timing + ldh [rTIMA], a + ld a, 5 ; Enable, tick every 16 CPU clocks + ldh [rTAC], a + ; Clear interrupts + xor a + ldh [rIF], a + jp VBlank + +; Align to $100 +ds $100 - ((@ - Bank0) & $FF) +VBlankJumpTable:: + dw HandleFrameRepeatAndUnshiftFramebuffer ; 144 + dw HandleSCXAndLoadPalette ;145 + dw LoadPalette ; 146 + dw CopyPCM24 ; 147 + dw CopyPCM26 ; 148 + dw CopyPCM26 ; 149 + dw CopyPCM26 ; 150 + dw CopyPCM26 ; 151 + dw CopyPCMLY152 ; 152 + ; dw ReturnToMain ; During line 153 LY almost always reads 0, so it's handled outside of this table + +VBlank:: + ; Wait for interrupt + halt + xor a + ldh [rIF], a + + ; Update PCM sample + ld h, audio_buffer >> 8 + ldh a, [rLY] + ld l, a + cp 143 ; Result not used, only here to match the cycle count in MainLoop + ld a, [hl] + ldh [rNR50], a + + ld a, l + sub 144 + jr c, ReturnToMain + add a + ld l, a + ld h, VBlankJumpTable >> 8 + ld a, [hli] + ld h, [hl] + ld l, a + jp hl + + + +ReturnToMain:: + + ld a, b + ld [$2000], a + ldh a, [current_bank + 1] + ld [$3000], a + ld de, 153 + ld hl, sp+0 + add hl, de + ld a, $80 + cp h + jr nz, NoAudioBankSwitch + + ldh a, [audio_bank] + inc a + ldh [audio_bank], a + ld sp, $4000 +NoAudioBankSwitch:: + ld [$ff00 + audio_address], sp + ld hl, $ff00 + current_line + ld a, [hli] + ld h, [hl] + ld l, a + ld sp, hl + ldh a, [first_sample_backup] + ld [audio_buffer], a + ld hl, line_buffer + ld c, rSCY & $ff + jp Main + +; We probably have enough CPU cycles to implement compression + +CopyPCMLY152:: + ld a, [audio_buffer] + ldh [first_sample_backup], a +CopyPCM26:: + ld h, audio_buffer >> 8 + ld l, c +rept 26 / 2 + pop de + ld [hl], e + inc l + ld [hl], d + inc l +endr + ld c, l + jp VBlank ; Not implemented + +CopyPCM24:: + ld [$ff00 + current_line], sp + ld a, b + ldh [current_bank], a + ld hl, $ff00 + audio_bank + ld a, [hli] + ld [$2000], a + xor a + ld [$3000], a + ld a, [hli] + ld h, [hl] + ld l, a + ld sp, hl + ld hl, audio_buffer + +rept 24 / 2 + pop de + ld [hl], e + inc l + ld [hl], d + inc l +endr + ld c, l + jp VBlank ; Not implemented + +HandleFrameRepeatAndUnshiftFramebuffer:: + ld hl, line_buffer + ld d, 144 +rept 20 + ld a, [hl] + add a, d + ld [hli], a +endr + ldh a, [rSCX] + and a + jp z, VBlank ; A video frame is 2 GB frames + ld a, b + inc a + jr nz, BankNotFF + ; A frame must not start at bank $ff + ld [MBC5_bank_low], a + ld b, a + inc a + ld [MBC5_bank_high], a + ldh [current_bank + 1], a + ld sp, $4000 +BankNotFF:: + ldh a, [frame_repeat] + dec a + jr nz, RepeatFrame + pop de + dec sp + ld a, e + and a + jp z, RestartPlayback + ldh [frame_repeat], a + ld [$ff00 + repeat_line], sp + ld a, b + ldh [repeat_bank], a + ldh a, [current_bank + 1] + ldh [repeat_bank + 1], a + jp VBlank +RepeatFrame:: + ldh [frame_repeat], a + ld hl, $ff00 + repeat_bank + ld a, [hli] + ld [MBC5_bank_low], a + ld b, a + ld a, [hli] + ld [MBC5_bank_high], a + ld a, [hli] + ld h, [hl] + ld l, a + ld sp, hl + jp VBlank + +HandleSCXAndLoadPalette: + ldh a, [rSCX] + xor 4 + ldh [rSCX], a +LoadPalette: + ldh a, [rSCX] + and a + jp nz, VBlank ; A video frame is 2 HW frames + ; When here, SP points to a color. Since color is stored in Big Endian + ; mode, that byte is not allowed to be $ff. If we do point to $ff, + ; it means we need to bank switch. + pop de + dec sp + dec sp + ld a, e + inc a + jp nz, NoBankSwitch + + inc b + ld h, MBC5_bank_low >> 8 + ld [hl], b ; Write to $20xx to bank switch + ld sp, $4000 ; Reset SP +NoBankSwitch: + ld hl, rBGPD +rept 16 + pop de + ld [hl], d + ld [hl], e +endr + jp VBlank + + +RestartPlayback:: + ; Clear palette + ld hl, rBGPD + xor a +rept 64 + ld [hl], a +endr + ; Set SCX to 4. When HandleFrameRepeatAndUnshiftFramebuffer runs, + ; it will set it back to 0 and consider this (HW) frame as the start + ; of a (video) frame. + ld a, 4 + ldh [rSCX], a + + + ; Init variables + xor a + ldh [current_bank + 1], a + + ld hl, audio_buffer + ld c, 154 +.loop + ld [hli], a + dec c + jr nz, .loop + + ld [$3000], a + + ; Audio starts at 1:4001 + inc a + ldh [audio_bank], a + ldh [audio_address], a + ldh [frame_repeat], a ; Set frame repeat to 1 so the VBlank functions load + ; a new frame + + ld [$2000], a + + ld a, $40 + ldh [audio_address + 1], a + + ; Fill line_buffer with $101 - 144. When HandleFrameRepeatAndUnshiftFramebuffer + ; runs, it will add 144 to all bytes, setting them to the expected initial + ; value of $01. (Note: the encoder currently does not make use of this property) + ld a, $101 - 144 + ld c, 20 + ld l, h ; hl = line_buffer +.loop2 + ld [hli], a + dec c + jr nz, .loop2 + + ld c, rSCY & $FF ; C is assumed to be SCY when entring Main + ; Audio comes first, first byte at ROM1 is the first video bank + ld a, [$4000] + ld b, a ; B is assumed to be the current bank + ld [$2000], a + ld sp, $4000 ; Point SP to video start + ld hl, line_buffer + + +.lyloop + ldh a, [rLY] + cp 144 + jr nz, .lyloop + + xor a + ldh [rIF], a + jp VBlank \ No newline at end of file