From 87f7183a2712cfe343530d96c6d57bec45f342bb Mon Sep 17 00:00:00 2001
From: nattthebear <goyuken@gmail.com>
Date: Sun, 16 Jul 2017 12:23:41 -0400
Subject: [PATCH] sameboy: initial commit

---
 waterbox/sameboy/Makefile           |   55 +
 waterbox/sameboy/apu.c              |  497 +++++++
 waterbox/sameboy/apu.h              |   78 ++
 waterbox/sameboy/camera.c           |  149 +++
 waterbox/sameboy/camera.h           |   29 +
 waterbox/sameboy/debugger.c         | 1850 +++++++++++++++++++++++++++
 waterbox/sameboy/debugger.h         |   32 +
 waterbox/sameboy/display.c          |  800 ++++++++++++
 waterbox/sameboy/display.h          |   40 +
 waterbox/sameboy/gb.c               |  563 ++++++++
 waterbox/sameboy/gb.h               |  570 +++++++++
 waterbox/sameboy/gb_struct_def.h    |    5 +
 waterbox/sameboy/joypad.c           |   63 +
 waterbox/sameboy/joypad.h           |   22 +
 waterbox/sameboy/mbc.c              |  154 +++
 waterbox/sameboy/mbc.h              |   31 +
 waterbox/sameboy/memory.c           |  726 +++++++++++
 waterbox/sameboy/memory.h           |   12 +
 waterbox/sameboy/printer.c          |  201 +++
 waterbox/sameboy/printer.h          |   59 +
 waterbox/sameboy/save_state.c       |  304 +++++
 waterbox/sameboy/save_state.h       |   24 +
 waterbox/sameboy/symbol_hash.c      |  106 ++
 waterbox/sameboy/symbol_hash.h      |   37 +
 waterbox/sameboy/timing.c           |  228 ++++
 waterbox/sameboy/timing.h           |   21 +
 waterbox/sameboy/z80_cpu.c          | 1381 ++++++++++++++++++++
 waterbox/sameboy/z80_cpu.h          |   10 +
 waterbox/sameboy/z80_disassembler.c |  788 ++++++++++++
 29 files changed, 8835 insertions(+)
 create mode 100644 waterbox/sameboy/Makefile
 create mode 100644 waterbox/sameboy/apu.c
 create mode 100644 waterbox/sameboy/apu.h
 create mode 100644 waterbox/sameboy/camera.c
 create mode 100644 waterbox/sameboy/camera.h
 create mode 100644 waterbox/sameboy/debugger.c
 create mode 100644 waterbox/sameboy/debugger.h
 create mode 100644 waterbox/sameboy/display.c
 create mode 100644 waterbox/sameboy/display.h
 create mode 100644 waterbox/sameboy/gb.c
 create mode 100644 waterbox/sameboy/gb.h
 create mode 100644 waterbox/sameboy/gb_struct_def.h
 create mode 100644 waterbox/sameboy/joypad.c
 create mode 100644 waterbox/sameboy/joypad.h
 create mode 100644 waterbox/sameboy/mbc.c
 create mode 100644 waterbox/sameboy/mbc.h
 create mode 100644 waterbox/sameboy/memory.c
 create mode 100644 waterbox/sameboy/memory.h
 create mode 100644 waterbox/sameboy/printer.c
 create mode 100644 waterbox/sameboy/printer.h
 create mode 100644 waterbox/sameboy/save_state.c
 create mode 100644 waterbox/sameboy/save_state.h
 create mode 100644 waterbox/sameboy/symbol_hash.c
 create mode 100644 waterbox/sameboy/symbol_hash.h
 create mode 100644 waterbox/sameboy/timing.c
 create mode 100644 waterbox/sameboy/timing.h
 create mode 100644 waterbox/sameboy/z80_cpu.c
 create mode 100644 waterbox/sameboy/z80_cpu.h
 create mode 100644 waterbox/sameboy/z80_disassembler.c

diff --git a/waterbox/sameboy/Makefile b/waterbox/sameboy/Makefile
new file mode 100644
index 0000000000..0fdbe63b73
--- /dev/null
+++ b/waterbox/sameboy/Makefile
@@ -0,0 +1,55 @@
+CC = x86_64-nt64-midipix-gcc
+CPP = x86_64-nt64-midipix-g++
+
+FLAGS:=-Wall -Werror=pointer-to-int-cast -Werror=int-to-pointer-cast -Werror=implicit-function-declaration \
+	-Wno-multichar \
+	-fomit-frame-pointer -fvisibility=hidden \
+	-O0 -g
+
+CCFLAGS:=$(FLAGS) -Ilib \
+	-I../emulibc \
+	-std=gnu99 \
+	-DLSB_FIRST -D_GNU_SOURCE -DGB_INTERNAL
+
+CPPFLAGS:=$(FLAGS) -DSPC_NO_COPY_STATE_FUNCS
+
+TARGET = sameboy.wbx
+
+LDFLAGS = -Wl,--dynamicbase,--export-all-symbols
+
+ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
+C_SRCS:=$(shell find $(ROOT_DIR) -type f -name '*.c')
+CPP_SRCS:=$(shell find $(ROOT_DIR) -type f -name '*.cpp')
+SRCS:=$(C_SRCS) $(CPP_SRCS)
+OBJ_DIR:=$(ROOT_DIR)/obj
+
+__OBJS:=$(SRCS:.c=.o)
+_OBJS:=$(__OBJS:.cpp=.opp)
+OBJS:=$(patsubst $(ROOT_DIR)%,$(OBJ_DIR)%,$(_OBJS))
+
+$(OBJ_DIR)/%.o: %.c
+	@mkdir -p $(@D)
+	@$(CC) -c -o $@ $< $(CCFLAGS)
+
+$(OBJ_DIR)/%.opp: %.cpp
+	@mkdir -p $(@D)
+	@$(CPP) -c -o $@ $< $(CPPFLAGS)
+
+all: $(TARGET)
+
+.PHONY: clean all
+
+$(TARGET).in: $(OBJS)
+	@$(CPP) -o $@ $(LDFLAGS) $(FLAGS) $(OBJS) ../emulibc/libemuhost.so
+
+$(TARGET): $(TARGET).in
+#	strip $< -o $@ -R /4 -R /14 -R /29 -R /41 -R /55 -R /67 -R /78 -R /89 -R /104
+	cp $< $@
+
+clean:
+	rm -rf $(OBJ_DIR)
+	rm -f $(TARGET).in
+	rm -f $(TARGET)
+
+#install:
+#	$(CP) $(TARGET) $(DEST_$(ARCH))
diff --git a/waterbox/sameboy/apu.c b/waterbox/sameboy/apu.c
new file mode 100644
index 0000000000..e0e0b89f28
--- /dev/null
+++ b/waterbox/sameboy/apu.c
@@ -0,0 +1,497 @@
+#include <stdint.h>
+#include <math.h>
+#include <string.h>
+#include "gb.h"
+
+#undef max
+#define max(a,b) \
+({ __typeof__ (a) _a = (a); \
+__typeof__ (b) _b = (b); \
+_a > _b ? _a : _b; })
+
+#undef min
+#define min(a,b) \
+({ __typeof__ (a) _a = (a); \
+__typeof__ (b) _b = (b); \
+_a < _b ? _a : _b; })
+
+#define APU_FREQUENCY 0x80000
+
+static int16_t generate_square(uint64_t phase, uint32_t wave_length, int16_t amplitude, uint8_t duty)
+{
+    if (!wave_length) return 0;
+    if (phase % wave_length > wave_length * duty / 8) {
+        return amplitude;
+    }
+    return 0;
+}
+
+static int16_t generate_wave(uint64_t phase, uint32_t wave_length, int16_t amplitude, int8_t *wave, uint8_t shift)
+{
+    if (!wave_length) wave_length = 1;
+    phase = phase % wave_length;
+    return ((wave[(int)(phase * 32 / wave_length)]) >> shift) * (int)amplitude / 0xF;
+}
+
+static int16_t generate_noise(int16_t amplitude, uint16_t lfsr)
+{
+    if (lfsr & 1) {
+        return amplitude;
+    }
+    return 0;
+}
+
+static int16_t step_lfsr(uint16_t lfsr, bool uses_7_bit)
+{
+    bool xor = (lfsr & 1) ^ ((lfsr & 2) >> 1);
+    lfsr >>= 1;
+    if (xor) {
+        lfsr |= 0x4000;
+    }
+    if (uses_7_bit) {
+        lfsr &= ~0x40;
+        if (xor) {
+            lfsr |= 0x40;
+        }
+    }
+    return lfsr;
+}
+
+/* General Todo: The APU emulation seems to fail many accuracy tests. It might require a rewrite with
+   these tests in mind. */
+
+static void GB_apu_run_internal(GB_gameboy_t *gb)
+{
+    while (!__sync_bool_compare_and_swap(&gb->apu_lock, false, true));
+    uint32_t steps = gb->apu.apu_cycles / (CPU_FREQUENCY/APU_FREQUENCY);
+    if (!steps) goto exit;
+
+    gb->apu.apu_cycles %= (CPU_FREQUENCY/APU_FREQUENCY);
+    for (uint8_t i = 0; i < 4; i++) {
+        /* Phase */
+        gb->apu.wave_channels[i].phase += steps;
+        while (gb->apu.wave_channels[i].wave_length && gb->apu.wave_channels[i].phase >= gb->apu.wave_channels[i].wave_length) {
+            if (i == 3) {
+                gb->apu.lfsr = step_lfsr(gb->apu.lfsr, gb->apu.lfsr_7_bit);
+            }
+
+            gb->apu.wave_channels[i].phase -= gb->apu.wave_channels[i].wave_length;
+        }
+        /* Stop on Length */
+        if (gb->apu.wave_channels[i].stop_on_length) {
+            if (gb->apu.wave_channels[i].sound_length > 0) {
+                gb->apu.wave_channels[i].sound_length -= steps;
+            }
+            if (gb->apu.wave_channels[i].sound_length <= 0) {
+                gb->apu.wave_channels[i].amplitude = 0;
+                gb->apu.wave_channels[i].is_playing = false;
+                gb->apu.wave_channels[i].sound_length = i == 2? APU_FREQUENCY : APU_FREQUENCY / 4;
+            }
+        }
+    }
+
+    gb->apu.envelope_step_timer += steps;
+    while (gb->apu.envelope_step_timer >= APU_FREQUENCY / 64) {
+        gb->apu.envelope_step_timer -= APU_FREQUENCY / 64;
+        for (uint8_t i = 0; i < 4; i++) {
+            if (gb->apu.wave_channels[i].envelope_steps && !--gb->apu.wave_channels[i].cur_envelope_steps) {
+                gb->apu.wave_channels[i].amplitude = min(max(gb->apu.wave_channels[i].amplitude + gb->apu.wave_channels[i].envelope_direction * CH_STEP, 0), MAX_CH_AMP);
+                gb->apu.wave_channels[i].cur_envelope_steps = gb->apu.wave_channels[i].envelope_steps;
+            }
+        }
+    }
+
+    gb->apu.sweep_step_timer += steps;
+    while (gb->apu.sweep_step_timer >= APU_FREQUENCY / 128) {
+        gb->apu.sweep_step_timer -= APU_FREQUENCY / 128;
+        if (gb->apu.wave_channels[0].sweep_steps && !--gb->apu.wave_channels[0].cur_sweep_steps) {
+
+            // Convert back to GB format
+            uint16_t temp = 2048 - gb->apu.wave_channels[0].wave_length / (APU_FREQUENCY / 131072);
+
+            // Apply sweep
+            temp = temp + gb->apu.wave_channels[0].sweep_direction *
+                   (temp / (1 << gb->apu.wave_channels[0].sweep_shift));
+            if (temp > 2047) {
+                temp = 0;
+            }
+
+            // Back to frequency
+            gb->apu.wave_channels[0].wave_length =  (2048 - temp) *  (APU_FREQUENCY / 131072);
+
+
+            gb->apu.wave_channels[0].cur_sweep_steps = gb->apu.wave_channels[0].sweep_steps;
+        }
+    }
+exit:
+    gb->apu_lock = false;
+}
+
+void GB_apu_get_samples_and_update_pcm_regs(GB_gameboy_t *gb, GB_sample_t *samples)
+{
+    GB_apu_run_internal(gb);
+
+    samples->left = samples->right = 0;
+    if (!gb->apu.global_enable) {
+        return;
+    }
+
+    gb->io_registers[GB_IO_PCM_12] = 0;
+    gb->io_registers[GB_IO_PCM_34] = 0;
+
+    {
+        int16_t sample = generate_square(gb->apu.wave_channels[0].phase,
+                                         gb->apu.wave_channels[0].wave_length,
+                                         gb->apu.wave_channels[0].amplitude,
+                                         gb->apu.wave_channels[0].duty);
+        if (gb->apu.wave_channels[0].left_on ) samples->left  += sample;
+        if (gb->apu.wave_channels[0].right_on) samples->right += sample;
+        gb->io_registers[GB_IO_PCM_12] = ((int)sample) * 0xF / MAX_CH_AMP;
+    }
+
+    {
+        int16_t sample = generate_square(gb->apu.wave_channels[1].phase,
+                                         gb->apu.wave_channels[1].wave_length,
+                                         gb->apu.wave_channels[1].amplitude,
+                                         gb->apu.wave_channels[1].duty);
+        if (gb->apu.wave_channels[1].left_on ) samples->left  += sample;
+        if (gb->apu.wave_channels[1].right_on) samples->right += sample;
+        gb->io_registers[GB_IO_PCM_12] |= (((int)sample) * 0xF / MAX_CH_AMP) << 4;
+    }
+
+    if (gb->apu.wave_channels[2].is_playing)
+    {
+        int16_t sample = generate_wave(gb->apu.wave_channels[2].phase,
+                                       gb->apu.wave_channels[2].wave_length,
+                                       MAX_CH_AMP,
+                                       gb->apu.wave_form,
+                                       gb->apu.wave_shift);
+        if (gb->apu.wave_channels[2].left_on ) samples->left  += sample;
+        if (gb->apu.wave_channels[2].right_on) samples->right += sample;
+        gb->io_registers[GB_IO_PCM_34] = ((int)sample) * 0xF / MAX_CH_AMP;
+    }
+
+    {
+        int16_t sample = generate_noise(gb->apu.wave_channels[3].amplitude,
+                                        gb->apu.lfsr);
+        if (gb->apu.wave_channels[3].left_on ) samples->left  += sample;
+        if (gb->apu.wave_channels[3].right_on) samples->right += sample;
+        gb->io_registers[GB_IO_PCM_34] |= (((int)sample) * 0xF / MAX_CH_AMP) << 4;
+    }
+
+    samples->left = (int) samples->left * gb->apu.left_volume / 7;
+    samples->right = (int) samples->right * gb->apu.right_volume / 7;
+}
+
+void GB_apu_run(GB_gameboy_t *gb)
+{
+    if (gb->sample_rate == 0) {
+        if (gb->apu.apu_cycles > 0xFF00) {
+            GB_sample_t dummy;
+            GB_apu_get_samples_and_update_pcm_regs(gb, &dummy);
+        }
+        return;
+    }
+    while (gb->audio_copy_in_progress);
+    double ticks_per_sample = (double) CPU_FREQUENCY / gb->sample_rate;
+
+    if (gb->audio_quality == 0) {
+        GB_sample_t sample;
+        GB_apu_get_samples_and_update_pcm_regs(gb, &sample);
+        gb->current_supersample.left += sample.left;
+        gb->current_supersample.right += sample.right;
+        gb->n_subsamples++;
+    }
+    else if (gb->audio_quality != 1) {
+        double ticks_per_subsample = ticks_per_sample / gb->audio_quality;
+        if (ticks_per_subsample < 1) {
+            ticks_per_subsample = 1;
+        }
+        if (gb->apu_subsample_cycles > ticks_per_subsample) {
+            gb->apu_subsample_cycles -= ticks_per_subsample;
+        }
+        
+        GB_sample_t sample;
+        GB_apu_get_samples_and_update_pcm_regs(gb, &sample);
+        gb->current_supersample.left += sample.left;
+        gb->current_supersample.right += sample.right;
+        gb->n_subsamples++;
+    }
+
+    if (gb->apu_sample_cycles > ticks_per_sample) {
+        gb->apu_sample_cycles -= ticks_per_sample;
+        if (gb->audio_position == gb->buffer_size) {
+            /*
+             if (!gb->turbo) {
+                 GB_log(gb, "Audio overflow\n");
+             }
+             */
+        }
+        else {
+            if (gb->audio_quality == 1) {
+                GB_apu_get_samples_and_update_pcm_regs(gb, &gb->audio_buffer[gb->audio_position++]);
+            }
+            else {
+                gb->audio_buffer[gb->audio_position].left = round(gb->current_supersample.left / gb->n_subsamples);
+                gb->audio_buffer[gb->audio_position].right = round(gb->current_supersample.right / gb->n_subsamples);
+                gb->n_subsamples = 0;
+                gb->current_supersample = (GB_double_sample_t){0, };
+                gb->audio_position++;
+            }
+        }
+    }
+}
+
+void GB_apu_copy_buffer(GB_gameboy_t *gb, GB_sample_t *dest, unsigned int count)
+{
+    gb->audio_copy_in_progress = true;
+
+    if (!gb->audio_stream_started) {
+        // Intentionally fail the first copy to sync the stream with the Gameboy.
+        gb->audio_stream_started = true;
+        gb->audio_position = 0;
+    }
+
+    if (count > gb->audio_position) {
+        // GB_log(gb, "Audio underflow: %d\n", count - gb->audio_position);
+        if (gb->audio_position != 0) {
+            for (unsigned i = 0; i < count - gb->audio_position; i++) {
+                dest[gb->audio_position + i] = gb->audio_buffer[gb->audio_position - 1];
+            }
+        }
+        else {
+            memset(dest + gb->audio_position, 0, (count - gb->audio_position) * sizeof(*gb->audio_buffer));
+        }
+        count = gb->audio_position;
+    }
+    memcpy(dest, gb->audio_buffer, count * sizeof(*gb->audio_buffer));
+    memmove(gb->audio_buffer, gb->audio_buffer + count, (gb->audio_position - count) * sizeof(*gb->audio_buffer));
+    gb->audio_position -= count;
+
+    gb->audio_copy_in_progress = false;
+}
+
+void GB_apu_init(GB_gameboy_t *gb)
+{
+    memset(&gb->apu, 0, sizeof(gb->apu));
+    gb->apu.wave_channels[0].duty = gb->apu.wave_channels[1].duty = 4;
+    gb->apu.lfsr = 0x7FFF;
+    gb->apu.left_volume = 7;
+    gb->apu.right_volume = 7;
+    for (int i = 0; i < 4; i++) {
+        gb->apu.wave_channels[i].left_on = gb->apu.wave_channels[i].right_on = 1;
+    }
+}
+
+uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg)
+{
+    GB_apu_run_internal(gb);
+
+    if (reg == GB_IO_NR52) {
+        uint8_t value = 0;
+        for (int i = 0; i < 4; i++) {
+            value >>= 1;
+            if (gb->apu.wave_channels[i].is_playing) {
+                value |= 0x8;
+            }
+        }
+        if (gb->apu.global_enable) {
+            value |= 0x80;
+        }
+        value |= 0x70;
+        return value;
+    }
+
+    static const char read_mask[GB_IO_WAV_END - GB_IO_NR10 + 1] = {
+     /* NRX0  NRX1  NRX2  NRX3  NRX4 */
+        0x80, 0x3F, 0x00, 0xFF, 0xBF, // NR1X
+        0xFF, 0x3F, 0x00, 0xFF, 0xBF, // NR2X
+        0x7F, 0xFF, 0x9F, 0xFF, 0xBF, // NR3X
+        0xFF, 0xFF, 0x00, 0x00, 0xBF, // NR4X
+        0x00, 0x00, 0x70, 0xFF, 0xFF, // NR5X
+
+        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Unused
+        // Wave RAM
+        0, /* ... */
+    };
+
+    if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END && gb->apu.wave_channels[2].is_playing) {
+        if (gb->apu.wave_channels[2].wave_length == 0) {
+            return gb->apu.wave_form[0];
+        }
+        gb->apu.wave_channels[2].phase %= gb->apu.wave_channels[2].wave_length;
+        return gb->apu.wave_form[(int)(gb->apu.wave_channels[2].phase * 32 / gb->apu.wave_channels[2].wave_length)];
+    }
+
+    return gb->io_registers[reg] | read_mask[reg - GB_IO_NR10];
+}
+
+void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value)
+{
+    GB_apu_run_internal(gb);
+
+    static const uint8_t duties[] = {1, 2, 4, 6}; /* Values are in 1/8 */
+    uint8_t channel = 0;
+
+    if (!gb->apu.global_enable && reg != GB_IO_NR52) {
+        return;
+    }
+
+    gb->io_registers[reg] = value;
+
+    switch (reg) {
+        case GB_IO_NR10:
+        case GB_IO_NR11:
+        case GB_IO_NR12:
+        case GB_IO_NR13:
+        case GB_IO_NR14:
+            channel = 0;
+            break;
+        case GB_IO_NR21:
+        case GB_IO_NR22:
+        case GB_IO_NR23:
+        case GB_IO_NR24:
+            channel = 1;
+            break;
+        case GB_IO_NR33:
+        case GB_IO_NR34:
+            channel = 2;
+            break;
+        case GB_IO_NR41:
+        case GB_IO_NR42:
+            channel = 3;
+        default:
+            break;
+    }
+
+    switch (reg) {
+        case GB_IO_NR10:
+            gb->apu.wave_channels[channel].sweep_direction = value & 8? -1 : 1;
+            gb->apu.wave_channels[channel].cur_sweep_steps =
+            gb->apu.wave_channels[channel].sweep_steps = (value & 0x70) >> 4;
+            gb->apu.wave_channels[channel].sweep_shift = value & 7;
+            break;
+        case GB_IO_NR11:
+        case GB_IO_NR21:
+        case GB_IO_NR41:
+            gb->apu.wave_channels[channel].duty = duties[value >> 6];
+            gb->apu.wave_channels[channel].sound_length = (64 - (value & 0x3F)) * (APU_FREQUENCY / 256);
+            if (gb->apu.wave_channels[channel].sound_length == 0) {
+                gb->apu.wave_channels[channel].is_playing = false;
+            }
+            break;
+        case GB_IO_NR12:
+        case GB_IO_NR22:
+        case GB_IO_NR42:
+            gb->apu.wave_channels[channel].start_amplitude =
+            gb->apu.wave_channels[channel].amplitude = CH_STEP * (value >> 4);
+            if (value >> 4 == 0) {
+                gb->apu.wave_channels[channel].is_playing = false;
+            }
+            gb->apu.wave_channels[channel].envelope_direction = value & 8? 1 : -1;
+            gb->apu.wave_channels[channel].cur_envelope_steps =
+            gb->apu.wave_channels[channel].envelope_steps = value & 7;
+            break;
+        case GB_IO_NR13:
+        case GB_IO_NR23:
+        case GB_IO_NR33:
+            gb->apu.wave_channels[channel].NRX3_X4_temp = (gb->apu.wave_channels[channel].NRX3_X4_temp & 0xFF00) | value;
+            gb->apu.wave_channels[channel].wave_length =  (2048 - gb->apu.wave_channels[channel].NRX3_X4_temp) *  (APU_FREQUENCY / 131072);
+            if (channel == 2) {
+                gb->apu.wave_channels[channel].wave_length *= 2;
+            }
+            break;
+        case GB_IO_NR14:
+        case GB_IO_NR24:
+        case GB_IO_NR34:
+            gb->apu.wave_channels[channel].stop_on_length = value & 0x40;
+            if ((value & 0x80) && (channel != 2 || gb->apu.wave_enable)) {
+                gb->apu.wave_channels[channel].is_playing = true;
+                gb->apu.wave_channels[channel].phase = 0;
+                gb->apu.wave_channels[channel].amplitude = gb->apu.wave_channels[channel].start_amplitude;
+                gb->apu.wave_channels[channel].cur_envelope_steps = gb->apu.wave_channels[channel].envelope_steps;
+            }
+
+            gb->apu.wave_channels[channel].NRX3_X4_temp = (gb->apu.wave_channels[channel].NRX3_X4_temp & 0xFF) | ((value & 0x7) << 8);
+            gb->apu.wave_channels[channel].wave_length =  (2048 - gb->apu.wave_channels[channel].NRX3_X4_temp) *  (APU_FREQUENCY / 131072);
+            if (channel == 2) {
+                gb->apu.wave_channels[channel].wave_length *= 2;
+            }
+            break;
+        case GB_IO_NR30:
+            gb->apu.wave_enable = value & 0x80;
+            gb->apu.wave_channels[2].is_playing &= gb->apu.wave_enable;
+            break;
+        case GB_IO_NR31:
+            gb->apu.wave_channels[2].sound_length = (256 - value) * (APU_FREQUENCY / 256);
+            if (gb->apu.wave_channels[2].sound_length == 0) {
+                gb->apu.wave_channels[2].is_playing = false;
+            }
+            break;
+        case GB_IO_NR32:
+            gb->apu.wave_shift = ((value >> 5) + 3) & 3;
+            if (gb->apu.wave_shift == 3) {
+                gb->apu.wave_shift = 4;
+            }
+            break;
+        case GB_IO_NR43:
+        {
+            double r = value & 0x7;
+            if (r == 0) r = 0.5;
+            uint8_t s = value >> 4;
+            gb->apu.wave_channels[3].wave_length = r * (1 << s) * (APU_FREQUENCY / 262144) ;
+            gb->apu.lfsr_7_bit = value & 0x8;
+            break;
+        }
+        case GB_IO_NR44:
+            gb->apu.wave_channels[3].stop_on_length = value & 0x40;
+            if (value & 0x80) {
+                gb->apu.wave_channels[3].is_playing = true;
+                gb->apu.lfsr = 0x7FFF;
+                gb->apu.wave_channels[3].amplitude = gb->apu.wave_channels[3].start_amplitude;
+                gb->apu.wave_channels[3].cur_envelope_steps = gb->apu.wave_channels[3].envelope_steps;
+            }
+            break;
+
+        case GB_IO_NR50:
+            gb->apu.left_volume = (value & 7);
+            gb->apu.right_volume = ((value >> 4) & 7);
+            break;
+
+        case GB_IO_NR51:
+            for (int i = 0; i < 4; i++) {
+                gb->apu.wave_channels[i].left_on = value & 1;
+                gb->apu.wave_channels[i].right_on = value & 0x10;
+                value >>= 1;
+            }
+            break;
+        case GB_IO_NR52:
+
+            if ((value & 0x80) && !gb->apu.global_enable) {
+                GB_apu_init(gb);
+                gb->apu.global_enable = true;
+            }
+            else if (!(value & 0x80) && gb->apu.global_enable)  {
+                memset(&gb->apu, 0, sizeof(gb->apu));
+                memset(gb->io_registers + GB_IO_NR10, 0, GB_IO_WAV_START - GB_IO_NR10);
+            }
+            break;
+
+        default:
+            if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END) {
+                gb->apu.wave_form[(reg - GB_IO_WAV_START) * 2] = value >> 4;
+                gb->apu.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = value & 0xF;
+            }
+            break;
+    }
+}
+
+void GB_set_audio_quality(GB_gameboy_t *gb, unsigned quality)
+{
+    gb->audio_quality = quality;
+}
+
+unsigned GB_apu_get_current_buffer_length(GB_gameboy_t *gb)
+{
+    return  gb->audio_position;
+}
diff --git a/waterbox/sameboy/apu.h b/waterbox/sameboy/apu.h
new file mode 100644
index 0000000000..ffc530e693
--- /dev/null
+++ b/waterbox/sameboy/apu.h
@@ -0,0 +1,78 @@
+#ifndef apu_h
+#define apu_h
+#include <stdbool.h>
+#include <stdint.h>
+#include "gb_struct_def.h"
+/* Divides nicely and never overflows with 4 channels */
+#define MAX_CH_AMP 0x1E00
+#define CH_STEP (0x1E00/0xF)
+
+
+typedef struct
+{
+    int16_t left;
+    int16_t right;
+} GB_sample_t;
+
+typedef struct
+{
+    double left;
+    double right;
+} GB_double_sample_t;
+
+/* Not all used on all channels */
+/* All lengths are in APU ticks */
+typedef struct
+{
+    uint32_t phase;
+    uint32_t wave_length;
+    int32_t sound_length;
+    bool stop_on_length;
+    uint8_t duty;
+    int16_t amplitude;
+    int16_t start_amplitude;
+    uint8_t envelope_steps;
+    uint8_t cur_envelope_steps;
+    int8_t envelope_direction;
+    uint8_t sweep_steps;
+    uint8_t cur_sweep_steps;
+    int8_t sweep_direction;
+    uint8_t sweep_shift;
+    bool is_playing;
+    uint16_t NRX3_X4_temp;
+    bool left_on;
+    bool right_on;
+} GB_apu_channel_t;
+
+typedef struct
+{
+    uint16_t apu_cycles;
+    bool global_enable;
+    uint32_t envelope_step_timer;
+    uint32_t sweep_step_timer;
+    int8_t wave_form[32];
+    uint8_t wave_shift;
+    bool wave_enable;
+    uint16_t lfsr;
+    bool lfsr_7_bit;
+    uint8_t left_volume;
+    uint8_t right_volume;
+    GB_apu_channel_t wave_channels[4];
+} GB_apu_t;
+
+void GB_set_sample_rate(GB_gameboy_t *gb, unsigned int sample_rate);
+/* Quality is the number of subsamples per sampling, for the sake of resampling.
+   1 means on resampling at all, 0 is maximum quality. Default is 4. */
+void GB_set_audio_quality(GB_gameboy_t *gb, unsigned quality);
+void GB_apu_copy_buffer(GB_gameboy_t *gb, GB_sample_t *dest, unsigned int count);
+unsigned GB_apu_get_current_buffer_length(GB_gameboy_t *gb);
+
+#ifdef GB_INTERNAL
+void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value);
+uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg);
+void GB_apu_get_samples_and_update_pcm_regs(GB_gameboy_t *gb, GB_sample_t *samples);
+void GB_apu_init(GB_gameboy_t *gb);
+void GB_apu_run(GB_gameboy_t *gb);
+#endif
+
+#endif /* apu_h */
diff --git a/waterbox/sameboy/camera.c b/waterbox/sameboy/camera.c
new file mode 100644
index 0000000000..9b34998af3
--- /dev/null
+++ b/waterbox/sameboy/camera.c
@@ -0,0 +1,149 @@
+#include "gb.h"
+
+static int noise_seed = 0;
+
+/* This is not a complete emulation of the camera chip. Only the features used by the GameBoy Camera ROMs are supported.
+    We also do not emulate the timing of the real cart, as it might be actually faster than the webcam. */
+
+static uint8_t generate_noise(uint8_t x, uint8_t y)
+{
+    int value = (x + y * 128 + noise_seed);
+    uint8_t *data = (uint8_t *) &value;
+    unsigned hash = 0;
+
+    while ((int *) data != &value + 1) {
+        hash ^= (*data << 8);
+        if (hash & 0x8000) {
+            hash ^= 0x8a00;
+            hash ^= *data;
+        }
+        data++;
+        hash <<= 1;
+    }
+    return (hash >> 8);
+}
+
+static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y)
+{
+    if (x >= 128) {
+        x = 0;
+    }
+    if (y >= 112) {
+        y = 0;
+    }
+
+    long color = gb->camera_get_pixel_callback? gb->camera_get_pixel_callback(gb, x, y) : (generate_noise(x, y));
+
+    static const double gain_values[] =
+        {0.8809390, 0.9149149, 0.9457498, 0.9739758,
+         1.0000000, 1.0241412, 1.0466537, 1.0677433,
+         1.0875793, 1.1240310, 1.1568911, 1.1868043,
+         1.2142561, 1.2396208, 1.2743837, 1.3157323,
+         1.3525190, 1.3856512, 1.4157897, 1.4434309,
+         1.4689574, 1.4926697, 1.5148087, 1.5355703,
+         1.5551159, 1.5735801, 1.5910762, 1.6077008,
+         1.6235366, 1.6386550, 1.6531183, 1.6669808};
+    /* Multiply color by gain value */
+    color *= gain_values[gb->camera_registers[GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS] & 0x1F];
+
+
+    /* Color is multiplied by the exposure register to simulate exposure. */
+    color = color * ((gb->camera_registers[GB_CAMERA_EXPOSURE_HIGH] << 8) + gb->camera_registers[GB_CAMERA_EXPOSURE_LOW]) / 0x1000;
+
+    return color;
+}
+
+uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) {
+        /* Forbid reading the image while the camera is busy. */
+        return 0xFF;
+    }
+    uint8_t tile_x = addr / 0x10 % 0x10;
+    uint8_t tile_y = addr / 0x10 / 0x10;
+
+    uint8_t y = ((addr >> 1) & 0x7) + tile_y * 8;
+    uint8_t bit = addr & 1;
+
+    uint8_t ret = 0;
+
+    for (uint8_t x = tile_x * 8; x < tile_x * 8 + 8; x++) {
+
+        long color = get_processed_color(gb, x, y);
+
+        static const double edge_enhancement_ratios[] = {0.5, 0.75, 1, 1.25, 2, 3, 4, 5};
+        double edge_enhancement_ratio = edge_enhancement_ratios[(gb->camera_registers[GB_CAMERA_EDGE_ENHANCEMENT_INVERT_AND_VOLTAGE] >> 4) & 0x7];
+        if ((gb->camera_registers[GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS] & 0xE0) == 0xE0) {
+                color += (color * 4) * edge_enhancement_ratio;
+                color -= get_processed_color(gb, x - 1, y) * edge_enhancement_ratio;
+                color -= get_processed_color(gb, x + 1, y) * edge_enhancement_ratio;
+                color -= get_processed_color(gb, x, y - 1) * edge_enhancement_ratio;
+                color -= get_processed_color(gb, x, y + 1) * edge_enhancement_ratio;
+        }
+
+
+        /* The camera's registers are used as a threshold pattern, which defines the dithering */
+        uint8_t pattern_base = ((x & 3) + (y & 3) * 4) * 3 + GB_CAMERA_DITHERING_PATTERN_START;
+
+        if (color < gb->camera_registers[pattern_base]) {
+            color = 3;
+        }
+        else if (color < gb->camera_registers[pattern_base + 1]) {
+            color = 2;
+        }
+        else if (color < gb->camera_registers[pattern_base + 2]) {
+            color = 1;
+        }
+        else {
+            color = 0;
+        }
+
+        ret <<= 1;
+        ret |= (color >> bit) & 1;
+    }
+
+    return ret;
+}
+
+void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_callback_t callback)
+{
+    gb->camera_get_pixel_callback = callback;
+}
+
+void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback)
+{
+    gb->camera_update_request_callback = callback;
+}
+
+void GB_camera_updated(GB_gameboy_t *gb)
+{
+    gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] &= ~1;
+}
+
+void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    addr &= 0x7F;
+    if (addr == GB_CAMERA_SHOOT_AND_1D_FLAGS) {
+        value &= 0x7;
+        noise_seed = rand();
+        if ((value & 1) && !(gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) && gb->camera_update_request_callback) {
+            /* If no callback is set, ignore the write as if the camera is instantly done */
+            gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] |= 1;
+            gb->camera_update_request_callback(gb);
+        }
+    }
+    else {
+        if (addr >= 0x36) {
+            GB_log(gb, "Wrote invalid camera register %02x: %2x\n", addr, value);
+            return;
+        }
+        gb->camera_registers[addr] = value;
+    }
+}
+uint8_t GB_camera_read_register(GB_gameboy_t *gb, uint16_t addr)
+{
+    if ((addr & 0x7F) == 0) {
+        return gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS];
+    }
+    return 0;
+}
diff --git a/waterbox/sameboy/camera.h b/waterbox/sameboy/camera.h
new file mode 100644
index 0000000000..21c69b68e6
--- /dev/null
+++ b/waterbox/sameboy/camera.h
@@ -0,0 +1,29 @@
+#ifndef camera_h
+#define camera_h
+#include <stdint.h>
+#include "gb_struct_def.h"
+
+typedef uint8_t (*GB_camera_get_pixel_callback_t)(GB_gameboy_t *gb, uint8_t x, uint8_t y);
+typedef void (*GB_camera_update_request_callback_t)(GB_gameboy_t *gb);
+
+enum {
+    GB_CAMERA_SHOOT_AND_1D_FLAGS = 0,
+    GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS = 1,
+    GB_CAMERA_EXPOSURE_HIGH = 2,
+    GB_CAMERA_EXPOSURE_LOW = 3,
+    GB_CAMERA_EDGE_ENHANCEMENT_INVERT_AND_VOLTAGE = 4,
+    GB_CAMERA_DITHERING_PATTERN_START = 6,
+    GB_CAMERA_DITHERING_PATTERN_END = 0x35,
+};
+
+uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr);
+
+void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_callback_t callback);
+void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback);
+
+void GB_camera_updated(GB_gameboy_t *gb);
+
+void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value);
+uint8_t GB_camera_read_register(GB_gameboy_t *gb, uint16_t addr);
+
+#endif
diff --git a/waterbox/sameboy/debugger.c b/waterbox/sameboy/debugger.c
new file mode 100644
index 0000000000..7fc7b2a9a6
--- /dev/null
+++ b/waterbox/sameboy/debugger.c
@@ -0,0 +1,1850 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include "gb.h"
+
+typedef struct {
+    bool has_bank;
+    uint16_t bank:9;
+    uint16_t value;
+} value_t;
+
+typedef struct {
+    enum {
+        LVALUE_MEMORY,
+        LVALUE_REG16,
+        LVALUE_REG_H,
+        LVALUE_REG_L,
+    } kind;
+    union {
+        uint16_t *register_address;
+        value_t memory_address;
+    };
+} lvalue_t;
+
+#define VALUE_16(x) ((value_t){false, 0, (x)})
+
+struct GB_breakpoint_s {
+    union {
+        struct {
+        uint16_t addr;
+        uint16_t bank; /* -1 = any bank*/
+        };
+        uint32_t key; /* For sorting and comparing */
+    };
+    char *condition;
+};
+
+#define BP_KEY(x) (((struct GB_breakpoint_s){.addr = ((x).value), .bank = (x).has_bank? (x).bank : -1 }).key)
+
+#define GB_WATCHPOINT_R (1)
+#define GB_WATCHPOINT_W (2)
+
+struct GB_watchpoint_s {
+    union {
+        struct {
+            uint16_t addr;
+            uint16_t bank; /* -1 = any bank*/
+        };
+        uint32_t key; /* For sorting and comparing */
+    };
+    char *condition;
+    uint8_t flags;
+};
+
+#define WP_KEY(x) (((struct GB_watchpoint_s){.addr = ((x).value), .bank = (x).has_bank? (x).bank : -1 }).key)
+
+static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (addr < 0x4000) {
+        return gb->mbc_rom0_bank;
+    }
+
+    if (addr < 0x8000) {
+        return gb->mbc_rom_bank;
+    }
+
+    if (addr < 0xD000) {
+        return 0;
+    }
+
+    if (addr < 0xE000) {
+        return gb->cgb_ram_bank;
+    }
+
+    return 0;
+}
+
+typedef struct {
+    uint16_t rom0_bank;
+    uint16_t rom_bank;
+    uint8_t mbc_ram_bank;
+    bool mbc_ram_enable;
+    uint8_t ram_bank;
+    uint8_t vram_bank;
+} banking_state_t;
+
+static inline void save_banking_state(GB_gameboy_t *gb, banking_state_t *state)
+{
+    state->rom0_bank = gb->mbc_rom0_bank;
+    state->rom_bank = gb->mbc_rom_bank;
+    state->mbc_ram_bank = gb->mbc_ram_bank;
+    state->mbc_ram_enable = gb->mbc_ram_enable;
+    state->ram_bank = gb->cgb_ram_bank;
+    state->vram_bank = gb->cgb_vram_bank;
+}
+
+static inline void restore_banking_state(GB_gameboy_t *gb, banking_state_t *state)
+{
+
+    gb->mbc_rom0_bank = state->rom0_bank;
+    gb->mbc_rom_bank = state->rom_bank;
+    gb->mbc_ram_bank = state->mbc_ram_bank;
+    gb->mbc_ram_enable = state->mbc_ram_enable;
+    gb->cgb_ram_bank = state->ram_bank;
+    gb->cgb_vram_bank = state->vram_bank;
+}
+
+static inline void switch_banking_state(GB_gameboy_t *gb, uint16_t bank)
+{
+    gb->mbc_rom0_bank = bank;
+    gb->mbc_rom_bank = bank;
+    gb->mbc_ram_bank = bank;
+    gb->mbc_ram_enable = true;
+    if (gb->is_cgb) {
+        gb->cgb_ram_bank = bank & 7;
+        gb->cgb_vram_bank = bank & 1;
+    }
+}
+
+static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer_name)
+{
+    static __thread char output[256];
+    const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, value);
+
+    if (symbol && (value - symbol->addr > 0x1000 || symbol->addr == 0) ) {
+        symbol = NULL;
+    }
+
+    /* Avoid overflow */
+    if (symbol && strlen(symbol->name) > 240) {
+        symbol = NULL;
+    }
+
+    if (!symbol) {
+        sprintf(output, "$%04x", value);
+    }
+
+    else if (symbol->addr == value) {
+        if (prefer_name) {
+            sprintf(output, "%s ($%04x)", symbol->name, value);
+        }
+        else {
+            sprintf(output, "$%04x (%s)", value, symbol->name);
+        }
+    }
+
+    else {
+        if (prefer_name) {
+            sprintf(output, "%s+$%03x ($%04x)", symbol->name, value - symbol->addr, value);
+        }
+        else {
+            sprintf(output, "$%04x (%s+$%03x)", value, symbol->name, value - symbol->addr);
+        }
+    }
+    return output;
+}
+
+static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, bool prefer_name)
+{
+    if (!value.has_bank) return value_to_string(gb, value.value, prefer_name);
+
+    static __thread char output[256];
+    const GB_bank_symbol_t *symbol = GB_map_find_symbol(gb->bank_symbols[value.bank], value.value);
+
+    if (symbol && (value.value - symbol->addr > 0x1000 || symbol->addr == 0) ) {
+        symbol = NULL;
+    }
+
+    /* Avoid overflow */
+    if (symbol && strlen(symbol->name) > 240) {
+        symbol = NULL;
+    }
+
+    if (!symbol) {
+        sprintf(output, "$%02x:$%04x", value.bank, value.value);
+    }
+
+    else if (symbol->addr == value.value) {
+        if (prefer_name) {
+            sprintf(output, "%s ($%02x:$%04x)", symbol->name, value.bank, value.value);
+        }
+        else {
+            sprintf(output, "$%02x:$%04x (%s)", value.bank, value.value, symbol->name);
+        }
+    }
+
+    else {
+        if (prefer_name) {
+            sprintf(output, "%s+$%03x ($%02x:$%04x)", symbol->name, value.value - symbol->addr, value.bank, value.value);
+        }
+        else {
+            sprintf(output, "$%02x:$%04x (%s+$%03x)", value.bank, value.value, symbol->name, value.value - symbol->addr);
+        }
+    }
+    return output;
+}
+
+static value_t read_lvalue(GB_gameboy_t *gb, lvalue_t lvalue)
+{
+    /* Not used until we add support for operators like += */
+    switch (lvalue.kind) {
+        case LVALUE_MEMORY:
+            if (lvalue.memory_address.has_bank) {
+                banking_state_t state;
+                save_banking_state(gb, &state);
+                switch_banking_state(gb, lvalue.memory_address.bank);
+                value_t r = VALUE_16(GB_read_memory(gb, lvalue.memory_address.value));
+                restore_banking_state(gb, &state);
+                return r;
+            }
+            return VALUE_16(GB_read_memory(gb, lvalue.memory_address.value));
+
+        case LVALUE_REG16:
+            return VALUE_16(*lvalue.register_address);
+
+        case LVALUE_REG_L:
+            return VALUE_16(*lvalue.register_address & 0x00FF);
+
+        case LVALUE_REG_H:
+            return VALUE_16(*lvalue.register_address >> 8);
+    }
+}
+
+static void write_lvalue(GB_gameboy_t *gb, lvalue_t lvalue, uint16_t value)
+{
+    switch (lvalue.kind) {
+        case LVALUE_MEMORY:
+            if (lvalue.memory_address.has_bank) {
+                banking_state_t state;
+                save_banking_state(gb, &state);
+                switch_banking_state(gb, lvalue.memory_address.bank);
+                GB_write_memory(gb, lvalue.memory_address.value, value);
+                restore_banking_state(gb, &state);
+                return;
+            }
+            GB_write_memory(gb, lvalue.memory_address.value, value);
+            return;
+
+        case LVALUE_REG16:
+            *lvalue.register_address = value;
+            return;
+
+        case LVALUE_REG_L:
+            *lvalue.register_address &= 0xFF00;
+            *lvalue.register_address |= value & 0xFF;
+            return;
+
+        case LVALUE_REG_H:
+            *lvalue.register_address &= 0x00FF;
+            *lvalue.register_address |= value << 8;
+            return;
+    }
+}
+
+/* 16 bit value   <op> 16 bit value   = 16 bit value
+   25 bit address <op> 16 bit value   = 25 bit address
+   16 bit value   <op> 25 bit address = 25 bit address
+   25 bit address <op> 25 bit address = 16 bit value (since adding pointers, for examples, makes no sense)
+ 
+   Boolean operators always return a 16-bit value
+   */
+#define FIX_BANK(x) ((value_t){a.has_bank ^ b.has_bank, a.has_bank? a.bank : b.bank, (x)})
+
+static value_t add(value_t a, value_t b) {return FIX_BANK(a.value + b.value);}
+static value_t sub(value_t a, value_t b) {return FIX_BANK(a.value - b.value);}
+static value_t mul(value_t a, value_t b) {return FIX_BANK(a.value * b.value);}
+static value_t _div(value_t a, value_t b) {
+    if (b.value == 0) {
+        return FIX_BANK(0);
+    }
+    return FIX_BANK(a.value / b.value);
+};
+static value_t mod(value_t a, value_t b) {
+    if (b.value == 0) {
+        return FIX_BANK(0);
+    }
+    return FIX_BANK(a.value % b.value);
+};
+static value_t and(value_t a, value_t b) {return FIX_BANK(a.value & b.value);}
+static value_t or(value_t a, value_t b) {return FIX_BANK(a.value | b.value);}
+static value_t xor(value_t a, value_t b) {return FIX_BANK(a.value ^ b.value);}
+static value_t shleft(value_t a, value_t b) {return FIX_BANK(a.value << b.value);}
+static value_t shright(value_t a, value_t b) {return FIX_BANK(a.value >> b.value);}
+static value_t assign(GB_gameboy_t *gb, lvalue_t a, uint16_t b)
+{
+    write_lvalue(gb, a, b);
+    return read_lvalue(gb, a);
+}
+
+static value_t bool_and(value_t a, value_t b) {return VALUE_16(a.value && b.value);}
+static value_t bool_or(value_t a, value_t b) {return VALUE_16(a.value || b.value);}
+static value_t equals(value_t a, value_t b) {return VALUE_16(a.value == b.value);}
+static value_t different(value_t a, value_t b) {return VALUE_16(a.value != b.value);}
+static value_t lower(value_t a, value_t b) {return VALUE_16(a.value < b.value);}
+static value_t greater(value_t a, value_t b) {return VALUE_16(a.value > b.value);}
+static value_t lower_equals(value_t a, value_t b) {return VALUE_16(a.value <= b.value);}
+static value_t greater_equals(value_t a, value_t b) {return VALUE_16(a.value >= b.value);}
+static value_t bank(value_t a, value_t b) {return (value_t) {true, a.value, b.value};}
+
+
+static struct {
+    const char *string;
+    char priority;
+    value_t (*operator)(value_t, value_t);
+    value_t (*lvalue_operator)(GB_gameboy_t *, lvalue_t, uint16_t);
+} operators[] =
+{
+    // Yes. This is not C-like. But it makes much more sense.
+    // Deal with it.
+    {"+", 0, add},
+    {"-", 0, sub},
+    {"||", 0, bool_or},
+    {"|", 0, or},
+    {"*", 1, mul},
+    {"/", 1, _div},
+    {"%", 1, mod},
+    {"&&", 1, bool_and},
+    {"&", 1, and},
+    {"^", 1, xor},
+    {"<<", 2, shleft},
+    {"<=", 3, lower_equals},
+    {"<", 3, lower},
+    {">>", 2, shright},
+    {">=", 3, greater_equals},
+    {">", 3, greater},
+    {"==", 3, equals},
+    {"=", -1, NULL, assign},
+    {"!=", 3, different},
+    {":", 4, bank},
+};
+
+value_t debugger_evaluate(GB_gameboy_t *gb, const char *string,
+                           size_t length, bool *error,
+                           uint16_t *watchpoint_address, uint8_t *watchpoint_new_value);
+
+static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string,
+                                         size_t length, bool *error,
+                                         uint16_t *watchpoint_address, uint8_t *watchpoint_new_value)
+{
+    *error = false;
+    // Strip whitespace
+    while (length && (string[0] == ' ' || string[0] == '\n' || string[0] == '\r' || string[0] == '\t')) {
+        string++;
+        length--;
+    }
+    while (length && (string[length-1] == ' ' || string[length-1] == '\n' || string[length-1] == '\r' || string[length-1] == '\t')) {
+        length--;
+    }
+    if (length == 0)
+    {
+        GB_log(gb, "Expected expression.\n");
+        *error = true;
+        return (lvalue_t){0,};
+    }
+    if (string[0] == '(' && string[length - 1] == ')') {
+        // Attempt to strip parentheses
+        signed int depth = 0;
+        for (int i = 0; i < length; i++) {
+            if (string[i] == '(') depth++;
+            if (depth == 0) {
+                // First and last are not matching
+                depth = 1;
+                break;
+            }
+            if (string[i] == ')') depth--;
+        }
+        if (depth == 0) return debugger_evaluate_lvalue(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value);
+    }
+    else if (string[0] == '[' && string[length - 1] == ']') {
+        // Attempt to strip square parentheses (memory dereference)
+        signed int depth = 0;
+        for (int i = 0; i < length; i++) {
+            if (string[i] == '[') depth++;
+            if (depth == 0) {
+                // First and last are not matching
+                depth = 1;
+                break;
+            }
+            if (string[i] == ']') depth--;
+        }
+        if (depth == 0) {
+            return (lvalue_t){LVALUE_MEMORY, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value)};
+        }
+    }
+
+    // Registers
+    if (string[0] != '$' && (string[0] < '0' || string[0] > '9')) {
+        if (length == 1) {
+            switch (string[0]) {
+                case 'a': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_AF]};
+                case 'f': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_AF]};
+                case 'b': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_BC]};
+                case 'c': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_BC]};
+                case 'd': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_DE]};
+                case 'e': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_DE]};
+                case 'h': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_HL]};
+                case 'l': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_HL]};
+            }
+        }
+        else if (length == 2) {
+            switch (string[0]) {
+                case 'a': if (string[1] == 'f') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_AF]};
+                case 'b': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_BC]};
+                case 'd': if (string[1] == 'e') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_DE]};
+                case 'h': if (string[1] == 'l') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_HL]};
+                case 's': if (string[1] == 'p') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_SP]};
+                case 'p': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->pc};
+            }
+        }
+        GB_log(gb, "Unknown register: %.*s\n", (unsigned int) length, string);
+        *error = true;
+        return (lvalue_t){0,};
+    }
+
+    GB_log(gb, "Expression is not an lvalue: %.*s\n", (unsigned int) length, string);
+    *error = true;
+    return (lvalue_t){0,};
+}
+
+#define ERROR ((value_t){0,})
+value_t debugger_evaluate(GB_gameboy_t *gb, const char *string,
+                          size_t length, bool *error,
+                          uint16_t *watchpoint_address, uint8_t *watchpoint_new_value)
+{
+    /* Disable watchpoints while evaulating expressions */
+    uint16_t n_watchpoints = gb->n_watchpoints;
+    gb->n_watchpoints = 0;
+    
+    value_t ret = ERROR;
+    
+    *error = false;
+    // Strip whitespace
+    while (length && (string[0] == ' ' || string[0] == '\n' || string[0] == '\r' || string[0] == '\t')) {
+        string++;
+        length--;
+    }
+    while (length && (string[length-1] == ' ' || string[length-1] == '\n' || string[length-1] == '\r' || string[length-1] == '\t')) {
+        length--;
+    }
+    if (length == 0)
+    {
+        GB_log(gb, "Expected expression.\n");
+        *error = true;
+        goto exit;
+    }
+    if (string[0] == '(' && string[length - 1] == ')') {
+        // Attempt to strip parentheses
+        signed int depth = 0;
+        for (int i = 0; i < length; i++) {
+            if (string[i] == '(') depth++;
+            if (depth == 0) {
+                // First and last are not matching
+                depth = 1;
+                break;
+            }
+            if (string[i] == ')') depth--;
+        }
+        if (depth == 0) {
+            ret = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value);
+            goto exit;
+        }
+    }
+    else if (string[0] == '[' && string[length - 1] == ']') {
+        // Attempt to strip square parentheses (memory dereference)
+        signed int depth = 0;
+        for (int i = 0; i < length; i++) {
+            if (string[i] == '[') depth++;
+            if (depth == 0) {
+                // First and last are not matching
+                depth = 1;
+                break;
+            }
+            if (string[i] == ']') depth--;
+        }
+
+        if (depth == 0) {
+            value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value);
+            banking_state_t state;
+            if (addr.bank) {
+                save_banking_state(gb, &state);
+                switch_banking_state(gb, addr.bank);
+            }
+            ret = VALUE_16(GB_read_memory(gb, addr.value));
+            if (addr.bank) {
+                restore_banking_state(gb, &state);
+            }
+            goto exit;
+        }
+
+    }
+    // Search for lowest priority operator
+    signed int depth = 0;
+    unsigned int operator_index = -1;
+    unsigned int operator_pos = 0;
+    for (int i = 0; i < length; i++) {
+        if (string[i] == '(') depth++;
+        else if (string[i] == ')') depth--;
+        else if (string[i] == '[') depth++;
+        else if (string[i] == ']') depth--;
+        else if (depth == 0) {
+            for (int j = 0; j < sizeof(operators) / sizeof(operators[0]); j++) {
+                if (strlen(operators[j].string) > length - i) continue; // Operator too big.
+                 // Priority higher than what we already have.
+                if (operator_index != -1 && operators[operator_index].priority < operators[j].priority) continue;
+                unsigned long operator_length = strlen(operators[j].string);
+                if (memcmp(string + i, operators[j].string, operator_length) == 0) {
+                    // Found an operator!
+                    operator_pos = i;
+                    operator_index = j;
+                    /* for supporting = vs ==, etc*/
+                    i += operator_length - 1;
+                    break;
+                }
+            }
+        }
+    }
+    if (operator_index != -1) {
+        unsigned int right_start = (unsigned int)(operator_pos + strlen(operators[operator_index].string));
+        value_t right = debugger_evaluate(gb, string + right_start, length - right_start, error, watchpoint_address, watchpoint_new_value);
+        if (*error) goto exit;
+        if (operators[operator_index].lvalue_operator) {
+            lvalue_t left = debugger_evaluate_lvalue(gb, string, operator_pos, error, watchpoint_address, watchpoint_new_value);
+            if (*error) goto exit;
+            ret = operators[operator_index].lvalue_operator(gb, left, right.value);
+            goto exit;
+        }
+        value_t left = debugger_evaluate(gb, string, operator_pos, error, watchpoint_address, watchpoint_new_value);
+        if (*error) goto exit;
+        ret = operators[operator_index].operator(left, right);
+        goto exit;
+    }
+
+    // Not an expression - must be a register or a literal
+
+    // Registers
+    if (string[0] != '$' && (string[0] < '0' || string[0] > '9')) {
+        if (length == 1) {
+            switch (string[0]) {
+                case 'a': ret = VALUE_16(gb->registers[GB_REGISTER_AF] >> 8); goto exit;
+                case 'f': ret = VALUE_16(gb->registers[GB_REGISTER_AF] & 0xFF); goto exit;
+                case 'b': ret = VALUE_16(gb->registers[GB_REGISTER_BC] >> 8); goto exit;
+                case 'c': ret = VALUE_16(gb->registers[GB_REGISTER_BC] & 0xFF); goto exit;
+                case 'd': ret = VALUE_16(gb->registers[GB_REGISTER_DE] >> 8); goto exit;
+                case 'e': ret = VALUE_16(gb->registers[GB_REGISTER_DE] & 0xFF); goto exit;
+                case 'h': ret = VALUE_16(gb->registers[GB_REGISTER_HL] >> 8); goto exit;
+                case 'l': ret = VALUE_16(gb->registers[GB_REGISTER_HL] & 0xFF); goto exit;
+            }
+        }
+        else if (length == 2) {
+            switch (string[0]) {
+                case 'a': if (string[1] == 'f') {ret = VALUE_16(gb->registers[GB_REGISTER_AF]); goto exit;}
+                case 'b': if (string[1] == 'c') {ret = VALUE_16(gb->registers[GB_REGISTER_BC]); goto exit;}
+                case 'd': if (string[1] == 'e') {ret = VALUE_16(gb->registers[GB_REGISTER_DE]); goto exit;}
+                case 'h': if (string[1] == 'l') {ret = VALUE_16(gb->registers[GB_REGISTER_HL]); goto exit;}
+                case 's': if (string[1] == 'p') {ret = VALUE_16(gb->registers[GB_REGISTER_SP]); goto exit;}
+                case 'p': if (string[1] == 'c') {ret = (value_t){true, bank_for_addr(gb, gb->pc), gb->pc};  goto exit;}
+            }
+        }
+        else if (length == 3) {
+            if (watchpoint_address && memcmp(string, "old", 3) == 0) {
+                ret = VALUE_16(GB_read_memory(gb, *watchpoint_address));
+                goto exit;
+            }
+
+            if (watchpoint_new_value && memcmp(string, "new", 3) == 0) {
+                ret = VALUE_16(*watchpoint_new_value);
+                goto exit;
+            }
+
+            /* $new is identical to $old in read conditions */
+            if (watchpoint_address && memcmp(string, "new", 3) == 0) {
+                ret = VALUE_16(GB_read_memory(gb, *watchpoint_address));
+                goto exit;
+            }
+        }
+
+        char symbol_name[length + 1];
+        memcpy(symbol_name, string, length);
+        symbol_name[length] = 0;
+        const GB_symbol_t *symbol = GB_reversed_map_find_symbol(&gb->reversed_symbol_map, symbol_name);
+        if (symbol) {
+            ret = (value_t){true, symbol->bank, symbol->addr};
+            goto exit;
+        }
+
+        GB_log(gb, "Unknown register or symbol: %.*s\n", (unsigned int) length, string);
+        *error = true;
+        goto exit;
+    }
+
+    char *end;
+    int base = 10;
+    if (string[0] == '$') {
+        string++;
+        base = 16;
+        length--;
+    }
+    uint16_t literal = (uint16_t) (strtol(string, &end, base));
+    if (end != string + length) {
+        GB_log(gb, "Failed to parse: %.*s\n", (unsigned int) length, string);
+        *error = true;
+        goto exit;
+    }
+    ret = VALUE_16(literal);
+exit:
+    gb->n_watchpoints = n_watchpoints;
+    return ret;
+}
+
+struct debugger_command_s;
+typedef bool debugger_command_imp_t(GB_gameboy_t *gb, char *arguments, char *modifiers, const struct debugger_command_s *command);
+
+typedef struct debugger_command_s {
+    const char *command;
+    uint8_t min_length;
+    debugger_command_imp_t *implementation;
+    const char *help_string; // Null if should not appear in help
+    const char *arguments_format; // For usage message
+    const char *modifiers_format; // For usage message
+} debugger_command_t;
+
+static const char *lstrip(const char *str)
+{
+    while (*str == ' ' || *str == '\t') {
+        str++;
+    }
+    return str;
+}
+
+#define STOPPED_ONLY \
+if (!gb->debug_stopped) { \
+GB_log(gb, "Program is running. \n"); \
+return false; \
+}
+
+#define NO_MODIFIERS \
+if (modifiers) { \
+print_usage(gb, command); \
+return true; \
+}
+
+static void print_usage(GB_gameboy_t *gb, const debugger_command_t *command)
+{
+    GB_log(gb, "Usage: %s", command->command);
+
+    if (command->modifiers_format) {
+        GB_log(gb, "[/%s]", command->modifiers_format);
+    }
+
+    if (command->arguments_format) {
+        GB_log(gb, " %s", command->arguments_format);
+    }
+
+    GB_log(gb, "\n");
+}
+
+static bool cont(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    STOPPED_ONLY
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    gb->debug_stopped = false;
+    return false;
+}
+
+static bool next(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    STOPPED_ONLY
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+    
+    gb->debug_stopped = false;
+    gb->debug_next_command = true;
+    gb->debug_call_depth = 0;
+    return false;
+}
+
+static bool step(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    STOPPED_ONLY
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    return false;
+}
+
+static bool finish(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    STOPPED_ONLY
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    gb->debug_stopped = false;
+    gb->debug_fin_command = true;
+    gb->debug_call_depth = 0;
+    return false;
+}
+
+static bool stack_leak_detection(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    STOPPED_ONLY
+    
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    gb->debug_stopped = false;
+    gb->stack_leak_detection = true;
+    gb->debug_call_depth = 0;
+    return false;
+}
+
+static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    GB_log(gb, "AF = $%04x\n", gb->registers[GB_REGISTER_AF]); /* AF can't really be an address */
+    GB_log(gb, "BC = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_BC], false));
+    GB_log(gb, "DE = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_DE], false));
+    GB_log(gb, "HL = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_HL], false));
+    GB_log(gb, "SP = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_SP], false));
+    GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false));
+    return true;
+}
+
+/* Find the index of the closest breakpoint equal or greater to addr */
+static uint16_t find_breakpoint(GB_gameboy_t *gb, value_t addr)
+{
+    if (!gb->breakpoints) {
+        return 0;
+    }
+
+    uint32_t key = BP_KEY(addr);
+
+    int min = 0;
+    int max = gb->n_breakpoints;
+    while (min < max) {
+        uint16_t pivot = (min + max) / 2;
+        if (gb->breakpoints[pivot].key == key) return pivot;
+        if (gb->breakpoints[pivot].key > key) {
+            max = pivot;
+        }
+        else {
+            min = pivot + 1;
+        }
+    }
+    return (uint16_t) min;
+}
+
+static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments)) == 0) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    if (gb->n_breakpoints == (typeof(gb->n_breakpoints)) -1) {
+        GB_log(gb, "Too many breakpoints set\n");
+        return true;
+    }
+
+    char *condition = NULL;
+    if ((condition = strstr(arguments, " if "))) {
+        *condition = 0;
+        condition += strlen(" if ");
+        /* Verify condition is sane (Todo: This might have side effects!) */
+        bool error;
+        debugger_evaluate(gb, condition, (unsigned int)strlen(condition), &error, NULL, NULL);
+        if (error) return true;
+
+    }
+
+    bool error;
+    value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    uint32_t key = BP_KEY(result);
+
+    if (error) return true;
+
+    uint16_t index = find_breakpoint(gb, result);
+    if (index < gb->n_breakpoints && gb->breakpoints[index].key == key) {
+        GB_log(gb, "Breakpoint already set at %s\n", debugger_value_to_string(gb, result, true));
+        if (!gb->breakpoints[index].condition && condition) {
+            GB_log(gb, "Added condition to breakpoint\n");
+            gb->breakpoints[index].condition = strdup(condition);
+        }
+        else if (gb->breakpoints[index].condition && condition) {
+            GB_log(gb, "Replaced breakpoint condition\n");
+            free(gb->breakpoints[index].condition);
+            gb->breakpoints[index].condition = strdup(condition);
+        }
+        else if (gb->breakpoints[index].condition && !condition) {
+            GB_log(gb, "Removed breakpoint condition\n");
+            free(gb->breakpoints[index].condition);
+            gb->breakpoints[index].condition = NULL;
+        }
+        return true;
+    }
+
+    gb->breakpoints = realloc(gb->breakpoints, (gb->n_breakpoints + 1) * sizeof(gb->breakpoints[0]));
+    memmove(&gb->breakpoints[index + 1], &gb->breakpoints[index], (gb->n_breakpoints - index) * sizeof(gb->breakpoints[0]));
+    gb->breakpoints[index].key = key;
+
+    if (condition) {
+        gb->breakpoints[index].condition = strdup(condition);
+    }
+    else {
+        gb->breakpoints[index].condition = NULL;
+    }
+    gb->n_breakpoints++;
+
+    GB_log(gb, "Breakpoint set at %s\n", debugger_value_to_string(gb, result, true));
+    return true;
+}
+
+static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments)) == 0) {
+        for (unsigned i = gb->n_breakpoints; i--;) {
+            if (gb->breakpoints[i].condition) {
+                free(gb->breakpoints[i].condition);
+            }
+        }
+        free(gb->breakpoints);
+        gb->breakpoints = NULL;
+        gb->n_breakpoints = 0;
+        return true;
+    }
+
+    bool error;
+    value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    uint32_t key = BP_KEY(result);
+
+    if (error) return true;
+
+    uint16_t index = find_breakpoint(gb, result);
+    if (index >= gb->n_breakpoints || gb->breakpoints[index].key != key) {
+        GB_log(gb, "No breakpoint set at %s\n", debugger_value_to_string(gb, result, true));
+        return true;
+    }
+
+    if (gb->breakpoints[index].condition) {
+        free(gb->breakpoints[index].condition);
+    }
+
+    memmove(&gb->breakpoints[index], &gb->breakpoints[index + 1], (gb->n_breakpoints - index - 1) * sizeof(gb->breakpoints[0]));
+    gb->n_breakpoints--;
+    gb->breakpoints = realloc(gb->breakpoints, gb->n_breakpoints * sizeof(gb->breakpoints[0]));
+
+    GB_log(gb, "Breakpoint removed from %s\n", debugger_value_to_string(gb, result, true));
+    return true;
+}
+
+/* Find the index of the closest watchpoint equal or greater to addr */
+static uint16_t find_watchpoint(GB_gameboy_t *gb, value_t addr)
+{
+    if (!gb->watchpoints) {
+        return 0;
+    }
+    uint32_t key = WP_KEY(addr);
+    int min = 0;
+    int max = gb->n_watchpoints;
+    while (min < max) {
+        uint16_t pivot = (min + max) / 2;
+        if (gb->watchpoints[pivot].key == key) return pivot;
+        if (gb->watchpoints[pivot].key > key) {
+            max = pivot;
+        }
+        else {
+            min = pivot + 1;
+        }
+    }
+    return (uint16_t) min;
+}
+
+static bool watch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    if (strlen(lstrip(arguments)) == 0) {
+print_usage:
+        print_usage(gb, command);
+        return true;
+    }
+
+    if (gb->n_watchpoints == (typeof(gb->n_watchpoints)) -1) {
+        GB_log(gb, "Too many watchpoints set\n");
+        return true;
+    }
+
+    if (!modifiers) {
+        modifiers = "w";
+    }
+
+    uint8_t flags = 0;
+    while (*modifiers) {
+        switch (*modifiers) {
+            case 'r':
+                flags |= GB_WATCHPOINT_R;
+                break;
+            case 'w':
+                flags |= GB_WATCHPOINT_W;
+                break;
+            default:
+                goto print_usage;
+        }
+        modifiers++;
+    }
+
+    if (!flags) {
+        goto print_usage;
+    }
+
+    char *condition = NULL;
+    if ((condition = strstr(arguments, " if "))) {
+        *condition = 0;
+        condition += strlen(" if ");
+        /* Verify condition is sane (Todo: This might have side effects!) */
+        bool error;
+        /* To make $new and $old legal */
+        uint16_t dummy = 0;
+        uint8_t dummy2 = 0;
+        debugger_evaluate(gb, condition, (unsigned int)strlen(condition), &error, &dummy, &dummy2);
+        if (error) return true;
+
+    }
+
+    bool error;
+    value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    uint32_t key = WP_KEY(result);
+
+    if (error) return true;
+
+    uint16_t index = find_watchpoint(gb, result);
+    if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) {
+        GB_log(gb, "Watchpoint already set at %s\n", debugger_value_to_string(gb, result, true));
+        if (gb->watchpoints[index].flags != flags) {
+            GB_log(gb, "Modified watchpoint type\n");
+            gb->watchpoints[index].flags = flags;
+        }
+        if (!gb->watchpoints[index].condition && condition) {
+            GB_log(gb, "Added condition to watchpoint\n");
+            gb->watchpoints[index].condition = strdup(condition);
+        }
+        else if (gb->watchpoints[index].condition && condition) {
+            GB_log(gb, "Replaced watchpoint condition\n");
+            free(gb->watchpoints[index].condition);
+            gb->watchpoints[index].condition = strdup(condition);
+        }
+        else if (gb->watchpoints[index].condition && !condition) {
+            GB_log(gb, "Removed watchpoint condition\n");
+            free(gb->watchpoints[index].condition);
+            gb->watchpoints[index].condition = NULL;
+        }
+        return true;
+    }
+
+    gb->watchpoints = realloc(gb->watchpoints, (gb->n_watchpoints + 1) * sizeof(gb->watchpoints[0]));
+    memmove(&gb->watchpoints[index + 1], &gb->watchpoints[index], (gb->n_watchpoints - index) * sizeof(gb->watchpoints[0]));
+    gb->watchpoints[index].key = key;
+    gb->watchpoints[index].flags = flags;
+    if (condition) {
+        gb->watchpoints[index].condition = strdup(condition);
+    }
+    else {
+        gb->watchpoints[index].condition = NULL;
+    }
+    gb->n_watchpoints++;
+
+    GB_log(gb, "Watchpoint set at %s\n", debugger_value_to_string(gb, result, true));
+    return true;
+}
+
+static bool unwatch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments)) == 0) {
+        for (unsigned i = gb->n_watchpoints; i--;) {
+            if (gb->watchpoints[i].condition) {
+                free(gb->watchpoints[i].condition);
+            }
+        }
+        free(gb->watchpoints);
+        gb->watchpoints = NULL;
+        gb->n_watchpoints = 0;
+        return true;
+    }
+
+    bool error;
+    value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    uint32_t key = WP_KEY(result);
+
+    if (error) return true;
+
+    uint16_t index = find_watchpoint(gb, result);
+    if (index >= gb->n_watchpoints || gb->watchpoints[index].key != key) {
+        GB_log(gb, "No watchpoint set at %s\n", debugger_value_to_string(gb, result, true));
+        return true;
+    }
+
+    if (gb->watchpoints[index].condition) {
+        free(gb->watchpoints[index].condition);
+    }
+
+    memmove(&gb->watchpoints[index], &gb->watchpoints[index + 1], (gb->n_watchpoints - index - 1) * sizeof(gb->watchpoints[0]));
+    gb->n_watchpoints--;
+    gb->watchpoints = realloc(gb->watchpoints, gb->n_watchpoints* sizeof(gb->watchpoints[0]));
+
+    GB_log(gb, "Watchpoint removed from %s\n", debugger_value_to_string(gb, result, true));
+    return true;
+}
+
+static bool list(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    if (gb->n_breakpoints == 0) {
+        GB_log(gb, "No breakpoints set.\n");
+    }
+    else {
+        GB_log(gb, "%d breakpoint(s) set:\n", gb->n_breakpoints);
+        for (uint16_t i = 0; i < gb->n_breakpoints; i++) {
+            value_t addr = (value_t){gb->breakpoints[i].bank != (uint16_t)-1, gb->breakpoints[i].bank, gb->breakpoints[i].addr};
+            if (gb->breakpoints[i].condition) {
+                GB_log(gb, " %d. %s (Condition: %s)\n", i + 1,
+                                                        debugger_value_to_string(gb, addr, addr.has_bank),
+                                                        gb->breakpoints[i].condition);
+            }
+            else {
+                GB_log(gb, " %d. %s\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank));
+            }
+        }
+    }
+
+    if (gb->n_watchpoints == 0) {
+        GB_log(gb, "No watchpoints set.\n");
+    }
+    else {
+        GB_log(gb, "%d watchpoint(s) set:\n", gb->n_watchpoints);
+        for (uint16_t i = 0; i < gb->n_watchpoints; i++) {
+            value_t addr = (value_t){gb->watchpoints[i].bank != (uint16_t)-1, gb->watchpoints[i].bank, gb->watchpoints[i].addr};
+            if (gb->watchpoints[i].condition) {
+                GB_log(gb, " %d. %s (%c%c, Condition: %s)\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank),
+                                                              (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-',
+                                                              (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-',
+                                                              gb->watchpoints[i].condition);
+            }
+            else {
+                GB_log(gb, " %d. %s (%c%c)\n", i + 1, debugger_value_to_string(gb,addr, addr.has_bank),
+                                               (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-',
+                                               (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-');
+            }
+        }
+    }
+
+    return true;
+}
+
+static bool _should_break(GB_gameboy_t *gb, value_t addr)
+{
+    uint16_t index = find_breakpoint(gb, addr);
+    uint32_t key = BP_KEY(addr);
+
+    if (index < gb->n_breakpoints && gb->breakpoints[index].key == key) {
+        if (!gb->breakpoints[index].condition) {
+            return true;
+        }
+        bool error;
+        bool condition = debugger_evaluate(gb, gb->breakpoints[index].condition,
+                                           (unsigned int)strlen(gb->breakpoints[index].condition), &error, NULL, NULL).value;
+        if (error) {
+            /* Should never happen */
+            GB_log(gb, "An internal error has occured\n");
+            return true;
+        }
+        return condition;
+    }
+    return false;
+}
+
+static bool should_break(GB_gameboy_t *gb, uint16_t addr)
+{
+    /* Try any-bank breakpoint */
+    value_t full_addr = (VALUE_16(addr));
+    if (_should_break(gb, full_addr)) return true;
+
+    /* Try bank-specific breakpoint */
+    full_addr.has_bank = true;
+    full_addr.bank = bank_for_addr(gb, addr);
+    return _should_break(gb, full_addr);
+}
+
+static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    if (strlen(lstrip(arguments)) == 0) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    if (!modifiers || !modifiers[0]) {
+        modifiers = "a";
+    }
+    else if (modifiers[1]) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    bool error;
+    value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    if (!error) {
+        switch (modifiers[0]) {
+            case 'a':
+                GB_log(gb, "=%s\n", debugger_value_to_string(gb, result, false));
+                break;
+            case 'd':
+                GB_log(gb, "=%d\n", result.value);
+                break;
+            case 'x':
+                GB_log(gb, "=$%x\n", result.value);
+                break;
+            case 'o':
+                GB_log(gb, "=0%o\n", result.value);
+                break;
+            case 'b':
+            {
+                if (!result.value) {
+                    GB_log(gb, "=%%0\n");
+                    break;
+                }
+                char binary[17];
+                binary[16] = 0;
+                char *ptr = &binary[16];
+                while (result.value) {
+                    *(--ptr) = (result.value & 1)? '1' : '0';
+                    result.value >>= 1;
+                }
+                GB_log(gb, "=%%%s\n", ptr);
+                break;
+            }
+            default:
+                break;
+        }
+    }
+    return true;
+}
+
+static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    if (strlen(lstrip(arguments)) == 0) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    bool error;
+    value_t addr = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    uint16_t count = 32;
+
+    if (modifiers) {
+        char *end;
+        count = (uint16_t) (strtol(modifiers, &end, 10));
+        if (*end) {
+            print_usage(gb, command);
+            return true;
+        }
+    }
+
+    if (!error) {
+        if (addr.has_bank) {
+            banking_state_t old_state;
+            save_banking_state(gb, &old_state);
+            switch_banking_state(gb, addr.bank);
+
+            while (count) {
+                GB_log(gb, "%02x:%04x: ", addr.bank, addr.value);
+                for (int i = 0; i < 16 && count; i++) {
+                    GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i));
+                    count--;
+                }
+                addr.value += 16;
+                GB_log(gb, "\n");
+            }
+
+            restore_banking_state(gb, &old_state);
+        }
+        else {
+            while (count) {
+                GB_log(gb, "%04x: ", addr.value);
+                for (int i = 0; i < 16 && count; i++) {
+                    GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i));
+                    count--;
+                }
+                addr.value += 16;
+                GB_log(gb, "\n");
+            }
+        }
+    }
+    return true;
+}
+
+static bool disassemble(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    if (strlen(lstrip(arguments)) == 0) {
+        arguments = "pc";
+    }
+
+    bool error;
+    value_t addr = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL);
+    uint16_t count = 5;
+
+    if (modifiers) {
+        char *end;
+        count = (uint16_t) (strtol(modifiers, &end, 10));
+        if (*end) {
+            print_usage(gb, command);
+            return true;
+        }
+    }
+
+    if (!error) {
+        if (addr.has_bank) {
+            banking_state_t old_state;
+            save_banking_state(gb, &old_state);
+            switch_banking_state(gb, addr.bank);
+
+            GB_cpu_disassemble(gb, addr.value, count);
+
+            restore_banking_state(gb, &old_state);
+        }
+        else {
+            GB_cpu_disassemble(gb, addr.value, count);
+        }
+    }
+    return true;
+}
+
+static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    const GB_cartridge_t *cartridge = gb->cartridge_type;
+
+    if (cartridge->has_ram) {
+        GB_log(gb, "Cartrdige includes%s RAM: $%x bytes\n", cartridge->has_battery? " battery-backed": "", gb->mbc_ram_size);
+    }
+    else {
+        GB_log(gb, "No cartridge RAM\n");
+    }
+
+    if (cartridge->mbc_type) {
+        static const char * const mapper_names[] = {
+            [GB_MBC1] = "MBC1",
+            [GB_MBC2] = "MBC2",
+            [GB_MBC3] = "MBC3",
+            [GB_MBC5] = "MBC5",
+            [GB_HUC1] = "HUC1",
+            [GB_HUC3] = "HUC3",
+        };
+        GB_log(gb, "%s\n", mapper_names[cartridge->mbc_type]);
+        GB_log(gb, "Current mapped ROM bank: %x\n", gb->mbc_rom_bank);
+        if (cartridge->has_ram) {
+            GB_log(gb, "Current mapped RAM bank: %x\n", gb->mbc_ram_bank);
+            GB_log(gb, "RAM is curently %s\n", gb->mbc_ram_enable? "enabled" : "disabled");
+        }
+        if (cartridge->mbc_type == GB_MBC1 && gb->mbc1_wiring == GB_STANDARD_MBC1_WIRING) {
+            GB_log(gb, "MBC1 banking mode is %s\n", gb->mbc1.mode == 1 ? "RAM" : "ROM");
+        }
+        if (cartridge->mbc_type == GB_MBC1 && gb->mbc1_wiring == GB_MBC1M_WIRING) {
+            GB_log(gb, "MBC1 uses MBC1M wiring. \n");
+            GB_log(gb, "Current mapped ROM0 bank: %x\n", gb->mbc_rom0_bank);
+            GB_log(gb, "MBC1 multicart banking mode is %s\n", gb->mbc1.mode == 1 ? "enabled" : "disabled");
+        }
+
+    }
+    else {
+        GB_log(gb, "No MBC\n");
+    }
+
+    if (cartridge->has_rumble) {
+        GB_log(gb, "Cart contains a rumble pak\n");
+    }
+
+    if (cartridge->has_rtc) {
+        GB_log(gb, "Cart contains a real time clock\n");
+    }
+
+    return true;
+}
+
+static bool backtrace(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+    
+    GB_log(gb, "  1. %s\n", debugger_value_to_string(gb, (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}, true));
+    for (unsigned int i = gb->backtrace_size; i--;) {
+        GB_log(gb, "%3d. %s\n", gb->backtrace_size - i + 1, debugger_value_to_string(gb, (value_t){true, gb->backtrace_returns[i].bank, gb->backtrace_returns[i].addr}, true));
+    }
+
+    return true;
+}
+
+static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    STOPPED_ONLY
+
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    GB_log(gb, "Ticks: %lu. (Resetting)\n", gb->debugger_ticks);
+    gb->debugger_ticks = 0;
+
+    return true;
+}
+
+
+static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+
+    if (!gb->is_cgb) {
+        GB_log(gb, "Not available on a DMG.\n");
+        return true;
+    }
+
+    GB_log(gb, "Background palettes: \n");
+    for (unsigned i = 0; i < 32; i++) {
+        GB_log(gb, "%04x ", ((uint16_t *)&gb->background_palettes_data)[i]);
+        if (i % 4 == 3) {
+            GB_log(gb, "\n");
+        }
+    }
+
+    GB_log(gb, "Sprites palettes: \n");
+    for (unsigned i = 0; i < 32; i++) {
+        GB_log(gb, "%04x ", ((uint16_t *)&gb->sprite_palettes_data)[i]);
+        if (i % 4 == 3) {
+            GB_log(gb, "\n");
+        }
+    }
+
+    return true;
+}
+
+static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
+{
+    NO_MODIFIERS
+    if (strlen(lstrip(arguments))) {
+        print_usage(gb, command);
+        return true;
+    }
+    GB_log(gb, "LCDC:\n");
+    GB_log(gb, "    LCD enabled: %s\n",(gb->io_registers[GB_IO_LCDC] & 128)? "Enabled" : "Disabled");
+    GB_log(gb, "    %s: %s\n", gb->is_cgb? (gb->cgb_mode? "Sprite priority flags" : "Background and Window") : "Background",
+                               (gb->io_registers[GB_IO_LCDC] & 1)? "Enabled" : "Disabled");
+    GB_log(gb, "    Objects: %s\n", (gb->io_registers[GB_IO_LCDC] & 2)? "Enabled" : "Disabled");
+    GB_log(gb, "    Object size: %s\n", (gb->io_registers[GB_IO_LCDC] & 4)? "8x16" : "8x8");
+    GB_log(gb, "    Background tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 8)? "$9C00" : "$9800");
+    GB_log(gb, "    Background and Window Tileset: %s\n", (gb->io_registers[GB_IO_LCDC] & 16)? "$8000" : "$8800");
+    GB_log(gb, "    Window: %s\n", (gb->io_registers[GB_IO_LCDC] & 32)? "Enabled" : "Disabled");
+    GB_log(gb, "    Window tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 64)? "$9C00" : "$9800");
+    
+    GB_log(gb, "\nSTAT:\n");
+    static const char *modes[] = {"Mode 0, H-Blank", "Mode 1, V-Blank", "Mode 2, OAM", "Mode 3, Rendering"};
+    GB_log(gb, "    Current mode: %s\n", modes[gb->io_registers[GB_IO_STAT] & 3]);
+    GB_log(gb, "    LYC flag: %s\n", (gb->io_registers[GB_IO_STAT] & 4)? "On" : "Off");
+    GB_log(gb, "    H-Blank interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 8)? "Enabled" : "Disabled");
+    GB_log(gb, "    V-Blank interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 16)? "Enabled" : "Disabled");
+    GB_log(gb, "    OAM interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 32)? "Enabled" : "Disabled");
+    GB_log(gb, "    LYC interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 64)? "Enabled" : "Disabled");
+    
+    
+    GB_log(gb, "\nCycles since frame start: %d\n", gb->display_cycles);
+    GB_log(gb, "Current line: %d\n", gb->display_cycles / 456);
+    GB_log(gb, "LY: %d\n", gb->io_registers[GB_IO_LY]);
+    GB_log(gb, "LYC: %d\n", gb->io_registers[GB_IO_LYC]);
+    GB_log(gb, "Window position: %d, %d\n", (signed) gb->io_registers[GB_IO_WX] - 7 , gb->io_registers[GB_IO_WY]);
+    GB_log(gb, "Interrupt line: %s\n", gb->stat_interrupt_line? "On" : "Off");
+    
+    return true;
+}
+
+static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command);
+
+#define HELP_NEWLINE "\n             "
+
+/* Commands without implementations are aliases of the previous non-alias commands */
+static const debugger_command_t commands[] = {
+    {"continue", 1, cont, "Continue running until next stop"},
+    {"next", 1, next, "Run the next instruction, skipping over function calls"},
+    {"step", 1, step, "Run the next instruction, stepping into function calls"},
+    {"finish", 1, finish, "Run until the current function returns"},
+    {"backtrace", 2, backtrace, "Display the current call stack"},
+    {"bt", 2, }, /* Alias */
+    {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected (Experimental)"},
+    {"ticks", 2, ticks, "Display the number of CPU ticks since the last time 'ticks' was used"},
+    {"registers", 1, registers, "Print values of processor registers and other important registers"},
+    {"cartridge", 2, mbc, "Displays information about the MBC and cartridge"},
+    {"mbc", 3, }, /* Alias */
+    {"lcd", 3, lcd, "Displays information about the current state of the LCD controller"},
+    {"palettes", 3, palettes, "Displays the current CGB palettes"},
+    {"breakpoint", 1, breakpoint, "Add a new breakpoint at the specified address/expression" HELP_NEWLINE
+                                  "Can also modify the condition of existing breakpoints.",
+                                  "<expression>[ if <condition expression>]"},
+    {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[<expression>]"},
+    {"watch", 1, watch, "Add a new watchpoint at the specified address/expression." HELP_NEWLINE
+                        "Can also modify the condition and type of existing watchpoints." HELP_NEWLINE
+                        "Default watchpoint type is write-only.",
+                        "<expression>[ if <condition expression>]", "(r|w|rw)"},
+    {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[<expression>]"},
+    {"list", 1, list, "List all set breakpoints and watchpoints"},
+    {"print", 1, print, "Evaluate and print an expression" HELP_NEWLINE
+                        "Use modifier to format as an address (a, default) or as a number in" HELP_NEWLINE
+                        "decimal (d), hexadecimal (x), octal (o) or binary (b).",
+                        "<expression>", "format"},
+    {"eval", 2, }, /* Alias */
+    {"examine", 2, examine, "Examine values at address", "<expression>", "count"},
+    {"x", 1, }, /* Alias */
+    {"disassemble", 1, disassemble, "Disassemble instructions at address", "<expression>", "count"},
+
+
+    {"help", 1, help, "List available commands or show help for the specified command", "[<command>]"},
+    {NULL,}, /* Null terminator */
+};
+
+static const debugger_command_t *find_command(const char *string)
+{
+    size_t length = strlen(string);
+    for (const debugger_command_t *command = commands; command->command; command++) {
+        if (command->min_length > length) continue;
+        if (memcmp(command->command, string, length) == 0) { /* Is a substring? */
+            /* Aliases */
+            while (!command->implementation) {
+                command--;
+            }
+            return command;
+        }
+    }
+
+    return NULL;
+}
+
+static void print_command_shortcut(GB_gameboy_t *gb, const debugger_command_t *command)
+{
+    GB_attributed_log(gb, GB_LOG_BOLD | GB_LOG_UNDERLINE, "%.*s", command->min_length, command->command);
+    GB_attributed_log(gb, GB_LOG_BOLD , "%s", command->command + command->min_length);
+}
+
+static void print_command_description(GB_gameboy_t *gb, const debugger_command_t *command)
+{
+    print_command_shortcut(gb, command);
+    GB_log(gb, ": ");
+    GB_log(gb, (const char *)&"           %s\n" + strlen(command->command), command->help_string);
+}
+
+static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *ignored)
+{
+    const debugger_command_t *command = find_command(arguments);
+    if (command) {
+        print_command_description(gb, command);
+        GB_log(gb, "\n");
+        print_usage(gb, command);
+
+        command++;
+        if (command->command && !command->implementation) { /* Command has aliases*/
+            GB_log(gb, "\nAliases: ");
+            do {
+                print_command_shortcut(gb, command);
+                GB_log(gb, " ");
+                command++;
+            } while (command->command && !command->implementation);
+            GB_log(gb, "\n");
+        }
+        return true;
+    }
+    for (command = commands; command->command; command++) {
+        if (command->help_string) {
+            print_command_description(gb, command);
+        }
+    }
+    return true;
+}
+
+void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr)
+{
+    /* Called just after the CPU calls a function/enters an interrupt/etc... */
+
+    if (gb->stack_leak_detection) {
+        if (gb->debug_call_depth >= sizeof(gb->sp_for_call_depth) / sizeof(gb->sp_for_call_depth[0])) {
+            GB_log(gb, "Potential stack overflow detected (Functions nest too much). \n");
+            gb->debug_stopped = true;
+        }
+        else {
+            gb->sp_for_call_depth[gb->debug_call_depth] = gb->registers[GB_REGISTER_SP];
+            gb->addr_for_call_depth[gb->debug_call_depth] = gb->pc;
+        }
+    }
+
+    if (gb->backtrace_size < sizeof(gb->backtrace_sps) / sizeof(gb->backtrace_sps[0])) {
+
+        while (gb->backtrace_size) {
+            if (gb->backtrace_sps[gb->backtrace_size - 1] < gb->registers[GB_REGISTER_SP]) {
+                gb->backtrace_size--;
+            }
+            else {
+                break;
+            }
+        }
+
+        gb->backtrace_sps[gb->backtrace_size] = gb->registers[GB_REGISTER_SP];
+        gb->backtrace_returns[gb->backtrace_size].bank = bank_for_addr(gb, call_addr);
+        gb->backtrace_returns[gb->backtrace_size].addr = call_addr;
+        gb->backtrace_size++;
+    }
+
+    gb->debug_call_depth++;
+}
+
+void GB_debugger_ret_hook(GB_gameboy_t *gb)
+{
+    /* Called just before the CPU runs ret/reti */
+
+    gb->debug_call_depth--;
+
+    if (gb->stack_leak_detection) {
+        if (gb->debug_call_depth < 0) {
+            GB_log(gb, "Function finished without a stack leak.\n");
+            gb->debug_stopped = true;
+        }
+        else {
+            if (gb->registers[GB_REGISTER_SP] != gb->sp_for_call_depth[gb->debug_call_depth]) {
+                GB_log(gb, "Stack leak detected for function %s!\n", value_to_string(gb, gb->addr_for_call_depth[gb->debug_call_depth], true));
+                GB_log(gb, "SP is $%04x, should be $%04x.\n", gb->registers[GB_REGISTER_SP],
+                                                            gb->sp_for_call_depth[gb->debug_call_depth]);
+                gb->debug_stopped = true;
+            }
+        }
+    }
+
+    while (gb->backtrace_size) {
+        if (gb->backtrace_sps[gb->backtrace_size - 1] <= gb->registers[GB_REGISTER_SP]) {
+            gb->backtrace_size--;
+        }
+        else {
+            break;
+        }
+    }
+}
+
+static bool _GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, value_t addr, uint8_t value)
+{
+    uint16_t index = find_watchpoint(gb, addr);
+    uint32_t key = WP_KEY(addr);
+
+    if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) {
+        if (!(gb->watchpoints[index].flags & GB_WATCHPOINT_W)) {
+            return false;
+        }
+        if (!gb->watchpoints[index].condition) {
+            gb->debug_stopped = true;
+            GB_log(gb, "Watchpoint: [%s] = $%02x\n", debugger_value_to_string(gb, addr, true), value);
+            return true;
+        }
+        bool error;
+        bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition,
+                                           (unsigned int)strlen(gb->watchpoints[index].condition), &error, &addr.value, &value).value;
+        if (error) {
+            /* Should never happen */
+            GB_log(gb, "An internal error has occured\n");
+            return false;
+        }
+        if (condition) {
+            gb->debug_stopped = true;
+            GB_log(gb, "Watchpoint: [%s] = $%02x\n", debugger_value_to_string(gb, addr, true), value);
+            return true;
+        }
+    }
+    return false;
+}
+
+void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    if (gb->debug_stopped) return;
+
+    /* Try any-bank breakpoint */
+    value_t full_addr = (VALUE_16(addr));
+    if (_GB_debugger_test_write_watchpoint(gb, full_addr, value)) return;
+
+    /* Try bank-specific breakpoint */
+    full_addr.has_bank = true;
+    full_addr.bank = bank_for_addr(gb, addr);
+    _GB_debugger_test_write_watchpoint(gb, full_addr, value);
+}
+
+static bool _GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, value_t addr)
+{
+    uint16_t index = find_watchpoint(gb, addr);
+    uint32_t key = WP_KEY(addr);
+
+    if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) {
+        if (!(gb->watchpoints[index].flags & GB_WATCHPOINT_R)) {
+            return false;
+        }
+        if (!gb->watchpoints[index].condition) {
+            gb->debug_stopped = true;
+            GB_log(gb, "Watchpoint: [%s]\n", debugger_value_to_string(gb, addr, true));
+            return true;
+        }
+        bool error;
+        bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition,
+                                           (unsigned int)strlen(gb->watchpoints[index].condition), &error, &addr.value, NULL).value;
+        if (error) {
+            /* Should never happen */
+            GB_log(gb, "An internal error has occured\n");
+            return false;
+        }
+        if (condition) {
+            gb->debug_stopped = true;
+            GB_log(gb, "Watchpoint: [%s]\n", debugger_value_to_string(gb, addr, true));
+            return true;
+        }
+    }
+    return false;
+}
+
+void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (gb->debug_stopped) return;
+
+    /* Try any-bank breakpoint */
+    value_t full_addr = (VALUE_16(addr));
+    if (_GB_debugger_test_read_watchpoint(gb, full_addr)) return;
+
+    /* Try bank-specific breakpoint */
+    full_addr.has_bank = true;
+    full_addr.bank = bank_for_addr(gb, addr);
+    _GB_debugger_test_read_watchpoint(gb, full_addr);
+}
+
+/* Returns true if debugger waits for more commands */
+bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input)
+{
+    if (!input[0]) {
+        return true;
+    }
+
+    char *command_string = input;
+    char *arguments = strchr(input, ' ');
+    if (arguments) {
+        /* Actually "split" the string. */
+        arguments[0] = 0;
+        arguments++;
+    }
+    else {
+        arguments = "";
+    }
+
+    char *modifiers = strchr(command_string, '/');
+    if (modifiers) {
+        /* Actually "split" the string. */
+        modifiers[0] = 0;
+        modifiers++;
+    }
+
+    const debugger_command_t *command = find_command(command_string);
+    if (command) {
+        return command->implementation(gb, arguments, modifiers, command);
+    }
+    else {
+        GB_log(gb, "%s: no such command.\n", command_string);
+        return true;
+    }
+}
+
+void GB_debugger_run(GB_gameboy_t *gb)
+{
+    if (gb->debug_disable) return;
+    
+    char *input = NULL;
+    if (gb->debug_next_command && gb->debug_call_depth <= 0) {
+        gb->debug_stopped = true;
+    }
+    if (gb->debug_fin_command && gb->debug_call_depth == -1) {
+        gb->debug_stopped = true;
+    }
+    if (gb->debug_stopped) {
+        GB_cpu_disassemble(gb, gb->pc, 5);
+    }
+next_command:
+    if (input) {
+        free(input);
+    }
+    if (gb->breakpoints && !gb->debug_stopped && should_break(gb, gb->pc)) {
+        gb->debug_stopped = true;
+        GB_log(gb, "Breakpoint: PC = %s\n", value_to_string(gb, gb->pc, true));
+        GB_cpu_disassemble(gb, gb->pc, 5);
+    }
+    if (gb->debug_stopped && !gb->debug_disable) {
+        gb->debug_next_command = false;
+        gb->debug_fin_command = false;
+        gb->stack_leak_detection = false;
+        input = gb->input_callback(gb);
+
+        if (input == NULL) {
+            /* Debugging is no currently available, continue running */
+            gb->debug_stopped = false;
+            return;
+        }
+
+        if (GB_debugger_execute_command(gb, input)) {
+            goto next_command;
+        }
+
+        free(input);
+    }
+}
+
+void GB_debugger_handle_async_commands(GB_gameboy_t *gb)
+{
+    char *input = NULL;
+
+    while (gb->async_input_callback && (input = gb->async_input_callback(gb))) {
+        GB_debugger_execute_command(gb, input);
+        free(input);
+    }
+}
+
+void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path)
+{
+    FILE *f = fopen(path, "r");
+    if (!f) return;
+
+    char *line = NULL;
+    size_t size = 0;
+    size_t length = 0;
+    while ((length = getline(&line, &size, f)) != -1) {
+        for (unsigned i = 0; i < length; i++) {
+            if (line[i] == ';' || line[i] == '\n' || line[i] == '\r') {
+                line[i] = 0;
+                length = i;
+                break;
+            }
+        }
+        if (length == 0) continue;
+
+        unsigned int bank, address;
+        char symbol[length];
+
+        if (sscanf(line, "%02x:%04x %s", &bank, &address, symbol) == 3) {
+            bank &= 0x1FF;
+            if (!gb->bank_symbols[bank]) {
+                gb->bank_symbols[bank] = GB_map_alloc();
+            }
+            GB_bank_symbol_t *allocated_symbol = GB_map_add_symbol(gb->bank_symbols[bank], address, symbol);
+            if (allocated_symbol) {
+                GB_reversed_map_add_symbol(&gb->reversed_symbol_map, bank, allocated_symbol);
+            }
+        }
+    }
+    free(line);
+    fclose(f);
+}
+
+const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr)
+{
+    uint16_t bank = bank_for_addr(gb, addr);
+
+    const GB_bank_symbol_t *symbol = GB_map_find_symbol(gb->bank_symbols[bank], addr);
+    if (symbol) return symbol;
+    if (bank != 0) return GB_map_find_symbol(gb->bank_symbols[0], addr); /* Maybe the symbol incorrectly uses bank 0? */
+
+    return NULL;
+}
+
+const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr)
+{
+    const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, addr);
+    if (symbol && symbol->addr == addr) return symbol->name;
+    return NULL;
+}
+
+/* The public version of debugger_evaluate */
+bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result, uint16_t *result_bank)
+{
+    bool error = false;
+    value_t value = debugger_evaluate(gb, string, strlen(string), &error, NULL, NULL);
+    if (result) {
+        *result = value.value;
+    }
+    if (result_bank) {
+        *result_bank = value.has_bank? value.value : -1;
+    }
+    return error;
+}
+
+void GB_debugger_break(GB_gameboy_t *gb)
+{
+    gb->debug_stopped = true;
+}
+
+bool GB_debugger_is_stopped(GB_gameboy_t *gb)
+{
+    return gb->debug_stopped;
+}
+
+void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled)
+{
+    gb->debug_disable = disabled;
+}
diff --git a/waterbox/sameboy/debugger.h b/waterbox/sameboy/debugger.h
new file mode 100644
index 0000000000..5d6491972c
--- /dev/null
+++ b/waterbox/sameboy/debugger.h
@@ -0,0 +1,32 @@
+#ifndef debugger_h
+#define debugger_h
+#include <stdbool.h>
+#include <stdint.h>
+#include "gb_struct_def.h"
+#include "symbol_hash.h"
+
+#ifdef GB_INTERNAL
+void GB_debugger_run(GB_gameboy_t *gb);
+void GB_debugger_handle_async_commands(GB_gameboy_t *gb);
+void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr);
+void GB_debugger_ret_hook(GB_gameboy_t *gb);
+void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value);
+void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr);
+const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr);
+#endif
+
+#ifdef GB_INTERNAL
+bool /* Returns true if debugger waits for more commands. Not relevant for non-GB_INTERNAL */
+#else
+void
+#endif
+GB_debugger_execute_command(GB_gameboy_t *gb, char *input); /* Destroys input. */
+
+
+void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path);
+const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr);
+bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result, uint16_t *result_bank); /* result_bank is -1 if unused. */
+void GB_debugger_break(GB_gameboy_t *gb);
+bool GB_debugger_is_stopped(GB_gameboy_t *gb);
+void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled);
+#endif /* debugger_h */
diff --git a/waterbox/sameboy/display.c b/waterbox/sameboy/display.c
new file mode 100644
index 0000000000..e624ac93b6
--- /dev/null
+++ b/waterbox/sameboy/display.c
@@ -0,0 +1,800 @@
+#include <stdbool.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+#include "gb.h"
+
+/*
+ Each line is 456 cycles, approximately:
+ Mode 2 - 80  cycles / OAM Transfer
+ Mode 3 - 172 cycles / Rendering
+ Mode 0 - 204 cycles / HBlank
+ 
+ Mode 1 is VBlank
+ 
+ Todo: Mode lengths are not constants, see http://blog.kevtris.org/blogfiles/Nitty%20Gritty%20Gameboy%20VRAM%20Timing.txt
+ */
+
+#define MODE2_LENGTH (80)
+#define MODE3_LENGTH (172)
+#define MODE0_LENGTH (204)
+#define LINE_LENGTH (MODE2_LENGTH + MODE3_LENGTH + MODE0_LENGTH) // = 456
+#define LINES (144)
+#define WIDTH (160)
+#define VIRTUAL_LINES (LCDC_PERIOD / LINE_LENGTH) // = 154
+
+typedef struct __attribute__((packed)) {
+    uint8_t y;
+    uint8_t x;
+    uint8_t tile;
+    uint8_t flags;
+} GB_sprite_t;
+
+static bool window_enabled(GB_gameboy_t *gb)
+{
+    if ((gb->io_registers[GB_IO_LCDC] & 0x1) == 0) {
+        if (!gb->cgb_mode && gb->is_cgb) {
+            return false;
+        }
+    }
+    return (gb->io_registers[GB_IO_LCDC] & 0x20) && gb->io_registers[GB_IO_WX] < 167;
+}
+
+static uint32_t get_pixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
+{
+    /*
+     Bit 7 - LCD Display Enable             (0=Off, 1=On)
+     Bit 6 - Window Tile Map Display Select (0=9800-9BFF, 1=9C00-9FFF)
+     Bit 5 - Window Display Enable          (0=Off, 1=On)
+     Bit 4 - BG & Window Tile Data Select   (0=8800-97FF, 1=8000-8FFF)
+     Bit 3 - BG Tile Map Display Select     (0=9800-9BFF, 1=9C00-9FFF)
+     Bit 2 - OBJ (Sprite) Size              (0=8x8, 1=8x16)
+     Bit 1 - OBJ (Sprite) Display Enable    (0=Off, 1=On)
+     Bit 0 - BG Display (for CGB see below) (0=Off, 1=On)
+     */
+    uint16_t map = 0x1800;
+    uint8_t tile = 0;
+    uint8_t attributes = 0;
+    uint8_t sprite_palette = 0;
+    uint16_t tile_address = 0;
+    uint8_t background_pixel = 0, sprite_pixel = 0;
+    GB_sprite_t *sprite = (GB_sprite_t *) &gb->oam;
+    uint8_t sprites_in_line = 0;
+    bool lcd_8_16_mode = (gb->io_registers[GB_IO_LCDC] & 4) != 0;
+    bool sprites_enabled = (gb->io_registers[GB_IO_LCDC] & 2) != 0;
+    uint8_t lowest_sprite_x = 0xFF;
+    bool use_obp1 = false, priority = false;
+    bool in_window = false;
+    bool bg_enabled = true;
+    bool bg_behind = false;
+    if ((gb->io_registers[GB_IO_LCDC] & 0x1) == 0) {
+        if (gb->cgb_mode) {
+            bg_behind = true;
+        }
+        else {
+            bg_enabled = false;
+        }
+    }
+    if (window_enabled(gb) && y >= gb->io_registers[GB_IO_WY] && x + 7 >= gb->io_registers[GB_IO_WX] && gb->current_window_line != 0xFF) {
+        in_window = true;
+    }
+
+    if (sprites_enabled) {
+        // Loop all sprites
+        for (uint8_t i = 40; i--; sprite++) {
+            int sprite_y = sprite->y - 16;
+            int sprite_x = sprite->x - 8;
+            // Is sprite in our line?
+            if (sprite_y <= y && sprite_y + (lcd_8_16_mode? 16:8) > y) {
+                uint8_t tile_x, tile_y, current_sprite_pixel;
+                uint16_t line_address;
+                // Limit to 10 sprites in one scan line.
+                if (++sprites_in_line == 11) break;
+                // Does not overlap our pixel.
+                if (sprite_x > x || sprite_x + 8 <= x) continue;
+                tile_x = x - sprite_x;
+                tile_y = y - sprite_y;
+                if (sprite->flags & 0x20) tile_x = 7 - tile_x;
+                if (sprite->flags & 0x40) tile_y = (lcd_8_16_mode? 15:7) - tile_y;
+                line_address = (lcd_8_16_mode? sprite->tile & 0xFE : sprite->tile) * 0x10 + tile_y * 2;
+                if (gb->cgb_mode && (sprite->flags & 0x8)) {
+                    line_address += 0x2000;
+                }
+                current_sprite_pixel = (((gb->vram[line_address    ] >> ((~tile_x)&7)) & 1 ) |
+                                        ((gb->vram[line_address + 1] >> ((~tile_x)&7)) & 1) << 1 );
+                /* From Pandocs:
+                     When sprites with different x coordinate values overlap, the one with the smaller x coordinate
+                     (closer to the left) will have priority and appear above any others. This applies in Non CGB Mode
+                     only. When sprites with the same x coordinate values overlap, they have priority according to table
+                     ordering. (i.e. $FE00 - highest, $FE04 - next highest, etc.) In CGB Mode priorities are always
+                     assigned like this.
+                 */
+                if (current_sprite_pixel != 0) {
+                    if (!gb->cgb_mode && sprite->x >= lowest_sprite_x) {
+                        break;
+                    }
+                    sprite_pixel = current_sprite_pixel;
+                    lowest_sprite_x = sprite->x;
+                    use_obp1 = (sprite->flags & 0x10) != 0;
+                    sprite_palette = sprite->flags & 7;
+                    priority = (sprite->flags & 0x80) != 0;
+                    if (gb->cgb_mode) {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    if (in_window) {
+        x -= gb->io_registers[GB_IO_WX] - 7; // Todo: This value is probably latched
+        y = gb->current_window_line;
+    }
+    else {
+        x += gb->effective_scx;
+        y += gb->io_registers[GB_IO_SCY];
+    }
+    if (gb->io_registers[GB_IO_LCDC] & 0x08 && !in_window) {
+        map = 0x1C00;
+    }
+    else if (gb->io_registers[GB_IO_LCDC] & 0x40 && in_window) {
+        map = 0x1C00;
+    }
+    tile = gb->vram[map + x/8 + y/8 * 32];
+    if (gb->cgb_mode) {
+        attributes = gb->vram[map + x/8 + y/8 * 32 + 0x2000];
+    }
+
+    if (attributes & 0x80) {
+        priority = !bg_behind && bg_enabled;
+    }
+
+    if (!priority && sprite_pixel) {
+        if (!gb->cgb_mode) {
+            sprite_pixel = (gb->io_registers[use_obp1? GB_IO_OBP1:GB_IO_OBP0] >> (sprite_pixel << 1)) & 3;
+            sprite_palette = use_obp1;
+        }
+        return gb->sprite_palettes_rgb[sprite_palette * 4 + sprite_pixel];
+    }
+
+    if (bg_enabled) {
+        if (gb->io_registers[GB_IO_LCDC] & 0x10) {
+            tile_address = tile * 0x10;
+        }
+        else {
+            tile_address = (int8_t) tile * 0x10 + 0x1000;
+        }
+        if (attributes & 0x8) {
+            tile_address += 0x2000;
+        }
+
+        if (attributes & 0x20) {
+            x = ~x;
+        }
+
+        if (attributes & 0x40) {
+            y = ~y;
+        }
+
+        background_pixel = (((gb->vram[tile_address + (y & 7) * 2    ] >> ((~x)&7)) & 1 ) |
+                            ((gb->vram[tile_address + (y & 7) * 2 + 1] >> ((~x)&7)) & 1) << 1 );
+    }
+
+    if (priority && sprite_pixel && !background_pixel) {
+        if (!gb->cgb_mode) {
+            sprite_pixel = (gb->io_registers[use_obp1? GB_IO_OBP1:GB_IO_OBP0] >> (sprite_pixel << 1)) & 3;
+            sprite_palette = use_obp1;
+        }
+        return gb->sprite_palettes_rgb[sprite_palette * 4 + sprite_pixel];
+    }
+
+    if (!gb->cgb_mode) {
+        background_pixel = ((gb->io_registers[GB_IO_BGP] >> (background_pixel << 1)) & 3);
+    }
+
+    return gb->background_palettes_rgb[(attributes & 7) * 4 + background_pixel];
+}
+
+static void display_vblank(GB_gameboy_t *gb)
+{
+    if (gb->turbo) {
+        if (GB_timing_sync_turbo(gb)) {
+            return;
+        }
+    }
+    
+    if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || gb->stopped) || gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON)) {
+        /* LCD is off, set screen to white */
+        uint32_t white = gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF);
+        for (unsigned i = 0; i < WIDTH * LINES; i++) {
+            gb ->screen[i] = white;
+        }
+    }
+
+    gb->vblank_callback(gb);
+    GB_timing_sync(gb);
+
+    gb->vblank_just_occured = true;
+}
+
+static inline uint8_t scale_channel(uint8_t x)
+{
+    x &= 0x1f;
+    return (x << 3) | (x >> 2);
+}
+
+void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index)
+{
+    uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->sprite_palettes_data;
+    uint16_t color = palette_data[index & ~1] | (palette_data[index | 1] << 8);
+
+    // No need to &, scale channel does that.
+    uint8_t r = scale_channel(color);
+    uint8_t g = scale_channel(color >> 5);
+    uint8_t b = scale_channel(color >> 10);
+    assert (gb->rgb_encode_callback);
+    (background_palette? gb->background_palettes_rgb : gb->sprite_palettes_rgb)[index / 2] = gb->rgb_encode_callback(gb, r, g, b);
+}
+
+/*
+ STAT interrupt is implemented based on this finding:
+ http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531
+ 
+ General timing is based on GiiBiiAdvance's documents:
+ https://github.com/AntonioND/giibiiadvance
+ 
+ */
+
+static void update_display_state(GB_gameboy_t *gb, uint8_t cycles)
+{
+    uint8_t previous_stat_interrupt_line = gb->stat_interrupt_line;
+    gb->stat_interrupt_line = false;
+    
+    if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) {
+        /* LCD is disabled, state is constant */
+        
+        /* When the LCD is off, LY is 0 and STAT mode is 0.
+           Todo: Verify the LY=LYC flag should be on. */
+        gb->io_registers[GB_IO_LY] = 0;
+        gb->io_registers[GB_IO_STAT] &= ~3;
+        gb->io_registers[GB_IO_STAT] |= 4;
+        if (gb->hdma_on_hblank) {
+            gb->hdma_on_hblank = false;
+            gb->hdma_on = false;
+            
+            /* Todo: is this correct? */
+            gb->hdma_steps_left = 0xff;
+        }
+        
+        gb->oam_read_blocked = false;
+        gb->vram_read_blocked = false;
+        gb->oam_write_blocked = false;
+        gb->vram_write_blocked = false;
+        
+        /* Keep sending vblanks to user even if the screen is off */
+        gb->display_cycles += cycles;
+        if (gb->display_cycles >= LCDC_PERIOD) {
+            /* VBlank! */
+            gb->display_cycles -= LCDC_PERIOD;
+            display_vblank(gb);
+        }
+
+        /* Reset window rendering state */
+        gb->current_window_line = 0xFF;
+        return;
+    }
+    
+    uint8_t atomic_increase = gb->cgb_double_speed? 2 : 4;
+    uint8_t stat_delay = gb->cgb_double_speed? 2 : (gb->cgb_mode? 0 : 4);
+    /* Todo: This is correct for DMG. Is it correct for the 3 CGB modes (DMG/single/double)?*/
+    uint8_t scx_delay = ((gb->effective_scx & 7) + atomic_increase - 1) & ~(atomic_increase - 1);
+    /* Todo: These are correct for DMG, DMG-mode CGB, and single speed CGB. Is is correct for double speed CGB? */
+    uint8_t oam_blocking_rush = gb->cgb_double_speed? 2 : 4;
+    uint8_t vram_blocking_rush = gb->is_cgb? 0 : 4;
+    
+    for (; cycles; cycles -= atomic_increase) {
+        
+        gb->display_cycles += atomic_increase;
+        /* The very first line is 2 (4 from the CPU's perseptive) clocks shorter when the LCD turns on.
+           Todo: Verify on the 3 CGB modes, especially double speed mode. */
+        if (gb->first_scanline && gb->display_cycles >= LINE_LENGTH - atomic_increase) {
+            gb->first_scanline = false;
+            gb->display_cycles += atomic_increase;
+        }
+        bool should_compare_ly = true;
+        uint8_t ly_for_comparison = gb->io_registers[GB_IO_LY] = gb->display_cycles / LINE_LENGTH;
+        
+        
+        /* Handle cycle completion. STAT's initial value depends on model and mode */
+        if (gb->display_cycles == LCDC_PERIOD) {
+            /* VBlank! */
+            gb->display_cycles = 0;
+            gb->io_registers[GB_IO_STAT] &= ~3;
+            if (gb->is_cgb) {
+                if (stat_delay) {
+                    gb->io_registers[GB_IO_STAT] |= 1;
+                }
+                else {
+                    gb->io_registers[GB_IO_STAT] |= 2;
+                }
+            }
+            ly_for_comparison = gb->io_registers[GB_IO_LY] = 0;
+            
+            /* Todo: verify timing */
+            gb->oam_read_blocked = true;
+            gb->vram_read_blocked = false;
+            gb->oam_write_blocked = true;
+            gb->vram_write_blocked = false;
+
+            
+            /* Reset window rendering state */
+            gb->current_window_line = 0xFF;
+        }
+        
+        /* Entered VBlank state, update STAT and IF */
+        else if (gb->display_cycles == LINES * LINE_LENGTH + stat_delay) {
+            gb->io_registers[GB_IO_STAT] &= ~3;
+            gb->io_registers[GB_IO_STAT] |= 1;
+            gb->io_registers[GB_IO_IF] |= 1;
+            
+            /* Entering VBlank state triggers the OAM interrupt. In CGB, it happens 4 cycles earlier */
+            if (gb->io_registers[GB_IO_STAT] & 0x20 && !gb->is_cgb) {
+                gb->stat_interrupt_line = true;
+            }
+            if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) {
+                if (!gb->is_cgb) {
+                    display_vblank(gb);
+                    gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED;
+                }
+                else {
+                    gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_SKIPPED;
+                }
+            }
+            else {
+                gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED;
+                display_vblank(gb);
+            }
+        }
+
+        
+        /* Handle line 0 right after turning the LCD on  */
+        else if (gb->first_scanline) {
+            /* OAM and VRAM blocking is not rushed in the very first scanline */
+            if (gb->display_cycles == atomic_increase) {
+                gb->io_registers[GB_IO_STAT] &= ~3;
+                gb->oam_read_blocked = false;
+                gb->vram_read_blocked = false;
+                gb->oam_write_blocked = false;
+                gb->vram_write_blocked = false;
+            }
+            else if (gb->display_cycles == MODE2_LENGTH) {
+                gb->io_registers[GB_IO_STAT] &= ~3;
+                gb->io_registers[GB_IO_STAT] |= 3;
+                gb->oam_read_blocked = true;
+                gb->vram_read_blocked = true;
+                gb->oam_write_blocked = true;
+                gb->vram_write_blocked = true;
+            }
+            else if (gb->display_cycles == MODE2_LENGTH + MODE3_LENGTH) {
+                gb->io_registers[GB_IO_STAT] &= ~3;
+                gb->oam_read_blocked = false;
+                gb->vram_read_blocked = false;
+                gb->oam_write_blocked = false;
+                gb->vram_write_blocked = false;
+            }
+        }
+        
+        /* Handle STAT changes for lines 0-143 */
+        else if (gb->display_cycles < LINES * LINE_LENGTH) {
+            unsigned position_in_line = gb->display_cycles % LINE_LENGTH;
+            
+            /* Handle OAM and VRAM blocking */
+            /* Todo: verify CGB timing for write blocking */
+            if (position_in_line == stat_delay - oam_blocking_rush ||
+                 // In case stat_delay is 0
+                (position_in_line == LINE_LENGTH + stat_delay - oam_blocking_rush && gb->io_registers[GB_IO_LY] != 143)) {
+                gb->oam_read_blocked = true;
+                gb->oam_write_blocked = gb->is_cgb;
+            }
+            else if (position_in_line == MODE2_LENGTH + stat_delay - vram_blocking_rush) {
+                gb->vram_read_blocked = true;
+                gb->vram_write_blocked = gb->is_cgb;
+            }
+            
+            if (position_in_line == stat_delay) {
+                gb->oam_write_blocked = true;
+            }
+            else if (!gb->is_cgb && position_in_line == MODE2_LENGTH + stat_delay - oam_blocking_rush) {
+                gb->oam_write_blocked = false;
+            }
+            else if (position_in_line == MODE2_LENGTH + stat_delay) {
+                gb->vram_write_blocked = true;
+                gb->oam_write_blocked = true;
+            }
+            
+            /* Handle everything else */
+            if (position_in_line == stat_delay) {
+                gb->io_registers[GB_IO_STAT] &= ~3;
+                gb->io_registers[GB_IO_STAT] |= 2;
+                if (window_enabled(gb) && gb->display_cycles / LINE_LENGTH >= gb->io_registers[GB_IO_WY]) {
+                    gb->current_window_line++;
+                }
+            }
+            else if (position_in_line == 0 && gb->display_cycles != 0) {
+                should_compare_ly = gb->is_cgb;
+                ly_for_comparison--;
+            }
+            else if (position_in_line == MODE2_LENGTH + stat_delay) {
+                gb->io_registers[GB_IO_STAT] &= ~3;
+                gb->io_registers[GB_IO_STAT] |= 3;
+                gb->effective_scx = gb->io_registers[GB_IO_SCX];
+                gb->previous_lcdc_x = - (gb->effective_scx & 0x7);
+                
+                /* Todo: This works on both 007 - The World Is Not Enough and Donkey Kong 94, but should be verified better */
+                if (window_enabled(gb) && gb->display_cycles / LINE_LENGTH == gb->io_registers[GB_IO_WY] && gb->current_window_line == 0xFF) {
+                    gb->current_window_line = 0;
+                }
+            }
+            else if (position_in_line == MODE2_LENGTH + MODE3_LENGTH + stat_delay + scx_delay) {
+                gb->io_registers[GB_IO_STAT] &= ~3;
+                gb->oam_read_blocked = false;
+                gb->vram_read_blocked = false;
+                gb->oam_write_blocked = false;
+                gb->vram_write_blocked = false;
+                if (gb->hdma_on_hblank) {
+                    gb->hdma_on = true;
+                    gb->hdma_cycles = 0;
+                }
+            }
+        }
+        
+        /* Line 153 is special */
+        else if (gb->display_cycles >= (VIRTUAL_LINES - 1) * LINE_LENGTH) {
+            /* DMG */
+            if (!gb->is_cgb) {
+                switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
+                    case 0:
+                        should_compare_ly = false;
+                        break;
+                    case 4:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        ly_for_comparison = VIRTUAL_LINES - 1;
+                        break;
+                    case 8:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        should_compare_ly = false;
+                        break;
+                    default:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        ly_for_comparison = 0;
+                }
+            }
+            /* CGB in DMG mode */
+            else if (!gb->cgb_mode) {
+                switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
+                    case 0:
+                        ly_for_comparison = VIRTUAL_LINES - 2;
+                        break;
+                    case 4:
+                        break;
+                    case 8:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        break;
+                    default:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        ly_for_comparison = 0;
+                }
+            }
+            /* Single speed CGB */
+            else if (!gb->cgb_double_speed) {
+                switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
+                    case 0:
+                        break;
+                    case 4:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        break;
+                    default:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        ly_for_comparison = 0;
+                }
+            }
+            
+            /* Double speed CGB */
+            else {
+                switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
+                    case 0:
+                        ly_for_comparison = VIRTUAL_LINES - 2;
+                        break;
+                    case 2:
+                    case 4:
+                        break;
+                    case 6:
+                    case 8:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        break;
+                    default:
+                        gb->io_registers[GB_IO_LY] = 0;
+                        ly_for_comparison = 0;
+                }
+            }
+        }
+        
+        /* Lines 144 - 152 */
+        else {
+            if (stat_delay && gb->display_cycles % LINE_LENGTH == 0) {
+                should_compare_ly = gb->is_cgb;
+                ly_for_comparison--;
+            }
+        }
+        
+        /* Set LY=LYC bit */
+        if (should_compare_ly && (ly_for_comparison == gb->io_registers[GB_IO_LYC])) {
+            gb->io_registers[GB_IO_STAT] |= 4;
+        }
+        else {
+            gb->io_registers[GB_IO_STAT] &= ~4;
+        }
+        
+        if (!gb->stat_interrupt_line) {
+            switch (gb->io_registers[GB_IO_STAT] & 3) {
+                case 0: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 8; break;
+                case 1: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 0x10; break;
+                case 2: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 0x20; break;
+            }
+            
+            /* Use requested a LY=LYC interrupt and the LY=LYC bit is on */
+            if ((gb->io_registers[GB_IO_STAT] & 0x44) == 0x44) {
+                gb->stat_interrupt_line = true;
+            }
+        }
+    }
+    
+    /* On the CGB, the last cycle of line 144 triggers an OAM interrupt
+     Todo: Verify timing for CGB in CGB mode and double speed CGB */
+    if (gb->is_cgb &&
+        gb->display_cycles == LINES * LINE_LENGTH + stat_delay - atomic_increase &&
+        (gb->io_registers[GB_IO_STAT] & 0x20)) {
+        gb->stat_interrupt_line = true;
+    }
+    
+    if (gb->stat_interrupt_line && !previous_stat_interrupt_line) {
+        gb->io_registers[GB_IO_IF] |= 2;
+    }
+    
+    /* The value of LY is glitched in the last cycle of every line in CGB mode CGB in single speed
+       This is based on GiiBiiAdvance's docs */
+    if (gb->cgb_mode && !gb->cgb_double_speed &&
+        gb->display_cycles % LINE_LENGTH == LINE_LENGTH - 4) {
+        uint8_t glitch_pattern[] = {0, 0, 2, 0, 4, 4, 6, 0, 8};
+        if ((gb->io_registers[GB_IO_LY] & 0xF) == 0xF) {
+            gb->io_registers[GB_IO_LY] = glitch_pattern[gb->io_registers[GB_IO_LY] >> 4] << 4;
+        }
+        else {
+            gb->io_registers[GB_IO_LY] = glitch_pattern[gb->io_registers[GB_IO_LY] & 7] | (gb->io_registers[GB_IO_LY] & 0xF8);
+        }
+    }
+}
+
+void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
+{
+    update_display_state(gb, cycles);
+    if (gb->disable_rendering) {
+        return;
+    }
+    
+    /*
+       Display controller bug: For some reason, the OAM STAT interrupt is called, as expected, for LY = 0..143.
+       However, it is also called from LY = 144.
+
+       See http://forums.nesdev.com/viewtopic.php?f=20&t=13727
+    */
+
+    if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) {
+        /* LCD is disabled, do nothing */
+        return;
+    }
+    if (gb->display_cycles >= LINE_LENGTH * 144) { /* VBlank */
+        return;
+    }
+    
+    uint8_t effective_ly = gb->display_cycles / LINE_LENGTH;
+
+
+    if (gb->display_cycles % LINE_LENGTH < MODE2_LENGTH) { /* Mode 2 */
+        return;
+    }
+
+
+    /* Render */
+    /* Todo: it appears that the actual rendering starts 4 cycles after mode 3 starts. Is this correct? */
+    int16_t current_lcdc_x = gb->display_cycles % LINE_LENGTH - MODE2_LENGTH - (gb->effective_scx & 0x7) - 4;
+    
+    for (;gb->previous_lcdc_x < current_lcdc_x; gb->previous_lcdc_x++) {
+        if (gb->previous_lcdc_x >= WIDTH) {
+            continue;
+        }
+        if (gb->previous_lcdc_x < 0) {
+            continue;
+        }
+        gb->screen[effective_ly * WIDTH + gb->previous_lcdc_x] =
+        get_pixel(gb, gb->previous_lcdc_x, effective_ly);
+    }
+}
+
+void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index)
+{
+    uint32_t none_palette[4];
+    uint32_t *palette = NULL;
+    
+    switch (gb->is_cgb? palette_type : GB_PALETTE_NONE) {
+        default:
+        case GB_PALETTE_NONE:
+            none_palette[0] = gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF);
+            none_palette[1] = gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA);
+            none_palette[2] = gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55);
+            none_palette[3] = gb->rgb_encode_callback(gb, 0,    0,    0   );
+            palette = none_palette;
+            break;
+        case GB_PALETTE_BACKGROUND:
+            palette = gb->background_palettes_rgb + (4 * (palette_index & 7));
+            break;
+        case GB_PALETTE_OAM:
+            palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7));
+            break;
+    }
+    
+    for (unsigned y = 0; y < 192; y++) {
+        for (unsigned x = 0; x < 256; x++) {
+            if (x >= 128 && !gb->is_cgb) {
+                *(dest++) = gb->background_palettes_rgb[0];
+                continue;
+            }
+            uint16_t tile = (x % 128) / 8 + y / 8 * 16;
+            uint16_t tile_address = tile * 0x10 + (x >= 128? 0x2000 : 0);
+            uint8_t pixel = (((gb->vram[tile_address + (y & 7) * 2    ] >> ((~x)&7)) & 1 ) |
+                             ((gb->vram[tile_address + (y & 7) * 2 + 1] >> ((~x)&7)) & 1) << 1);
+            
+            if (!gb->cgb_mode) {
+                if (palette_type == GB_PALETTE_BACKGROUND) {
+                    pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3);
+                }
+                else if (!gb->cgb_mode) {
+                    if (palette_type == GB_PALETTE_OAM) {
+                        pixel = ((gb->io_registers[palette_index == 0? GB_IO_OBP0 : GB_IO_OBP1] >> (pixel << 1)) & 3);
+                    }
+                }
+            }
+            
+            
+            *(dest++) = palette[pixel];
+        }
+    }
+}
+
+void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index, GB_map_type_t map_type, GB_tileset_type_t tileset_type)
+{
+    uint32_t none_palette[4];
+    uint32_t *palette = NULL;
+    uint16_t map = 0x1800;
+    
+    switch (gb->is_cgb? palette_type : GB_PALETTE_NONE) {
+        case GB_PALETTE_NONE:
+            none_palette[0] = gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF);
+            none_palette[1] = gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA);
+            none_palette[2] = gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55);
+            none_palette[3] = gb->rgb_encode_callback(gb, 0,    0,    0   );
+            palette = none_palette;
+            break;
+        case GB_PALETTE_BACKGROUND:
+            palette = gb->background_palettes_rgb + (4 * (palette_index & 7));
+            break;
+        case GB_PALETTE_OAM:
+            palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7));
+            break;
+        case GB_PALETTE_AUTO:
+            break;
+    }
+    
+    if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && gb->io_registers[GB_IO_LCDC] & 0x08)) {
+        map = 0x1c00;
+    }
+    
+    if (tileset_type == GB_TILESET_AUTO) {
+        tileset_type = (gb->io_registers[GB_IO_LCDC] & 0x10)? GB_TILESET_8800 : GB_TILESET_8000;
+    }
+    
+    for (unsigned y = 0; y < 256; y++) {
+        for (unsigned x = 0; x < 256; x++) {
+            uint8_t tile = gb->vram[map + x/8 + y/8 * 32];
+            uint16_t tile_address;
+            uint8_t attributes = 0;
+            
+            if (tileset_type == GB_TILESET_8800) {
+                tile_address = tile * 0x10;
+            }
+            else {
+                tile_address = (int8_t) tile * 0x10 + 0x1000;
+            }
+            
+            if (gb->cgb_mode) {
+                attributes = gb->vram[map + x/8 + y/8 * 32 + 0x2000];
+            }
+            
+            if (attributes & 0x8) {
+                tile_address += 0x2000;
+            }
+            
+            uint8_t pixel = (((gb->vram[tile_address + (((attributes & 0x40)? ~y : y) & 7) * 2    ] >> (((attributes & 0x20)? x : ~x)&7)) & 1 ) |
+                             ((gb->vram[tile_address + (((attributes & 0x40)? ~y : y) & 7) * 2 + 1] >> (((attributes & 0x20)? x : ~x)&7)) & 1) << 1);
+            
+            if (!gb->cgb_mode && (palette_type == GB_PALETTE_BACKGROUND || palette_type == GB_PALETTE_AUTO)) {
+                pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3);
+            }
+            
+            if (palette) {
+                *(dest++) = palette[pixel];
+            }
+            else {
+                *(dest++) = gb->background_palettes_rgb[(attributes & 7) * 4 + pixel];
+            }
+        }
+    }
+}
+
+uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height)
+{
+    uint8_t count = 0;
+    *sprite_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8;
+    uint8_t oam_to_dest_index[40] = {0,};
+    for (unsigned y = 0; y < LINES; y++) {
+        GB_sprite_t *sprite = (GB_sprite_t *) &gb->oam;
+        uint8_t sprites_in_line = 0;
+        for (uint8_t i = 0; i < 40; i++, sprite++) {
+            int sprite_y = sprite->y - 16;
+            bool obscured = false;
+            // Is sprite not in this line?
+            if (sprite_y > y || sprite_y + *sprite_height <= y) continue;
+            if (++sprites_in_line == 11) obscured = true;
+            
+            GB_oam_info_t *info = NULL;
+            if (!oam_to_dest_index[i]) {
+                info = dest + count;
+                oam_to_dest_index[i] = ++count;
+                info->x = sprite->x;
+                info->y = sprite->y;
+                info->tile = *sprite_height == 16? sprite->tile & 0xFE : sprite->tile;
+                info->flags = sprite->flags;
+                info->obscured_by_line_limit = false;
+                info->oam_addr = 0xFE00 + i * sizeof(*sprite);
+            }
+            else {
+                info = dest + oam_to_dest_index[i] - 1;
+            }
+            info->obscured_by_line_limit |= obscured;
+        }
+    }
+    
+    
+    for (unsigned i = 0; i < count; i++) {
+        uint16_t vram_address = dest[i].tile * 0x10;
+        uint8_t flags = dest[i].flags;
+        uint8_t palette = gb->cgb_mode? (flags & 7) : ((flags & 0x10)? 1 : 0);
+        if (gb->is_cgb && (flags & 0x8)) {
+            vram_address += 0x2000;
+        }
+
+        for (unsigned y = 0; y < *sprite_height; y++) {
+            for (unsigned x = 0; x < 8; x++) {
+                uint8_t color = (((gb->vram[vram_address    ] >> ((~x)&7)) & 1 ) |
+                                 ((gb->vram[vram_address + 1] >> ((~x)&7)) & 1) << 1 );
+                
+                if (!gb->cgb_mode) {
+                    color = (gb->io_registers[palette? GB_IO_OBP1:GB_IO_OBP0] >> (color << 1)) & 3;
+                }
+                dest[i].image[((flags & 0x20)?7-x:x) + ((flags & 0x40)?*sprite_height - 1 -y:y) * 8] = gb->sprite_palettes_rgb[palette * 4 + color];
+            }
+            vram_address += 2;
+        }
+    }
+    return count;
+}
diff --git a/waterbox/sameboy/display.h b/waterbox/sameboy/display.h
new file mode 100644
index 0000000000..3967787237
--- /dev/null
+++ b/waterbox/sameboy/display.h
@@ -0,0 +1,40 @@
+#ifndef display_h
+#define display_h
+
+#include "gb.h"
+#ifdef GB_INTERNAL
+void GB_display_run(GB_gameboy_t *gb, uint8_t cycles);
+void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index);
+#endif
+
+typedef enum {
+    GB_PALETTE_NONE,
+    GB_PALETTE_BACKGROUND,
+    GB_PALETTE_OAM,
+    GB_PALETTE_AUTO,
+} GB_palette_type_t;
+
+typedef enum {
+    GB_MAP_AUTO,
+    GB_MAP_9800,
+    GB_MAP_9C00,
+} GB_map_type_t;
+
+typedef enum {
+    GB_TILESET_AUTO,
+    GB_TILESET_8800,
+    GB_TILESET_8000,
+} GB_tileset_type_t;
+
+typedef struct {
+    uint32_t image[128];
+    uint8_t x, y, tile, flags;
+    uint16_t oam_addr;
+    bool obscured_by_line_limit;
+} GB_oam_info_t;
+
+void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index);
+void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index, GB_map_type_t map_type, GB_tileset_type_t tileset_type);
+uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height);
+
+#endif /* display_h */
diff --git a/waterbox/sameboy/gb.c b/waterbox/sameboy/gb.c
new file mode 100644
index 0000000000..56697a1e83
--- /dev/null
+++ b/waterbox/sameboy/gb.c
@@ -0,0 +1,563 @@
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <string.h>
+#include <errno.h>
+#include <stdarg.h>
+#ifndef _WIN32
+#include <unistd.h>
+#include <sys/select.h>
+#endif
+#include "gb.h"
+
+void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, va_list args)
+{
+    char *string = NULL;
+    vasprintf(&string, fmt, args);
+    if (string) {
+        if (gb->log_callback) {
+            gb->log_callback(gb, string, attributes);
+        }
+        else {
+            /* Todo: Add ANSI escape sequences for attributed text */
+            printf("%s", string);
+        }
+    }
+    free(string);
+}
+
+void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    GB_attributed_logv(gb, attributes, fmt, args);
+    va_end(args);
+}
+
+void GB_log(GB_gameboy_t *gb, const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    GB_attributed_logv(gb, 0, fmt, args);
+    va_end(args);
+}
+
+static char *default_input_callback(GB_gameboy_t *gb)
+{
+    char *expression = NULL;
+    size_t size = 0;
+
+    if (getline(&expression, &size, stdin) == -1) {
+        /* The user doesn't have STDIN or used ^D. We make sure the program keeps running. */
+        GB_set_async_input_callback(gb, NULL); /* Disable async input */
+        return strdup("c");
+    }
+
+    if (!expression) {
+        return strdup("");
+    }
+
+    size_t length = strlen(expression);
+    if (expression[length - 1] == '\n') {
+        expression[length - 1] = 0;
+    }
+    return expression;
+}
+
+static char *default_async_input_callback(GB_gameboy_t *gb)
+{
+#ifndef _WIN32
+    fd_set set;
+    FD_ZERO(&set);
+    FD_SET(STDIN_FILENO, &set);
+    struct timeval time = {0,};
+    if (select(1, &set, NULL, NULL, &time) == 1) {
+        if (feof(stdin)) {
+            GB_set_async_input_callback(gb, NULL); /* Disable async input */
+            return NULL;
+        }
+        return default_input_callback(gb);
+    }
+#endif
+    return NULL;
+}
+
+void GB_init(GB_gameboy_t *gb)
+{
+    memset(gb, 0, sizeof(*gb));
+    gb->ram = malloc(gb->ram_size = 0x2000);
+    gb->vram = malloc(gb->vram_size = 0x2000);
+
+    gb->input_callback = default_input_callback;
+    gb->async_input_callback = default_async_input_callback;
+    gb->cartridge_type = &GB_cart_defs[0]; // Default cartridge type
+    gb->audio_quality = 4;
+    
+    GB_reset(gb);
+}
+
+void GB_init_cgb(GB_gameboy_t *gb)
+{
+    memset(gb, 0, sizeof(*gb));
+    gb->ram = malloc(gb->ram_size = 0x2000 * 8);
+    gb->vram = malloc(gb->vram_size = 0x2000 * 2);
+    gb->is_cgb = true;
+
+    gb->input_callback = default_input_callback;
+    gb->async_input_callback = default_async_input_callback;
+    gb->cartridge_type = &GB_cart_defs[0]; // Default cartridge type
+    gb->audio_quality = 4;
+
+    GB_reset(gb);
+}
+
+void GB_free(GB_gameboy_t *gb)
+{
+    gb->magic = 0;
+    if (gb->ram) {
+        free(gb->ram);
+    }
+    if (gb->vram) {
+        free(gb->vram);
+    }
+    if (gb->mbc_ram) {
+        free(gb->mbc_ram);
+    }
+    if (gb->rom) {
+        free(gb->rom);
+    }
+    if (gb->audio_buffer) {
+        free(gb->audio_buffer);
+    }
+    if (gb->breakpoints) {
+        free(gb->breakpoints);
+    }
+    for (int i = 0x200; i--;) {
+        if (gb->bank_symbols[i]) {
+            GB_map_free(gb->bank_symbols[i]);
+        }
+    }
+    for (int i = 0x400; i--;) {
+        if (gb->reversed_symbol_map.buckets[i]) {
+            GB_symbol_t *next = gb->reversed_symbol_map.buckets[i]->next;
+            free(gb->reversed_symbol_map.buckets[i]);
+            gb->reversed_symbol_map.buckets[i] = next;
+        }
+    }
+    memset(gb, 0, sizeof(*gb));
+}
+
+int GB_load_boot_rom(GB_gameboy_t *gb, const char *path)
+{
+    FILE *f = fopen(path, "rb");
+    if (!f) {
+        GB_log(gb, "Could not open boot ROM: %s.\n", strerror(errno));
+        return errno;
+    }
+    fread(gb->boot_rom, sizeof(gb->boot_rom), 1, f);
+    fclose(f);
+    return 0;
+}
+
+int GB_load_rom(GB_gameboy_t *gb, const char *path)
+{
+    FILE *f = fopen(path, "rb");
+    if (!f) {
+        GB_log(gb, "Could not open ROM: %s.\n", strerror(errno));
+        return errno;
+    }
+    fseek(f, 0, SEEK_END);
+    gb->rom_size = (ftell(f) + 0x3FFF) & ~0x3FFF; /* Round to bank */
+    /* And then round to a power of two */
+    while (gb->rom_size & (gb->rom_size - 1)) {
+        /* I promise this works. */
+        gb->rom_size |= gb->rom_size >> 1;
+        gb->rom_size++;
+    }
+    fseek(f, 0, SEEK_SET);
+    if (gb->rom) {
+        free(gb->rom);
+    }
+    gb->rom = malloc(gb->rom_size);
+    memset(gb->rom, 0xFF, gb->rom_size); /* Pad with 0xFFs */
+    fread(gb->rom, gb->rom_size, 1, f);
+    fclose(f);
+    GB_configure_cart(gb);
+
+    return 0;
+}
+
+int GB_save_battery(GB_gameboy_t *gb, const char *path)
+{
+    if (!gb->cartridge_type->has_battery) return 0; // Nothing to save.
+    if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */
+    FILE *f = fopen(path, "wb");
+    if (!f) {
+        GB_log(gb, "Could not open battery save: %s.\n", strerror(errno));
+        return errno;
+    }
+
+    if (fwrite(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) {
+        fclose(f);
+        return EIO;
+    }
+    if (gb->cartridge_type->has_rtc) {
+        if (fwrite(&gb->rtc_real, 1, sizeof(gb->rtc_real), f) != sizeof(gb->rtc_real)) {
+            fclose(f);
+            return EIO;
+        }
+
+        if (fwrite(&gb->last_rtc_second, 1, sizeof(gb->last_rtc_second), f) != sizeof(gb->last_rtc_second)) {
+            fclose(f);
+            return EIO;
+        }
+    }
+
+    errno = 0;
+    fclose(f);
+    return errno;
+}
+
+/* Loading will silently stop if the format is incomplete */
+void GB_load_battery(GB_gameboy_t *gb, const char *path)
+{
+    FILE *f = fopen(path, "rb");
+    if (!f) {
+        return;
+    }
+
+    if (fread(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) {
+        goto reset_rtc;
+    }
+
+    if (fread(&gb->rtc_real, 1, sizeof(gb->rtc_real), f) != sizeof(gb->rtc_real)) {
+        goto reset_rtc;
+    }
+
+    if (fread(&gb->last_rtc_second, 1, sizeof(gb->last_rtc_second), f) != sizeof(gb->last_rtc_second)) {
+        goto reset_rtc;
+    }
+
+    if (gb->last_rtc_second > time(NULL)) {
+        /* We must reset RTC here, or it will not advance. */
+        goto reset_rtc;
+    }
+
+    if (gb->last_rtc_second < 852076800) { /* 1/1/97. There weren't any RTC games that time,
+                                            so if the value we read is lower it means it wasn't
+                                            really RTC data. */
+        goto reset_rtc;
+    }
+    goto exit;
+reset_rtc:
+    gb->last_rtc_second = time(NULL);
+    gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */
+exit:
+    fclose(f);
+    return;
+}
+
+void GB_run(GB_gameboy_t *gb)
+{
+    GB_debugger_run(gb);
+    GB_cpu_run(gb);
+    if (gb->vblank_just_occured) {
+        GB_update_joyp(gb);
+        GB_rtc_run(gb);
+        GB_debugger_handle_async_commands(gb);
+    }
+}
+
+uint64_t GB_run_frame(GB_gameboy_t *gb)
+{
+    /* Configure turbo temporarily, the user wants to handle FPS capping manually. */
+    bool old_turbo = gb->turbo;
+    bool old_dont_skip = gb->turbo_dont_skip;
+    gb->turbo = true;
+    gb->turbo_dont_skip = true;
+    
+    gb->cycles_since_last_sync = 0;
+    while (true) {
+        GB_run(gb);
+        if (gb->vblank_just_occured) {
+            break;
+        }
+    }
+    gb->turbo = old_turbo;
+    gb->turbo_dont_skip = old_dont_skip;
+    return gb->cycles_since_last_sync * FRAME_LENGTH * LCDC_PERIOD;
+}
+
+void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output)
+{
+    gb->screen = output;
+}
+
+void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback)
+{
+    gb->vblank_callback = callback;
+}
+
+void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback)
+{
+    gb->log_callback = callback;
+}
+
+void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback)
+{
+    if (gb->input_callback == default_input_callback) {
+        gb->async_input_callback = NULL;
+    }
+    gb->input_callback = callback;
+}
+
+void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback)
+{
+    gb->async_input_callback = callback;
+}
+
+void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback)
+{
+    if (!gb->rgb_encode_callback && !gb->is_cgb) {
+        gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] =
+        callback(gb, 0xFF, 0xFF, 0xFF);
+        gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] =
+        callback(gb, 0xAA, 0xAA, 0xAA);
+        gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] =
+        callback(gb, 0x55, 0x55, 0x55);
+        gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] =
+        callback(gb, 0, 0, 0);
+    }
+    gb->rgb_encode_callback = callback;
+}
+
+void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback)
+{
+    gb->infrared_callback = callback;
+}
+
+void GB_set_infrared_input(GB_gameboy_t *gb, bool state)
+{
+    gb->infrared_input = state;
+    gb->cycles_since_input_ir_change = 0;
+    gb->ir_queue_length = 0;
+}
+
+void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, long cycles_after_previous_change)
+{
+    if (gb->ir_queue_length == GB_MAX_IR_QUEUE) {
+        GB_log(gb, "IR Queue is full\n");
+        return;
+    }
+    gb->ir_queue[gb->ir_queue_length++] = (GB_ir_queue_item_t){state, cycles_after_previous_change};
+}
+
+void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback)
+{
+    gb->rumble_callback = callback;
+}
+
+void GB_set_serial_transfer_start_callback(GB_gameboy_t *gb, GB_serial_transfer_start_callback_t callback)
+{
+    gb->serial_transfer_start_callback = callback;
+}
+
+void GB_set_serial_transfer_end_callback(GB_gameboy_t *gb, GB_serial_transfer_end_callback_t callback)
+{
+    gb->serial_transfer_end_callback = callback;
+}
+
+uint8_t GB_serial_get_data(GB_gameboy_t *gb)
+{
+    if (gb->io_registers[GB_IO_SC] & 1) {
+        /* Internal Clock */
+        GB_log(gb, "Serial read request while using internal clock. \n");
+        return 0xFF;
+    }
+    return gb->io_registers[GB_IO_SB];
+}
+void GB_serial_set_data(GB_gameboy_t *gb, uint8_t data)
+{
+    if (gb->io_registers[GB_IO_SC] & 1) {
+        /* Internal Clock */
+        GB_log(gb, "Serial write request while using internal clock. \n");
+        return;
+    }
+    gb->io_registers[GB_IO_SB] = data;
+    gb->io_registers[GB_IO_IF] |= 8;
+}
+
+void GB_set_sample_rate(GB_gameboy_t *gb, unsigned int sample_rate)
+{
+    if (gb->audio_buffer) {
+        free(gb->audio_buffer);
+    }
+    gb->buffer_size = sample_rate / 25; // 40ms delay
+    gb->audio_buffer = malloc(gb->buffer_size * sizeof(*gb->audio_buffer));
+    gb->sample_rate = sample_rate;
+    gb->audio_position = 0;
+}
+
+void GB_disconnect_serial(GB_gameboy_t *gb)
+{
+    gb->serial_transfer_start_callback = NULL;
+    gb->serial_transfer_end_callback = NULL;
+    
+    /* Reset any internally-emulated device. Currently, only the printer. */
+    memset(&gb->printer, 0, sizeof(gb->printer));
+}
+
+bool GB_is_inited(GB_gameboy_t *gb)
+{
+    return gb->magic == 'SAME';
+}
+
+bool GB_is_cgb(GB_gameboy_t *gb)
+{
+    return gb->is_cgb;
+}
+
+void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip)
+{
+    gb->turbo = on;
+    gb->turbo_dont_skip = no_frame_skip;
+}
+
+void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled)
+{
+    gb->disable_rendering = disabled;
+}
+
+void *GB_get_user_data(GB_gameboy_t *gb)
+{
+    return gb->user_data;
+}
+
+void GB_set_user_data(GB_gameboy_t *gb, void *data)
+{
+    gb->user_data = data;
+}
+
+void GB_reset(GB_gameboy_t *gb)
+{
+    uint32_t mbc_ram_size = gb->mbc_ram_size;
+    bool cgb = gb->is_cgb;
+    memset(gb, 0, (size_t)GB_GET_SECTION((GB_gameboy_t *) 0, unsaved));
+    gb->version = GB_STRUCT_VERSION;
+    
+    gb->mbc_rom_bank = 1;
+    gb->last_rtc_second = time(NULL);
+    gb->cgb_ram_bank = 1;
+    gb->io_registers[GB_IO_JOYP] = 0xF;
+    gb->mbc_ram_size = mbc_ram_size;
+    if (cgb) {
+        gb->ram_size = 0x2000 * 8;
+        memset(gb->ram, 0, gb->ram_size);
+        gb->vram_size = 0x2000 * 2;
+        memset(gb->vram, 0, gb->vram_size);
+        
+        gb->is_cgb = true;
+        gb->cgb_mode = true;
+        gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = 0x00;
+    }
+    else {
+        gb->ram_size = 0x2000;
+        memset(gb->ram, 0, gb->ram_size);
+        gb->vram_size = 0x2000;
+        memset(gb->vram, 0, gb->vram_size);
+        
+        if (gb->rgb_encode_callback) {
+            gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] =
+                gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF);
+            gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] =
+                gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA);
+            gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] =
+                gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55);
+            gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] =
+                gb->rgb_encode_callback(gb, 0, 0, 0);
+        }
+        gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = 0xFF;
+    }
+    /* The serial interrupt always occur on the 0xF8th cycle of every 0x100 cycle since boot. */
+    gb->serial_cycles = 0x100 - 0xF8;
+    gb->io_registers[GB_IO_SC] = 0x7E;
+    gb->magic = (uintptr_t)'SAME';
+}
+
+void GB_switch_model_and_reset(GB_gameboy_t *gb, bool is_cgb)
+{
+    if (is_cgb) {
+        gb->ram = realloc(gb->ram, gb->ram_size = 0x2000 * 8);
+        gb->vram = realloc(gb->vram, gb->vram_size = 0x2000 * 2);
+    }
+    else {
+        gb->ram = realloc(gb->ram, gb->ram_size = 0x2000);
+        gb->vram = realloc(gb->vram, gb->vram_size = 0x2000);
+    }
+    gb->is_cgb = is_cgb;
+    GB_reset(gb);
+
+}
+
+void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank)
+{
+    /* Set size and bank to dummy pointers if not set */
+    size_t dummy_size;
+    uint16_t dummy_bank;
+    if (!size) {
+        size = &dummy_size;
+    }
+    
+    if (!bank) {
+        bank = &dummy_bank;
+    }
+    
+    
+    switch (access) {
+        case GB_DIRECT_ACCESS_ROM:
+            *size = gb->rom_size;
+            *bank = gb->mbc_rom_bank;
+            return gb->rom;
+        case GB_DIRECT_ACCESS_RAM:
+            *size = gb->ram_size;
+            *bank = gb->cgb_ram_bank;
+            return gb->ram;
+        case GB_DIRECT_ACCESS_CART_RAM:
+            *size = gb->mbc_ram_size;
+            *bank = gb->mbc_ram_bank;
+            return gb->mbc_ram;
+        case GB_DIRECT_ACCESS_VRAM:
+            *size = gb->vram_size;
+            *bank = gb->cgb_vram_bank;
+            return gb->vram;
+        case GB_DIRECT_ACCESS_HRAM:
+            *size = sizeof(gb->hram);
+            *bank = 0;
+            return &gb->hram;
+        case GB_DIRECT_ACCESS_IO:
+            *size = sizeof(gb->io_registers);
+            *bank = 0;
+            return &gb->io_registers;
+        case GB_DIRECT_ACCESS_BOOTROM:
+            *size = gb->is_cgb? sizeof(gb->boot_rom) : 0x100;
+            *bank = 0;
+            return &gb->boot_rom;
+        case GB_DIRECT_ACCESS_OAM:
+            *size = sizeof(gb->oam);
+            *bank = 0;
+            return &gb->oam;
+        case GB_DIRECT_ACCESS_BGP:
+            *size = sizeof(gb->background_palettes_data);
+            *bank = 0;
+            return &gb->background_palettes_data;
+        case GB_DIRECT_ACCESS_OBP:
+            *size = sizeof(gb->sprite_palettes_data);
+            *bank = 0;
+            return &gb->sprite_palettes_data;
+        default:
+            *size = 0;
+            *bank = 0;
+            return NULL;
+    }
+}
diff --git a/waterbox/sameboy/gb.h b/waterbox/sameboy/gb.h
new file mode 100644
index 0000000000..82608821fd
--- /dev/null
+++ b/waterbox/sameboy/gb.h
@@ -0,0 +1,570 @@
+#ifndef GB_h
+#define GB_h
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <time.h>
+
+#include "gb_struct_def.h"
+#include "save_state.h"
+
+#include "apu.h"
+#include "camera.h"
+#include "debugger.h"
+#include "display.h"
+#include "joypad.h"
+#include "mbc.h"
+#include "memory.h"
+#include "printer.h"
+#include "timing.h"
+#include "z80_cpu.h"
+#include "symbol_hash.h"
+
+#define GB_STRUCT_VERSION 11
+
+enum {
+    GB_REGISTER_AF,
+    GB_REGISTER_BC,
+    GB_REGISTER_DE,
+    GB_REGISTER_HL,
+    GB_REGISTER_SP,
+    GB_REGISTERS_16_BIT /* Count */
+};
+
+/* Todo: Actually use these! */
+enum {
+    GB_CARRY_FLAG = 16,
+    GB_HALF_CARRY_FLAG = 32,
+    GB_SUBSTRACT_FLAG = 64,
+    GB_ZERO_FLAG = 128,
+};
+
+#define GB_MAX_IR_QUEUE 256
+
+enum {
+    /* Joypad and Serial */
+    GB_IO_JOYP       = 0x00, // Joypad (R/W)
+    GB_IO_SB         = 0x01, // Serial transfer data (R/W)
+    GB_IO_SC         = 0x02, // Serial Transfer Control (R/W)
+
+    /* Missing */
+
+    /* Timers */
+    GB_IO_DIV        = 0x04, // Divider Register (R/W)
+    GB_IO_TIMA       = 0x05, // Timer counter (R/W)
+    GB_IO_TMA        = 0x06, // Timer Modulo (R/W)
+    GB_IO_TAC        = 0x07, // Timer Control (R/W)
+
+    /* Missing */
+
+    GB_IO_IF         = 0x0f, // Interrupt Flag (R/W)
+
+    /* Sound */
+    GB_IO_NR10       = 0x10, // Channel 1 Sweep register (R/W)
+    GB_IO_NR11       = 0x11, // Channel 1 Sound length/Wave pattern duty (R/W)
+    GB_IO_NR12       = 0x12, // Channel 1 Volume Envelope (R/W)
+    GB_IO_NR13       = 0x13, // Channel 1 Frequency lo (Write Only)
+    GB_IO_NR14       = 0x14, // Channel 1 Frequency hi (R/W)
+    GB_IO_NR21       = 0x16, // Channel 2 Sound Length/Wave Pattern Duty (R/W)
+    GB_IO_NR22       = 0x17, // Channel 2 Volume Envelope (R/W)
+    GB_IO_NR23       = 0x18, // Channel 2 Frequency lo data (W)
+    GB_IO_NR24       = 0x19, // Channel 2 Frequency hi data (R/W)
+    GB_IO_NR30       = 0x1a, // Channel 3 Sound on/off (R/W)
+    GB_IO_NR31       = 0x1b, // Channel 3 Sound Length
+    GB_IO_NR32       = 0x1c, // Channel 3 Select output level (R/W)
+    GB_IO_NR33       = 0x1d, // Channel 3 Frequency's lower data (W)
+    GB_IO_NR34       = 0x1e, // Channel 3 Frequency's higher data (R/W)
+
+    /* Missing */
+
+    GB_IO_NR41       = 0x20, // Channel 4 Sound Length (R/W)
+    GB_IO_NR42       = 0x21, // Channel 4 Volume Envelope (R/W)
+    GB_IO_NR43       = 0x22, // Channel 4 Polynomial Counter (R/W)
+    GB_IO_NR44       = 0x23, // Channel 4 Counter/consecutive, Inital (R/W)
+    GB_IO_NR50       = 0x24, // Channel control / ON-OFF / Volume (R/W)
+    GB_IO_NR51       = 0x25, // Selection of Sound output terminal (R/W)
+    GB_IO_NR52       = 0x26, // Sound on/off
+
+    /* Missing */
+
+    GB_IO_WAV_START  = 0x30, // Wave pattern start
+    GB_IO_WAV_END    = 0x3f, // Wave pattern end
+
+    /* Graphics */
+    GB_IO_LCDC       = 0x40, // LCD Control (R/W)
+    GB_IO_STAT       = 0x41, // LCDC Status (R/W)
+    GB_IO_SCY        = 0x42, // Scroll Y (R/W)
+    GB_IO_SCX        = 0x43, // Scroll X (R/W)
+    GB_IO_LY         = 0x44, // LCDC Y-Coordinate (R)
+    GB_IO_LYC        = 0x45, // LY Compare (R/W)
+    GB_IO_DMA        = 0x46, // DMA Transfer and Start Address (W)
+    GB_IO_BGP        = 0x47, // BG Palette Data (R/W) - Non CGB Mode Only
+    GB_IO_OBP0       = 0x48, // Object Palette 0 Data (R/W) - Non CGB Mode Only
+    GB_IO_OBP1       = 0x49, // Object Palette 1 Data (R/W) - Non CGB Mode Only
+    GB_IO_WY         = 0x4a, // Window Y Position (R/W)
+    GB_IO_WX         = 0x4b, // Window X Position minus 7 (R/W)
+    // Has some undocumented compatibility flags written at boot.
+    // Unfortunately it is not readable or writable after boot has finished, so research of this
+    // register is quite limited. The value written to this register, however, can be controlled
+    // in some cases.
+    GB_IO_DMG_EMULATION = 0x4c,
+
+    /* General CGB features */
+    GB_IO_KEY1       = 0x4d, // CGB Mode Only - Prepare Speed Switch
+
+    /* Missing */
+
+    GB_IO_VBK        = 0x4f, // CGB Mode Only - VRAM Bank
+    GB_IO_BIOS       = 0x50, // Write to disable the BIOS mapping
+
+    /* CGB DMA */
+    GB_IO_HDMA1      = 0x51, // CGB Mode Only - New DMA Source, High
+    GB_IO_HDMA2      = 0x52, // CGB Mode Only - New DMA Source, Low
+    GB_IO_HDMA3      = 0x53, // CGB Mode Only - New DMA Destination, High
+    GB_IO_HDMA4      = 0x54, // CGB Mode Only - New DMA Destination, Low
+    GB_IO_HDMA5      = 0x55, // CGB Mode Only - New DMA Length/Mode/Start
+
+    /* IR */
+    GB_IO_RP         = 0x56, // CGB Mode Only - Infrared Communications Port
+
+    /* Missing */
+
+    /* CGB Paletts */
+    GB_IO_BGPI       = 0x68, // CGB Mode Only - Background Palette Index
+    GB_IO_BGPD       = 0x69, // CGB Mode Only - Background Palette Data
+    GB_IO_OBPI       = 0x6a, // CGB Mode Only - Sprite Palette Index
+    GB_IO_OBPD       = 0x6b, // CGB Mode Only - Sprite Palette Data
+
+    // 1 is written for DMG ROMs on a CGB. Does not appear to have an effect.
+    GB_IO_DMG_EMULATION_INDICATION   = 0x6c, // (FEh) Bit 0 (Read/Write)
+
+    /* Missing */
+
+    GB_IO_SVBK       = 0x70, // CGB Mode Only - WRAM Bank
+    GB_IO_UNKNOWN2   = 0x72, // (00h) - Bit 0-7 (Read/Write)
+    GB_IO_UNKNOWN3   = 0x73, // (00h) - Bit 0-7 (Read/Write)
+    GB_IO_UNKNOWN4   = 0x74, // (00h) - Bit 0-7 (Read/Write) - CGB Mode Only
+    GB_IO_UNKNOWN5   = 0x75, // (8Fh) - Bit 4-6 (Read/Write)
+    GB_IO_PCM_12     = 0x76, // Channels 1 and 2 amplitudes
+    GB_IO_PCM_34     = 0x77, // Channels 3 and 4 amplitudes
+    GB_IO_UNKNOWN8   = 0x7F, // Unknown, write only
+};
+
+typedef enum {
+    GB_LOG_BOLD = 1,
+    GB_LOG_DASHED_UNDERLINE = 2,
+    GB_LOG_UNDERLINE = 4,
+    GB_LOG_UNDERLINE_MASK =  GB_LOG_DASHED_UNDERLINE | GB_LOG_UNDERLINE
+} GB_log_attributes;
+
+#ifdef GB_INTERNAL
+#define LCDC_PERIOD 70224
+#define CPU_FREQUENCY 0x400000
+#define DIV_CYCLES (0x100)
+#define INTERNAL_DIV_CYCLES (0x40000)
+#define FRAME_LENGTH 16742706 // in nanoseconds
+#endif
+
+typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb);
+typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes);
+typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb);
+typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b);
+typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on, long cycles_since_last_update);
+typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, bool rumble_on);
+typedef void (*GB_serial_transfer_start_callback_t)(GB_gameboy_t *gb, uint8_t byte_to_send);
+typedef uint8_t (*GB_serial_transfer_end_callback_t)(GB_gameboy_t *gb);
+
+typedef struct {
+    bool state;
+    long delay;
+} GB_ir_queue_item_t;
+
+struct GB_breakpoint_s;
+struct GB_watchpoint_s;
+
+/* When state saving, each section is dumped independently of other sections.
+   This allows adding data to the end of the section without worrying about future compatibility.
+   Some other changes might be "safe" as well.
+   This struct is not packed, but dumped sections exclusively use types that have the same alignment in both 32 and 64
+   bit platforms. */
+
+/* We make sure bool is 1 for cross-platform save state compatibility. */
+/* Todo: We might want to typedef our own bool if this prevents SameBoy from working on specific platforms. */
+_Static_assert(sizeof(bool) == 1, "sizeof(bool) != 1");
+
+#ifdef GB_INTERNAL
+struct GB_gameboy_s {
+#else
+struct GB_gameboy_internal_s {
+#endif
+    GB_SECTION(header,
+        /* The magic makes sure a state file is:
+            - Indeed a SameBoy state file.
+            - Has the same endianess has the current platform. */
+        volatile uint32_t magic;
+        /* The version field makes sure we don't load save state files with a completely different structure.
+           This happens when struct fields are removed/resized in an backward incompatible manner. */
+        uint32_t version;
+    );
+
+    GB_SECTION(core_state,
+        /* Registers */
+        uint16_t pc;
+           union {
+               uint16_t registers[GB_REGISTERS_16_BIT];
+               struct {
+                   uint16_t af,
+                            bc,
+                            de,
+                            hl,
+                            sp;
+               };
+               struct {
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+                   uint8_t a, f,
+                           b, c,
+                           d, e,
+                           h, l;
+#elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+                   uint8_t f, a,
+                           c, b,
+                           e, d,
+                           l, h;
+#else
+#error Unable to detect endianess
+#endif
+               };
+               
+           };
+        uint8_t ime;
+        uint8_t interrupt_enable;
+        uint8_t cgb_ram_bank;
+
+        /* CPU and General Hardware Flags*/
+        bool cgb_mode;
+        bool is_cgb;
+        bool cgb_double_speed;
+        bool halted;
+        bool stopped;
+        bool boot_rom_finished;
+        bool ime_toggle; /* ei (and di in CGB) have delayed effects.*/
+        bool halt_bug;
+
+        /* Misc state */
+        bool infrared_input;
+        GB_printer_t printer;
+    );
+
+    /* DMA and HDMA */
+    GB_SECTION(dma,
+        bool hdma_on;
+        bool hdma_on_hblank;
+        uint8_t hdma_steps_left;
+        uint16_t hdma_cycles;
+        uint16_t hdma_current_src, hdma_current_dest;
+
+        uint8_t dma_steps_left;
+        uint8_t dma_current_dest;
+        uint16_t dma_current_src;
+        int16_t dma_cycles;
+        bool is_dma_restarting;
+    );
+    
+    /* MBC */
+    GB_SECTION(mbc,
+        uint16_t mbc_rom_bank;
+        uint8_t mbc_ram_bank;
+        uint32_t mbc_ram_size;
+        bool mbc_ram_enable;
+        union {
+            struct {
+                uint8_t bank_low:5;
+                uint8_t bank_high:2;
+                uint8_t padding:1; // Save state compatibility with 0.9
+                uint8_t mode:1;
+            } mbc1;
+
+            struct {
+                uint8_t rom_bank:4;
+            } mbc2;
+
+            struct {
+                uint8_t rom_bank:7;
+                uint8_t padding:1;
+                uint8_t ram_bank:4;
+            } mbc3;
+
+            struct {
+                uint8_t rom_bank_low;
+                uint8_t rom_bank_high:1;
+                uint8_t ram_bank:4;
+            } mbc5;
+            
+            struct {
+                uint8_t bank_low:6;
+                uint8_t bank_high:3;
+                uint8_t mode:1;
+            } huc1;
+
+            struct {
+                uint8_t rom_bank;
+                uint8_t ram_bank;
+            } huc3;
+        };
+        uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */
+        bool camera_registers_mapped;
+        uint8_t camera_registers[0x36];
+        bool rumble_state;
+    );
+
+
+    /* HRAM and HW Registers */
+    GB_SECTION(hram,
+        uint8_t hram[0xFFFF - 0xFF80];
+        uint8_t io_registers[0x80];
+    );
+
+    /* Timing */
+    GB_SECTION(timing,
+        uint32_t display_cycles;
+        uint32_t div_cycles;
+        uint8_t tima_reload_state; /* After TIMA overflows, it becomes 0 for 4 cycles before actually reloading. */
+        GB_PADDING(uint16_t, serial_cycles);
+        uint16_t serial_cycles; /* This field changed its meaning in v0.10 */
+        uint16_t serial_length;
+    );
+
+    /* APU */
+    GB_SECTION(apu,
+        GB_apu_t apu;
+    );
+
+    /* RTC */
+    GB_SECTION(rtc,
+        union {
+            struct {
+                uint8_t seconds;
+                uint8_t minutes;
+                uint8_t hours;
+                uint8_t days;
+                uint8_t high;
+            };
+            uint8_t data[5];
+        } rtc_real, rtc_latched;
+        time_t last_rtc_second;
+        bool rtc_latch;
+    );
+
+    /* Video Display */
+    GB_SECTION(video,
+        uint32_t vram_size; // Different between CGB and DMG
+        uint8_t cgb_vram_bank;
+        uint8_t oam[0xA0];
+        uint8_t background_palettes_data[0x40];
+        uint8_t sprite_palettes_data[0x40];
+        uint32_t background_palettes_rgb[0x20];
+        uint32_t sprite_palettes_rgb[0x20];
+        int16_t previous_lcdc_x;
+        bool stat_interrupt_line;
+        uint8_t effective_scx;
+        uint8_t current_window_line;
+        /* The LCDC will skip the first frame it renders after turning it on.
+           On the CGB, a frame is not skipped if the previous frame was skipped as well.
+           See https://www.reddit.com/r/EmuDev/comments/6exyxu/ */
+        enum {
+            GB_FRAMESKIP_LCD_TURNED_ON, // On a DMG, the LCD renders a blank screen during this state,
+                                        // on a CGB, the previous frame is repeated (which might be
+                                        // blank if the LCD was off for more than a few cycles)
+            GB_FRAMESKIP_FIRST_FRAME_SKIPPED, // This state is 'skipped' when emulating a DMG
+            GB_FRAMESKIP_SECOND_FRAME_RENDERED,
+        } frame_skip_state;
+        bool first_scanline; // The very first scan line after turning the LCD behaves differently.
+        bool oam_read_blocked;
+        bool vram_read_blocked;
+        bool oam_write_blocked;
+        bool vram_write_blocked;
+    );
+
+    /* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */
+    /* This data is reserved on reset and must come last in the struct */
+    GB_SECTION(unsaved,
+        /* ROM */
+        uint8_t *rom;
+        uint32_t rom_size;
+        const GB_cartridge_t *cartridge_type;
+        enum {
+            GB_STANDARD_MBC1_WIRING,
+            GB_MBC1M_WIRING,
+        } mbc1_wiring;
+
+        /* Various RAMs */
+        uint8_t *ram;
+        uint8_t *vram;
+        uint8_t *mbc_ram;
+
+        /* I/O */
+        uint32_t *screen;
+        GB_sample_t *audio_buffer;
+        bool keys[GB_KEY_MAX];
+               
+        /* Timing */
+        uint64_t last_sync;
+        uint64_t cycles_since_last_sync;
+
+        /* Audio */
+        unsigned buffer_size;
+        unsigned sample_rate;
+        unsigned audio_position;
+        bool audio_stream_started; /* detects first copy request to minimize lag */
+        volatile bool audio_copy_in_progress;
+        volatile bool apu_lock;
+        double apu_sample_cycles;
+        double apu_subsample_cycles;
+        GB_double_sample_t current_supersample;
+        unsigned n_subsamples;
+        unsigned audio_quality;
+        
+
+        /* Callbacks */
+        void *user_data;
+        GB_log_callback_t log_callback;
+        GB_input_callback_t input_callback;
+        GB_input_callback_t async_input_callback;
+        GB_rgb_encode_callback_t rgb_encode_callback;
+        GB_vblank_callback_t vblank_callback;
+        GB_infrared_callback_t infrared_callback;
+        GB_camera_get_pixel_callback_t camera_get_pixel_callback;
+        GB_camera_update_request_callback_t camera_update_request_callback;
+        GB_rumble_callback_t rumble_callback;
+        GB_serial_transfer_start_callback_t serial_transfer_start_callback;
+        GB_serial_transfer_end_callback_t serial_transfer_end_callback;
+               
+        /* IR */
+        long cycles_since_ir_change;
+        long cycles_since_input_ir_change;
+        GB_ir_queue_item_t ir_queue[GB_MAX_IR_QUEUE];
+        size_t ir_queue_length;
+
+        /*** Debugger ***/
+        volatile bool debug_stopped, debug_disable;
+        bool debug_fin_command, debug_next_command;
+
+        /* Breakpoints */
+        uint16_t n_breakpoints;
+        struct GB_breakpoint_s *breakpoints;
+
+        /* SLD (Todo: merge with backtrace) */
+        bool stack_leak_detection;
+        int debug_call_depth;
+        uint16_t sp_for_call_depth[0x200]; /* Should be much more than enough */
+        uint16_t addr_for_call_depth[0x200];
+
+        /* Backtrace */
+        unsigned int backtrace_size;
+        uint16_t backtrace_sps[0x200];
+        struct {
+            uint16_t bank;
+            uint16_t addr;
+        } backtrace_returns[0x200];
+
+        /* Watchpoints */
+        uint16_t n_watchpoints;
+        struct GB_watchpoint_s *watchpoints;
+
+        /* Symbol tables */
+        GB_symbol_map_t *bank_symbols[0x200];
+        GB_reversed_symbol_map_t reversed_symbol_map;
+
+        /* Ticks command */
+        unsigned long debugger_ticks;
+
+        /* Misc */
+        bool turbo;
+        bool turbo_dont_skip;
+        bool disable_rendering;
+        uint32_t ram_size; // Different between CGB and DMG
+        uint8_t boot_rom[0x900];
+        bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank
+   );
+};
+    
+#ifndef GB_INTERNAL
+struct GB_gameboy_s {
+    char __internal[sizeof(struct GB_gameboy_internal_s)];
+};
+#endif
+
+
+#ifndef __printflike
+/* Missing from Linux headers. */
+#define __printflike(fmtarg, firstvararg) \
+__attribute__((__format__ (__printf__, fmtarg, firstvararg)))
+#endif
+
+void GB_init(GB_gameboy_t *gb);
+void GB_init_cgb(GB_gameboy_t *gb);
+bool GB_is_inited(GB_gameboy_t *gb);
+bool GB_is_cgb(GB_gameboy_t *gb);
+void GB_free(GB_gameboy_t *gb);
+void GB_reset(GB_gameboy_t *gb);
+void GB_switch_model_and_reset(GB_gameboy_t *gb, bool is_cgb);
+void GB_run(GB_gameboy_t *gb);
+/* Returns the time passed since the last frame, in nanoseconds */
+uint64_t GB_run_frame(GB_gameboy_t *gb);
+
+typedef enum {
+    GB_DIRECT_ACCESS_ROM,
+    GB_DIRECT_ACCESS_RAM,
+    GB_DIRECT_ACCESS_CART_RAM,
+    GB_DIRECT_ACCESS_VRAM,
+    GB_DIRECT_ACCESS_HRAM,
+    GB_DIRECT_ACCESS_IO, /* Warning: Some registers can only be read/written correctly via GB_memory_read/write. */
+    GB_DIRECT_ACCESS_BOOTROM,
+    GB_DIRECT_ACCESS_OAM,
+    GB_DIRECT_ACCESS_BGP,
+    GB_DIRECT_ACCESS_OBP,
+} GB_direct_access_t;
+
+/* Returns a mutable pointer to various hardware memories. If that memory is banked, the current bank
+   is returned at *bank, even if only a portion of the memory is banked. */
+void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank);
+
+void *GB_get_user_data(GB_gameboy_t *gb);
+void GB_set_user_data(GB_gameboy_t *gb, void *data);
+
+int GB_load_boot_rom(GB_gameboy_t *gb, const char *path);
+int GB_load_rom(GB_gameboy_t *gb, const char *path);
+    
+int GB_save_battery(GB_gameboy_t *gb, const char *path);
+void GB_load_battery(GB_gameboy_t *gb, const char *path);
+
+void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip);
+void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled);
+    
+void GB_log(GB_gameboy_t *gb, const char *fmt, ...) __printflike(2, 3);
+void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) __printflike(3, 4);
+
+void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output);
+
+void GB_set_infrared_input(GB_gameboy_t *gb, bool state);
+void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, long cycles_after_previous_change);
+    
+void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback);
+void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback);
+void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback);
+void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback);
+void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback);
+void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback);
+void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback);
+
+/* These APIs are used when using internal clock */
+void GB_set_serial_transfer_start_callback(GB_gameboy_t *gb, GB_serial_transfer_start_callback_t callback);
+void GB_set_serial_transfer_end_callback(GB_gameboy_t *gb, GB_serial_transfer_end_callback_t callback);
+
+/* These APIs are used when using external clock */
+uint8_t GB_serial_get_data(GB_gameboy_t *gb);
+void GB_serial_set_data(GB_gameboy_t *gb, uint8_t data);
+    
+void GB_disconnect_serial(GB_gameboy_t *gb);
+
+#endif /* GB_h */
diff --git a/waterbox/sameboy/gb_struct_def.h b/waterbox/sameboy/gb_struct_def.h
new file mode 100644
index 0000000000..0e0ebd12ee
--- /dev/null
+++ b/waterbox/sameboy/gb_struct_def.h
@@ -0,0 +1,5 @@
+#ifndef gb_struct_def_h
+#define gb_struct_def_h
+struct GB_gameboy_s;
+typedef struct GB_gameboy_s GB_gameboy_t;
+#endif 
diff --git a/waterbox/sameboy/joypad.c b/waterbox/sameboy/joypad.c
new file mode 100644
index 0000000000..c5c4f08961
--- /dev/null
+++ b/waterbox/sameboy/joypad.c
@@ -0,0 +1,63 @@
+#include <stdio.h>
+#include "gb.h"
+#include <assert.h>
+
+void GB_update_joyp(GB_gameboy_t *gb)
+{
+    uint8_t key_selection = 0;
+    uint8_t previous_state = 0;
+
+    /* Todo: add delay to key selection */
+    previous_state = gb->io_registers[GB_IO_JOYP] & 0xF;
+    key_selection = (gb->io_registers[GB_IO_JOYP] >> 4) & 3;
+    gb->io_registers[GB_IO_JOYP] &= 0xF0;
+    switch (key_selection) {
+        case 3:
+            /* Nothing is wired, all up */
+            gb->io_registers[GB_IO_JOYP] |= 0x0F;
+            break;
+
+        case 2:
+            /* Direction keys */
+            for (uint8_t i = 0; i < 4; i++) {
+                gb->io_registers[GB_IO_JOYP] |= (!gb->keys[i]) << i;
+            }
+            /* Forbid pressing two opposing keys, this breaks a lot of games; even if it's somewhat possible. */
+            if (!(gb->io_registers[GB_IO_JOYP] & 1)) {
+                gb->io_registers[GB_IO_JOYP] |= 2;
+            }
+            if (!(gb->io_registers[GB_IO_JOYP] & 4)) {
+                gb->io_registers[GB_IO_JOYP] |= 8;
+            }
+            break;
+
+        case 1:
+            /* Other keys */
+            for (uint8_t i = 0; i < 4; i++) {
+                gb->io_registers[GB_IO_JOYP] |= (!gb->keys[i + 4]) << i;
+            }
+            break;
+
+        case 0:
+            /* Todo: verifiy this is correct */
+            for (uint8_t i = 0; i < 4; i++) {
+                gb->io_registers[GB_IO_JOYP] |= (!gb->keys[i]) << i;
+                gb->io_registers[GB_IO_JOYP] |= (!gb->keys[i + 4]) << i;
+            }
+            break;
+
+        default:
+            break;
+    }
+    if (previous_state != (gb->io_registers[GB_IO_JOYP] & 0xF)) {
+        /* Todo: disable when emulating CGB */
+        gb->io_registers[GB_IO_IF] |= 0x10;
+    }
+    gb->io_registers[GB_IO_JOYP] |= 0xC0; // No SGB support
+}
+
+void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed)
+{
+    assert(index >= 0 && index < GB_KEY_MAX);
+    gb->keys[index] = pressed;
+}
diff --git a/waterbox/sameboy/joypad.h b/waterbox/sameboy/joypad.h
new file mode 100644
index 0000000000..def4b9ac82
--- /dev/null
+++ b/waterbox/sameboy/joypad.h
@@ -0,0 +1,22 @@
+#ifndef joypad_h
+#define joypad_h
+#include "gb_struct_def.h"
+
+typedef enum {
+    GB_KEY_RIGHT,
+    GB_KEY_LEFT,
+    GB_KEY_UP,
+    GB_KEY_DOWN,
+    GB_KEY_A,
+    GB_KEY_B,
+    GB_KEY_SELECT,
+    GB_KEY_START,
+    GB_KEY_MAX
+} GB_key_t;
+
+void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed);
+
+#ifdef GB_INTERNAL
+void GB_update_joyp(GB_gameboy_t *gb);
+#endif
+#endif /* joypad_h */
diff --git a/waterbox/sameboy/mbc.c b/waterbox/sameboy/mbc.c
new file mode 100644
index 0000000000..d3791a180f
--- /dev/null
+++ b/waterbox/sameboy/mbc.c
@@ -0,0 +1,154 @@
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include "gb.h"
+
+const GB_cartridge_t GB_cart_defs[256] = {
+    // From http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header#0147_-_Cartridge_Type
+    /* MBC        SUBTYPE          RAM    BAT.   RTC    RUMB.   */
+    {  GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 00h  ROM ONLY
+    {  GB_MBC1  , GB_STANDARD_MBC, false, false, false, false}, // 01h  MBC1
+    {  GB_MBC1  , GB_STANDARD_MBC, true , false, false, false}, // 02h  MBC1+RAM
+    {  GB_MBC1  , GB_STANDARD_MBC, true , true , false, false}, // 03h  MBC1+RAM+BATTERY
+    [5] =
+    {  GB_MBC2  , GB_STANDARD_MBC, true , false, false, false}, // 05h  MBC2
+    {  GB_MBC2  , GB_STANDARD_MBC, true , true , false, false}, // 06h  MBC2+BATTERY
+    [8] =
+    {  GB_NO_MBC, GB_STANDARD_MBC, true , false, false, false}, // 08h  ROM+RAM
+    {  GB_NO_MBC, GB_STANDARD_MBC, true , true , false, false}, // 09h  ROM+RAM+BATTERY
+    [0xB] =
+    /* Todo: Not supported yet */
+    {  GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Bh  MMM01
+    {  GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Ch  MMM01+RAM
+    {  GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Dh  MMM01+RAM+BATTERY
+    [0xF] =
+    {  GB_MBC3  , GB_STANDARD_MBC, false, true,  true , false}, // 0Fh  MBC3+TIMER+BATTERY
+    {  GB_MBC3  , GB_STANDARD_MBC, true , true,  true , false}, // 10h  MBC3+TIMER+RAM+BATTERY
+    {  GB_MBC3  , GB_STANDARD_MBC, false, false, false, false}, // 11h  MBC3
+    {  GB_MBC3  , GB_STANDARD_MBC, true , false, false, false}, // 12h  MBC3+RAM
+    {  GB_MBC3  , GB_STANDARD_MBC, true , true , false, false}, // 13h  MBC3+RAM+BATTERY
+    [0x19] =
+    {  GB_MBC5  , GB_STANDARD_MBC, false, false, false, false}, // 19h  MBC5
+    {  GB_MBC5  , GB_STANDARD_MBC, true , false, false, false}, // 1Ah  MBC5+RAM
+    {  GB_MBC5  , GB_STANDARD_MBC, true , true , false, false}, // 1Bh  MBC5+RAM+BATTERY
+    {  GB_MBC5  , GB_STANDARD_MBC, false, false, false, true }, // 1Ch  MBC5+RUMBLE
+    {  GB_MBC5  , GB_STANDARD_MBC, true , false, false, true }, // 1Dh  MBC5+RUMBLE+RAM
+    {  GB_MBC5  , GB_STANDARD_MBC, true , true , false, true }, // 1Eh  MBC5+RUMBLE+RAM+BATTERY
+    [0xFC] =
+    {  GB_MBC5  , GB_CAMERA      , true , true , false, false}, // FCh  POCKET CAMERA
+    {  GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // FDh  BANDAI TAMA5 (Todo: Not supported)
+    {  GB_HUC3  , GB_STANDARD_MBC, true , true , false, false}, // FEh  HuC3 (Todo: Mapper support only)
+    {  GB_HUC1  , GB_STANDARD_MBC, true , true , false, false}, // FFh  HuC1+RAM+BATTERY (Todo: No IR bindings)
+};
+
+void GB_update_mbc_mappings(GB_gameboy_t *gb)
+{
+    switch (gb->cartridge_type->mbc_type) {
+        case GB_NO_MBC: return;
+        case GB_MBC1:
+            switch (gb->mbc1_wiring) {
+                case GB_STANDARD_MBC1_WIRING:
+                    gb->mbc_rom_bank = gb->mbc1.bank_low | (gb->mbc1.bank_high << 5);
+                    if (gb->mbc1.mode == 0) {
+                        gb->mbc_ram_bank = 0;
+                        gb->mbc_rom0_bank = 0;
+                    }
+                    else {
+                        gb->mbc_ram_bank = gb->mbc1.bank_high;
+                        gb->mbc_rom0_bank = gb->mbc1.bank_high << 5;
+                    }
+                    if ((gb->mbc_rom_bank & 0x1F) == 0) {
+                        gb->mbc_rom_bank++;
+                    }
+                    break;
+                case GB_MBC1M_WIRING:
+                    gb->mbc_rom_bank = (gb->mbc1.bank_low & 0xF) | (gb->mbc1.bank_high << 4);
+                    if (gb->mbc1.mode == 0) {
+                        gb->mbc_ram_bank = 0;
+                        gb->mbc_rom0_bank = 0;
+                    }
+                    else {
+                        gb->mbc_rom0_bank = gb->mbc1.bank_high << 4;
+                        gb->mbc_ram_bank = 0;
+                    }
+                    if ((gb->mbc1.bank_low & 0x1F) == 0) {
+                        gb->mbc_rom_bank++;
+                    }
+                    break;
+            }
+            break;
+        case GB_MBC2:
+            gb->mbc_rom_bank = gb->mbc2.rom_bank;
+            if ((gb->mbc_rom_bank & 0xF) == 0) {
+                gb->mbc_rom_bank = 1;
+            }
+            break;
+        case GB_MBC3:
+            gb->mbc_rom_bank = gb->mbc3.rom_bank;
+            gb->mbc_ram_bank = gb->mbc3.ram_bank;
+            if (gb->mbc_rom_bank == 0) {
+                gb->mbc_rom_bank = 1;
+            }
+            break;
+        case GB_MBC5:
+            gb->mbc_rom_bank = gb->mbc5.rom_bank_low | (gb->mbc5.rom_bank_high << 8);
+            gb->mbc_ram_bank = gb->mbc5.ram_bank;
+            break;
+        case GB_HUC1:
+            if (gb->huc1.mode == 0) {
+                gb->mbc_rom_bank = gb->huc1.bank_low | (gb->mbc1.bank_high << 6);
+                gb->mbc_ram_bank = 0;
+            }
+            else {
+                gb->mbc_rom_bank = gb->huc1.bank_low;
+                gb->mbc_ram_bank = gb->huc1.bank_high;
+            }
+            break;
+        case GB_HUC3:
+            gb->mbc_rom_bank = gb->huc3.rom_bank;
+            gb->mbc_ram_bank = gb->huc3.ram_bank;
+            break;
+    }
+}
+
+void GB_configure_cart(GB_gameboy_t *gb)
+{
+    gb->cartridge_type = &GB_cart_defs[gb->rom[0x147]];
+    
+    if (gb->rom[0x147] == 0 && gb->rom_size > 0x8000) {
+        GB_log(gb, "ROM header reports no MBC, but file size is over 32Kb. Assuming cartridge uses MBC3.\n");
+        gb->cartridge_type = &GB_cart_defs[0x11];
+    }
+    else if (gb->rom[0x147] != 0 && memcmp(gb->cartridge_type, &GB_cart_defs[0], sizeof(GB_cart_defs[0])) == 0) {
+        GB_log(gb, "Cartridge type %02x is not yet supported.\n", gb->rom[0x147]);
+    }
+
+    if (gb->cartridge_type->has_ram) {
+        if (gb->cartridge_type->mbc_type == GB_MBC2) {
+            gb->mbc_ram_size = 0x200;
+        }
+        else {
+            static const int ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000};
+            gb->mbc_ram_size = ram_sizes[gb->rom[0x149]];
+        }
+        gb->mbc_ram = malloc(gb->mbc_ram_size);
+
+        /* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridges types? */
+        memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size);
+    }
+
+    /* MBC1 has at least 3 types of wiring (We currently support two (Standard and 4bit-MBC1M) of these).
+       See http://forums.nesdev.com/viewtopic.php?f=20&t=14099 */
+
+    /* Attempt to "guess" wiring */
+    if (gb->cartridge_type->mbc_type == GB_MBC1) {
+        if (gb->rom_size >= 0x44000 && memcmp(gb->rom + 0x104, gb->rom + 0x40104, 0x30) == 0) {
+            gb->mbc1_wiring = GB_MBC1M_WIRING;
+        }
+    }
+    
+    /* Set MBC5's bank to 1 correctly */
+    if (gb->cartridge_type->mbc_type == GB_MBC5) {
+        gb->mbc5.rom_bank_low = 1;
+    }
+}
diff --git a/waterbox/sameboy/mbc.h b/waterbox/sameboy/mbc.h
new file mode 100644
index 0000000000..e7260f5643
--- /dev/null
+++ b/waterbox/sameboy/mbc.h
@@ -0,0 +1,31 @@
+#ifndef MBC_h
+#define MBC_h
+#include "gb_struct_def.h"
+
+typedef struct {
+    enum {
+        GB_NO_MBC,
+        GB_MBC1,
+        GB_MBC2,
+        GB_MBC3,
+        GB_MBC5,
+        GB_HUC1, /* Todo: HUC1 features are not emulated. Should be unified with the CGB IR sensor API. */
+        GB_HUC3,
+    } mbc_type;
+    enum {
+        GB_STANDARD_MBC,
+        GB_CAMERA,
+    } mbc_subtype;
+    bool has_ram;
+    bool has_battery;
+    bool has_rtc;
+    bool has_rumble;
+} GB_cartridge_t;
+
+#ifdef GB_INTERNAL
+extern const GB_cartridge_t GB_cart_defs[256];
+void GB_update_mbc_mappings(GB_gameboy_t *gb);
+void GB_configure_cart(GB_gameboy_t *gb);
+#endif
+
+#endif /* MBC_h */
diff --git a/waterbox/sameboy/memory.c b/waterbox/sameboy/memory.c
new file mode 100644
index 0000000000..c914e55965
--- /dev/null
+++ b/waterbox/sameboy/memory.c
@@ -0,0 +1,726 @@
+#include <stdio.h>
+#include <stdbool.h>
+#include "gb.h"
+
+typedef uint8_t GB_read_function_t(GB_gameboy_t *gb, uint16_t addr);
+typedef void GB_write_function_t(GB_gameboy_t *gb, uint16_t addr, uint8_t value);
+
+typedef enum {
+    GB_BUS_MAIN, /* In DMG: Cart and RAM. In CGB: Cart only */
+    GB_BUS_RAM, /* In CGB only. */
+    GB_BUS_VRAM,
+    GB_BUS_INTERNAL, /* Anything in highram. Might not be the most correct name. */
+} GB_bus_t;
+
+static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (addr < 0x8000) {
+        return GB_BUS_MAIN;
+    }
+    if (addr < 0xA000) {
+        return GB_BUS_VRAM;
+    }
+    if (addr < 0xC000) {
+        return GB_BUS_MAIN;
+    }
+    if (addr < 0xFE00) {
+        return gb->is_cgb? GB_BUS_RAM : GB_BUS_MAIN;
+    }
+    return GB_BUS_INTERNAL;
+}
+
+static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (!gb->dma_steps_left || (gb->dma_cycles < 0 && !gb->is_dma_restarting)) return false;
+    return bus_for_addr(gb, addr) == bus_for_addr(gb, gb->dma_current_src);
+}
+
+static uint8_t read_rom(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (addr < 0x100 && !gb->boot_rom_finished) {
+        return gb->boot_rom[addr];
+    }
+
+    if (addr >= 0x200 && addr < 0x900 && gb->is_cgb && !gb->boot_rom_finished) {
+        return gb->boot_rom[addr];
+    }
+
+    if (!gb->rom_size) {
+        return 0xFF;
+    }
+    unsigned int effective_address = (addr & 0x3FFF) + gb->mbc_rom0_bank * 0x4000;
+    return gb->rom[effective_address & (gb->rom_size - 1)];
+}
+
+static uint8_t read_mbc_rom(GB_gameboy_t *gb, uint16_t addr)
+{
+    unsigned int effective_address = (addr & 0x3FFF) + gb->mbc_rom_bank * 0x4000;
+    return gb->rom[effective_address & (gb->rom_size - 1)];
+}
+
+static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (gb->vram_read_blocked) {
+        return 0xFF;
+    }
+    return gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000];
+}
+
+static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr)
+{
+    if ((!gb->mbc_ram_enable || !gb->mbc_ram_size) &&
+        gb->cartridge_type->mbc_subtype != GB_CAMERA &&
+        gb->cartridge_type->mbc_type != GB_HUC1) return 0xFF;
+    
+    if (gb->cartridge_type->has_rtc && gb->mbc_ram_bank >= 8 && gb->mbc_ram_bank <= 0xC) {
+        /* RTC read */
+        gb->rtc_latched.high |= ~0xC1; /* Not all bytes in RTC high are used. */
+        return gb->rtc_latched.data[gb->mbc_ram_bank - 8];
+    }
+
+    if (gb->camera_registers_mapped) {
+        return GB_camera_read_register(gb, addr);
+    }
+
+    if (!gb->mbc_ram) {
+        return 0xFF;
+    }
+
+    if (gb->cartridge_type->mbc_subtype == GB_CAMERA && gb->mbc_ram_bank == 0 && addr >= 0xa100 && addr < 0xaf00) {
+        return GB_camera_read_image(gb, addr - 0xa100);
+    }
+
+    uint8_t ret = gb->mbc_ram[((addr & 0x1FFF) + gb->mbc_ram_bank * 0x2000) & (gb->mbc_ram_size - 1)];
+    if (gb->cartridge_type->mbc_type == GB_MBC2) {
+        ret |= 0xF0;
+    }
+    return ret;
+}
+
+static uint8_t read_ram(GB_gameboy_t *gb, uint16_t addr)
+{
+    return gb->ram[addr & 0x0FFF];
+}
+
+static uint8_t read_banked_ram(GB_gameboy_t *gb, uint16_t addr)
+{
+    return gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000];
+}
+
+static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr)
+{
+
+    if (addr < 0xFE00) {
+        return gb->ram[addr & 0x0FFF];
+    }
+
+    if (addr < 0xFEA0) {
+        if (gb->oam_read_blocked || (gb->dma_steps_left && (gb->dma_cycles > 0 || gb->is_dma_restarting))) {
+            return 0xFF;
+        }
+        return gb->oam[addr & 0xFF];
+    }
+
+    if (addr < 0xFF00) {
+        /* Unusable. CGB results are verified, but DMG results were tested on a SGB2  */
+        if ((gb->io_registers[GB_IO_STAT] & 0x3) >= 2) { /* Seems to be disabled in Modes 2 and 3 */
+            return 0xFF;
+        }
+        if (gb->is_cgb) {
+            return (addr & 0xF0) | ((addr >> 4) & 0xF);
+        }
+        return 0;
+
+    }
+
+    if (addr < 0xFF80) {
+        switch (addr & 0xFF) {
+            case GB_IO_IF:
+                return gb->io_registers[GB_IO_IF] | 0xE0;
+            case GB_IO_TAC:
+                return gb->io_registers[GB_IO_TAC] | 0xF8;
+            case GB_IO_STAT:
+                return gb->io_registers[GB_IO_STAT] | 0x80;
+            case GB_IO_DMG_EMULATION_INDICATION:
+                if (!gb->cgb_mode) {
+                    return 0xFF;
+                }
+                return gb->io_registers[GB_IO_DMG_EMULATION_INDICATION] | 0xFE;
+
+            case GB_IO_PCM_12:
+            case GB_IO_PCM_34:
+            {
+                if (!gb->is_cgb) return 0xFF;
+                GB_sample_t dummy;
+                GB_apu_get_samples_and_update_pcm_regs(gb, &dummy);
+            }
+            /* Fall through */
+            case GB_IO_JOYP:
+            case GB_IO_TMA:
+            case GB_IO_LCDC:
+            case GB_IO_SCY:
+            case GB_IO_SCX:
+            case GB_IO_LY:
+            case GB_IO_LYC:
+            case GB_IO_BGP:
+            case GB_IO_OBP0:
+            case GB_IO_OBP1:
+            case GB_IO_WY:
+            case GB_IO_WX:
+            case GB_IO_SC:
+            case GB_IO_SB:
+                return gb->io_registers[addr & 0xFF];
+            case GB_IO_TIMA:
+                if (gb->tima_reload_state == GB_TIMA_RELOADING) {
+                    return 0;
+                }
+                return gb->io_registers[GB_IO_TIMA];
+            case GB_IO_DIV:
+                return gb->div_cycles >> 8;
+            case GB_IO_HDMA5:
+                if (!gb->cgb_mode) return 0xFF;
+                return ((gb->hdma_on || gb->hdma_on_hblank)? 0 : 0x80) | ((gb->hdma_steps_left - 1) & 0x7F);
+            case GB_IO_SVBK:
+                if (!gb->cgb_mode) {
+                    return 0xFF;
+                }
+                return gb->cgb_ram_bank | ~0x7;
+            case GB_IO_VBK:
+                if (!gb->is_cgb) {
+                    return 0xFF;
+                }
+                return gb->cgb_vram_bank | ~0x1;
+
+            /* Todo: It seems that a CGB in DMG mode can access BGPI and OBPI, but not BGPD and OBPD? */
+            case GB_IO_BGPI:
+            case GB_IO_OBPI:
+                if (!gb->is_cgb) {
+                    return 0xFF;
+                }
+                return gb->io_registers[addr & 0xFF] | 0x40;
+
+            case GB_IO_BGPD:
+            case GB_IO_OBPD:
+            {
+                if (!gb->cgb_mode && gb->boot_rom_finished) {
+                    return 0xFF;
+                }
+                uint8_t index_reg = (addr & 0xFF) - 1;
+                return ((addr & 0xFF) == GB_IO_BGPD?
+                       gb->background_palettes_data :
+                       gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F];
+            }
+
+            case GB_IO_KEY1:
+                if (!gb->cgb_mode) {
+                    return 0xFF;
+                }
+                return (gb->io_registers[GB_IO_KEY1] & 0x7F) | (gb->cgb_double_speed? 0xFE : 0x7E);
+
+            case GB_IO_RP: {
+                if (!gb->cgb_mode) return 0xFF;
+                /* You will read your own IR LED if it's on. */
+                bool read_value = gb->infrared_input || (gb->io_registers[GB_IO_RP] & 1);
+                uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x3C;
+                if ((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && read_value) {
+                    ret |= 2;
+                }
+                return ret;
+            }
+            case GB_IO_DMA:
+                /* Todo: is this documented? */
+                return gb->is_cgb? 0x00 : 0xFF;
+            case GB_IO_UNKNOWN2:
+            case GB_IO_UNKNOWN3:
+                return gb->is_cgb? gb->io_registers[addr & 0xFF] : 0xFF;
+            case GB_IO_UNKNOWN4:
+                return gb->cgb_mode? gb->io_registers[addr & 0xFF] : 0xFF;
+            case GB_IO_UNKNOWN5:
+                return gb->is_cgb? gb->io_registers[addr & 0xFF] | 0x8F : 0xFF;
+            default:
+                if ((addr & 0xFF) >= GB_IO_NR10 && (addr & 0xFF) <= GB_IO_WAV_END) {
+                    return GB_apu_read(gb, addr & 0xFF);
+                }
+                return 0xFF;
+        }
+        /* Hardware registers */
+        return 0;
+    }
+
+    if (addr == 0xFFFF) {
+        /* Interrupt Mask */
+        return gb->interrupt_enable;
+    }
+
+    /* HRAM */
+    return gb->hram[addr - 0xFF80];
+}
+
+static GB_read_function_t * const read_map[] =
+{
+    read_rom,         read_rom,         read_rom, read_rom,         /* 0XXX, 1XXX, 2XXX, 3XXX */
+    read_mbc_rom,     read_mbc_rom,     read_mbc_rom, read_mbc_rom, /* 4XXX, 5XXX, 6XXX, 7XXX */
+    read_vram,        read_vram,                                    /* 8XXX, 9XXX */
+    read_mbc_ram,     read_mbc_ram,                                 /* AXXX, BXXX */
+    read_ram,         read_banked_ram,                              /* CXXX, DXXX */
+    read_high_memory, read_high_memory,                             /* EXXX FXXX */
+};
+
+uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr)
+{
+    if (gb->n_watchpoints) {
+        GB_debugger_test_read_watchpoint(gb, addr);
+    }
+    if (is_addr_in_dma_use(gb, addr)) {
+        addr = gb->dma_current_src;
+    }
+    return read_map[addr >> 12](gb, addr);
+}
+
+static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    switch (gb->cartridge_type->mbc_type) {
+        case GB_NO_MBC: return;
+        case GB_MBC1:
+            switch (addr & 0xF000) {
+                case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break;
+                case 0x2000: case 0x3000: gb->mbc1.bank_low  = value; break;
+                case 0x4000: case 0x5000: gb->mbc1.bank_high = value; break;
+                case 0x6000: case 0x7000: gb->mbc1.mode      = value; break;
+            }
+            break;
+        case GB_MBC2:
+            switch (addr & 0xF000) {
+                case 0x0000: case 0x1000: if (!(addr & 0x100)) gb->mbc_ram_enable = (value & 0xF) == 0xA; break;
+                case 0x2000: case 0x3000: if (  addr & 0x100)  gb->mbc2.rom_bank  = value; break;
+            }
+            break;
+        case GB_MBC3:
+            switch (addr & 0xF000) {
+                case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break;
+                case 0x2000: case 0x3000: gb->mbc3.rom_bank  = value; break;
+                case 0x4000: case 0x5000: gb->mbc3.ram_bank  = value; break;
+                case 0x6000: case 0x7000:
+                    if (!gb->rtc_latch && (value & 1)) { /* Todo: verify condition is correct */
+                        memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real));
+                    }
+                    gb->rtc_latch = value & 1;
+                    break;
+            }
+            break;
+        case GB_MBC5:
+            switch (addr & 0xF000) {
+                case 0x0000: case 0x1000: gb->mbc_ram_enable      = (value & 0xF) == 0xA; break;
+                case 0x2000:              gb->mbc5.rom_bank_low   = value; break;
+                case 0x3000:              gb->mbc5.rom_bank_high  = value; break;
+                case 0x4000: case 0x5000:
+                    if (gb->cartridge_type->has_rumble) {
+                        if (!!(value & 8) != gb->rumble_state) {
+                            gb->rumble_state = !gb->rumble_state;
+                            if (gb->rumble_callback) {
+                                gb->rumble_callback(gb, gb->rumble_state);
+                            }
+                        }
+                        value &= 7;
+                    }
+                    gb->mbc5.ram_bank = value;
+                    gb->camera_registers_mapped = (value & 0x10) && gb->cartridge_type->mbc_subtype == GB_CAMERA;
+                    break;
+            }
+            break;
+        case GB_HUC1:
+            switch (addr & 0xF000) {
+                case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break;
+                case 0x2000: case 0x3000: gb->huc1.bank_low  = value; break;
+                case 0x4000: case 0x5000: gb->huc1.bank_high = value; break;
+                case 0x6000: case 0x7000: gb->huc1.mode      = value; break;
+            }
+            break;
+        case GB_HUC3:
+            switch (addr & 0xF000) {
+                case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break;
+                case 0x2000: case 0x3000: gb->huc3.rom_bank  = value; break;
+                case 0x4000: case 0x5000: gb->huc3.ram_bank  = value; break;
+            }
+            break;
+    }
+    GB_update_mbc_mappings(gb);
+}
+
+static void write_vram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    if (gb->vram_write_blocked) {
+        //GB_log(gb, "Wrote %02x to %04x (VRAM) during mode 3\n", value, addr);
+        return;
+    }
+    gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000] = value;
+}
+
+static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    if (gb->camera_registers_mapped) {
+        GB_camera_write_register(gb, addr, value);
+        return;
+    }
+    
+    if (!gb->mbc_ram_enable || !gb->mbc_ram_size) return;
+
+    if (gb->cartridge_type->has_rtc && gb->mbc_ram_bank >= 8 && gb->mbc_ram_bank <= 0xC) {
+        /* RTC read */
+        gb->rtc_latched.data[gb->mbc_ram_bank - 8] = gb->rtc_real.data[gb->mbc_ram_bank - 8] = value; /* Todo: does it really write both? */
+    }
+
+    if (!gb->mbc_ram) {
+        return;
+    }
+
+    gb->mbc_ram[((addr & 0x1FFF) + gb->mbc_ram_bank * 0x2000) & (gb->mbc_ram_size - 1)] = value;
+}
+
+static void write_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    gb->ram[addr & 0x0FFF] = value;
+}
+
+static void write_banked_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000] = value;
+}
+
+static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    if (addr < 0xFE00) {
+        GB_log(gb, "Wrote %02x to %04x (RAM Mirror)\n", value, addr);
+        gb->ram[addr & 0x0FFF] = value;
+        return;
+    }
+
+    if (addr < 0xFEA0) {
+        if (gb->oam_write_blocked|| (gb->dma_steps_left && (gb->dma_cycles > 0 || gb->is_dma_restarting))) {
+            return;
+        }
+        gb->oam[addr & 0xFF] = value;
+        return;
+    }
+
+    if (addr < 0xFF00) {
+        GB_log(gb, "Wrote %02x to %04x (Unused)\n", value, addr);
+        return;
+    }
+
+    if (addr < 0xFF80) {
+        /* Hardware registers */
+        switch (addr & 0xFF) {
+
+            case GB_IO_SCX:
+            case GB_IO_IF:
+            case GB_IO_SCY:
+            case GB_IO_LYC:
+            case GB_IO_BGP:
+            case GB_IO_OBP0:
+            case GB_IO_OBP1:
+            case GB_IO_WY:
+            case GB_IO_WX:
+            case GB_IO_SB:
+            case GB_IO_DMG_EMULATION_INDICATION:
+            case GB_IO_UNKNOWN2:
+            case GB_IO_UNKNOWN3:
+            case GB_IO_UNKNOWN4:
+            case GB_IO_UNKNOWN5:
+                gb->io_registers[addr & 0xFF] = value;
+                return;
+                
+            case GB_IO_TIMA:
+                if (gb->tima_reload_state != GB_TIMA_RELOADED) {
+                    gb->io_registers[GB_IO_TIMA] = value;
+                }
+                return;
+
+            case GB_IO_TMA:
+                gb->io_registers[GB_IO_TMA] = value;
+                if (gb->tima_reload_state != GB_TIMA_RUNNING) {
+                    gb->io_registers[GB_IO_TIMA] = value;
+                }
+                return;
+
+            case GB_IO_TAC:
+                GB_emulate_timer_glitch(gb, gb->io_registers[GB_IO_TAC], value);
+                gb->io_registers[GB_IO_TAC] = value;
+                return;
+
+
+            case GB_IO_LCDC:
+                if ((value & 0x80) && !(gb->io_registers[GB_IO_LCDC] & 0x80)) {
+                    /* It appears that there's a slight delay after enabling the screen? */
+                    /* Todo: verify this. */
+                    gb->display_cycles = 0;
+                    gb->first_scanline = true;
+                    if (gb->frame_skip_state == GB_FRAMESKIP_SECOND_FRAME_RENDERED) {
+                        gb->frame_skip_state = GB_FRAMESKIP_LCD_TURNED_ON;
+                    }
+                }
+                else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) {
+                    /* Sync after turning off LCD */
+                    GB_timing_sync(gb);
+                }
+                gb->io_registers[GB_IO_LCDC] = value;
+                return;
+
+            case GB_IO_STAT:
+                /* A DMG bug: http://www.devrs.com/gb/files/faqs.html#GBBugs */
+                if (!gb->is_cgb && !gb->stat_interrupt_line &&
+                    (gb->io_registers[GB_IO_STAT] & 0x3) < 2 && (gb->io_registers[GB_IO_LCDC] & 0x80)) {
+                    gb->io_registers[GB_IO_IF] |= 2;
+                }
+                /* Delete previous R/W bits */
+                gb->io_registers[GB_IO_STAT] &= 7;
+                /* Set them by value */
+                gb->io_registers[GB_IO_STAT] |= value & ~7;
+                /* Set unused bit to 1 */
+                gb->io_registers[GB_IO_STAT] |= 0x80;
+                return;
+
+            case GB_IO_DIV:
+                GB_set_internal_div_counter(gb, 0);
+                return;
+
+            case GB_IO_JOYP:
+                gb->io_registers[GB_IO_JOYP] &= 0x0F;
+                gb->io_registers[GB_IO_JOYP] |= value & 0xF0;
+                GB_update_joyp(gb);
+                return;
+
+            case GB_IO_BIOS:
+                gb->boot_rom_finished = true;
+                return;
+
+            case GB_IO_DMG_EMULATION:
+                if (gb->is_cgb && !gb->boot_rom_finished) {
+                    gb->cgb_mode = value != 4; /* The real "contents" of this register aren't quite known yet. */
+                }
+                return;
+
+            case GB_IO_DMA:
+                if (value <= 0xE0) {
+                    if (gb->dma_steps_left) {
+                        /* This is not correct emulation, since we're not really delaying the second DMA.
+                           One write that should have happened in the first DMA will not happen. However,
+                           since that byte will be overwritten by the second DMA before it can actually be
+                           read, it doesn't actually matter. */
+                        gb->is_dma_restarting = true;
+                    }
+                    gb->dma_cycles = -7;
+                    gb->dma_current_dest = 0;
+                    gb->dma_current_src = value << 8;
+                    gb->dma_steps_left = 0xa0;
+                }
+                /* else { what? } */
+
+                return;
+            case GB_IO_SVBK:
+                if (!gb->cgb_mode) {
+                    return;
+                }
+                gb->cgb_ram_bank = value & 0x7;
+                if (!gb->cgb_ram_bank) {
+                    gb->cgb_ram_bank++;
+                }
+                return;
+            case GB_IO_VBK:
+                if (!gb->cgb_mode) {
+                    return;
+                }
+                gb->cgb_vram_bank = value & 0x1;
+                return;
+
+            case GB_IO_BGPI:
+            case GB_IO_OBPI:
+                if (!gb->is_cgb) {
+                    return;
+                }
+                gb->io_registers[addr & 0xFF] = value;
+                return;
+            case GB_IO_BGPD:
+            case GB_IO_OBPD:
+                if (!gb->cgb_mode && gb->boot_rom_finished) {
+                    /* Todo: Due to the behavior of a broken Game & Watch Gallery 2 ROM on a real CGB. A proper test ROM
+                       is required. */
+                    return;
+                }
+                uint8_t index_reg = (addr & 0xFF) - 1;
+                ((addr & 0xFF) == GB_IO_BGPD?
+                 gb->background_palettes_data :
+                 gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F] = value;
+                GB_palette_changed(gb, (addr & 0xFF) == GB_IO_BGPD, gb->io_registers[index_reg] & 0x3F);
+                if (gb->io_registers[index_reg] & 0x80) {
+                    gb->io_registers[index_reg]++;
+                    gb->io_registers[index_reg] |= 0x80;
+                }
+                return;
+            case GB_IO_KEY1:
+                if (!gb->is_cgb) {
+                    return;
+                }
+                gb->io_registers[GB_IO_KEY1] = value;
+                return;
+            case GB_IO_HDMA1:
+                if (gb->cgb_mode) {
+                    gb->hdma_current_src &= 0xF0;
+                    gb->hdma_current_src |= value << 8;
+                }
+                return;
+            case GB_IO_HDMA2:
+                if (gb->cgb_mode) {
+                    gb->hdma_current_src &= 0xFF00;
+                    gb->hdma_current_src |= value & 0xF0;
+                }
+                return;
+            case GB_IO_HDMA3:
+                if (gb->cgb_mode) {
+                    gb->hdma_current_dest &= 0xF0;
+                    gb->hdma_current_dest |= value << 8;
+                }
+                return;
+            case GB_IO_HDMA4:
+                if (gb->cgb_mode) {
+                    gb->hdma_current_dest &= 0x1F00;
+                    gb->hdma_current_dest |= value & 0xF0;
+                }
+                return;
+            case GB_IO_HDMA5:
+                if (!gb->cgb_mode) return;
+                if ((value & 0x80) == 0 && gb->hdma_on_hblank) {
+                    gb->hdma_on_hblank = false;
+                    return;
+                }
+                gb->hdma_on = (value & 0x80) == 0;
+                gb->hdma_on_hblank = (value & 0x80) != 0;
+                if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0) {
+                    gb->hdma_on = true;
+                    gb->hdma_cycles = 0;
+                }
+                gb->io_registers[GB_IO_HDMA5] = value;
+                gb->hdma_steps_left = (gb->io_registers[GB_IO_HDMA5] & 0x7F) + 1;
+                /* Todo: Verify this. Gambatte's DMA tests require this. */
+                if (gb->hdma_current_dest + (gb->hdma_steps_left << 4) > 0xFFFF) {
+                    gb->hdma_steps_left = (0x10000 - gb->hdma_current_dest) >> 4;
+                }
+                gb->hdma_cycles = 0;
+                return;
+
+            /*  Todo: what happens when starting a transfer during a transfer?
+                What happens when starting a transfer during external clock? 
+            */
+            case GB_IO_SC:
+                if (!gb->cgb_mode) {
+                    value |= 2;
+                }
+                gb->io_registers[GB_IO_SC] = value | (~0x83);
+                if ((value & 0x80) && (value & 0x1) ) {
+                    gb->serial_length = gb->cgb_mode && (value & 2)? 128 : 4096;
+                    /* Todo: This is probably incorrect for CGB's faster clock mode. */
+                    gb->serial_cycles &= 0xFF;
+                    if (gb->serial_transfer_start_callback) {
+                        gb->serial_transfer_start_callback(gb, gb->io_registers[GB_IO_SB]);
+                    }
+                }
+                else {
+                    gb->serial_length = 0;
+                }
+                return;
+
+            case GB_IO_RP: {
+                if (!gb->is_cgb) {
+                    return;
+                }
+                if ((value & 1) != (gb->io_registers[GB_IO_RP] & 1)) {
+                    if (gb->infrared_callback) {
+                        gb->infrared_callback(gb, value & 1, gb->cycles_since_ir_change);
+                        gb->cycles_since_ir_change = 0;
+                    }
+                }
+                gb->io_registers[GB_IO_RP] = value;
+                return;
+            }
+
+            default:
+                if ((addr & 0xFF) >= GB_IO_NR10 && (addr & 0xFF) <= GB_IO_WAV_END) {
+                    GB_apu_write(gb, addr & 0xFF, value);
+                    return;
+                }
+                GB_log(gb, "Wrote %02x to %04x (HW Register)\n", value, addr);
+                return;
+        }
+    }
+
+    if (addr == 0xFFFF) {
+        /* Interrupt mask */
+        gb->interrupt_enable = value;
+        return;
+    }
+    
+    /* HRAM */
+    gb->hram[addr - 0xFF80] = value;
+}
+
+
+
+static GB_write_function_t * const write_map[] =
+{
+    write_mbc,         write_mbc,        write_mbc, write_mbc, /* 0XXX, 1XXX, 2XXX, 3XXX */
+    write_mbc,         write_mbc,        write_mbc, write_mbc, /* 4XXX, 5XXX, 6XXX, 7XXX */
+    write_vram,        write_vram,                             /* 8XXX, 9XXX */
+    write_mbc_ram,     write_mbc_ram,                          /* AXXX, BXXX */
+    write_ram,         write_banked_ram,                       /* CXXX, DXXX */
+    write_high_memory, write_high_memory,                      /* EXXX FXXX */
+};
+
+void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
+{
+    if (gb->n_watchpoints) {
+        GB_debugger_test_write_watchpoint(gb, addr, value);
+    }
+    if (is_addr_in_dma_use(gb, addr)) {
+        /* Todo: What should happen? Will this affect DMA? Will data be written? What and where? */
+        return;
+    }
+    write_map[addr >> 12](gb, addr, value);
+}
+
+void GB_dma_run(GB_gameboy_t *gb)
+{
+    while (gb->dma_cycles >= 4 && gb->dma_steps_left) {
+        /* Todo: measure this value */
+        gb->dma_cycles -= 4;
+        gb->dma_steps_left--;
+        gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src);
+        /* dma_current_src must be the correct value during GB_read_memory */
+        gb->dma_current_src++;
+        if (!gb->dma_steps_left) {
+            gb->is_dma_restarting = false;
+        }
+    }
+}
+
+void GB_hdma_run(GB_gameboy_t *gb)
+{
+    if (!gb->hdma_on) return;
+    while (gb->hdma_cycles >= 8) {
+        gb->hdma_cycles -= 8;
+
+        for (uint8_t i = 0; i < 0x10; i++) {
+            GB_write_memory(gb, 0x8000 | (gb->hdma_current_dest++ & 0x1FFF), GB_read_memory(gb, (gb->hdma_current_src++)));
+        }
+        
+        if(--gb->hdma_steps_left == 0){
+            gb->hdma_on = false;
+            gb->hdma_on_hblank = false;
+            gb->io_registers[GB_IO_HDMA5] &= 0x7F;
+            break;
+        }
+        if (gb->hdma_on_hblank) {
+            gb->hdma_on = false;
+            break;
+        }
+    }
+}
diff --git a/waterbox/sameboy/memory.h b/waterbox/sameboy/memory.h
new file mode 100644
index 0000000000..16b3b9220f
--- /dev/null
+++ b/waterbox/sameboy/memory.h
@@ -0,0 +1,12 @@
+#ifndef memory_h
+#define memory_h
+#include "gb.h"
+
+uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr);
+void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value);
+#ifdef GB_INTERNAL
+void GB_dma_run(GB_gameboy_t *gb);
+void GB_hdma_run(GB_gameboy_t *gb);
+#endif
+
+#endif /* memory_h */
diff --git a/waterbox/sameboy/printer.c b/waterbox/sameboy/printer.c
new file mode 100644
index 0000000000..bc1d45ab47
--- /dev/null
+++ b/waterbox/sameboy/printer.c
@@ -0,0 +1,201 @@
+#include "gb.h"
+
+/* TODO: Emulation is VERY basic and assumes the ROM correctly uses the printer's interface.
+         Incorrect usage is not correctly emulated, as it's not well documented, nor do I
+         have my own GB Printer to figure it out myself.
+ 
+         It also does not currently emulate communication timeout, which means that a bug
+         might prevent the printer operation until the GameBoy is restarted.
+ 
+         Also, field mask values are assumed. */
+
+static void handle_command(GB_gameboy_t *gb)
+{
+    
+    switch (gb->printer.command_id) {
+        case GB_PRINTER_INIT_COMMAND:
+            gb->printer.status = 0;
+            gb->printer.image_offset = 0;
+            break;
+            
+        case GB_PRINTER_START_COMMAND:
+            if (gb->printer.command_length == 4) {
+                gb->printer.status = 6; /* Printing */
+                uint32_t image[gb->printer.image_offset];
+                uint8_t palette = gb->printer.command_data[2];
+                uint32_t colors[4] = {gb->rgb_encode_callback(gb, 0xff, 0xff, 0xff),
+                                      gb->rgb_encode_callback(gb, 0xaa, 0xaa, 0xaa),
+                                      gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55),
+                                      gb->rgb_encode_callback(gb, 0x00, 0x00, 0x00)};
+                for (unsigned i = 0; i < gb->printer.image_offset; i++) {
+                    image[i] = colors[(palette >> (gb->printer.image[i] * 2)) & 3];
+                }
+                
+                if (gb->printer.callback) {
+                    gb->printer.callback(gb, image, gb->printer.image_offset / 160,
+                                         gb->printer.command_data[1] >> 4, gb->printer.command_data[1] & 7,
+                                         gb->printer.command_data[3] & 0x7F);
+                }
+                
+                gb->printer.image_offset = 0;
+            }
+            break;
+            
+        case GB_PRINTER_DATA_COMMAND:
+            if (gb->printer.command_length == GB_PRINTER_DATA_SIZE) {
+                gb->printer.image_offset %= sizeof(gb->printer.image);
+                gb->printer.status = 8; /* Received 0x280 bytes */
+                
+                uint8_t *byte = gb->printer.command_data;
+                
+                for (unsigned row = 2; row--; ) {
+                    for (unsigned tile_x = 0; tile_x < 160 / 8; tile_x++) {
+                        for (unsigned y = 0; y < 8; y++, byte += 2) {
+                            for (unsigned x_pixel = 0; x_pixel < 8; x_pixel++) {
+                                    gb->printer.image[gb->printer.image_offset + tile_x * 8 + x_pixel + y * 160] =
+                                        ((*byte) >> 7) | (((*(byte + 1)) >> 7) << 1);
+                                    (*byte) <<= 1;
+                                    (*(byte + 1)) <<= 1;
+                            }
+                        }
+                    }
+                    
+                    gb->printer.image_offset += 8 * 160;
+                }
+            }
+            
+        case GB_PRINTER_NOP_COMMAND:
+        default:
+            break;
+    }
+}
+
+static void serial_start(GB_gameboy_t *gb, uint8_t byte_received)
+{
+    gb->printer.byte_to_send = 0;
+    switch (gb->printer.command_state) {
+        case GB_PRINTER_COMMAND_MAGIC1:
+            if (byte_received != 0x88) {
+                return;
+            }
+            gb->printer.status &= ~1;
+            gb->printer.command_length = 0;
+            gb->printer.checksum = 0;
+            break;
+            
+        case GB_PRINTER_COMMAND_MAGIC2:
+            if (byte_received != 0x33) {
+                if (byte_received != 0x88) {
+                    gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1;
+                }
+                return;
+            }
+            break;
+            
+        case GB_PRINTER_COMMAND_ID:
+            gb->printer.command_id = byte_received & 0xF;
+            break;
+            
+        case GB_PRINTER_COMMAND_COMPRESSION:
+            gb->printer.compression = byte_received & 1;
+            break;
+            
+        case GB_PRINTER_COMMAND_LENGTH_LOW:
+            gb->printer.length_left = byte_received;
+            break;
+            
+        case GB_PRINTER_COMMAND_LENGTH_HIGH:
+            gb->printer.length_left |= (byte_received & 3) << 8;
+            break;
+            
+        case GB_PRINTER_COMMAND_DATA:
+            if (gb->printer.command_length != GB_PRINTER_MAX_COMMAND_LENGTH) {
+                if (gb->printer.compression) {
+                    if (!gb->printer.compression_run_lenth) {
+                        gb->printer.compression_run_is_compressed = byte_received & 0x80;
+                        gb->printer.compression_run_lenth = (byte_received & 0x7F) + 1 + gb->printer.compression_run_is_compressed;
+                    }
+                    else if (gb->printer.compression_run_is_compressed) {
+                        while (gb->printer.compression_run_lenth) {
+                            gb->printer.command_data[gb->printer.command_length++] = byte_received;
+                            gb->printer.compression_run_lenth--;
+                            if (gb->printer.command_length == GB_PRINTER_MAX_COMMAND_LENGTH) {
+                                gb->printer.compression_run_lenth = 0;
+                            }
+                        }
+                    }
+                    else {
+                        gb->printer.command_data[gb->printer.command_length++] = byte_received;
+                        gb->printer.compression_run_lenth--;
+                    }
+                }
+                else {
+                    gb->printer.command_data[gb->printer.command_length++] = byte_received;
+                }
+            }
+            gb->printer.length_left--;
+            break;
+            
+        case GB_PRINTER_COMMAND_CHECKSUM_LOW:
+            gb->printer.checksum ^= byte_received;
+            break;
+            
+        case GB_PRINTER_COMMAND_CHECKSUM_HIGH:
+            gb->printer.checksum ^= byte_received << 8;
+            if (gb->printer.checksum) {
+                gb->printer.status |= 1; /* Checksum error*/
+                gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1;
+                return;
+            }
+            break;
+        case GB_PRINTER_COMMAND_ACTIVE:
+            gb->printer.byte_to_send = 0x81;
+            break;
+        case GB_PRINTER_COMMAND_STATUS:
+            
+            if ((gb->printer.command_id & 0xF) == GB_PRINTER_INIT_COMMAND) {
+                /* Games expect INIT commands to return 0? */
+                gb->printer.byte_to_send = 0;
+            }
+            else {
+                gb->printer.byte_to_send = gb->printer.status;
+            }
+            
+            /* Printing is done instantly, but let the game recieve a 6 (Printing) status at least once, for compatibility */
+            if (gb->printer.status == 6) {
+               gb->printer.status = 4; /* Done */
+            }
+            
+            gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1;
+            handle_command(gb);
+            return;
+    }
+
+    if (gb->printer.command_state >= GB_PRINTER_COMMAND_ID && gb->printer.command_state < GB_PRINTER_COMMAND_CHECKSUM_LOW) {
+        gb->printer.checksum += byte_received;
+    }
+
+    if (gb->printer.command_state != GB_PRINTER_COMMAND_DATA) {
+        gb->printer.command_state++;
+    }
+
+    if (gb->printer.command_state == GB_PRINTER_COMMAND_DATA) {
+        if (gb->printer.length_left == 0) {
+            gb->printer.command_state++;
+        }
+    }
+
+}
+
+static uint8_t serial_end(GB_gameboy_t *gb)
+{
+    return gb->printer.byte_to_send;
+}
+
+void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback)
+{
+    memset(&gb->printer, 0, sizeof(gb->printer));
+    GB_set_serial_transfer_start_callback(gb, serial_start);
+    GB_set_serial_transfer_end_callback(gb, serial_end);
+    gb->printer.callback = callback;
+}
\ No newline at end of file
diff --git a/waterbox/sameboy/printer.h b/waterbox/sameboy/printer.h
new file mode 100644
index 0000000000..e5d9036a08
--- /dev/null
+++ b/waterbox/sameboy/printer.h
@@ -0,0 +1,59 @@
+#ifndef printer_h
+#define printer_h
+#include <stdint.h>
+#include <stdbool.h>
+#include "gb_struct_def.h"
+#define GB_PRINTER_MAX_COMMAND_LENGTH 0x280
+#define GB_PRINTER_DATA_SIZE 0x280
+
+typedef void (*GB_print_image_callback_t)(GB_gameboy_t *gb,
+                                          uint32_t *image,
+                                          uint8_t height,
+                                          uint8_t top_margin,
+                                          uint8_t bottom_margin,
+                                          uint8_t exposure);
+
+
+typedef struct
+{
+    /* Communication state machine */
+
+    enum {
+        GB_PRINTER_COMMAND_MAGIC1,
+        GB_PRINTER_COMMAND_MAGIC2,
+        GB_PRINTER_COMMAND_ID,
+        GB_PRINTER_COMMAND_COMPRESSION,
+        GB_PRINTER_COMMAND_LENGTH_LOW,
+        GB_PRINTER_COMMAND_LENGTH_HIGH,
+        GB_PRINTER_COMMAND_DATA,
+        GB_PRINTER_COMMAND_CHECKSUM_LOW,
+        GB_PRINTER_COMMAND_CHECKSUM_HIGH,
+        GB_PRINTER_COMMAND_ACTIVE,
+        GB_PRINTER_COMMAND_STATUS,
+    } command_state : 8;
+    enum {
+        GB_PRINTER_INIT_COMMAND = 1,
+        GB_PRINTER_START_COMMAND = 2,
+        GB_PRINTER_DATA_COMMAND = 4,
+        GB_PRINTER_NOP_COMMAND = 0xF,
+    } command_id : 8;
+    bool compression;
+    uint16_t length_left;
+    uint8_t command_data[GB_PRINTER_MAX_COMMAND_LENGTH];
+    uint16_t command_length;
+    uint16_t checksum;
+    uint8_t status;
+    uint8_t byte_to_send;
+    
+    uint8_t image[160 * 200];
+    uint16_t image_offset;
+    
+    GB_print_image_callback_t callback;
+    
+    uint8_t compression_run_lenth;
+    bool compression_run_is_compressed;
+} GB_printer_t;
+
+
+void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback);
+#endif
diff --git a/waterbox/sameboy/save_state.c b/waterbox/sameboy/save_state.c
new file mode 100644
index 0000000000..cd579aa800
--- /dev/null
+++ b/waterbox/sameboy/save_state.c
@@ -0,0 +1,304 @@
+#include "gb.h"
+#include <stdio.h>
+#include <errno.h>
+
+static bool dump_section(FILE *f, const void *src, uint32_t size)
+{
+    if (fwrite(&size, 1, sizeof(size), f) != sizeof(size)) {
+        return false;
+    }
+    
+    if (fwrite(src, 1, size, f) != size) {
+        return false;
+    }
+    
+    return true;
+}
+
+#define DUMP_SECTION(gb, f, section) dump_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section))
+
+/* Todo: we need a sane and protable save state format. */
+int GB_save_state(GB_gameboy_t *gb, const char *path)
+{
+    FILE *f = fopen(path, "wb");
+    if (!f) {
+        GB_log(gb, "Could not open save state: %s.\n", strerror(errno));
+        return errno;
+    }
+    
+    if (fwrite(GB_GET_SECTION(gb, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error;
+    if (!DUMP_SECTION(gb, f, core_state)) goto error;
+    if (!DUMP_SECTION(gb, f, dma       )) goto error;
+    if (!DUMP_SECTION(gb, f, mbc       )) goto error;
+    if (!DUMP_SECTION(gb, f, hram      )) goto error;
+    if (!DUMP_SECTION(gb, f, timing    )) goto error;
+    if (!DUMP_SECTION(gb, f, apu       )) goto error;
+    if (!DUMP_SECTION(gb, f, rtc       )) goto error;
+    if (!DUMP_SECTION(gb, f, video     )) goto error;
+    
+    
+    if (fwrite(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) {
+        goto error;
+    }
+    
+    if (fwrite(gb->ram, 1, gb->ram_size, f) != gb->ram_size) {
+        goto error;
+    }
+    
+    if (fwrite(gb->vram, 1, gb->vram_size, f) != gb->vram_size) {
+        goto error;
+    }
+    
+    errno = 0;
+    
+error:
+    fclose(f);
+    return errno;
+}
+
+#undef DUMP_SECTION
+
+size_t GB_get_save_state_size(GB_gameboy_t *gb)
+{
+    return GB_SECTION_SIZE(header)
+    + GB_SECTION_SIZE(core_state) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(dma       ) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(mbc       ) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(hram      ) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(timing    ) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(apu       ) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(rtc       ) + sizeof(uint32_t)
+    + GB_SECTION_SIZE(video     ) + sizeof(uint32_t)
+    + gb->mbc_ram_size
+    + gb->ram_size
+    + gb->vram_size;
+}
+
+/* A write-line function for memory copying */
+static void buffer_write(const void *src, size_t size, uint8_t **dest)
+{
+    memcpy(*dest, src, size);
+    *dest += size;
+}
+
+static void buffer_dump_section(uint8_t **buffer, const void *src, uint32_t size)
+{
+    buffer_write(&size, sizeof(size), buffer);
+    buffer_write(src, size, buffer);
+}
+
+#define DUMP_SECTION(gb, buffer, section) buffer_dump_section(&buffer, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section))
+void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer)
+{
+    buffer_write(GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header), &buffer);
+    DUMP_SECTION(gb, buffer, core_state);
+    DUMP_SECTION(gb, buffer, dma       );
+    DUMP_SECTION(gb, buffer, mbc       );
+    DUMP_SECTION(gb, buffer, hram      );
+    DUMP_SECTION(gb, buffer, timing    );
+    DUMP_SECTION(gb, buffer, apu       );
+    DUMP_SECTION(gb, buffer, rtc       );
+    DUMP_SECTION(gb, buffer, video     );
+    
+    
+    buffer_write(gb->mbc_ram, gb->mbc_ram_size, &buffer);
+    buffer_write(gb->ram, gb->ram_size, &buffer);
+    buffer_write(gb->vram, gb->vram_size, &buffer);
+}
+
+/* Best-effort read function for maximum future compatibility. */
+static bool read_section(FILE *f, void *dest, uint32_t size)
+{
+    uint32_t saved_size = 0;
+    if (fread(&saved_size, 1, sizeof(size), f) != sizeof(size)) {
+        return false;
+    }
+    
+    if (saved_size <= size) {
+        if (fread(dest, 1, saved_size, f) != saved_size) {
+            return false;
+        }
+    }
+    else {
+        if (fread(dest, 1, size, f) != size) {
+            return false;
+        }
+        fseek(f, saved_size - size, SEEK_CUR);
+    }
+    
+    return true;
+}
+#undef DUMP_SECTION
+
+static bool verify_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save)
+{
+    if (gb->magic != save->magic) {
+        GB_log(gb, "File is not a save state, or is from an incompatible operating system.\n");
+        return false;
+    }
+    
+    if (gb->version != save->version) {
+        GB_log(gb, "Save state is for a different version of SameBoy.\n");
+        return false;
+    }
+    
+    if (gb->mbc_ram_size < save->mbc_ram_size) {
+        GB_log(gb, "Save state has non-matching MBC RAM size.\n");
+        return false;
+    }
+    
+    if (gb->ram_size != save->ram_size) {
+        GB_log(gb, "Save state has non-matching RAM size. Try changing emulated model.\n");
+        return false;
+    }
+    
+    if (gb->vram_size != save->vram_size) {
+        GB_log(gb, "Save state has non-matching VRAM size. Try changing emulated model.\n");
+        return false;
+    }
+    
+    return true;
+}
+
+#define READ_SECTION(gb, f, section) read_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section))
+
+int GB_load_state(GB_gameboy_t *gb, const char *path)
+{
+    GB_gameboy_t save;
+    
+    /* Every unread value should be kept the same. */
+    memcpy(&save, gb, sizeof(save));
+    
+    FILE *f = fopen(path, "rb");
+    if (!f) {
+        GB_log(gb, "Could not open save state: %s.\n", strerror(errno));
+        return errno;
+    }
+    
+    if (fread(GB_GET_SECTION(&save, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error;
+    if (!READ_SECTION(&save, f, core_state)) goto error;
+    if (!READ_SECTION(&save, f, dma       )) goto error;
+    if (!READ_SECTION(&save, f, mbc       )) goto error;
+    if (!READ_SECTION(&save, f, hram      )) goto error;
+    if (!READ_SECTION(&save, f, timing    )) goto error;
+    if (!READ_SECTION(&save, f, apu       )) goto error;
+    if (!READ_SECTION(&save, f, rtc       )) goto error;
+    if (!READ_SECTION(&save, f, video     )) goto error;
+    
+    if (!verify_state_compatibility(gb, &save)) {
+        errno = -1;
+        goto error;
+    }
+    
+    memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size);
+    if (fread(gb->mbc_ram, 1, save.mbc_ram_size, f) != save.mbc_ram_size) {
+        fclose(f);
+        return EIO;
+    }
+    
+    if (fread(gb->ram, 1, gb->ram_size, f) != gb->ram_size) {
+        fclose(f);
+        return EIO;
+    }
+    
+    if (fread(gb->vram, 1, gb->vram_size, f) != gb->vram_size) {
+        fclose(f);
+        return EIO;
+    }
+    
+    memcpy(gb, &save, sizeof(save));
+    errno = 0;
+    
+    if (gb->cartridge_type->has_rumble && gb->rumble_callback) {
+        gb->rumble_callback(gb, gb->rumble_state);
+    }
+    
+error:
+    fclose(f);
+    return errno;
+}
+
+#undef READ_SECTION
+
+/* An read-like function for buffer-copying */
+static size_t buffer_read(void *dest, size_t length, const uint8_t **buffer, size_t *buffer_length)
+{
+    if (length > *buffer_length) {
+        length = *buffer_length;
+    }
+    
+    memcpy(dest, *buffer, length);
+    *buffer += length;
+    *buffer_length -= length;
+    
+    return length;
+}
+
+static bool buffer_read_section(const uint8_t **buffer, size_t *buffer_length, void *dest, uint32_t size)
+{
+    uint32_t saved_size = 0;
+    if (buffer_read(&saved_size, sizeof(size), buffer, buffer_length) != sizeof(size)) {
+        return false;
+    }
+    
+    if (saved_size <= size) {
+        if (buffer_read(dest, saved_size, buffer, buffer_length) != saved_size) {
+            return false;
+        }
+    }
+    else {
+        if (buffer_read(dest, size, buffer, buffer_length) != size) {
+            return false;
+        }
+        *buffer += saved_size - size;
+        *buffer_length -= saved_size - size;
+    }
+    
+    return true;
+}
+
+#define READ_SECTION(gb, buffer, length, section) buffer_read_section(&buffer, &length, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section))
+int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length)
+{
+    GB_gameboy_t save;
+    
+    /* Every unread value should be kept the same. */
+    memcpy(&save, gb, sizeof(save));
+    
+    if (buffer_read(GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header), &buffer, &length) != GB_SECTION_SIZE(header)) return -1;
+    if (!READ_SECTION(&save, buffer, length, core_state)) return -1;
+    if (!READ_SECTION(&save, buffer, length, dma       )) return -1;
+    if (!READ_SECTION(&save, buffer, length, mbc       )) return -1;
+    if (!READ_SECTION(&save, buffer, length, hram      )) return -1;
+    if (!READ_SECTION(&save, buffer, length, timing    )) return -1;
+    if (!READ_SECTION(&save, buffer, length, apu       )) return -1;
+    if (!READ_SECTION(&save, buffer, length, rtc       )) return -1;
+    if (!READ_SECTION(&save, buffer, length, video     )) return -1;
+    
+    if (!verify_state_compatibility(gb, &save)) {
+        return -1;
+    }
+    
+    memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size);
+    if (buffer_read(gb->mbc_ram, save.mbc_ram_size, &buffer, &length) != save.mbc_ram_size) {
+        return -1;
+    }
+    
+    if (buffer_read(gb->ram, gb->ram_size, &buffer, &length) != gb->ram_size) {
+        return -1;
+    }
+    
+    if (buffer_read(gb->vram,gb->vram_size, &buffer, &length) != gb->vram_size) {
+        return -1;
+    }
+    
+    memcpy(gb, &save, sizeof(save));
+    
+    if (gb->cartridge_type->has_rumble && gb->rumble_callback) {
+        gb->rumble_callback(gb, gb->rumble_state);
+    }
+    
+    return 0;
+}
+
+#undef READ_SECTION
diff --git a/waterbox/sameboy/save_state.h b/waterbox/sameboy/save_state.h
new file mode 100644
index 0000000000..546ac2d95a
--- /dev/null
+++ b/waterbox/sameboy/save_state.h
@@ -0,0 +1,24 @@
+/* Macros to make the GB_gameboy_t struct more future compatible when state saving */
+#ifndef save_state_h
+#define save_state_h
+#include <stddef.h>
+
+#define GB_PADDING(type, old_usage) type old_usage##__do_not_use
+
+#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) struct {} name##_section_start; __VA_ARGS__; struct {} name##_section_end
+#define GB_SECTION_OFFSET(name) (offsetof(GB_gameboy_t, name##_section_start))
+#define GB_SECTION_SIZE(name) (offsetof(GB_gameboy_t, name##_section_end) - offsetof(GB_gameboy_t, name##_section_start))
+#define GB_GET_SECTION(gb, name) ((void*)&((gb)->name##_section_start))
+
+#define GB_aligned_double __attribute__ ((aligned (8))) double
+
+
+/* Public calls related to save states */
+int GB_save_state(GB_gameboy_t *gb, const char *path);
+size_t GB_get_save_state_size(GB_gameboy_t *gb);
+/* Assumes buffer is big enough to contain the save state. Use with GB_get_save_state_size(). */
+void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer);
+
+int GB_load_state(GB_gameboy_t *gb, const char *path);
+int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length);
+#endif /* save_state_h */
diff --git a/waterbox/sameboy/symbol_hash.c b/waterbox/sameboy/symbol_hash.c
new file mode 100644
index 0000000000..709421c233
--- /dev/null
+++ b/waterbox/sameboy/symbol_hash.c
@@ -0,0 +1,106 @@
+#include "gb.h"
+
+static size_t GB_map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr)
+{
+    if (!map->symbols) {
+        return 0;
+    }
+    ssize_t min = 0;
+    ssize_t max = map->n_symbols;
+    while (min < max) {
+        size_t pivot = (min + max) / 2;
+        if (map->symbols[pivot].addr == addr) return pivot;
+        if (map->symbols[pivot].addr > addr) {
+            max = pivot;
+        }
+        else {
+            min = pivot + 1;
+        }
+    }
+    return (size_t) min;
+}
+
+GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name)
+{
+    size_t index = GB_map_find_symbol_index(map, addr);
+
+    if (index < map->n_symbols && map->symbols[index].addr == addr) return NULL;
+
+    map->symbols = realloc(map->symbols, (map->n_symbols + 1) * sizeof(map->symbols[0]));
+    memmove(&map->symbols[index + 1], &map->symbols[index], (map->n_symbols - index) * sizeof(map->symbols[0]));
+    map->symbols[index].addr = addr;
+    map->symbols[index].name = strdup(name);
+    map->n_symbols++;
+    return &map->symbols[index];
+}
+
+const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr)
+{
+    if (!map) return NULL;
+    size_t index = GB_map_find_symbol_index(map, addr);
+    if (index < map->n_symbols && map->symbols[index].addr != addr) {
+        index--;
+    }
+    if (index < map->n_symbols) {
+        return &map->symbols[index];
+    }
+    return NULL;
+}
+
+GB_symbol_map_t *GB_map_alloc(void)
+{
+    GB_symbol_map_t *map = malloc(sizeof(*map));
+    memset(map, 0, sizeof(*map));
+    return map;
+}
+
+void GB_map_free(GB_symbol_map_t *map)
+{
+    for (unsigned i = 0; i < map->n_symbols; i++) {
+        free(map->symbols[i].name);
+    }
+
+    if (map->symbols) {
+        free(map->symbols);
+    }
+
+    free(map);
+}
+
+static int hash_name(const char *name)
+{
+    int r = 0;
+    while (*name) {
+        r <<= 1;
+        if (r & 0x400) {
+            r ^= 0x401;
+        }
+        r += (unsigned char)*(name++);
+    }
+
+    return r & 0x3FF;
+}
+
+void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *bank_symbol)
+{
+    int hash = hash_name(bank_symbol->name);
+    GB_symbol_t *symbol = malloc(sizeof(*symbol));
+    symbol->name = bank_symbol->name;
+    symbol->addr = bank_symbol->addr;
+    symbol->bank = bank;
+    symbol->next = map->buckets[hash];
+    map->buckets[hash] = symbol;
+}
+
+const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name)
+{
+    int hash = hash_name(name);
+    GB_symbol_t *symbol = map->buckets[hash];
+
+    while (symbol) {
+        if (strcmp(symbol->name, name) == 0) return symbol;
+        symbol = symbol->next;
+    }
+
+    return NULL;
+}
diff --git a/waterbox/sameboy/symbol_hash.h b/waterbox/sameboy/symbol_hash.h
new file mode 100644
index 0000000000..239b0e313e
--- /dev/null
+++ b/waterbox/sameboy/symbol_hash.h
@@ -0,0 +1,37 @@
+#ifndef symbol_hash_h
+#define symbol_hash_h
+
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct {
+    char *name;
+    uint16_t addr;
+} GB_bank_symbol_t;
+
+typedef struct GB_symbol_s {
+    struct GB_symbol_s *next;
+    const char *name;
+    uint16_t bank;
+    uint16_t addr;
+} GB_symbol_t;
+
+typedef struct {
+    GB_bank_symbol_t *symbols;
+    size_t n_symbols;
+} GB_symbol_map_t;
+
+typedef struct {
+    GB_symbol_t *buckets[0x400];
+} GB_reversed_symbol_map_t;
+
+#ifdef GB_INTERNAL
+void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *symbol);
+const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name);
+GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name);
+const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr);
+GB_symbol_map_t *GB_map_alloc(void);
+void GB_map_free(GB_symbol_map_t *map);
+#endif
+
+#endif /* symbol_hash_h */
diff --git a/waterbox/sameboy/timing.c b/waterbox/sameboy/timing.c
new file mode 100644
index 0000000000..77348fe232
--- /dev/null
+++ b/waterbox/sameboy/timing.c
@@ -0,0 +1,228 @@
+#include "gb.h"
+#ifdef _WIN32
+#define _WIN32_WINNT 0x0500
+#include <Windows.h>
+#else
+#include <sys/time.h>
+#endif
+
+static int64_t get_nanoseconds(void)
+{
+#ifndef _WIN32
+    struct timeval now;
+    gettimeofday(&now, NULL);
+    return (now.tv_usec) * 1000 + now.tv_sec * 1000000000L;
+#else
+    FILETIME time;
+    GetSystemTimeAsFileTime(&time);
+    return (((int64_t)time.dwHighDateTime << 32) | time.dwLowDateTime) * 100L;
+#endif
+}
+
+static void nsleep(uint64_t nanoseconds)
+{
+#ifndef _WIN32
+    struct timespec sleep = {0, nanoseconds};
+    nanosleep(&sleep, NULL);
+#else
+    HANDLE timer;
+    LARGE_INTEGER time;
+    timer = CreateWaitableTimer(NULL, true, NULL);
+    time.QuadPart = -(nanoseconds / 100L);
+    SetWaitableTimer(timer, &time, 0, NULL, NULL, false);
+    WaitForSingleObject(timer, INFINITE);
+    CloseHandle(timer);
+#endif
+}
+
+bool GB_timing_sync_turbo(GB_gameboy_t *gb)
+{
+    if (!gb->turbo_dont_skip) {
+        int64_t nanoseconds = get_nanoseconds();
+        if (nanoseconds <= gb->last_sync + FRAME_LENGTH) {
+            return true;
+        }
+        gb->last_sync = nanoseconds;
+    }
+    return false;
+}
+
+void GB_timing_sync(GB_gameboy_t *gb)
+{
+    if (gb->turbo) {
+        gb->cycles_since_last_sync = 0;
+        return;
+    }
+    /* Prevent syncing if not enough time has passed.*/
+    if (gb->cycles_since_last_sync < LCDC_PERIOD / 4) return;
+
+    uint64_t target_nanoseconds = gb->cycles_since_last_sync * FRAME_LENGTH / LCDC_PERIOD;
+    int64_t nanoseconds = get_nanoseconds();
+    if (labs((signed long)(nanoseconds - gb->last_sync)) < target_nanoseconds ) {
+        nsleep(target_nanoseconds  + gb->last_sync - nanoseconds);
+        gb->last_sync += target_nanoseconds;
+    }
+    else {
+        gb->last_sync = nanoseconds;
+    }
+
+    gb->cycles_since_last_sync = 0;
+}
+
+static void GB_ir_run(GB_gameboy_t *gb)
+{
+    if (gb->ir_queue_length == 0) return;
+    if (gb->cycles_since_input_ir_change >= gb->ir_queue[0].delay) {
+        gb->cycles_since_input_ir_change -= gb->ir_queue[0].delay;
+        gb->infrared_input = gb->ir_queue[0].state;
+        gb->ir_queue_length--;
+        memmove(&gb->ir_queue[0], &gb->ir_queue[1], sizeof(gb->ir_queue[0]) * (gb->ir_queue_length));
+    }
+}
+
+static void advance_tima_state_machine(GB_gameboy_t *gb)
+{
+    if (gb->tima_reload_state == GB_TIMA_RELOADED) {
+        gb->tima_reload_state = GB_TIMA_RUNNING;
+    }
+    else if (gb->tima_reload_state == GB_TIMA_RELOADING) {
+        gb->tima_reload_state = GB_TIMA_RELOADED;
+    }
+}
+
+void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
+{
+    // Affected by speed boost
+    gb->dma_cycles += cycles;
+
+    advance_tima_state_machine(gb);
+    for (int i = 0; i < cycles; i += 4) {
+        GB_set_internal_div_counter(gb, gb->div_cycles + 4);
+    }
+
+    if (cycles > 4) {
+        advance_tima_state_machine(gb);
+        if (cycles > 8) {
+            advance_tima_state_machine(gb);
+        }
+    }
+
+    uint16_t previous_serial_cycles = gb->serial_cycles;
+    gb->serial_cycles += cycles;
+    if (gb->serial_length) {
+        if ((gb->serial_cycles & gb->serial_length) != (previous_serial_cycles & gb->serial_length)) {
+            gb->serial_length = 0;
+            gb->io_registers[GB_IO_SC] &= ~0x80;
+            /* TODO: Does SB "update" bit by bit? */
+            if (gb->serial_transfer_end_callback) {
+                gb->io_registers[GB_IO_SB] = gb->serial_transfer_end_callback(gb);
+            }
+            else {
+                gb->io_registers[GB_IO_SB] = 0xFF;
+            }
+            
+            gb->io_registers[GB_IO_IF] |= 8;
+        }
+    }
+
+    gb->debugger_ticks += cycles;
+
+    if (gb->cgb_double_speed) {
+        cycles >>=1;
+    }
+
+    // Not affected by speed boost
+    gb->hdma_cycles += cycles;
+    gb->apu_sample_cycles += cycles;
+    gb->apu_subsample_cycles += cycles;
+    gb->apu.apu_cycles += cycles;
+    gb->cycles_since_ir_change += cycles;
+    gb->cycles_since_input_ir_change += cycles;
+    gb->cycles_since_last_sync += cycles;
+    GB_dma_run(gb);
+    GB_hdma_run(gb);
+    GB_apu_run(gb);
+    GB_display_run(gb, cycles);
+    GB_ir_run(gb);
+}
+
+/* Standard Timers */
+static const unsigned int GB_TAC_RATIOS[] = {1024, 16, 64, 256};
+
+static void increase_tima(GB_gameboy_t *gb)
+{
+    gb->io_registers[GB_IO_TIMA]++;
+    if (gb->io_registers[GB_IO_TIMA] == 0) {
+        gb->io_registers[GB_IO_TIMA] = gb->io_registers[GB_IO_TMA];
+        gb->io_registers[GB_IO_IF] |= 4;
+        gb->tima_reload_state = GB_TIMA_RELOADING;
+    }
+}
+
+static bool counter_overflow_check(uint32_t old, uint32_t new, uint32_t max)
+{
+    return (old & (max >> 1)) && !(new & (max >> 1));
+}
+
+void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value)
+{
+    /* TIMA increases when a specific high-bit becomes a low-bit. */
+    value &= INTERNAL_DIV_CYCLES - 1;
+    if ((gb->io_registers[GB_IO_TAC] & 4) &&
+        counter_overflow_check(gb->div_cycles, value, GB_TAC_RATIOS[gb->io_registers[GB_IO_TAC] & 3])) {
+        increase_tima(gb);
+    }
+    gb->div_cycles = value;
+}
+
+/* 
+   This glitch is based on the expected results of mooneye-gb rapid_toggle test.
+   This glitch happens because how TIMA is increased, see GB_set_internal_div_counter.
+   According to GiiBiiAdvance, GBC's behavior is different, but this was not tested or implemented.
+*/
+void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac)
+{
+    /* Glitch only happens when old_tac is enabled. */
+    if (!(old_tac & 4)) return;
+
+    unsigned int old_clocks = GB_TAC_RATIOS[old_tac & 3];
+    unsigned int new_clocks = GB_TAC_RATIOS[new_tac & 3];
+
+    /* The bit used for overflow testing must have been 1 */
+    if (gb->div_cycles & (old_clocks >> 1)) {
+        /* And now either the timer must be disabled, or the new bit used for overflow testing be 0. */
+        if (!(new_tac & 4) || gb->div_cycles & (new_clocks >> 1)) {
+            increase_tima(gb);
+        }
+    }
+}
+
+void GB_rtc_run(GB_gameboy_t *gb)
+{
+    if ((gb->rtc_real.high & 0x40) == 0) { /* is timer running? */
+        time_t current_time = time(NULL);
+        while (gb->last_rtc_second < current_time) {
+            gb->last_rtc_second++;
+            if (++gb->rtc_real.seconds == 60)
+            {
+                gb->rtc_real.seconds = 0;
+                if (++gb->rtc_real.minutes == 60)
+                {
+                    gb->rtc_real.minutes = 0;
+                    if (++gb->rtc_real.hours == 24)
+                    {
+                        gb->rtc_real.hours = 0;
+                        if (++gb->rtc_real.days == 0)
+                        {
+                            if (gb->rtc_real.high & 1) /* Bit 8 of days*/
+                            {
+                                gb->rtc_real.high |= 0x80; /* Overflow bit */
+                            }
+                            gb->rtc_real.high ^= 1;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/waterbox/sameboy/timing.h b/waterbox/sameboy/timing.h
new file mode 100644
index 0000000000..ed9e15a513
--- /dev/null
+++ b/waterbox/sameboy/timing.h
@@ -0,0 +1,21 @@
+#ifndef timing_h
+#define timing_h
+#include "gb.h"
+
+#ifdef GB_INTERNAL
+void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles);
+void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value);
+void GB_rtc_run(GB_gameboy_t *gb);
+void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac);
+bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */
+void GB_timing_sync(GB_gameboy_t *gb);
+
+
+enum {
+    GB_TIMA_RUNNING = 0,
+    GB_TIMA_RELOADING = 1,
+    GB_TIMA_RELOADED = 2
+};
+#endif
+
+#endif /* timing_h */
diff --git a/waterbox/sameboy/z80_cpu.c b/waterbox/sameboy/z80_cpu.c
new file mode 100644
index 0000000000..0a1bde5dd7
--- /dev/null
+++ b/waterbox/sameboy/z80_cpu.c
@@ -0,0 +1,1381 @@
+#include <stdio.h>
+#include <stdbool.h>
+#include "gb.h"
+
+
+typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode);
+
+static void ill(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_log(gb, "Illegal Opcode. Halting.\n");
+    gb->interrupt_enable = 0;
+    gb->halted = true;
+}
+
+static void nop(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+}
+
+static void stop(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    if (gb->io_registers[GB_IO_KEY1] & 0x1) {
+        /* Make sure we don't leave display_cycles not divisble by 4 in single speed mode */
+        if (gb->display_cycles % 4 == 2) {
+            GB_advance_cycles(gb, 4);
+        }
+        
+        /* Todo: the switch is not instant. We should emulate this. */
+        gb->cgb_double_speed ^= true;
+        gb->io_registers[GB_IO_KEY1] = 0;
+    }
+    else {
+        gb->stopped = true;
+    }
+    gb->pc++;
+}
+
+/* Operand naming conventions for functions:
+   r = 8-bit register
+   lr = low 8-bit register
+   hr = high 8-bit register
+   rr = 16-bit register
+   d8 = 8-bit imm
+   d16 = 16-bit imm
+   d.. = [..]
+   cc = condition code (z, nz, c, nc)
+   */
+
+static void ld_rr_d16(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    uint16_t value;
+    GB_advance_cycles(gb, 4);
+    register_id = (opcode >> 4) + 1;
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    value |= GB_read_memory(gb, gb->pc++) << 8;
+    GB_advance_cycles(gb, 4);
+    gb->registers[register_id] = value;
+}
+
+static void ld_drr_a(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 4);
+    register_id = (opcode >> 4) + 1;
+    GB_write_memory(gb, gb->registers[register_id], gb->registers[GB_REGISTER_AF] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void inc_rr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 8);
+    register_id = (opcode >> 4) + 1;
+    gb->registers[register_id]++;
+}
+
+static void inc_hr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 4);
+    register_id = ((opcode >> 4) + 1) & 0x03;
+    gb->registers[register_id] += 0x100;
+    gb->registers[GB_REGISTER_AF] &= ~(GB_SUBSTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG);
+
+    if ((gb->registers[register_id] & 0x0F00) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((gb->registers[register_id] & 0xFF00) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+static void dec_hr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 4);
+    register_id = ((opcode >> 4) + 1) & 0x03;
+    gb->registers[register_id] -= 0x100;
+    gb->registers[GB_REGISTER_AF] &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG);
+    gb->registers[GB_REGISTER_AF] |= GB_SUBSTRACT_FLAG;
+
+    if ((gb->registers[register_id] & 0x0F00) == 0xF00) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((gb->registers[register_id] & 0xFF00) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void ld_hr_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 4);
+    register_id = ((opcode >> 4) + 1) & 0x03;
+    gb->registers[register_id] &= 0xFF;
+    gb->registers[register_id] |= GB_read_memory(gb, gb->pc++) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void rlca(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool carry = (gb->registers[GB_REGISTER_AF] & 0x8000) != 0;
+
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] & 0xFF00) << 1;
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG | 0x0100;
+    }
+}
+
+static void rla(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool bit7 = (gb->registers[GB_REGISTER_AF] & 0x8000) != 0;
+    bool carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] & 0xFF00) << 1;
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= 0x0100;
+    }
+    if (bit7) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode)
+{
+    /* Todo: Verify order is correct */
+    uint16_t addr;
+    GB_advance_cycles(gb, 4);
+    addr = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    addr |= GB_read_memory(gb, gb->pc++) << 8;
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, addr, gb->registers[GB_REGISTER_SP] & 0xFF);
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, addr+1, gb->registers[GB_REGISTER_SP] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint16_t hl = gb->registers[GB_REGISTER_HL];
+    uint16_t rr;
+    uint8_t register_id;
+    GB_advance_cycles(gb, 8);
+    register_id = (opcode >> 4) + 1;
+    rr = gb->registers[register_id];
+    gb->registers[GB_REGISTER_HL] = hl + rr;
+    gb->registers[GB_REGISTER_AF] &= ~(GB_SUBSTRACT_FLAG | GB_CARRY_FLAG | GB_HALF_CARRY_FLAG);
+
+    /* The meaning of the Half Carry flag is really hard to track -_- */
+    if (((hl & 0xFFF) + (rr & 0xFFF)) & 0x1000) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ( ((unsigned long) hl) + ((unsigned long) rr) & 0x10000) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void ld_a_drr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    register_id = (opcode >> 4) + 1;
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF;
+    gb->registers[GB_REGISTER_AF] |= GB_read_memory(gb, gb->registers[register_id]) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void dec_rr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 8);
+    register_id = (opcode >> 4) + 1;
+    gb->registers[register_id]--;
+}
+
+static void inc_lr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    register_id = (opcode >> 4) + 1;
+
+    value = (gb->registers[register_id] & 0xFF) + 1;
+    gb->registers[register_id] = (gb->registers[register_id] & 0xFF00) | value;
+
+    gb->registers[GB_REGISTER_AF] &= ~(GB_SUBSTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG);
+
+    if ((gb->registers[register_id] & 0x0F) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((gb->registers[register_id] & 0xFF) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+static void dec_lr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    register_id = (opcode >> 4) + 1;
+
+    value = (gb->registers[register_id] & 0xFF) - 1;
+    gb->registers[register_id] = (gb->registers[register_id] & 0xFF00) | value;
+
+    gb->registers[GB_REGISTER_AF] &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG);
+    gb->registers[GB_REGISTER_AF] |= GB_SUBSTRACT_FLAG;
+
+    if ((gb->registers[register_id] & 0x0F) == 0xF) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((gb->registers[register_id] & 0xFF) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void ld_lr_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 4);
+    register_id = (opcode >> 4) + 1;
+    gb->registers[register_id] &= 0xFF00;
+    gb->registers[register_id] |= GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+}
+
+static void rrca(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool carry = (gb->registers[GB_REGISTER_AF] & 0x100) != 0;
+
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] >> 1) & 0xFF00;
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG | 0x8000;
+    }
+}
+
+static void rra(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool bit1 = (gb->registers[GB_REGISTER_AF] & 0x0100) != 0;
+    bool carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] >> 1) & 0xFF00;
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= 0x8000;
+    }
+    if (bit1) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void jr_r8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    /* Todo: Verify cycles are not 8 and 4 instead */
+    GB_advance_cycles(gb, 4);
+    gb->pc += (int8_t) GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 8);
+}
+
+static bool condition_code(GB_gameboy_t *gb, uint8_t opcode)
+{
+    switch ((opcode >> 3) & 0x3) {
+        case 0:
+            return !(gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG);
+        case 1:
+            return (gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG);
+        case 2:
+            return !(gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG);
+        case 3:
+            return (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG);
+    }
+
+    return false;
+}
+
+static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    if (condition_code(gb, opcode)) {
+        GB_advance_cycles(gb, 4);
+        gb->pc += (int8_t)GB_read_memory(gb, gb->pc++);
+        GB_advance_cycles(gb, 8);
+    }
+    else {
+        GB_advance_cycles(gb, 8);
+        gb->pc += 1;
+    }
+}
+
+static void daa(GB_gameboy_t *gb, uint8_t opcode)
+{
+    /* This function is UGLY and UNREADABLE! But it passes Blargg's daa test! */
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= ~GB_ZERO_FLAG;
+    if (gb->registers[GB_REGISTER_AF] & GB_SUBSTRACT_FLAG) {
+        if (gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) {
+            gb->registers[GB_REGISTER_AF] &= ~GB_HALF_CARRY_FLAG;
+            if (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) {
+                gb->registers[GB_REGISTER_AF] += 0x9A00;
+            }
+            else {
+                gb->registers[GB_REGISTER_AF] += 0xFA00;
+            }
+        }
+        else if(gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) {
+            gb->registers[GB_REGISTER_AF] += 0xA000;
+        }
+    }
+    else {
+        if (gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) {
+            uint16_t number = gb->registers[GB_REGISTER_AF] >> 8;
+            if (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) {
+                number += 0x100;
+            }
+            gb->registers[GB_REGISTER_AF] = 0;
+            number += 0x06;
+            if (number >= 0xa0) {
+                number -= 0xa0;
+                gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+            }
+            gb->registers[GB_REGISTER_AF] |= number << 8;
+        }
+        else {
+            uint16_t number = gb->registers[GB_REGISTER_AF] >> 8;
+            if (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) {
+                number += 0x100;
+            }
+            if (number > 0x99) {
+                number += 0x60;
+            }
+            number = (number & 0x0F) + ((number & 0x0F) > 9 ? 6 : 0) + (number & 0xFF0);
+            gb->registers[GB_REGISTER_AF] = number << 8;
+            if (number & 0xFF00) {
+                gb->registers[GB_REGISTER_AF]  |= GB_CARRY_FLAG;
+            }
+        }
+    }
+    if ((gb->registers[GB_REGISTER_AF] & 0xFF00) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void cpl(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] ^= 0xFF00;
+    gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG | GB_SUBSTRACT_FLAG;
+}
+
+static void scf(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBSTRACT_FLAG);
+}
+
+static void ccf(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] ^= GB_CARRY_FLAG;
+    gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBSTRACT_FLAG);
+}
+
+static void ld_dhli_a(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_HL]++, gb->registers[GB_REGISTER_AF] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_dhld_a(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_HL]--, gb->registers[GB_REGISTER_AF] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_a_dhli(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF;
+    gb->registers[GB_REGISTER_AF] |= GB_read_memory(gb, gb->registers[GB_REGISTER_HL]++) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_a_dhld(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF;
+    gb->registers[GB_REGISTER_AF] |= GB_read_memory(gb, gb->registers[GB_REGISTER_HL]--) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->registers[GB_REGISTER_HL]) + 1;
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_HL], value);
+    GB_advance_cycles(gb, 4);
+
+    gb->registers[GB_REGISTER_AF] &= ~(GB_SUBSTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG);
+    if ((value & 0x0F) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((value & 0xFF) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->registers[GB_REGISTER_HL]) - 1;
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_HL], value);
+    GB_advance_cycles(gb, 4);
+
+    gb->registers[GB_REGISTER_AF] &= ~( GB_ZERO_FLAG | GB_HALF_CARRY_FLAG);
+    gb->registers[GB_REGISTER_AF] |= GB_SUBSTRACT_FLAG;
+    if ((value & 0x0F) == 0x0F) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((value & 0xFF) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    uint8_t data = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_HL], data);
+    GB_advance_cycles(gb, 4);
+}
+
+uint8_t get_src_value(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t src_register_id;
+    uint8_t src_low;
+    src_register_id = ((opcode >> 1) + 1) & 3;
+    src_low = opcode & 1;
+    if (src_register_id == GB_REGISTER_AF) {
+        if (src_low) {
+            return gb->registers[GB_REGISTER_AF] >> 8;
+        }
+        uint8_t ret = GB_read_memory(gb, gb->registers[GB_REGISTER_HL]);
+        GB_advance_cycles(gb, 4);
+        return ret;
+    }
+    if (src_low) {
+        return gb->registers[src_register_id] & 0xFF;
+    }
+    return gb->registers[src_register_id] >> 8;
+}
+
+static void set_src_value(GB_gameboy_t *gb, uint8_t opcode, uint8_t value)
+{
+    uint8_t src_register_id;
+    uint8_t src_low;
+    src_register_id = ((opcode >> 1) + 1) & 3;
+    src_low = opcode & 1;
+
+    if (src_register_id == GB_REGISTER_AF) {
+        if (src_low) {
+            gb->registers[GB_REGISTER_AF] &= 0xFF;
+            gb->registers[GB_REGISTER_AF] |= value << 8;
+        }
+        else {
+            GB_write_memory(gb, gb->registers[GB_REGISTER_HL], value);
+            GB_advance_cycles(gb, 4);
+        }
+    }
+    else {
+        if (src_low) {
+            gb->registers[src_register_id] &= 0xFF00;
+            gb->registers[src_register_id] |= value;
+        }
+        else {
+            gb->registers[src_register_id] &= 0xFF;
+            gb->registers[src_register_id] |= value << 8;
+        }
+    }
+}
+
+/* The LD r,r instruction is extremely common and extremely simple. Decoding this opcode at runtime is a significent
+   performance hit, so we generate functions for every ld x,y couple (including [hl]) at compile time using macros. */
+
+/* Todo: It's probably wise to do the same to all opcodes. */
+
+#define LD_X_Y(x, y) \
+static void ld_##x##_##y(GB_gameboy_t *gb, uint8_t opcode) \
+{ \
+    GB_advance_cycles(gb, 4); \
+    gb->x = gb->y;\
+}
+
+#define LD_X_DHL(x) \
+static void ld_##x##_##dhl(GB_gameboy_t *gb, uint8_t opcode) \
+{ \
+GB_advance_cycles(gb, 4); \
+gb->x = GB_read_memory(gb, gb->registers[GB_REGISTER_HL]); \
+GB_advance_cycles(gb, 4);\
+}
+
+#define LD_DHL_Y(y) \
+static void ld_##dhl##_##y(GB_gameboy_t *gb, uint8_t opcode) \
+{ \
+GB_advance_cycles(gb, 4); \
+GB_write_memory(gb, gb->registers[GB_REGISTER_HL], gb->y); \
+GB_advance_cycles(gb, 4);\
+}
+
+LD_X_Y(b,c) LD_X_Y(b,d) LD_X_Y(b,e) LD_X_Y(b,h) LD_X_Y(b,l)             LD_X_DHL(b) LD_X_Y(b,a)
+LD_X_Y(c,b)             LD_X_Y(c,d) LD_X_Y(c,e) LD_X_Y(c,h) LD_X_Y(c,l) LD_X_DHL(c) LD_X_Y(c,a)
+LD_X_Y(d,b) LD_X_Y(d,c)             LD_X_Y(d,e) LD_X_Y(d,h) LD_X_Y(d,l) LD_X_DHL(d) LD_X_Y(d,a)
+LD_X_Y(e,b) LD_X_Y(e,c) LD_X_Y(e,d)             LD_X_Y(e,h) LD_X_Y(e,l) LD_X_DHL(e) LD_X_Y(e,a)
+LD_X_Y(h,b) LD_X_Y(h,c) LD_X_Y(h,d) LD_X_Y(h,e)             LD_X_Y(h,l) LD_X_DHL(h) LD_X_Y(h,a)
+LD_X_Y(l,b) LD_X_Y(l,c) LD_X_Y(l,d) LD_X_Y(l,e) LD_X_Y(l,h)             LD_X_DHL(l) LD_X_Y(l,a)
+LD_DHL_Y(b) LD_DHL_Y(c) LD_DHL_Y(d) LD_DHL_Y(e) LD_DHL_Y(h) LD_DHL_Y(l)             LD_DHL_Y(a)
+LD_X_Y(a,b) LD_X_Y(a,c) LD_X_Y(a,d) LD_X_Y(a,e) LD_X_Y(a,h) LD_X_Y(a,l) LD_X_DHL(a)
+
+
+static void add_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = (a + value) << 8;
+    if ((uint8_t)(a + value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) + (value & 0xF) > 0x0F) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (((unsigned long) a) + ((unsigned long) value) > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void adc_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a, carry;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+    gb->registers[GB_REGISTER_AF] = (a + value + carry) << 8;
+
+    if ((uint8_t)(a + value + carry) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) + (value & 0xF) + carry > 0x0F) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (((unsigned long) a) + ((unsigned long) value) + carry > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void sub_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBSTRACT_FLAG;
+    if (a == value) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) < (value & 0xF)) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (a < value) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a, carry;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+    gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBSTRACT_FLAG;
+
+    if ((uint8_t) (a - value - carry) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) < (value & 0xF) + carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (((unsigned long) a) - ((unsigned long) value) - carry > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void and_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG;
+    if ((a & value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void xor_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = (a ^ value) << 8;
+    if ((a ^ value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void or_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = (a | value) << 8;
+    if ((a | value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    gb->registers[GB_REGISTER_AF] |= GB_SUBSTRACT_FLAG;
+    if (a == value) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) < (value & 0xF)) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (a < value) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void halt(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->halted = true;
+    /* Despite what some online documentations say, the HALT bug also happens on a CGB, in both CGB and DMG modes. */
+    if (!gb->ime && (gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F) != 0) {
+        gb->halted = false;
+        gb->halt_bug = true;
+    }
+}
+
+static void ret_cc(GB_gameboy_t *gb, uint8_t opcode)
+{
+    /* Todo: Verify timing */
+    if (condition_code(gb, opcode)) {
+        GB_debugger_ret_hook(gb);
+        GB_advance_cycles(gb, 8);
+        gb->pc = GB_read_memory(gb, gb->registers[GB_REGISTER_SP]);
+        GB_advance_cycles(gb, 4);
+        gb->pc |= GB_read_memory(gb, gb->registers[GB_REGISTER_SP] + 1) << 8;
+        GB_advance_cycles(gb, 8);
+        gb->registers[GB_REGISTER_SP] += 2;
+    }
+    else {
+        GB_advance_cycles(gb, 8);
+    }
+}
+
+static void pop_rr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 4);
+    register_id = ((opcode >> 4) + 1) & 3;
+    gb->registers[register_id] = GB_read_memory(gb, gb->registers[GB_REGISTER_SP]);
+    GB_advance_cycles(gb, 4);
+    gb->registers[register_id] |= GB_read_memory(gb, gb->registers[GB_REGISTER_SP] + 1) << 8;
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFFF0; // Make sure we don't set impossible flags on F! See Blargg's PUSH AF test.
+    gb->registers[GB_REGISTER_SP] += 2;
+}
+
+static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode)
+{
+    if (condition_code(gb, opcode)) {
+        GB_advance_cycles(gb, 4);
+        uint16_t addr = GB_read_memory(gb, gb->pc);
+        GB_advance_cycles(gb, 4);
+        addr |= (GB_read_memory(gb, gb->pc + 1) << 8);
+        GB_advance_cycles(gb, 8);
+        gb->pc = addr;
+
+    }
+    else {
+        GB_advance_cycles(gb, 12);
+        gb->pc += 2;
+    }
+}
+
+static void jp_a16(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    uint16_t addr = GB_read_memory(gb, gb->pc);
+    GB_advance_cycles(gb, 4);
+    addr |= (GB_read_memory(gb, gb->pc + 1) << 8);
+    GB_advance_cycles(gb, 8);
+    gb->pc = addr;}
+
+static void call_cc_a16(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint16_t call_addr = gb->pc - 1;
+    if (condition_code(gb, opcode)) {
+        GB_advance_cycles(gb, 4);
+        gb->registers[GB_REGISTER_SP] -= 2;
+        uint16_t addr = GB_read_memory(gb, gb->pc++);
+        GB_advance_cycles(gb, 4);
+        addr |= (GB_read_memory(gb, gb->pc++) << 8);
+        GB_advance_cycles(gb, 8);
+        GB_write_memory(gb, gb->registers[GB_REGISTER_SP] + 1, (gb->pc) >> 8);
+        GB_advance_cycles(gb, 4);
+        GB_write_memory(gb, gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF);
+        GB_advance_cycles(gb, 4);
+        gb->pc = addr;
+
+        GB_debugger_call_hook(gb, call_addr);
+    }
+    else {
+        GB_advance_cycles(gb, 12);
+        gb->pc += 2;
+    }
+}
+
+static void push_rr(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t register_id;
+    GB_advance_cycles(gb, 8);
+    register_id = ((opcode >> 4) + 1) & 3;
+    gb->registers[GB_REGISTER_SP] -= 2;
+    GB_write_memory(gb, gb->registers[GB_REGISTER_SP] + 1, (gb->registers[register_id]) >> 8);
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_SP], (gb->registers[register_id]) & 0xFF);
+    GB_advance_cycles(gb, 4);
+}
+
+static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = (a + value) << 8;
+    if ((uint8_t) (a + value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) + (value & 0xF) > 0x0F) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (((unsigned long) a) + ((unsigned long) value) > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a, carry;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+    gb->registers[GB_REGISTER_AF] = (a + value + carry) << 8;
+
+    if (gb->registers[GB_REGISTER_AF] == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) + (value & 0xF) + carry > 0x0F) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (((unsigned long) a) + ((unsigned long) value) + carry > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBSTRACT_FLAG;
+    if (a == value) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) < (value & 0xF)) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (a < value) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a, carry;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+    gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBSTRACT_FLAG;
+
+    if ((uint8_t) (a - value - carry) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) < (value & 0xF) + carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (((unsigned long) a) - ((unsigned long) value) - carry > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG;
+    if ((a & value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = (a ^ value) << 8;
+    if ((a ^ value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] = (a | value) << 8;
+    if ((a | value) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value, a;
+    GB_advance_cycles(gb, 4);
+    value = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    a = gb->registers[GB_REGISTER_AF] >> 8;
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    gb->registers[GB_REGISTER_AF] |= GB_SUBSTRACT_FLAG;
+    if (a == value) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+    if ((a & 0xF) < (value & 0xF)) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+    if (a < value) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void rst(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint16_t call_addr = gb->pc - 1;
+    GB_advance_cycles(gb, 8);
+    gb->registers[GB_REGISTER_SP] -= 2;
+    GB_write_memory(gb, gb->registers[GB_REGISTER_SP] + 1, (gb->pc) >> 8);
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF);
+    GB_advance_cycles(gb, 4);
+    gb->pc = opcode ^ 0xC7;
+    GB_debugger_call_hook(gb, call_addr);
+}
+
+static void ret(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_debugger_ret_hook(gb);
+    GB_advance_cycles(gb, 4);
+    gb->pc = GB_read_memory(gb, gb->registers[GB_REGISTER_SP]);
+    GB_advance_cycles(gb, 4);
+    gb->pc |= GB_read_memory(gb, gb->registers[GB_REGISTER_SP] + 1) << 8;
+    GB_advance_cycles(gb, 8);
+    gb->registers[GB_REGISTER_SP] += 2;
+}
+
+static void reti(GB_gameboy_t *gb, uint8_t opcode)
+{
+    ret(gb, opcode);
+    gb->ime = true;
+}
+
+static void call_a16(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint16_t call_addr = gb->pc - 1;
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_SP] -= 2;
+    uint16_t addr = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    addr |= (GB_read_memory(gb, gb->pc++) << 8);
+    GB_advance_cycles(gb, 8);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_SP] + 1, (gb->pc) >> 8);
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF);
+    GB_advance_cycles(gb, 4);
+    gb->pc = addr;
+    GB_debugger_call_hook(gb, call_addr);
+}
+
+static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    uint8_t temp = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, 0xFF00 + temp, gb->registers[GB_REGISTER_AF] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF;
+    uint8_t temp = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] |= GB_read_memory(gb, 0xFF00 + temp) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_dc_a(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, 0xFF00 + (gb->registers[GB_REGISTER_BC] & 0xFF), gb->registers[GB_REGISTER_AF] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_a_dc(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF;
+    gb->registers[GB_REGISTER_AF] |= GB_read_memory(gb, 0xFF00 + (gb->registers[GB_REGISTER_BC] & 0xFF)) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void add_sp_r8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    int16_t offset;
+    uint16_t sp = gb->registers[GB_REGISTER_SP];
+    GB_advance_cycles(gb, 4);
+    offset = (int8_t) GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 12);
+    gb->registers[GB_REGISTER_SP] += offset;
+
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+
+    /* A new instruction, a new meaning for Half Carry! */
+    if ((sp & 0xF) + (offset & 0xF) > 0xF) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((sp & 0xFF) + (offset & 0xFF) > 0xFF)  {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void jp_hl(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    gb->pc = gb->registers[GB_REGISTER_HL];
+}
+
+static void ld_da16_a(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint16_t addr;
+    GB_advance_cycles(gb, 4);
+    addr = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    addr |= GB_read_memory(gb, gb->pc++) << 8;
+    GB_advance_cycles(gb, 4);
+    GB_write_memory(gb, addr, gb->registers[GB_REGISTER_AF] >> 8);
+    GB_advance_cycles(gb, 4);
+}
+
+static void ld_a_da16(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint16_t addr;
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF;
+    addr = GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 4);
+    addr |= GB_read_memory(gb, gb->pc++) << 8 ;
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] |= GB_read_memory(gb, addr) << 8;
+    GB_advance_cycles(gb, 4);
+}
+
+static void di(GB_gameboy_t *gb, uint8_t opcode)
+{
+    /* DI is NOT delayed, not even on a CGB. Mooneye's di_timing-GS test fails on a CGB
+       for different reasons.*/
+    GB_advance_cycles(gb, 4);
+    gb->ime = false;
+}
+
+static void ei(GB_gameboy_t *gb, uint8_t opcode)
+{
+    /* ei is actually "disable interrupts for one instruction, then enable them". */
+    GB_advance_cycles(gb, 4);
+    gb->ime = false;
+    gb->ime_toggle = true;
+}
+
+static void ld_hl_sp_r8(GB_gameboy_t *gb, uint8_t opcode)
+{
+    int16_t offset;
+    GB_advance_cycles(gb, 4);
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    offset = (int8_t) GB_read_memory(gb, gb->pc++);
+    GB_advance_cycles(gb, 8);
+    gb->registers[GB_REGISTER_HL] = gb->registers[GB_REGISTER_SP] + offset;
+
+    if ((gb->registers[GB_REGISTER_SP] & 0xF) + (offset & 0xF) > 0xF) {
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+    }
+
+    if ((gb->registers[GB_REGISTER_SP] & 0xFF)  + (offset & 0xFF) > 0xFF) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+}
+
+static void ld_sp_hl(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 8);
+    gb->registers[GB_REGISTER_SP] = gb->registers[GB_REGISTER_HL];
+}
+
+static void rlc_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool carry;
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    carry = (value & 0x80) != 0;
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    set_src_value(gb, opcode, (value << 1) | carry);
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    if (!(value << 1)) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void rrc_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool carry;
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    carry = (value & 0x01) != 0;
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    value = (value >> 1) | (carry << 7);
+    set_src_value(gb, opcode, value);
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    if (value == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void rl_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool carry;
+    uint8_t value;
+    bool bit7;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+    bit7 = (value & 0x80) != 0;
+
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    value = (value << 1) | carry;
+    set_src_value(gb, opcode, value);
+    if (bit7) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    if (value == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void rr_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    bool carry;
+    uint8_t value;
+    bool bit1;
+
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
+    bit1 = (value & 0x1) != 0;
+
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    value = (value >> 1) | (carry << 7);
+    set_src_value(gb, opcode, value);
+    if (bit1) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    if (value == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void sla_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value;
+    bool carry;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    carry = (value & 0x80) != 0;
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    set_src_value(gb, opcode, (value << 1));
+    if (carry) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    if ((value & 0x7F) == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void sra_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t bit7;
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    bit7 = value & 0x80;
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    if (value & 1) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    value = (value >> 1) | bit7;
+    set_src_value(gb, opcode, value);
+    if (value == 0) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void srl_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    set_src_value(gb, opcode, (value >> 1));
+    if (value & 1) {
+        gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG;
+    }
+    if (!(value >> 1)) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void swap_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    gb->registers[GB_REGISTER_AF] &= 0xFF00;
+    set_src_value(gb, opcode, (value >> 4) | (value << 4));
+    if (!value) {
+        gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+    }
+}
+
+static void bit_r(GB_gameboy_t *gb, uint8_t opcode)
+{
+    uint8_t value;
+    uint8_t bit;
+    GB_advance_cycles(gb, 4);
+    value = get_src_value(gb, opcode);
+    bit = 1 << ((opcode >> 3) & 7);
+    if ((opcode & 0xC0) == 0x40) { /* Bit */
+        gb->registers[GB_REGISTER_AF] &= 0xFF00 | GB_CARRY_FLAG;
+        gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG;
+        if (!(bit & value)) {
+            gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG;
+        }
+    }
+    else if ((opcode & 0xC0) == 0x80) { /* res */
+        set_src_value(gb, opcode, value & ~bit) ;
+    }
+    else if ((opcode & 0xC0) == 0xC0) { /* set */
+        set_src_value(gb, opcode, value | bit) ;
+    }
+}
+
+static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode)
+{
+    GB_advance_cycles(gb, 4);
+    opcode = GB_read_memory(gb, gb->pc++);
+    switch (opcode >> 3) {
+        case 0:
+            rlc_r(gb, opcode);
+            break;
+        case 1:
+            rrc_r(gb, opcode);
+            break;
+        case 2:
+            rl_r(gb, opcode);
+            break;
+        case 3:
+            rr_r(gb, opcode);
+            break;
+        case 4:
+            sla_r(gb, opcode);
+            break;
+        case 5:
+            sra_r(gb, opcode);
+            break;
+        case 6:
+            swap_r(gb, opcode);
+            break;
+        case 7:
+            srl_r(gb, opcode);
+            break;
+        default:
+            bit_r(gb, opcode);
+            break;
+    }
+}
+
+static GB_opcode_t *opcodes[256] = {
+    /*  X0          X1          X2          X3          X4          X5          X6          X7                */
+    /*  X8          X9          Xa          Xb          Xc          Xd          Xe          Xf                */
+    nop,        ld_rr_d16,  ld_drr_a,   inc_rr,     inc_hr,     dec_hr,     ld_hr_d8,   rlca,       /* 0X */
+    ld_da16_sp, add_hl_rr,  ld_a_drr,   dec_rr,     inc_lr,     dec_lr,     ld_lr_d8,   rrca,
+    stop,       ld_rr_d16,  ld_drr_a,   inc_rr,     inc_hr,     dec_hr,     ld_hr_d8,   rla,        /* 1X */
+    jr_r8,      add_hl_rr,  ld_a_drr,   dec_rr,     inc_lr,     dec_lr,     ld_lr_d8,   rra,
+    jr_cc_r8,   ld_rr_d16,  ld_dhli_a,  inc_rr,     inc_hr,     dec_hr,     ld_hr_d8,   daa,        /* 2X */
+    jr_cc_r8,   add_hl_rr,  ld_a_dhli,  dec_rr,     inc_lr,     dec_lr,     ld_lr_d8,   cpl,
+    jr_cc_r8,   ld_rr_d16,  ld_dhld_a,  inc_rr,     inc_dhl,    dec_dhl,    ld_dhl_d8,  scf,        /* 3X */
+    jr_cc_r8,   add_hl_rr,  ld_a_dhld,  dec_rr,     inc_hr,     dec_hr,     ld_hr_d8,   ccf,
+    nop,        ld_b_c,     ld_b_d,     ld_b_e,     ld_b_h,     ld_b_l,     ld_b_dhl,   ld_b_a,     /* 4X */
+    ld_c_b,     nop,        ld_c_d,     ld_c_e,     ld_c_h,     ld_c_l,     ld_c_dhl,   ld_c_a,
+    ld_d_b,     ld_d_c,     nop,        ld_d_e,     ld_d_h,     ld_d_l,     ld_d_dhl,   ld_d_a,     /* 5X */
+    ld_e_b,     ld_e_c,     ld_e_d,     nop,        ld_e_h,     ld_e_l,     ld_e_dhl,   ld_e_a,
+    ld_h_b,     ld_h_c,     ld_h_d,     ld_h_e,     nop,        ld_h_l,     ld_h_dhl,   ld_h_a,     /* 6X */
+    ld_l_b,     ld_l_c,     ld_l_d,     ld_l_e,     ld_l_h,     nop,        ld_l_dhl,   ld_l_a,
+    ld_dhl_b,   ld_dhl_c,   ld_dhl_d,   ld_dhl_e,   ld_dhl_h,   ld_dhl_l,   halt,       ld_dhl_a,   /* 7X */
+    ld_a_b,     ld_a_c,     ld_a_d,     ld_a_e,     ld_a_h,     ld_a_l,     ld_a_dhl,   nop,
+    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    /* 8X */
+    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,
+    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    /* 9X */
+    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,
+    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    /* aX */
+    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,
+    or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     /* bX */
+    cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,
+    ret_cc,     pop_rr,     jp_cc_a16,  jp_a16,     call_cc_a16,push_rr,    add_a_d8,   rst,        /* cX */
+    ret_cc,     ret,        jp_cc_a16,  cb_prefix,  call_cc_a16,call_a16,   adc_a_d8,   rst,
+    ret_cc,     pop_rr,     jp_cc_a16,  ill,        call_cc_a16,push_rr,    sub_a_d8,   rst,        /* dX */
+    ret_cc,     reti,       jp_cc_a16,  ill,        call_cc_a16,ill,        sbc_a_d8,   rst,
+    ld_da8_a,   pop_rr,     ld_dc_a,    ill,        ill,        push_rr,    and_a_d8,   rst,        /* eX */
+    add_sp_r8,  jp_hl,      ld_da16_a,  ill,        ill,        ill,        xor_a_d8,   rst,
+    ld_a_da8,   pop_rr,     ld_a_dc,    di,         ill,        push_rr,    or_a_d8,    rst,        /* fX */
+    ld_hl_sp_r8,ld_sp_hl,   ld_a_da16,  ei,         ill,        ill,        cp_a_d8,    rst,
+};
+void GB_cpu_run(GB_gameboy_t *gb)
+{
+    gb->vblank_just_occured = false;
+    bool interrupt = gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F;
+
+    if (interrupt) {
+        gb->halted = false;
+    }
+
+    if (gb->hdma_on) {
+        GB_advance_cycles(gb, 4);
+        return;
+    }
+
+    bool effecitve_ime = gb->ime;
+    if (gb->ime_toggle) {
+        gb->ime = !gb->ime;
+        gb->ime_toggle = false;
+    }
+
+    if (effecitve_ime && interrupt) {
+        uint8_t interrupt_bit = 0;
+        uint8_t interrupt_queue = gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F;
+        while (!(interrupt_queue & 1)) {
+            interrupt_queue >>= 1;
+            interrupt_bit++;
+        }
+        gb->io_registers[GB_IO_IF] &= ~(1 << interrupt_bit);
+        gb->ime = false;
+        nop(gb, 0);
+        /* Run pseudo instructions rst 40-60*/
+        rst(gb, 0x87 + interrupt_bit * 8);
+    }
+    else if(!gb->halted && !gb->stopped) {
+        uint8_t opcode = GB_read_memory(gb, gb->pc++);
+        if (gb->halt_bug) {
+            gb->pc--;
+            gb->halt_bug = false;
+        }
+        opcodes[opcode](gb, opcode);
+    }
+    else {
+        GB_advance_cycles(gb, 4);
+    }
+}
diff --git a/waterbox/sameboy/z80_cpu.h b/waterbox/sameboy/z80_cpu.h
new file mode 100644
index 0000000000..1434ed7b96
--- /dev/null
+++ b/waterbox/sameboy/z80_cpu.h
@@ -0,0 +1,10 @@
+#ifndef z80_cpu_h
+#define z80_cpu_h
+#include "gb.h"
+
+void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count);
+#ifdef GB_INTERNAL
+void GB_cpu_run(GB_gameboy_t *gb);
+#endif
+
+#endif /* z80_cpu_h */
diff --git a/waterbox/sameboy/z80_disassembler.c b/waterbox/sameboy/z80_disassembler.c
new file mode 100644
index 0000000000..08fb62f039
--- /dev/null
+++ b/waterbox/sameboy/z80_disassembler.c
@@ -0,0 +1,788 @@
+#include <stdio.h>
+#include <stdbool.h>
+#include "gb.h"
+
+
+typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc);
+
+static void ill(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, ".BYTE $%02x\n", opcode);
+    (*pc)++;
+}
+
+static void nop(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "NOP\n");
+    (*pc)++;
+}
+
+static void stop(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint8_t next = GB_read_memory(gb, (*pc)++);
+    if (next) {
+        GB_log(gb, "CORRUPTED STOP (%02x)\n", next);
+    }
+    else {
+        GB_log(gb, "STOP\n");
+    }
+}
+
+static char *register_names[] = {"af", "bc", "de", "hl", "sp"};
+
+static void ld_rr_d16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    uint16_t value;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+    value = GB_read_memory(gb, (*pc)++);
+    value |= GB_read_memory(gb, (*pc)++) << 8;
+    const char *symbol = GB_debugger_name_for_address(gb, value);
+    if (symbol) {
+        GB_log(gb, "LD %s, %s ; =$%04x\n", register_names[register_id], symbol, value);
+    }
+    else {
+        GB_log(gb, "LD %s, $%04x\n", register_names[register_id], value);
+    }
+}
+
+static void ld_drr_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+    GB_log(gb, "LD [%s], a\n", register_names[register_id]);
+}
+
+static void inc_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+    GB_log(gb, "INC %s\n", register_names[register_id]);
+}
+
+static void inc_hr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    (*pc)++;
+    register_id = ((opcode >> 4) + 1) & 0x03;
+    GB_log(gb, "INC %c\n", register_names[register_id][0]);
+
+}
+static void dec_hr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    (*pc)++;
+    register_id = ((opcode >> 4) + 1) & 0x03;
+    GB_log(gb, "DEC %c\n", register_names[register_id][0]);
+}
+
+static void ld_hr_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    (*pc)++;
+    register_id = ((opcode >> 4) + 1) & 0x03;
+    GB_log(gb, "LD %c, $%02x\n", register_names[register_id][0], GB_read_memory(gb, (*pc)++));
+}
+
+static void rlca(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RLCA\n");
+}
+
+static void rla(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RLA\n");
+}
+
+static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc){
+    uint16_t addr;
+    (*pc)++;
+    addr = GB_read_memory(gb, (*pc)++);
+    addr |= GB_read_memory(gb, (*pc)++) << 8;
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_log(gb, "LD [%s], sp ; =$%04x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "LD [$%04x], sp\n", addr);
+    }
+}
+
+static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    (*pc)++;
+    register_id = (opcode >> 4) + 1;
+    GB_log(gb, "ADD hl, %s\n", register_names[register_id]);
+}
+
+static void ld_a_drr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+    GB_log(gb, "LD a, [%s]\n", register_names[register_id]);
+}
+
+static void dec_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+    GB_log(gb, "DEC %s\n", register_names[register_id]);
+}
+
+static void inc_lr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+
+    GB_log(gb, "INC %c\n", register_names[register_id][1]);
+}
+static void dec_lr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+
+    GB_log(gb, "DEC %c\n", register_names[register_id][1]);
+}
+
+static void ld_lr_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1;
+
+    GB_log(gb, "LD %c, $%02x\n", register_names[register_id][1], GB_read_memory(gb, (*pc)++));
+}
+
+static void rrca(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "RRCA\n");
+    (*pc)++;
+}
+
+static void rra(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "RRA\n");
+    (*pc)++;
+}
+
+static void jr_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = *pc + (int8_t) GB_read_memory(gb, (*pc)) + 1;
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_attributed_log(gb, GB_LOG_UNDERLINE, "JR %s ; =$%04x\n", symbol, addr);
+    }
+    else {
+        GB_attributed_log(gb, GB_LOG_UNDERLINE, "JR $%04x\n", addr);
+    }
+    (*pc)++;
+}
+
+static const char *condition_code(uint8_t opcode)
+{
+    switch ((opcode >> 3) & 0x3) {
+        case 0:
+            return "nz";
+        case 1:
+            return "z";
+        case 2:
+            return "nc";
+        case 3:
+            return "c";
+    }
+
+    return NULL;
+}
+
+static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = *pc + (int8_t) GB_read_memory(gb, (*pc)) + 1;
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_attributed_log(gb,  GB_LOG_DASHED_UNDERLINE, "JR %s, %s ; =$%04x\n", condition_code(opcode), symbol, addr);
+    }
+    else {
+        GB_attributed_log(gb,  GB_LOG_DASHED_UNDERLINE, "JR %s, $%04x\n", condition_code(opcode), addr);
+    }
+    (*pc)++;
+}
+
+static void daa(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "DAA\n");
+    (*pc)++;
+}
+
+static void cpl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "CPL\n");
+    (*pc)++;
+}
+
+static void scf(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "SCF\n");
+    (*pc)++;
+}
+
+static void ccf(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "CCF\n");
+    (*pc)++;
+}
+
+static void ld_dhli_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "LD [hli], a\n");
+    (*pc)++;
+}
+
+static void ld_dhld_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "LD [hld], a\n");
+    (*pc)++;
+}
+
+static void ld_a_dhli(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "LD a, [hli]\n");
+    (*pc)++;
+}
+
+static void ld_a_dhld(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "LD a, [hld]\n");
+    (*pc)++;
+}
+
+static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "INC [hl]\n");
+    (*pc)++;
+}
+
+static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    GB_log(gb, "DEC [hl]\n");
+    (*pc)++;
+}
+
+static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "LD [hl], $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static const char *get_src_name(uint8_t opcode)
+{
+    uint8_t src_register_id;
+    uint8_t src_low;
+    src_register_id = ((opcode >> 1) + 1) & 3;
+    src_low = (opcode & 1);
+    if (src_register_id == GB_REGISTER_AF) {
+        return src_low? "a": "[hl]";
+    }
+    if (src_low) {
+        return register_names[src_register_id] + 1;
+    }
+    static const char *high_register_names[] = {"a", "b", "d", "h"};
+    return high_register_names[src_register_id];
+}
+
+static const char *get_dst_name(uint8_t opcode)
+{
+    uint8_t dst_register_id;
+    uint8_t dst_low;
+    dst_register_id = ((opcode >> 4) + 1) & 3;
+    dst_low = opcode & 8;
+    if (dst_register_id == GB_REGISTER_AF) {
+        return dst_low? "a": "[hl]";
+    }
+    if (dst_low) {
+        return register_names[dst_register_id] + 1;
+    }
+    static const char *high_register_names[] = {"a", "b", "d", "h"};
+    return high_register_names[dst_register_id];
+}
+
+static void ld_r_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "LD %s, %s\n", get_dst_name(opcode), get_src_name(opcode));
+}
+
+static void add_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "ADD %s\n",  get_src_name(opcode));
+}
+
+static void adc_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "ADC %s\n",  get_src_name(opcode));
+}
+
+static void sub_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SUB %s\n",  get_src_name(opcode));
+}
+
+static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SBC %s\n",  get_src_name(opcode));
+}
+
+static void and_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "AND %s\n",  get_src_name(opcode));
+}
+
+static void xor_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "XOR %s\n",  get_src_name(opcode));
+}
+
+static void or_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "OR %s\n",  get_src_name(opcode));
+}
+
+static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "CP %s\n",  get_src_name(opcode));
+}
+
+static void halt(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "HALT\n");
+}
+
+static void ret_cc(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_attributed_log(gb,  GB_LOG_DASHED_UNDERLINE, "RET %s\n", condition_code(opcode));
+}
+
+static void pop_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = ((GB_read_memory(gb, (*pc)++) >> 4) + 1) & 3;
+    GB_log(gb, "POP %s\n", register_names[register_id]);
+}
+
+static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8);
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "JP %s, %s ; =$%04x\n", condition_code(opcode), symbol, addr);
+    }
+    else {
+        GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "JP %s, $%04x\n", condition_code(opcode), addr);
+    }
+    (*pc) += 2;
+}
+
+static void jp_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8);
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_log(gb, "JP %s ; =$%04x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "JP $%04x\n", addr);
+    }
+    (*pc) += 2;
+}
+
+static void call_cc_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8);
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_log(gb, "CALL %s, %s ; =$%04x\n", condition_code(opcode), symbol, addr);
+    }
+    else {
+        GB_log(gb, "CALL %s, $%04x\n", condition_code(opcode), addr);
+    }
+    (*pc) += 2;
+}
+
+static void push_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t register_id;
+    register_id = ((GB_read_memory(gb, (*pc)++) >> 4) + 1) & 3;
+    GB_log(gb, "PUSH %s\n", register_names[register_id]);
+}
+
+static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "ADD $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "ADC $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SUB $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SBC $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "AND $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "XOR $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "OR $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "CP $%02x\n", GB_read_memory(gb, (*pc)++));
+}
+
+static void rst(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RST $%02x\n", opcode ^ 0xC7);
+
+}
+
+static void ret(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_attributed_log(gb, GB_LOG_UNDERLINE, "RET\n");
+}
+
+static void reti(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_attributed_log(gb, GB_LOG_UNDERLINE, "RETI\n");
+}
+
+static void call_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8);
+    const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr);
+    if (symbol) {
+        GB_log(gb, "CALL %s ; =$%04x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "CALL $%04x\n", addr);
+    }
+    (*pc) += 2;
+}
+
+static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint8_t addr = GB_read_memory(gb, (*pc)++);
+    const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr);
+    if (symbol) {
+        GB_log(gb, "LDH [%s & $FF], a ; =$%02x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "LDH [$%02x], a\n", addr);
+    }
+}
+
+static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint8_t addr = GB_read_memory(gb, (*pc)++);
+    const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr);
+    if (symbol) {
+        GB_log(gb, "LDH a, [%s & $FF] ; =$%02x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "LDH a, [$%02x]\n", addr);
+    }
+}
+
+static void ld_dc_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "LDH [c], a\n");
+}
+
+static void ld_a_dc(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "LDH a, [c]\n");
+}
+
+static void add_sp_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    int8_t temp = GB_read_memory(gb, (*pc)++);
+    GB_log(gb, "ADD SP, %s$%02x\n", temp < 0? "-" : "", temp < 0? -temp : temp);
+}
+
+static void jp_hl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "JP hl\n");
+}
+
+static void ld_da16_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8);
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_log(gb, "LD [%s], a ; =$%04x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "LD [$%04x], a\n", addr);
+    }
+    (*pc) += 2;
+}
+
+static void ld_a_da16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8);
+    const char *symbol = GB_debugger_name_for_address(gb, addr);
+    if (symbol) {
+        GB_log(gb, "LD a, [%s] ; =$%04x\n", symbol, addr);
+    }
+    else {
+        GB_log(gb, "LD a, [$%04x]\n", addr);
+    }
+    (*pc) += 2;
+}
+
+static void di(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "DI\n");
+}
+
+static void ei(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "EI\n");
+}
+
+static void ld_hl_sp_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    int8_t temp = GB_read_memory(gb, (*pc)++);
+    GB_log(gb, "LD hl, sp, %s$%02x\n", temp < 0? "-" : "", temp < 0? -temp : temp);
+}
+
+static void ld_sp_hl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "LD sp, hl\n");
+}
+
+static void rlc_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RLC %s\n",  get_src_name(opcode));
+}
+
+static void rrc_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RRC %s\n",  get_src_name(opcode));
+}
+
+static void rl_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RL %s\n",  get_src_name(opcode));
+}
+
+static void rr_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RR %s\n",  get_src_name(opcode));
+}
+
+static void sla_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SLA %s\n",  get_src_name(opcode));
+}
+
+static void sra_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SRA %s\n",  get_src_name(opcode));
+}
+
+static void srl_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "SRL %s\n",  get_src_name(opcode));
+}
+
+static void swap_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    (*pc)++;
+    GB_log(gb, "RLC %s\n",  get_src_name(opcode));
+}
+
+static void bit_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    uint8_t bit;
+    (*pc)++;
+    bit = ((opcode >> 3) & 7);
+    if ((opcode & 0xC0) == 0x40) { /* Bit */
+        GB_log(gb, "BIT %s, %d\n",  get_src_name(opcode), bit);
+    }
+    else if ((opcode & 0xC0) == 0x80) { /* res */
+        GB_log(gb, "RES %s, %d\n",  get_src_name(opcode), bit);
+    }
+    else if ((opcode & 0xC0) == 0xC0) { /* set */
+        GB_log(gb, "SET %s, %d\n",  get_src_name(opcode), bit);
+    }
+}
+
+static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc)
+{
+    opcode = GB_read_memory(gb, ++*pc);
+    switch (opcode >> 3) {
+        case 0:
+            rlc_r(gb, opcode, pc);
+            break;
+        case 1:
+            rrc_r(gb, opcode, pc);
+            break;
+        case 2:
+            rl_r(gb, opcode, pc);
+            break;
+        case 3:
+            rr_r(gb, opcode, pc);
+            break;
+        case 4:
+            sla_r(gb, opcode, pc);
+            break;
+        case 5:
+            sra_r(gb, opcode, pc);
+            break;
+        case 6:
+            swap_r(gb, opcode, pc);
+            break;
+        case 7:
+            srl_r(gb, opcode, pc);
+            break;
+        default:
+            bit_r(gb, opcode, pc);
+            break;
+    }
+}
+
+static GB_opcode_t *opcodes[256] = {
+    /*  X0          X1          X2          X3          X4          X5          X6          X7                */
+    /*  X8          X9          Xa          Xb          Xc          Xd          Xe          Xf                */
+    nop,        ld_rr_d16,  ld_drr_a,   inc_rr,     inc_hr,     dec_hr,     ld_hr_d8,   rlca,       /* 0X */
+    ld_da16_sp, add_hl_rr,  ld_a_drr,   dec_rr,     inc_lr,     dec_lr,     ld_lr_d8,   rrca,
+    stop,       ld_rr_d16,  ld_drr_a,   inc_rr,     inc_hr,     dec_hr,     ld_hr_d8,   rla,        /* 1X */
+    jr_r8,      add_hl_rr,  ld_a_drr,   dec_rr,     inc_lr,     dec_lr,     ld_lr_d8,   rra,
+    jr_cc_r8,   ld_rr_d16,  ld_dhli_a,  inc_rr,     inc_hr,     dec_hr,     ld_hr_d8,   daa,        /* 2X */
+    jr_cc_r8,   add_hl_rr,  ld_a_dhli,  dec_rr,     inc_lr,     dec_lr,     ld_lr_d8,   cpl,
+    jr_cc_r8,   ld_rr_d16,  ld_dhld_a,  inc_rr,     inc_dhl,    dec_dhl,    ld_dhl_d8,  scf,        /* 3X */
+    jr_cc_r8,   add_hl_rr,  ld_a_dhld,  dec_rr,     inc_hr,     dec_hr,     ld_hr_d8,   ccf,
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     /* 4X */
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     /* 5X */
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     /* 6X */
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     halt,       ld_r_r,     /* 7X */
+    ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,     ld_r_r,
+    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    add_a_r,    /* 8X */
+    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,    adc_a_r,
+    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    sub_a_r,    /* 9X */
+    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,    sbc_a_r,
+    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    and_a_r,    /* aX */
+    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,    xor_a_r,
+    or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     or_a_r,     /* bX */
+    cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,     cp_a_r,
+    ret_cc,     pop_rr,     jp_cc_a16,  jp_a16,     call_cc_a16,push_rr,    add_a_d8,   rst,        /* cX */
+    ret_cc,     ret,        jp_cc_a16,  cb_prefix,  call_cc_a16,call_a16,   adc_a_d8,   rst,
+    ret_cc,     pop_rr,     jp_cc_a16,  ill,        call_cc_a16,push_rr,    sub_a_d8,   rst,        /* dX */
+    ret_cc,     reti,       jp_cc_a16,  ill,        call_cc_a16,ill,        sbc_a_d8,   rst,
+    ld_da8_a,   pop_rr,     ld_dc_a,    ill,        ill,        push_rr,    and_a_d8,   rst,        /* eX */
+    add_sp_r8,  jp_hl,      ld_da16_a,  ill,        ill,        ill,        xor_a_d8,   rst,
+    ld_a_da8,   pop_rr,     ld_a_dc,    di,         ill,        push_rr,    or_a_d8,    rst,        /* fX */
+    ld_hl_sp_r8,ld_sp_hl,   ld_a_da16,  ei,         ill,        ill,        cp_a_d8,    rst,
+};
+
+
+
+void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count)
+{
+    const GB_bank_symbol_t *function_symbol = GB_debugger_find_symbol(gb, pc);
+
+    if (function_symbol && pc - function_symbol->addr > 0x1000) {
+        function_symbol = NULL;
+    }
+
+    if (function_symbol && pc != function_symbol->addr) {
+        GB_log(gb, "%s:\n", function_symbol->name);
+    }
+
+    uint16_t current_function = function_symbol? function_symbol->addr : 0;
+
+    while (count--) {
+        function_symbol = GB_debugger_find_symbol(gb, pc);
+        if (function_symbol && function_symbol->addr == pc) {
+            if (current_function != function_symbol->addr) {
+                GB_log(gb, "\n");
+            }
+            GB_log(gb, "%s:\n", function_symbol->name);
+        }
+        if (function_symbol) {
+            GB_log(gb, "%s%04x <+%03x>: ", pc == gb->pc? "  ->": "    ", pc, pc - function_symbol->addr);
+        }
+        else {
+            GB_log(gb, "%s%04x: ", pc == gb->pc? "  ->": "    ", pc);
+        }
+        uint8_t opcode = GB_read_memory(gb, pc);
+        opcodes[opcode](gb, opcode, &pc);
+    }
+}