diff --git a/shard.yml b/shard.yml index aab63c5..c290af2 100644 --- a/shard.yml +++ b/shard.yml @@ -8,7 +8,7 @@ targets: crab: main: src/crab.cr -crystal: 0.36.0 +crystal: ">=0.36.1, < 2.0.0" dependencies: bitfield: @@ -19,4 +19,9 @@ dependencies: lib_gl: github: nulldotpro/LibGL +development_dependencies: + stumpy_png: + github: stumpycr/stumpy_png + version: "~> 5.0" + license: MIT diff --git a/src/crab.cr b/src/crab.cr index b4d8efa..292c290 100644 --- a/src/crab.cr +++ b/src/crab.cr @@ -1,8 +1,11 @@ require "colorize" +require "option_parser" require "bitfield" require "sdl" +require "./crab/common/*" +require "./crab/gb" require "./crab/gba" Colorize.on_tty_only! @@ -13,9 +16,45 @@ module Crab extend self def run - gba = GBA.new ARGV[0], ARGV[1] - gba.post_init - gba.run + rom = nil + bios = nil + fifo = false + pink = false + sync = true + headless = false + + OptionParser.parse do |parser| + parser.banner = "#{"crab".colorize.bold} - An accurate and readable Game Boy (Color) (Advance) emulator" + parser.separator + parser.separator("Usage: bin/crab [BIOS] ROM") + parser.separator + parser.on("-h", "--help", "Show the help message") do + puts parser + exit + end + parser.on("--fifo", "Enable FIFO rendering") { fifo = true } + parser.on("--pink", "Set the 2-bit DMG color theme to pink") { pink = true } + parser.on("--no-sync", "Disable audio syncing") { sync = false } + parser.on("--headless", "Don't open window or play audio") { headless = true } + parser.unknown_args do |args| + case args.size + when 1 then rom = args[0] + when 2 then bios, rom = args[0], args[1] + end + abort parser if rom.nil? + abort "GBA ROMs need a bios provided" if rom.not_nil!.ends_with?(".gba") && bios.nil? + end + end + + if rom.not_nil!.ends_with?(".gba") + gba = GBA::GBA.new bios.not_nil!, rom.not_nil! + gba.post_init + gba.run + else + gb = GB::GB.new bios, rom.not_nil!, fifo, sync, headless + gb.post_init + gb.run + end end end diff --git a/src/crab/apu.cr b/src/crab/apu.cr deleted file mode 100644 index 59bfb70..0000000 --- a/src/crab/apu.cr +++ /dev/null @@ -1,202 +0,0 @@ -require "./apu/abstract_channels" # so that channels don't need to all import -require "./apu/*" - -lib LibSDL - fun queue_audio = SDL_QueueAudio(dev : AudioDeviceID, data : Void*, len : UInt32) : Int - fun get_queued_audio_size = SDL_GetQueuedAudioSize(dev : AudioDeviceID) : UInt32 - fun clear_queued_audio = SDL_ClearQueuedAudio(dev : AudioDeviceID) - fun delay = SDL_Delay(ms : UInt32) : Nil -end - -class APU - CHANNELS = 2 # Left / Right - BUFFER_SIZE = 1024 - SAMPLE_RATE = 32768 # Hz - SAMPLE_PERIOD = CPU::CLOCK_SPEED // SAMPLE_RATE - - FRAME_SEQUENCER_RATE = 512 # Hz - FRAME_SEQUENCER_PERIOD = CPU::CLOCK_SPEED // FRAME_SEQUENCER_RATE - - @soundcnt_l = Reg::SOUNDCNT_L.new 0 - getter soundcnt_h = Reg::SOUNDCNT_H.new 0 - @sound_enabled : Bool = false - @soundbias = Reg::SOUNDBIAS.new 0x3FE - - @buffer = Slice(Int16).new BUFFER_SIZE - @buffer_pos = 0 - @frame_sequencer_stage = 0 - getter first_half_of_length_period = false - - @audiospec : LibSDL::AudioSpec - @obtained_spec : LibSDL::AudioSpec - - @sync : Bool = true - - def initialize(@gba : GBA) - @audiospec = LibSDL::AudioSpec.new - @audiospec.freq = SAMPLE_RATE - @audiospec.format = LibSDL::AUDIO_S16 - @audiospec.channels = CHANNELS - @audiospec.samples = BUFFER_SIZE - @audiospec.callback = nil - @audiospec.userdata = nil - - @obtained_spec = LibSDL::AudioSpec.new - - @channel1 = Channel1.new @gba - @channel2 = Channel2.new @gba - @channel3 = Channel3.new @gba - @channel4 = Channel4.new @gba - @dma_channels = DMAChannels.new @gba, @soundcnt_h - - tick_frame_sequencer - get_sample - - raise "Failed to open audio" if LibSDL.open_audio(pointerof(@audiospec), pointerof(@obtained_spec)) > 0 - - LibSDL.pause_audio 0 - end - - def toggle_sync - @sync = !@sync - end - - def tick_frame_sequencer : Nil - @first_half_of_length_period = @frame_sequencer_stage & 1 == 0 - case @frame_sequencer_stage - when 0 - @channel1.length_step - @channel2.length_step - @channel3.length_step - @channel4.length_step - when 1 then nil - when 2 - @channel1.length_step - @channel2.length_step - @channel3.length_step - @channel4.length_step - @channel1.sweep_step - when 3 then nil - when 4 - @channel1.length_step - @channel2.length_step - @channel3.length_step - @channel4.length_step - when 5 then nil - when 6 - @channel1.length_step - @channel2.length_step - @channel3.length_step - @channel4.length_step - @channel1.sweep_step - when 7 - @channel1.volume_step - @channel2.volume_step - @channel4.volume_step - else nil - end - @frame_sequencer_stage = 0 if (@frame_sequencer_stage += 1) > 7 - @gba.scheduler.schedule FRAME_SEQUENCER_PERIOD, ->tick_frame_sequencer - end - - def get_sample : Nil - abort "Prohibited sound 1-4 volume #{@soundcnt_h.sound_volume}" if @soundcnt_h.sound_volume >= 3 - # Gets PSGs on scale of -0x80..0x80 each - psg_sound = ((@channel1.get_amplitude * @soundcnt_l.channel_1_left) + - (@channel2.get_amplitude * @soundcnt_l.channel_2_left) + - (@channel3.get_amplitude * @soundcnt_l.channel_3_left) + - (@channel4.get_amplitude * @soundcnt_l.channel_4_left)) - # Keep PSGs on scale of -0x200...0x200 (shift by `5 - vol` to account for `*8` from left/right vol) - psg_left = (psg_sound * @soundcnt_l.left_volume) >> (5 - @soundcnt_h.sound_volume) - psg_right = (psg_sound * @soundcnt_l.right_volume) >> (5 - @soundcnt_h.sound_volume) - - # Gets DMAs on scale of -0x100...0x100 - dma_a, dma_b = @dma_channels.get_amplitude - # Puts DMAs on scale of -0x200...0x200 - dma_a <<= @soundcnt_h.dma_sound_a_volume - dma_b <<= @soundcnt_h.dma_sound_b_volume - dma_left = dma_a * @soundcnt_h.dma_sound_a_left + dma_b * @soundcnt_h.dma_sound_b_left - dma_right = dma_a * @soundcnt_h.dma_sound_a_right + dma_b * @soundcnt_h.dma_sound_b_right - - total_left = (psg_left + dma_left + @soundbias.bias_level).clamp(0_i16..0x3FF_i16) - @soundbias.bias_level - total_right = (psg_right + dma_right + @soundbias.bias_level).clamp(0_i16..0x3FF_i16) - @soundbias.bias_level - - @buffer[@buffer_pos] = total_left * 32 - @buffer[@buffer_pos + 1] = total_right * 32 - @buffer_pos += 2 - - # push to SDL if buffer is full - if @buffer_pos >= BUFFER_SIZE - LibSDL.clear_queued_audio 1 unless @sync - while LibSDL.get_queued_audio_size(1) > BUFFER_SIZE * sizeof(Int16) * 2 - LibSDL.delay(1) - end - LibSDL.queue_audio 1, @buffer, BUFFER_SIZE * sizeof(Int16) - @buffer_pos = 0 - end - - @gba.scheduler.schedule SAMPLE_PERIOD, ->get_sample - end - - def timer_overflow(timer : Int) : Nil - @dma_channels.timer_overflow timer - end - - def read_io(io_addr : Int) : UInt8 - case io_addr - when @channel1 then @channel1.read_io io_addr - when @channel2 then @channel2.read_io io_addr - when @channel3 then @channel3.read_io io_addr - when @channel4 then @channel4.read_io io_addr - when @dma_channels then @dma_channels.read_io io_addr - when 0x80 then @soundcnt_l.value.to_u8! - when 0x81 then (@soundcnt_l.value >> 8).to_u8! - when 0x82 then @soundcnt_h.value.to_u8! - when 0x83 then (@soundcnt_h.value >> 8).to_u8! - when 0x84 - 0x70_u8 | - (@sound_enabled ? 0x80 : 0) | - (@channel4.enabled ? 0b1000 : 0) | - (@channel3.enabled ? 0b0100 : 0) | - (@channel2.enabled ? 0b0010 : 0) | - (@channel1.enabled ? 0b0001 : 0) - when 0x85 then 0_u8 # unused - when 0x88 then @soundbias.value.to_u8! - when 0x89 then (@soundbias.value >> 8).to_u8! - else puts "Unmapped APU read ~ addr:#{hex_str io_addr.to_u8}".colorize.fore(:red); 0_u8 # todo: open bus - end - end - - # write to apu memory - def write_io(io_addr : Int, value : UInt8) : Nil - return unless @sound_enabled || 0x82 <= io_addr <= 0x89 || Channel3::WAVE_RAM_RANGE.includes?(io_addr) - case io_addr - when @channel1 then @channel1.write_io io_addr, value - when @channel2 then @channel2.write_io io_addr, value - when @channel3 then @channel3.write_io io_addr, value - when @channel4 then @channel4.write_io io_addr, value - when @dma_channels then @dma_channels.write_io io_addr, value - when 0x80 then @soundcnt_l.value = (@soundcnt_l.value & 0xFF00) | value - when 0x81 then @soundcnt_l.value = (@soundcnt_l.value & 0x00FF) | value.to_u16 << 8 - when 0x82 then @soundcnt_h.value = (@soundcnt_h.value & 0xFF00) | value - when 0x83 then @soundcnt_h.value = (@soundcnt_h.value & 0x00FF) | value.to_u16 << 8 - when 0x84 - if value & 0x80 == 0 && @sound_enabled - (0x60..0x81).each { |addr| self.write_io addr, 0x00 } - @sound_enabled = false - elsif value & 0x80 > 0 && !@sound_enabled - @sound_enabled = true - @frame_sequencer_stage = 0 - @channel1.length_counter = 0 - @channel2.length_counter = 0 - @channel3.length_counter = 0 - @channel4.length_counter = 0 - end - when 0x85 # unused - when 0x88 then @soundbias.value = (@soundbias.value & 0xFF00) | value - when 0x89 then @soundbias.value = (@soundbias.value & 0x00FF) | value.to_u16 << 8 - when 0xA8..0xAF # unused - else puts "Unmapped APU write ~ addr:#{hex_str io_addr.to_u8}, val:#{value}".colorize(:yellow) - end - end -end diff --git a/src/crab/apu/abstract_channels.cr b/src/crab/apu/abstract_channels.cr deleted file mode 100644 index 31835bb..0000000 --- a/src/crab/apu/abstract_channels.cr +++ /dev/null @@ -1,98 +0,0 @@ -# All of the channels were developed using the following guide on gbdev -# https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware - -abstract class SoundChannel - property enabled : Bool = false - @dac_enabled : Bool = false - - # NRx1 - property length_counter = 0 - - # NRx4 - @length_enable : Bool = false - - def initialize(@gba : GBA) - end - - # Step the channel, calling helpers to reload the period and step the wave generation - def step : Nil - step_wave_generation - schedule_reload frequency_timer - end - - # Step the length, disabling the channel if the length counter expires - def length_step : Nil - if @length_enable && @length_counter > 0 - @length_counter -= 1 - @enabled = false if @length_counter == 0 - end - end - - # Used so that channels can be matched with case..when statements - abstract def ===(value) - - # Calculate the frequency timer - abstract def frequency_timer : UInt32 - - abstract def schedule_reload(frequency_timer : UInt32) : Nil - - # Called when @period reaches 0 - abstract def step_wave_generation : Nil - - abstract def get_amplitude : Int16 - - abstract def read_io(index : Int) : UInt8 - abstract def write_io(index : Int, value : UInt8) : Nil -end - -abstract class VolumeEnvelopeChannel < SoundChannel - # NRx2 - @starting_volume : UInt8 = 0x00 - @envelope_add_mode : Bool = false - @period : UInt8 = 0x00 - - @volume_envelope_timer : UInt8 = 0x00 - @current_volume : UInt8 = 0x00 - - @volume_envelope_is_updating = false - - def volume_step : Nil - if @period != 0 - @volume_envelope_timer -= 1 if @volume_envelope_timer > 0 - if @volume_envelope_timer == 0 - @volume_envelope_timer = @period - if (@current_volume < 0xF && @envelope_add_mode) || (@current_volume > 0 && !@envelope_add_mode) - @current_volume += (@envelope_add_mode ? 1 : -1) - else - @volume_envelope_is_updating = false - end - end - end - end - - def init_volume_envelope : Nil - @volume_envelope_timer = @period - @current_volume = @starting_volume - @volume_envelope_is_updating = true - end - - def read_NRx2 : UInt8 - @starting_volume << 4 | (@envelope_add_mode ? 0x08 : 0) | @period - end - - def write_NRx2(value : UInt8) : Nil - envelope_add_mode = value & 0x08 > 0 - if @enabled # Zombie mode glitch - @current_volume += 1 if (@period == 0 && @volume_envelope_is_updating) || !@envelope_add_mode - @current_volume = 0x10_u8 - @current_volume if (envelope_add_mode != @envelope_add_mode) - @current_volume &= 0x0F - end - - @starting_volume = value >> 4 - @envelope_add_mode = envelope_add_mode - @period = value & 0x07 - # Internal values - @dac_enabled = value & 0xF8 > 0 - @enabled = false if !@dac_enabled - end -end diff --git a/src/crab/apu/channel1.cr b/src/crab/apu/channel1.cr deleted file mode 100644 index 4405045..0000000 --- a/src/crab/apu/channel1.cr +++ /dev/null @@ -1,145 +0,0 @@ -class Channel1 < VolumeEnvelopeChannel - WAVE_DUTY = [ - [-8, -8, -8, -8, -8, -8, -8, +8], # 12.5% - [+8, -8, -8, -8, -8, -8, -8, +8], # 25% - [+8, -8, -8, -8, -8, +8, +8, +8], # 50% - [-8, +8, +8, +8, +8, +8, +8, -8], # 75% - ] - - RANGE = 0x60..0x67 - - def ===(value) : Bool - value.is_a?(Int) && RANGE.includes?(value) - end - - @wave_duty_position = 0 - - # NR10 - @sweep_period : UInt8 = 0x00 - @negate : Bool = false - @shift : UInt8 = 0x00 - - @sweep_timer : UInt8 = 0x00 - @frequency_shadow : UInt16 = 0x0000 - @sweep_enabled : Bool = false - @negate_has_been_used : Bool = false - - # NR11 - @duty : UInt8 = 0x00 - @length_load : UInt8 = 0x00 - - # NR13 / NR14 - @frequency : UInt16 = 0x00 - - def step_wave_generation : Nil - @wave_duty_position = (@wave_duty_position + 1) & 7 - end - - def frequency_timer : UInt32 - (0x800_u32 - @frequency) * 4 * 4 - end - - def schedule_reload(frequency_timer : UInt32) : Nil - @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel1 - end - - def sweep_step : Nil - @sweep_timer -= 1 if @sweep_timer > 0 - if @sweep_timer == 0 - @sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8 - if @sweep_enabled && @sweep_period > 0 - calculated = frequency_calculation - if calculated <= 0x07FF && @shift > 0 - @frequency_shadow = @frequency = calculated - frequency_calculation - end - end - end - end - - # Outputs a value -0x80..0x80 - def get_amplitude : Int16 - if @enabled && @dac_enabled - WAVE_DUTY[@duty][@wave_duty_position].to_i16 * @current_volume - else - 0_i16 - end - end - - # Calculate the new shadow frequency, disable channel if overflow 11 bits - # https://gist.github.com/drhelius/3652407#file-game-boy-sound-operation-L243-L250 - def frequency_calculation : UInt16 - calculated = @frequency_shadow >> @shift - calculated = @frequency_shadow + (@negate ? -1 : 1) * calculated - @negate_has_been_used = true if @negate - @enabled = false if calculated > 0x07FF - calculated - end - - def read_io(index : Int) : UInt8 - case index - when 0x60 then 0x80_u8 | @sweep_period << 4 | (@negate ? 0x08 : 0) | @shift - when 0x62 then 0x3F_u8 | @duty << 6 - when 0x63 then read_NRx2 - when 0x64 then 0xFF_u8 # write-only - when 0x65 then 0xBF_u8 | (@length_enable ? 0x40 : 0) - else puts "Reading from invalid Channel1 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus - end - end - - def write_io(index : Int, value : UInt8) : Nil - case index - when 0x60 - @sweep_period = (value & 0x70) >> 4 - @negate = value & 0x08 > 0 - @shift = value & 0x07 - # Internal values - @enabled = false if !@negate && @negate_has_been_used - when 0x61 # not used - when 0x62 - @duty = (value & 0xC0) >> 6 - @length_load = value & 0x3F - # Internal values - @length_counter = 0x40 - @length_load - when 0x63 - write_NRx2 value - when 0x64 - @frequency = (@frequency & 0x0700) | value - when 0x65 - @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 - length_enable = value & 0x40 > 0 - # Obscure length counter behavior #1 - if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 - @length_counter -= 1 - @enabled = false if @length_counter == 0 - end - @length_enable = length_enable - trigger = value & 0x80 > 0 - if trigger - @enabled = true if @dac_enabled - # Init length - if @length_counter == 0 - @length_counter = 0x40 - # Obscure length counter behavior #2 - @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period - end - # Init frequency - @gba.scheduler.clear Scheduler::EventType::APUChannel1 - schedule_reload frequency_timer - # Init volume envelope - init_volume_envelope - # Init sweep - @frequency_shadow = @frequency - @sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8 - @sweep_enabled = @sweep_period > 0 || @shift > 0 - @negate_has_been_used = false - if @shift > 0 # If sweep shift is non-zero, frequency calculation and overflow check are performed immediately - frequency_calculation - end - end - when 0x66 # not used - when 0x67 # not used - else raise "Writing to invalid Channel1 register: #{hex_str index.to_u16}" - end - end -end diff --git a/src/crab/apu/channel2.cr b/src/crab/apu/channel2.cr deleted file mode 100644 index a2e1a49..0000000 --- a/src/crab/apu/channel2.cr +++ /dev/null @@ -1,97 +0,0 @@ -class Channel2 < VolumeEnvelopeChannel - WAVE_DUTY = [ - [-8, -8, -8, -8, -8, -8, -8, +8], # 12.5% - [+8, -8, -8, -8, -8, -8, -8, +8], # 25% - [+8, -8, -8, -8, -8, +8, +8, +8], # 50% - [-8, +8, +8, +8, +8, +8, +8, -8], # 75% - ] - - RANGE = 0x68..0x6F - - def ===(value) : Bool - value.is_a?(Int) && RANGE.includes?(value) - end - - @wave_duty_position = 0 - - # NR21 - @duty : UInt8 = 0x00 - @length_load : UInt8 = 0x00 - - # NR23 / NR24 - @frequency : UInt16 = 0x00 - - def step_wave_generation : Nil - @wave_duty_position = (@wave_duty_position + 1) & 7 - end - - def frequency_timer : UInt32 - (0x800_u32 - @frequency) * 4 * 4 - end - - def schedule_reload(frequency_timer : UInt32) : Nil - @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel2 - end - - # Outputs a value -0x80..0x80 - def get_amplitude : Int16 - if @enabled && @dac_enabled - WAVE_DUTY[@duty][@wave_duty_position].to_i16 * @current_volume - else - 0_i16 - end - end - - def read_io(index : Int) : UInt8 - case index - when 0x68 then 0x3F_u8 | @duty << 6 - when 0x69 then read_NRx2 - when 0x6C then 0xFF_u8 # write-only - when 0x6D then 0xBF_u8 | (@length_enable ? 0x40 : 0) - else puts "Reading from invalid Channel2 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus - end - end - - def write_io(index : Int, value : UInt8) : Nil - case index - when 0x68 - @duty = (value & 0xC0) >> 6 - @length_load = value & 0x3F - # Internal values - @length_counter = 0x40 - @length_load - when 0x69 - write_NRx2 value - when 0x6A # not used - when 0x6B # not used - when 0x6C - @frequency = (@frequency & 0x0700) | value - when 0x6D - @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 - length_enable = value & 0x40 > 0 - # Obscure length counter behavior #1 - if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 - @length_counter -= 1 - @enabled = false if @length_counter == 0 - end - @length_enable = length_enable - trigger = value & 0x80 > 0 - if trigger - @enabled = true if @dac_enabled - # Init length - if @length_counter == 0 - @length_counter = 0x40 - # Obscure length counter behavior #2 - @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period - end - # Init frequency - @gba.scheduler.clear Scheduler::EventType::APUChannel2 - schedule_reload frequency_timer - # Init volume envelope - init_volume_envelope - end - when 0x6E # not used - when 0x6F # not used - else raise "Writing to invalid Channel2 register: #{hex_str index.to_u16}" - end - end -end diff --git a/src/crab/apu/channel3.cr b/src/crab/apu/channel3.cr deleted file mode 100644 index 87c85d9..0000000 --- a/src/crab/apu/channel3.cr +++ /dev/null @@ -1,125 +0,0 @@ -class Channel3 < SoundChannel - RANGE = 0x70..0x77 - WAVE_RAM_RANGE = 0x90..0x9F - - def ===(value) : Bool - value.is_a?(Int) && RANGE.includes?(value) || WAVE_RAM_RANGE.includes?(value) - end - - @wave_ram = Array(Bytes).new 2, Bytes.new(WAVE_RAM_RANGE.size) { |idx| idx & 1 == 0 ? 0x00_u8 : 0xFF_u8 } - @wave_ram_position : UInt8 = 0 - @wave_ram_sample_buffer : UInt8 = 0x00 - - # NR30 - @wave_ram_dimension : Bool = false - @wave_ram_bank : UInt8 = 0 - - # NR31 - @length_load : UInt8 = 0x00 - - # NR32 - @volume_code : UInt8 = 0x00 - @volume_force : Bool = false - - # NR33 / NR34 - @frequency : UInt16 = 0x00 - - def step_wave_generation : Nil - @wave_ram_position = (@wave_ram_position + 1) % (WAVE_RAM_RANGE.size * 2) - @wave_ram_bank ^= 1 if @wave_ram_position == 0 && @wave_ram_dimension - full_sample = @wave_ram[@wave_ram_bank][@wave_ram_position // 2] - @wave_ram_sample_buffer = (full_sample >> (@wave_ram_position & 1 == 0 ? 4 : 0)) & 0xF - end - - def frequency_timer : UInt32 - (0x800_u32 - @frequency) * 2 * 4 - end - - def schedule_reload(frequency_timer : UInt32) : Nil - @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel3 - end - - # Outputs a value -0x80..0x80 - def get_amplitude : Int16 - if @enabled && @dac_enabled - (@wave_ram_sample_buffer.to_i16 - 8) * 4 * (@volume_force ? 3 : {0, 4, 2, 1}[@volume_code]) - else - 0_i16 - end - end - - def read_io(index : Int) : UInt8 - case index - when 0x70 then 0x7F | (@dac_enabled ? 0x80 : 0) - when 0x72 then 0xFF - when 0x73 then 0x9F | @volume_code << 5 - when 0x74 then 0xFF - when 0x75 then 0xBF | (@length_enable ? 0x40 : 0) - when WAVE_RAM_RANGE - if @enabled - @wave_ram[@wave_ram_bank][@wave_ram_position // 2] - else - @wave_ram[@wave_ram_bank][index - WAVE_RAM_RANGE.begin] - end - else puts "Reading from invalid Channel3 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus - end.to_u8 - end - - def write_io(index : Int, value : UInt8) : Nil - case index - when 0x70 - @dac_enabled = value & 0x80 > 0 - @enabled = false if !@dac_enabled - @wave_ram_dimension = bit?(value, 5) - @wave_ram_bank = bits(value, 6..6) - when 0x71 # not used - when 0x72 - @length_load = value - # Internal values - @length_counter = 0x100 - @length_load - when 0x73 - @volume_code = (value & 0x60) >> 5 - @volume_force = bit?(value, 7) - when 0x74 - @frequency = (@frequency & 0x0700) | value - when 0x75 - @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 - length_enable = value & 0x40 > 0 - # Obscure length counter behavior #1 - if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 - @length_counter -= 1 - @enabled = false if @length_counter == 0 - end - @length_enable = length_enable - trigger = value & 0x80 > 0 - if trigger - @enabled = true if @dac_enabled - # Init length - if @length_counter == 0 - @length_counter = 0x100 - # Obscure length counter behavior #2 - @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period - end - # Init frequency - # todo: I'm patching in an extra 6 T-cycles here with the `+ 6`. This is specifically - # to get blargg's "09-wave read while on.s" to pass. I'm _not_ refilling the - # frequency timer with this extra cycles when it reaches 0. For now, I'm letting - # this be in order to work on other audio behavior. Note that this is pretty - # brittle in it's current state though... - @gba.scheduler.clear Scheduler::EventType::APUChannel3 - schedule_reload frequency_timer + 6 - # Init wave ram position - @wave_ram_position = 0 - end - when 0x76 # not used - when 0x77 # not used - when WAVE_RAM_RANGE - if @enabled - @wave_ram[@wave_ram_bank][@wave_ram_position // 2] = value - else - @wave_ram[@wave_ram_bank][index - WAVE_RAM_RANGE.begin] = value - end - else raise "Writing to invalid Channel3 register: #{hex_str index.to_u16}" - end - end -end diff --git a/src/crab/apu/channel4.cr b/src/crab/apu/channel4.cr deleted file mode 100644 index 65f5a4e..0000000 --- a/src/crab/apu/channel4.cr +++ /dev/null @@ -1,99 +0,0 @@ -class Channel4 < VolumeEnvelopeChannel - RANGE = 0x78..0x7F - - def ===(value) : Bool - value.is_a?(Int) && RANGE.includes?(value) - end - - @lfsr : UInt16 = 0x0000 - - # NR41 - @length_load : UInt8 = 0x00 - - # NR43 - @clock_shift : UInt8 = 0x00 - @width_mode : UInt8 = 0x00 - @divisor_code : UInt8 = 0x00 - - def step_wave_generation : Nil - new_bit = (@lfsr & 0b01) ^ ((@lfsr & 0b10) >> 1) - @lfsr >>= 1 - @lfsr |= new_bit << 14 - if @width_mode != 0 - @lfsr &= ~(1 << 6) - @lfsr |= new_bit << 6 - end - end - - def frequency_timer : UInt32 - ((@divisor_code == 0 ? 8_u32 : @divisor_code.to_u32 << 4) << @clock_shift) * 4 - end - - def schedule_reload(frequency_timer : UInt32) : Nil - @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel4 - end - - # Outputs a value -0x80..0x80 - def get_amplitude : Int16 - if @enabled && @dac_enabled - ((~@lfsr & 1).to_i16 * 16 - 8) * @current_volume - else - 0_i16 - end - end - - def read_io(index : Int) : UInt8 - case index - when 0x78 then 0xFF - when 0x79 then read_NRx2 - when 0x7C then @clock_shift << 4 | @width_mode << 3 | @divisor_code - when 0x7D then 0xBF | (@length_enable ? 0x40 : 0) - else puts "Reading from invalid Channel4 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus - end.to_u8 - end - - def write_io(index : Int, value : UInt8) : Nil - case index - when 0x78 - @length_load = value & 0x3F - # Internal values - @length_counter = 0x40 - @length_load - when 0x79 - write_NRx2 value - when 0x7A # not used - when 0x7B # not used - when 0x7C - @clock_shift = value >> 4 - @width_mode = (value & 0x08) >> 3 - @divisor_code = value & 0x07 - when 0x7D - length_enable = value & 0x40 > 0 - # Obscure length counter behavior #1 - if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 - @length_counter -= 1 - @enabled = false if @length_counter == 0 - end - @length_enable = length_enable - trigger = value & 0x80 > 0 - if trigger - @enabled = true if @dac_enabled - # Init length - if @length_counter == 0 - @length_counter = 0x40 - # Obscure length counter behavior #2 - @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period - end - # Init frequency - @gba.scheduler.clear Scheduler::EventType::APUChannel4 - schedule_reload frequency_timer - # Init volume envelope - init_volume_envelope - # Init lfsr - @lfsr = 0x7FFF - end - when 0x7E # not used - when 0x7F # not used - else raise "Writing to invalid Channel4 register: #{hex_str index.to_u16}" - end - end -end diff --git a/src/crab/apu/dma_channels.cr b/src/crab/apu/dma_channels.cr deleted file mode 100644 index bb8f06c..0000000 --- a/src/crab/apu/dma_channels.cr +++ /dev/null @@ -1,56 +0,0 @@ -class DMAChannels - RANGE = 0xA0..0xA7 - - @fifos = Array(Array(Int8)).new 2 { Array(Int8).new 32, 0 } - @positions = Array(Int32).new 2, 0 - @sizes = Array(Int32).new 2, 0 - @timers : Array(Proc(UInt16)) - @latches = Array(Int16).new 2, 0 - - def ===(value) : Bool - value.is_a?(Int) && RANGE.includes?(value) - end - - def initialize(@gba : GBA, @control : Reg::SOUNDCNT_H) - @timers = [ - ->{ @control.dma_sound_a_timer }, - ->{ @control.dma_sound_b_timer }, - ] - end - - def read_io(index : Int) : UInt8 - 0_u8 - end - - def write_io(index : Int, value : Byte) : Nil - channel = bit?(index, 2).to_unsafe - if @sizes[channel] < 32 - @fifos[channel][(@positions[channel] + @sizes[channel]) % 32] = value.to_i8! - @sizes[channel] += 1 - else - log "Writing #{hex_str value} to fifo #{(channel + 65).chr}, but it's already full".colorize.fore(:red) - end - end - - def timer_overflow(timer : Int) : Nil - 2.times do |channel| - if timer == @timers[channel].call - if @sizes[channel] > 0 - log "Timer overflow good; channel:#{channel}, timer:#{timer}".colorize.fore(:yellow) - @latches[channel] = @fifos[channel][@positions[channel]].to_i16 << 1 # put on scale of -0x100..0x100 - @positions[channel] = (@positions[channel] + 1) % 32 - @sizes[channel] -= 1 - else - log "Timer overflow but empty; channel:#{channel}, timer:#{timer}".colorize.fore(:yellow) - @latches[channel] = 0 - end - end - @gba.dma.trigger_fifo(channel) if @sizes[channel] < 16 - end - end - - # Outputs a value -0x100...0x100 - def get_amplitude : Tuple(Int16, Int16) - {@latches[0], @latches[1]} - end -end diff --git a/src/crab/arm/arm.cr b/src/crab/arm/arm.cr deleted file mode 100644 index 5c9b710..0000000 --- a/src/crab/arm/arm.cr +++ /dev/null @@ -1,98 +0,0 @@ -# The `private defs` here are effectively meaningless since I only run ARM -# functions from the CPU where I include it, but I'm just using it as an -# indicator that the functions should not be called directly outside of the -# module. - -module ARM - def arm_execute(instr : Word) : Nil - if check_cond bits(instr, 28..31) - hash = hash_instr instr - lut[hash].call instr - else - log "Skipping instruction, cond: #{hex_str instr >> 28}" - step_arm - end - end - - private def hash_instr(instr : Word) : Word - (instr >> 16 & 0x0FF0) | (instr >> 4 & 0xF) - end - - def fill_lut : Slice(Proc(Word, Nil)) - lut = Slice(Proc(Word, Nil)).new 4096, ->arm_unimplemented(Word) - 4096.times do |idx| - if idx & 0b111100000000 == 0b111100000000 - lut[idx] = ->arm_software_interrupt(Word) - elsif idx & 0b111100000001 == 0b111000000001 - # coprocessor register transfer - elsif idx & 0b111100000001 == 0b111000000001 - # coprocessor data operation - elsif idx & 0b111000000000 == 0b110000000000 - # coprocessor data transfer - elsif idx & 0b111000000000 == 0b101000000000 - lut[idx] = ->arm_branch(Word) - elsif idx & 0b111000000000 == 0b100000000000 - lut[idx] = ->arm_block_data_transfer(Word) - elsif idx & 0b111000000001 == 0b011000000001 - # undefined - elsif idx & 0b110000000000 == 0b010000000000 - lut[idx] = ->arm_single_data_transfer(Word) - elsif idx & 0b111111111111 == 0b000100100001 - lut[idx] = ->arm_branch_exchange(Word) - elsif idx & 0b111110111111 == 0b000100001001 - lut[idx] = ->arm_single_data_swap(Word) - elsif idx & 0b111110001111 == 0b000010001001 - lut[idx] = ->arm_multiply_long(Word) - elsif idx & 0b111111001111 == 0b000000001001 - lut[idx] = ->arm_multiply(Word) - elsif idx & 0b111001001001 == 0b000001001001 - lut[idx] = ->arm_halfword_data_transfer_immediate(Word) - elsif idx & 0b111001001001 == 0b000000001001 - lut[idx] = ->arm_halfword_data_transfer_register(Word) - elsif idx & 0b110110010000 == 0b000100000000 - lut[idx] = ->arm_psr_transfer(Word) - elsif idx & 0b110000000000 == 0b000000000000 - lut[idx] = ->arm_data_processing(Word) - else - lut[idx] = ->arm_unused(Word) - end - end - lut - end - - def arm_unimplemented(instr : Word) : Nil - puts "Unimplemented instruction: #{hex_str instr}" - exit 1 - end - - def arm_unused(instr : Word) : Nil - puts "Unused instruction: #{hex_str instr}" - end - - def rotate_register(instr : Word, carry_out : Pointer(Bool), allow_register_shifts : Bool) : Word - reg = bits(instr, 0..3) - shift_type = bits(instr, 5..6) - immediate = !(allow_register_shifts && bit?(instr, 4)) - if immediate - shift_amount = bits(instr, 7..11) - else - shift_register = bits(instr, 8..11) - # todo weird logic if bottom byte of reg > 31 - shift_amount = @r[shift_register] & 0xFF - end - case shift_type - when 0b00 then lsl(@r[reg], shift_amount, carry_out) - when 0b01 then lsr(@r[reg], shift_amount, immediate, carry_out) - when 0b10 then asr(@r[reg], shift_amount, immediate, carry_out) - when 0b11 then ror(@r[reg], shift_amount, immediate, carry_out) - else raise "Impossible shift type: #{hex_str shift_type}" - end - end - - def immediate_offset(instr : Word, carry_out : Pointer(Bool)) : Word - rotate = bits(instr, 8..11) - imm = bits(instr, 0..7) - # todo putting "false" here causes the gba-suite tests to pass, but _why_ - ror(imm, 2 * rotate, false, carry_out) - end -end diff --git a/src/crab/arm/block_data_transfer.cr b/src/crab/arm/block_data_transfer.cr deleted file mode 100644 index 8a7a66c..0000000 --- a/src/crab/arm/block_data_transfer.cr +++ /dev/null @@ -1,51 +0,0 @@ -module ARM - def arm_block_data_transfer(instr : Word) : Nil - pre_index = bit?(instr, 24) - add = bit?(instr, 23) - s_bit = bit?(instr, 22) - write_back = bit?(instr, 21) - load = bit?(instr, 20) - rn = bits(instr, 16..19) - list = bits(instr, 0..15) - - if s_bit - abort "todo: handle cases with r15 in list" if bit?(list, 15) - mode = @cpsr.mode - switch_mode CPU::Mode::USR - end - - address = @r[rn] - bits_set = count_set_bits(list) - if bits_set == 0 # odd behavior on empty list, tested in gba-suite - bits_set = 16 - list = 0x8000 - end - final_addr = address + bits_set * (add ? 4 : -4) - if add - address += 4 if pre_index - else - address = final_addr - address += 4 unless pre_index - end - first_transfer = false - 16.times do |idx| # always transfered to/from incrementing addresses - if bit?(list, idx) - if load - set_reg(idx, @gba.bus.read_word(address)) - else - @gba.bus[address] = @r[idx] - @gba.bus[address] &+= 4 if idx == 15 # pc reads 12 ahead instead of 8 - end - address += 4 # can always do these post since the address was accounted for up front - set_reg(rn, final_addr) if write_back && !first_transfer && !(load && bit?(list, rn)) - first_transfer = true # writeback happens on second cycle of the instruction - end - end - - if s_bit - switch_mode CPU::Mode.from_value mode.not_nil! - end - - step_arm unless load && bit?(list, 15) - end -end diff --git a/src/crab/arm/branch.cr b/src/crab/arm/branch.cr deleted file mode 100644 index 550c214..0000000 --- a/src/crab/arm/branch.cr +++ /dev/null @@ -1,8 +0,0 @@ -module ARM - def arm_branch(instr : Word) : Nil - link = bit?(instr, 24) - offset = (bits(instr, 0..23) << 8).to_i32! >> 6 - set_reg(14, @r[15] - 4) if link - set_reg(15, @r[15] &+ offset) - end -end diff --git a/src/crab/arm/branch_exchange.cr b/src/crab/arm/branch_exchange.cr deleted file mode 100644 index 6587fe6..0000000 --- a/src/crab/arm/branch_exchange.cr +++ /dev/null @@ -1,11 +0,0 @@ -module ARM - def arm_branch_exchange(instr : Word) : Nil - rn = bits(instr, 0..3) - if bit?(@r[rn], 0) - @cpsr.thumb = true - set_reg(15, @r[rn]) - else - set_reg(15, @r[rn]) - end - end -end diff --git a/src/crab/arm/data_processing.cr b/src/crab/arm/data_processing.cr deleted file mode 100644 index 8c1eabe..0000000 --- a/src/crab/arm/data_processing.cr +++ /dev/null @@ -1,106 +0,0 @@ -module ARM - def arm_data_processing(instr : Word) : Nil - imm_flag = bit?(instr, 25) - opcode = bits(instr, 21..24) - set_conditions = bit?(instr, 20) - rn = bits(instr, 16..19) - rd = bits(instr, 12..15) - # The PC value will be the address of the instruction, plus 8 or 12 bytes due to instruction - # prefetching. If the shift amount is specified in the instruction, the PC will be 8 bytes - # ahead. If a register is used to specify the shift amount the PC will be 12 bytes ahead. - pc_reads_12_ahead = !imm_flag && bit?(instr, 4) - @r[15] &+= 4 if pc_reads_12_ahead - barrel_shifter_carry_out = @cpsr.carry - operand_2 = if imm_flag # Operand 2 is an immediate - immediate_offset bits(instr, 0..11), pointerof(barrel_shifter_carry_out) - else # Operand 2 is a register - rotate_register bits(instr, 0..11), pointerof(barrel_shifter_carry_out), allow_register_shifts: true - end - case opcode - when 0b0000 # AND - set_reg(rd, @r[rn] & operand_2) - if set_conditions - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = barrel_shifter_carry_out - end - step_arm unless rd == 15 - when 0b0001 # EOR - set_reg(rd, @r[rn] ^ operand_2) - if set_conditions - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = barrel_shifter_carry_out - end - step_arm unless rd == 15 - when 0b0010 # SUB - set_reg(rd, sub(@r[rn], operand_2, set_conditions)) - step_arm unless rd == 15 - when 0b0011 # RSB - set_reg(rd, sub(operand_2, @r[rn], set_conditions)) - step_arm unless rd == 15 - when 0b0100 # ADD - set_reg(rd, add(@r[rn], operand_2, set_conditions)) - step_arm unless rd == 15 - when 0b0101 # ADC - set_reg(rd, adc(@r[rn], operand_2, set_conditions)) - step_arm unless rd == 15 - when 0b0110 # SBC - set_reg(rd, sbc(@r[rn], operand_2, set_conditions)) - step_arm unless rd == 15 - when 0b0111 # RSC - set_reg(rd, sbc(operand_2, @r[rn], set_conditions)) - step_arm unless rd == 15 - when 0b1000 # TST - set_neg_and_zero_flags(@r[rn] & operand_2) - @cpsr.carry = barrel_shifter_carry_out - step_arm - when 0b1001 # TEQ - set_neg_and_zero_flags(@r[rn] ^ operand_2) - @cpsr.carry = barrel_shifter_carry_out - step_arm - when 0b1010 # CMP - sub(@r[rn], operand_2, set_conditions) - step_arm - when 0b1011 # CMN - add(@r[rn], operand_2, set_conditions) - step_arm - when 0b1100 # ORR - set_reg(rd, @r[rn] | operand_2) - if set_conditions - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = barrel_shifter_carry_out - end - step_arm unless rd == 15 - when 0b1101 # MOV - set_reg(rd, operand_2) - if set_conditions - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = barrel_shifter_carry_out - end - step_arm unless rd == 15 - when 0b1110 # BIC - set_reg(rd, @r[rn] & ~operand_2) - if set_conditions - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = barrel_shifter_carry_out - end - step_arm unless rd == 15 - when 0b1111 # MVN - set_reg(rd, ~operand_2) - if set_conditions - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = barrel_shifter_carry_out - end - step_arm unless rd == 15 - else raise "Unimplemented execution of data processing opcode: #{hex_str opcode}" - end - @r[15] &-= 4 if pc_reads_12_ahead # todo: this is probably borked if there's a write to r15, but it needs some more thought.. - if rd == 15 && set_conditions - @r[15] &-= 4 if @spsr.thumb # writing to r15 will have already cleared the pipeline and bumped r15 for arm mode - old_spsr = @spsr.value - new_mode = CPU::Mode.from_value(@spsr.mode) - switch_mode new_mode - @cpsr.value = old_spsr - @spsr.value = new_mode.bank == 0 ? @cpsr.value : @spsr_banks[new_mode.bank] - end - end -end diff --git a/src/crab/arm/halfword_data_transfer_imm.cr b/src/crab/arm/halfword_data_transfer_imm.cr deleted file mode 100644 index a0c288b..0000000 --- a/src/crab/arm/halfword_data_transfer_imm.cr +++ /dev/null @@ -1,57 +0,0 @@ -module ARM - def arm_halfword_data_transfer_immediate(instr : Word) : Nil - pre_index = bit?(instr, 24) - add = bit?(instr, 23) - write_back = bit?(instr, 21) - load = bit?(instr, 20) - rn = bits(instr, 16..19) - rd = bits(instr, 12..15) - offset_high = bits(instr, 8..11) - sh = bits(instr, 5..6) - offset_low = bits(instr, 0..3) - - address = @r[rn] - offset = offset_high << 4 | offset_low - - if pre_index - if add - address &+= offset - else - address &-= offset - end - end - - case sh - when 0b00 # swp, no docs on this? - abort "HalfwordDataTransferReg swp #{hex_str instr}" - when 0b01 # ldrh/strh - if load - set_reg(rd, @gba.bus.read_half_rotate address) - else - @gba.bus[address] = 0xFFFF_u16 & @r[rd] - # When R15 is the source register (Rd) of a register store (STR) instruction, the stored - # value will be address of the instruction plus 12. - @gba.bus[address] &+= 4 if rd == 15 - end - when 0b10 # ldrsb - set_reg(rd, @gba.bus[address].to_i8!.to_u32!) - when 0b11 # ldrsh - set_reg(rd, @gba.bus.read_half_signed(address)) - else raise "Invalid halfword data transfer imm op: #{sh}" - end - - unless pre_index - if add - address &+= offset - else - address &-= offset - end - end - # In the case of post-indexed addressing, the write back bit is redundant and is always set to - # zero, since the old base value can be retained by setting the offset to zero. Therefore - # post-indexed data transfers always write back the modified base. - set_reg(rn, address) if (write_back || !pre_index) && (rd != rn || !load) - - step_arm unless load && rd == 15 - end -end diff --git a/src/crab/arm/halfword_data_transfer_reg.cr b/src/crab/arm/halfword_data_transfer_reg.cr deleted file mode 100644 index 60cb1b6..0000000 --- a/src/crab/arm/halfword_data_transfer_reg.cr +++ /dev/null @@ -1,56 +0,0 @@ -module ARM - def arm_halfword_data_transfer_register(instr : Word) : Nil - pre_index = bit?(instr, 24) - add = bit?(instr, 23) - write_back = bit?(instr, 21) - load = bit?(instr, 20) - rn = bits(instr, 16..19) - rd = bits(instr, 12..15) - sh = bits(instr, 5..6) - rm = bits(instr, 0..3) - - address = @r[rn] - offset = @r[rm] - - if pre_index - if add - address &+= offset - else - address &-= offset - end - end - - case sh - when 0b00 # swp, no docs on this? - abort "HalfwordDataTransferReg swp #{hex_str instr}" - when 0b01 # ldrh/strh - if load - set_reg(rd, @gba.bus.read_half_rotate address) - else - @gba.bus[address] = 0xFFFF_u16 & @r[rd] - # When R15 is the source register (Rd) of a register store (STR) instruction, the stored - # value will be address of the instruction plus 12. - @gba.bus[address] &+= 4 if rd == 15 - end - when 0b10 # ldrsb - set_reg(rd, @gba.bus[address].to_i8!.to_u32!) - when 0b11 # ldrsh - set_reg(rd, @gba.bus.read_half_signed(address)) - else raise "Invalid halfword data transfer imm op: #{sh}" - end - - unless pre_index - if add - address &+= offset - else - address &-= offset - end - end - # In the case of post-indexed addressing, the write back bit is redundant and is always set to - # zero, since the old base value can be retained by setting the offset to zero. Therefore - # post-indexed data transfers always write back the modified base. - set_reg(rn, address) if (write_back || !pre_index) && (rd != rn || !load) - - step_arm unless load && rd == 15 - end -end diff --git a/src/crab/arm/multiply.cr b/src/crab/arm/multiply.cr deleted file mode 100644 index 6c51dd5..0000000 --- a/src/crab/arm/multiply.cr +++ /dev/null @@ -1,15 +0,0 @@ -module ARM - def arm_multiply(instr : Word) : Nil - accumulate = bit?(instr, 21) - set_conditions = bit?(instr, 20) - rd = bits(instr, 16..19) - rn = bits(instr, 12..15) - rs = bits(instr, 8..11) - rm = bits(instr, 0..3) - - set_reg(rd, @r[rm] &* @r[rs] &+ (accumulate ? @r[rn] : 0)) - set_neg_and_zero_flags(@r[rd]) if set_conditions - - step_arm unless rd == 15 - end -end diff --git a/src/crab/arm/multiply_long.cr b/src/crab/arm/multiply_long.cr deleted file mode 100644 index fd6e990..0000000 --- a/src/crab/arm/multiply_long.cr +++ /dev/null @@ -1,27 +0,0 @@ -module ARM - def arm_multiply_long(instr : Word) : Nil - signed = bit?(instr, 22) - accumulate = bit?(instr, 21) - set_conditions = bit?(instr, 20) - rdhi = bits(instr, 16..19) - rdlo = bits(instr, 12..15) - rs = bits(instr, 8..11) - rm = bits(instr, 0..3) - - res = if signed - @r[rm].to_i32!.to_i64 &* @r[rs].to_i32! - else - @r[rm].to_u64 &* @r[rs] - end - res &+= @r[rdhi].to_u64 << 32 | @r[rdlo] if accumulate - - set_reg(rdhi, (res >> 32).to_u32!) - set_reg(rdlo, res.to_u32!) - if set_conditions - @cpsr.negative = bit?(@r[rdhi], 31) - @cpsr.zero = res == 0 - end - - step_arm unless rdhi == 15 || rdlo == 15 - end -end diff --git a/src/crab/arm/psr_transfer.cr b/src/crab/arm/psr_transfer.cr deleted file mode 100644 index 37c2e37..0000000 --- a/src/crab/arm/psr_transfer.cr +++ /dev/null @@ -1,43 +0,0 @@ -module ARM - def arm_psr_transfer(instr : Word) : Nil - spsr = bit?(instr, 22) - mode = CPU::Mode.from_value @cpsr.mode - has_spsr = mode != CPU::Mode::USR && mode != CPU::Mode::SYS - - if bit?(instr, 21) # MSR - mask = 0_u32 - mask |= 0xFF000000 if bit?(instr, 19) # f (aka _flg) - mask |= 0x00FF0000 if bit?(instr, 18) # s - mask |= 0x0000FF00 if bit?(instr, 17) # x - mask |= 0x000000FF if bit?(instr, 16) # c (aka _ctl) - - if bit?(instr, 25) # immediate - barrel_shifter_carry_out = false # unused, doesn't matter - value = immediate_offset bits(instr, 0..11), pointerof(barrel_shifter_carry_out) - else # register value - value = @r[bits(instr, 0..3)] - end - - value &= mask - if spsr - if has_spsr - @spsr.value = (@spsr.value & ~mask) | value - end - else - thumb = @cpsr.thumb - switch_mode CPU::Mode.from_value value & 0x1F if mask & 0xFF > 0 - @cpsr.value = (@cpsr.value & ~mask) | value - @cpsr.thumb = thumb - end - else # MRS - rd = bits(instr, 12..15) - if spsr && has_spsr - set_reg(rd, @spsr.value) - else - set_reg(rd, @cpsr.value) - end - end - - step_arm unless !bit?(instr, 21) && bits(instr, 12..15) == 15 - end -end diff --git a/src/crab/arm/single_data_swap.cr b/src/crab/arm/single_data_swap.cr deleted file mode 100644 index 9ed92a0..0000000 --- a/src/crab/arm/single_data_swap.cr +++ /dev/null @@ -1,19 +0,0 @@ -module ARM - def arm_single_data_swap(instr : Word) : Nil - byte_quantity = bit?(instr, 22) - rn = bits(instr, 16..19) - rd = bits(instr, 12..15) - rm = bits(instr, 0..3) - if byte_quantity - tmp = @gba.bus[@r[rn]] - @gba.bus[@r[rn]] = @r[rm].to_u8! - set_reg(rd, tmp.to_u32) - else - tmp = @gba.bus.read_word_rotate @r[rn] - @gba.bus[@r[rn]] = @r[rm] - set_reg(rd, tmp) - end - - step_arm unless rd == 15 - end -end diff --git a/src/crab/arm/single_data_transfer.cr b/src/crab/arm/single_data_transfer.cr deleted file mode 100644 index cd93edb..0000000 --- a/src/crab/arm/single_data_transfer.cr +++ /dev/null @@ -1,60 +0,0 @@ -module ARM - def arm_single_data_transfer(instr : Word) : Nil - imm_flag = bit?(instr, 25) - pre_indexing = bit?(instr, 24) - add_offset = bit?(instr, 23) - byte_quantity = bit?(instr, 22) - write_back = bit?(instr, 21) - load = bit?(instr, 20) - rn = bits(instr, 16..19) - rd = bits(instr, 12..15) - - barrel_shifter_carry_out = false # unused, doesn't matter - offset = if imm_flag # Operand 2 is a register (opposite of data processing for some reason) - rotate_register bits(instr, 0..11), pointerof(barrel_shifter_carry_out), allow_register_shifts: false - else # Operand 2 is an immediate offset - bits(instr, 0..11) - end - - address = @r[rn] - - if pre_indexing - if add_offset - address &+= offset - else - address &-= offset - end - end - - if load - if byte_quantity - set_reg(rd, @gba.bus[address].to_u32) - else - set_reg(rd, @gba.bus.read_word_rotate address) - end - else - if byte_quantity - @gba.bus[address] = @r[rd].to_u8! - else - @gba.bus[address] = @r[rd] - end - # When R15 is the source register (Rd) of a register store (STR) instruction, the stored - # value will be address of the instruction plus 12. - @gba.bus[address] &+= 4 if rd == 15 - end - - unless pre_indexing - if add_offset - address &+= offset - else - address &-= offset - end - end - # In the case of post-indexed addressing, the write back bit is redundant and is always set to - # zero, since the old base value can be retained by setting the offset to zero. Therefore - # post-indexed data transfers always write back the modified base. - set_reg(rn, address) if (write_back || !pre_indexing) && (rd != rn || !load) - - step_arm unless load && rd == 15 - end -end diff --git a/src/crab/arm/software_interrupt.cr b/src/crab/arm/software_interrupt.cr deleted file mode 100644 index 3179210..0000000 --- a/src/crab/arm/software_interrupt.cr +++ /dev/null @@ -1,9 +0,0 @@ -module ARM - def arm_software_interrupt(instr : Word) : Nil - lr = @r[15] - 4 - switch_mode CPU::Mode::SVC - set_reg(14, lr) - @cpsr.irq_disable = true - set_reg(15, 0x08) - end -end diff --git a/src/crab/bus.cr b/src/crab/bus.cr deleted file mode 100644 index e506cd1..0000000 --- a/src/crab/bus.cr +++ /dev/null @@ -1,175 +0,0 @@ -class Bus - getter bios = Bytes.new 0x4000 - getter wram_board = Bytes.new 0x40000 - getter wram_chip = Bytes.new 0x08000 - - def initialize(@gba : GBA, bios_path : String) - File.open(bios_path) { |file| file.read @bios } - end - - def [](index : Int) : Byte - case bits(index, 24..27) - when 0x0 then @bios[index & 0x3FFF] - when 0x1 then 0_u8 # todo: open bus - when 0x2 then @wram_board[index & 0x3FFFF] - when 0x3 then @wram_chip[index & 0x7FFF] - when 0x4 then @gba.mmio[index] - when 0x5 then @gba.ppu.pram[index & 0x3FF] - when 0x6 - address = 0x1FFFF_u32 & index - address -= 0x8000 if address > 0x17FFF - @gba.ppu.vram[address] - when 0x7 then @gba.ppu.oam[index & 0x3FF] - when 0x8, 0x9, - 0xA, 0xB, - 0xC, 0xD then @gba.cartridge.rom[index & 0x01FFFFFF] - when 0xE, 0xF then @gba.storage[index] - else abort "Unmapped read: #{hex_str index.to_u32}" - end - end - - def read_half(index : Int) : HalfWord - index &= ~1 - case bits(index, 24..27) - when 0x0 then (@bios.to_unsafe + (index & 0x3FFF)).as(HalfWord*).value - when 0x1 then 0_u16 # todo: open bus - when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(HalfWord*).value - when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(HalfWord*).value - when 0x4 then read_half_slow(index) - when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(HalfWord*).value - when 0x6 - address = 0x1FFFF_u32 & index - address -= 0x8000 if address > 0x17FFF - (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value - when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value - when 0x8, 0x9, - 0xA, 0xB, - 0xC, 0xD then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value - when 0xE, 0xF then @gba.storage.read_half(index) - else abort "Unmapped read: #{hex_str index.to_u32}" - end - end - - def read_half_rotate(index : Int) : Word - half = read_half(index).to_u32! - bits = (index & 1) << 3 - half >> bits | half << (32 - bits) - end - - # On ARM7 aka ARMv4 aka NDS7/GBA: - # LDRH Rd,[odd] --> LDRH Rd,[odd-1] ROR 8 ;read to bit0-7 and bit24-31 - # LDRSH Rd,[odd] --> LDRSB Rd,[odd] ;sign-expand BYTE value - def read_half_signed(index : Int) : Word - if bit?(index, 0) - self[index].to_i8!.to_u32! - else - read_half(index).to_i16!.to_u32! - end - end - - def read_word(index : Int) : Word - index &= ~3 - case bits(index, 24..27) - when 0x0 then (@bios.to_unsafe + (index & 0x3FFF)).as(Word*).value - when 0x1 then 0_u32 # todo: open bus - when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(Word*).value - when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(Word*).value - when 0x4 then read_word_slow(index) - when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(Word*).value - when 0x6 - address = 0x1FFFF_u32 & index - address -= 0x8000 if address > 0x17FFF - (@gba.ppu.vram.to_unsafe + address).as(Word*).value - when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value - when 0x8, 0x9, - 0xA, 0xB, - 0xC, 0xD then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(Word*).value - when 0xE, 0xF then @gba.storage.read_word(index) - else abort "Unmapped read: #{hex_str index.to_u32}" - end - end - - def read_word_rotate(index : Int) : Word - word = read_word index - bits = (index & 3) << 3 - word >> bits | word << (32 - bits) - end - - def []=(index : Int, value : Byte) : Nil - return if bits(index, 28..31) > 0 - @gba.cpu.fill_pipeline if index <= @gba.cpu.r[15] && index >= @gba.cpu.r[15] &- 4 # detect writes near pc - case bits(index, 24..27) - when 0x2 then @wram_board[index & 0x3FFFF] = value - when 0x3 then @wram_chip[index & 0x7FFF] = value - when 0x4 then @gba.mmio[index] = value - when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FE)).as(HalfWord*).value = 0x0101_u16 * value - when 0x6 - address = 0x1FFFE_u32 & index # todo ignored range is different when in bitmap mode - (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = 0x0101_u16 * value if address <= 0x0FFFF - when 0xE, 0xF then @gba.storage[index] = value - else log "Unmapped write: #{hex_str index.to_u32}" - end - end - - def []=(index : Int, value : HalfWord) : Nil - return if bits(index, 28..31) > 0 - index &= ~1 - @gba.cpu.fill_pipeline if index <= @gba.cpu.r[15] && index >= @gba.cpu.r[15] &- 4 # detect writes near pc - case bits(index, 24..27) - when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(HalfWord*).value = value - when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(HalfWord*).value = value - when 0x4 then write_half_slow(index, value) - when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(HalfWord*).value = value - when 0x6 - address = 0x1FFFF_u32 & index - address -= 0x8000 if address > 0x17FFF - (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = value - when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value = value - when 0xE, 0xF then write_half_slow(index, value) - else log "Unmapped write: #{hex_str index.to_u32}" - end - end - - def []=(index : Int, value : Word) : Nil - return if bits(index, 28..31) > 0 - index &= ~3 - @gba.cpu.fill_pipeline if index <= @gba.cpu.r[15] && index >= @gba.cpu.r[15] &- 4 # detect writes near pc - case bits(index, 24..27) - when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(Word*).value = value - when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(Word*).value = value - when 0x4 then write_word_slow(index, value) - when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(Word*).value = value - when 0x6 - address = 0x1FFFF_u32 & index - address -= 0x8000 if address > 0x17FFF - (@gba.ppu.vram.to_unsafe + address).as(Word*).value = value - when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value = value - when 0xE, 0xF then write_word_slow(index, value) - else log "Unmapped write: #{hex_str index.to_u32}" - end - end - - private def read_half_slow(index : Int) : HalfWord - self[index].to_u16! | - (self[index + 1].to_u16! << 8) - end - - private def read_word_slow(index : Int) : Word - self[index].to_u32! | - (self[index + 1].to_u32! << 8) | - (self[index + 2].to_u32! << 16) | - (self[index + 3].to_u32! << 24) - end - - private def write_half_slow(index : Int, value : HalfWord) : Nil - self[index] = value.to_u8! - self[index + 1] = (value >> 8).to_u8! - end - - private def write_word_slow(index : Int, value : Word) : Nil - self[index] = value.to_u8! - self[index + 1] = (value >> 8).to_u8! - self[index + 2] = (value >> 16).to_u8! - self[index + 3] = (value >> 24).to_u8! - end -end diff --git a/src/crab/cartridge.cr b/src/crab/cartridge.cr deleted file mode 100644 index 53f2968..0000000 --- a/src/crab/cartridge.cr +++ /dev/null @@ -1,32 +0,0 @@ -class Cartridge - getter rom : Bytes - - getter title : String { - io = IO::Memory.new - io.write @rom[0x0A0...0x0AC] - io.to_s - } - - @rom = Bytes.new 0x02000000 do |addr| - oob = 0xFFFF & (addr >> 1) - (oob >> (8 * (addr & 1))).to_u8! - end - - def initialize(rom_path : String) - File.open(rom_path) { |file| file.read @rom } - # The following logic accounts for improperly dumped ROMs or bad homebrews. - # All proper ROMs should have a power-of-two size. The handling here is - # really pretty arbitrary. mGBA chooses to fill the entire ROM address - # space with zeros in this case, although gba-suite/unsafe tests that there - # are zeros up to the next power of two. I've chosen to just make that test - # pass, although there's an argument to be made that it's better to match - # mGBA behavior instead. Either way, if a ROM relies on this behavior, it's - # a buggy ROM. This is just an attempt to match the some expected behavior. - size = File.size(rom_path) - if count_set_bits(size) != 1 - last_bit = last_set_bit(size) - next_power = 2 ** (last_bit + 1) - (size...next_power).each { |i| @rom[i] = 0 } - end - end -end diff --git a/src/crab/common/bindings.cr b/src/crab/common/bindings.cr new file mode 100644 index 0000000..7ec6ae1 --- /dev/null +++ b/src/crab/common/bindings.cr @@ -0,0 +1,6 @@ +lib LibSDL + fun queue_audio = SDL_QueueAudio(dev : AudioDeviceID, data : Void*, len : UInt32) : Int + fun get_queued_audio_size = SDL_GetQueuedAudioSize(dev : AudioDeviceID) : UInt32 + fun clear_queued_audio = SDL_ClearQueuedAudio(dev : AudioDeviceID) + fun delay = SDL_Delay(ms : UInt32) : Nil +end diff --git a/src/crab/util.cr b/src/crab/common/util.cr similarity index 69% rename from src/crab/util.cr rename to src/crab/common/util.cr index 22bc7e6..73b7e93 100644 --- a/src/crab/util.cr +++ b/src/crab/common/util.cr @@ -40,6 +40,24 @@ def last_set_bit(n : Int) : Int count end +def array_to_uint8(array : Array(Bool | Int)) : UInt8 + raise "Array needs to have a length of 8" if array.size != 8 + value = 0_u8 + array.each_with_index do |bit, index| + value |= (bit == false || bit == 0 ? 0 : 1) << (7 - index) + end + value +end + +def array_to_uint16(array : Array(Bool | Int)) : UInt16 + raise "Array needs to have a length of 16" if array.size != 16 + value = 0_u16 + array.each_with_index do |bit, index| + value |= (bit == false || bit == 0 ? 0 : 1) << (15 - index) + end + value +end + macro trace(value, newline = true) {% if flag? :trace %} {% if newline %} diff --git a/src/crab/cpu.cr b/src/crab/cpu.cr deleted file mode 100644 index 4655e34..0000000 --- a/src/crab/cpu.cr +++ /dev/null @@ -1,298 +0,0 @@ -require "./arm/*" -require "./thumb/*" -require "./pipeline" - -class CPU - include ARM - include THUMB - - CLOCK_SPEED = 2**24 - - enum Mode : UInt32 - USR = 0b10000 - FIQ = 0b10001 - IRQ = 0b10010 - SVC = 0b10011 - ABT = 0b10111 - UND = 0b11011 - SYS = 0b11111 - - def bank : Int - case self - in Mode::USR, Mode::SYS then 0 - in Mode::FIQ then 1 - in Mode::IRQ then 2 - in Mode::SVC then 3 - in Mode::ABT then 4 - in Mode::UND then 5 - end - end - end - - class PSR < BitField(UInt32) - bool negative - bool zero - bool carry - bool overflow - num reserved, 20 - bool irq_disable - bool fiq_disable - bool thumb - num mode, 5 - end - - getter r = Slice(Word).new 16 - @cpsr : PSR = PSR.new CPU::Mode::SYS.value - @spsr : PSR = PSR.new CPU::Mode::SYS.value - getter pipeline = Pipeline.new - getter lut : Slice(Proc(Word, Nil)) { fill_lut } - getter thumb_lut : Slice(Proc(Word, Nil)) { fill_thumb_lut } - @reg_banks = Array(Array(Word)).new 6 { Array(Word).new 7, 0 } - @spsr_banks = Array(Word).new 6, CPU::Mode::SYS.value # logically independent of typical register banks - property halted = false - - def initialize(@gba : GBA) - @reg_banks[Mode::USR.bank][5] = @r[13] = 0x03007F00 - @reg_banks[Mode::IRQ.bank][5] = 0x03007FA0 - @reg_banks[Mode::SVC.bank][5] = 0x03007FE0 - @r[15] = 0x08000000 - clear_pipeline - end - - def switch_mode(new_mode : Mode, caller = __FILE__) : Nil - old_mode = Mode.from_value @cpsr.mode - return if new_mode == old_mode - new_bank = new_mode.bank - old_bank = old_mode.bank - if new_mode == Mode::FIQ || old_mode == Mode::FIQ - 5.times do |idx| - @reg_banks[old_bank][idx] = @r[8 + idx] - @r[8 + idx] = @reg_banks[new_bank][idx] - end - end - # store old regs - @reg_banks[old_bank][5] = @r[13] - @reg_banks[old_bank][6] = @r[14] - @spsr_banks[old_bank] = @spsr.value - # load new regs - @r[13] = @reg_banks[new_bank][5] - @r[14] = @reg_banks[new_bank][6] - @spsr.value = @cpsr.value - @cpsr.mode = new_mode.value - end - - def irq : Nil - unless @cpsr.irq_disable - lr = @r[15] - (@cpsr.thumb ? 0 : 4) - switch_mode CPU::Mode::IRQ - @cpsr.thumb = false - @cpsr.irq_disable = true - set_reg(14, lr) - set_reg(15, 0x18) - end - end - - def fill_pipeline : Nil - if @cpsr.thumb - pc = @r[15] & ~1 - @pipeline.push @gba.bus.read_half(@r[15] &- 2).to_u32! if @pipeline.size == 0 - @pipeline.push @gba.bus.read_half(@r[15]).to_u32! if @pipeline.size == 1 - else - pc = @r[15] & ~3 - @pipeline.push @gba.bus.read_word(@r[15] &- 4) if @pipeline.size == 0 - @pipeline.push @gba.bus.read_word(@r[15]) if @pipeline.size == 1 - end - end - - def clear_pipeline : Nil - @pipeline.clear - if @cpsr.thumb - @r[15] &+= 4 - else - @r[15] &+= 8 - end - end - - def read_instr : Word - if @pipeline.size == 0 - if @cpsr.thumb - @r[15] &= ~1 - @gba.bus.read_half(@r[15] &- 4).to_u32! - else - @r[15] &= ~3 - @gba.bus.read_word(@r[15] &- 8) - end - else - @pipeline.shift - end - end - - def tick : Nil - unless @halted - instr = read_instr - {% if flag? :trace %} print_state instr {% end %} - if @cpsr.thumb - thumb_execute instr - else - arm_execute instr - end - @gba.scheduler.tick 1 - else - @gba.scheduler.fast_forward - end - end - - def check_cond(cond : Word) : Bool - case cond - when 0x0 then @cpsr.zero - when 0x1 then !@cpsr.zero - when 0x2 then @cpsr.carry - when 0x3 then !@cpsr.carry - when 0x4 then @cpsr.negative - when 0x5 then !@cpsr.negative - when 0x6 then @cpsr.overflow - when 0x7 then !@cpsr.overflow - when 0x8 then @cpsr.carry && !@cpsr.zero - when 0x9 then !@cpsr.carry || @cpsr.zero - when 0xA then @cpsr.negative == @cpsr.overflow - when 0xB then @cpsr.negative != @cpsr.overflow - when 0xC then !@cpsr.zero && @cpsr.negative == @cpsr.overflow - when 0xD then @cpsr.zero || @cpsr.negative != @cpsr.overflow - when 0xE then true - else raise "Cond 0xF is reserved" - end - end - - def step_arm : Nil - @r[15] &+= 4 - end - - def step_thumb : Nil - @r[15] &+= 2 - end - - @[AlwaysInline] - def set_reg(reg : Int, value : Int) : UInt32 - @r[reg] = value.to_u32! - clear_pipeline if reg == 15 - value.to_u32! - end - - @[AlwaysInline] - def set_neg_and_zero_flags(value : Int) : Nil - @cpsr.negative = bit?(value, 31) - @cpsr.zero = value == 0 - end - - # Logical shift left - def lsl(word : Word, bits : Int, carry_out : Pointer(Bool)) : Word - log "lsl - word:#{hex_str word}, bits:#{bits}" - return word if bits == 0 - carry_out.value = bit?(word, 32 - bits) - word << bits - end - - # Logical shift right - def lsr(word : Word, bits : Int, immediate : Bool, carry_out : Pointer(Bool)) : Word - log "lsr - word:#{hex_str word}, bits:#{bits}" - if bits == 0 - return word unless immediate - bits = 32 - end - carry_out.value = bit?(word, bits - 1) - word >> bits - end - - # Arithmetic shift right - def asr(word : Word, bits : Int, immediate : Bool, carry_out : Pointer(Bool)) : Word - log "asr - word:#{hex_str word}, bits:#{bits}" - if bits == 0 - return word unless immediate - bits = 32 - end - if bits <= 31 - carry_out.value = bit?(word, bits - 1) - word >> bits | (0xFFFFFFFF_u32 &* (word >> 31)) << (32 - bits) - else - # ASR by 32 or more has result filled with and carry out equal to bit 31 of Rm. - carry_out.value = bit?(word, 31) - 0xFFFFFFFF_u32 &* (word >> 31) - end - end - - # Rotate right - def ror(word : Word, bits : Int, immediate : Bool, carry_out : Pointer(Bool)) : Word - log "ror - word:#{hex_str word}, bits:#{bits}" - if bits == 0 # RRX #1 - return word unless immediate - res = (word >> 1) | (@cpsr.carry.to_unsafe << 31) - carry_out.value = bit?(word, 0) - res - else - bits &= 31 # ROR by n where n is greater than 32 will give the same result and carry out as ROR by n-32 - bits = 32 if bits == 0 # ROR by 32 has result equal to Rm, carry out equal to bit 31 of Rm. - carry_out.value = bit?(word, bits - 1) - word >> bits | word << (32 - bits) - end - end - - # Subtract two values - def sub(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word - log "sub - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" - res = operand_1 &- operand_2 - if set_conditions - set_neg_and_zero_flags(res) - @cpsr.carry = operand_1 >= operand_2 - @cpsr.overflow = bit?((operand_1 ^ operand_2) & (operand_1 ^ res), 31) - end - res - end - - # Subtract two values with carry - def sbc(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word - log "sbc - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" - res = operand_1 &- operand_2 &- 1 &+ @cpsr.carry.to_unsafe - if set_conditions - set_neg_and_zero_flags(res) - @cpsr.carry = operand_1 >= operand_2.to_u64 + 1 - @cpsr.carry.to_unsafe - @cpsr.overflow = bit?((operand_1 ^ operand_2) & (operand_1 ^ res), 31) - end - res - end - - # Add two values - def add(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word - log "add - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" - res = operand_1 &+ operand_2 - if set_conditions - set_neg_and_zero_flags(res) - @cpsr.carry = res < operand_1 - @cpsr.overflow = bit?(~(operand_1 ^ operand_2) & (operand_2 ^ res), 31) - end - res - end - - # Add two values with carry - def adc(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word - log "adc - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" - res = operand_1 &+ operand_2 &+ @cpsr.carry.to_unsafe - if set_conditions - set_neg_and_zero_flags(res) - @cpsr.carry = res < operand_1.to_u64 + @cpsr.carry.to_unsafe - @cpsr.overflow = bit?(~(operand_1 ^ operand_2) & (operand_2 ^ res), 31) - end - res - end - - def print_state(instr : Word? = nil) : Nil - @r.each_with_index do |val, reg| - print "#{hex_str reg == 15 ? val - (@cpsr.thumb ? 2 : 4) : val, prefix: false} " - end - instr ||= @pipeline.peek - if @cpsr.thumb - puts "cpsr: #{hex_str @cpsr.value, prefix: false} | #{hex_str instr.to_u16, prefix: false}" - else - puts "cpsr: #{hex_str @cpsr.value, prefix: false} | #{hex_str instr, prefix: false}" - end - end -end diff --git a/src/crab/debugger.cr b/src/crab/debugger.cr deleted file mode 100644 index a187f07..0000000 --- a/src/crab/debugger.cr +++ /dev/null @@ -1,103 +0,0 @@ -class Debugger - getter breakpoints = [] of Word - - def initialize(@gba : GBA) - end - - def break_on(addr : Word) - {% if flag? :debugger %} breakpoints << addr {% end %} - end - - def check_debug : Nil - {% if flag? :debugger %} debug if breakpoints.includes? @gba.cpu.r[15] {% end %} - end - - private def debug : Nil - puts "#{"----- DEBUGGER -----".colorize.mode(:bold)} #{"`help` for list of commands".colorize.mode(:dim)}" - @gba.cpu.print_state - while true - input = gets - case input - when .nil?, "exit", "continue" then break - when "step", "next", "tick" - @gba.cpu.tick - @gba.cpu.print_state - when "bios" then less @gba.bus.bios.hexdump - when "ewram" then less @gba.bus.wram_board.hexdump - when "iwram" then less @gba.bus.wram_chip.hexdump - when "pram" then less @gba.ppu.pram.hexdump - when "vram" then less @gba.ppu.vram.hexdump - when "oam" then less @gba.ppu.oam.hexdump - when "rom" then less @gba.cartridge.rom.hexdump - when "sram" then less @gba.cartridge.sram.hexdump - when "list" then print_breakpoints - when /(b|break) (0x\d+)/ - match = /(b|break) (0x\d+)/.match(input.not_nil!).not_nil! - breakpoints << match[2].to_i(base: 16, prefix: true).to_u32! - print_breakpoints - when "clear" - breakpoints.clear - print_breakpoints - when /clear (0x\d+)/ - match = /clear (0x\d+)/.match(input.not_nil!).not_nil! - breakpoints.delete(match[1].to_i(base: 16, prefix: true)) - print_breakpoints - when /\[(0x\d+)\]$/, /\[(0x\d+)\], word/ - match = /\[(0x\d+)\]/.match(input.not_nil!).not_nil! - puts hex_str @gba.bus.read_word(match[1].to_i(base: 16, prefix: true)) - when /\[(0x\d+)\], half/ - match = /\[(0x\d+)\]/.match(input.not_nil!).not_nil! - puts hex_str @gba.bus.read_half(match[1].to_i(base: 16, prefix: true)) - when /\[(0x\d+)\], byte/ - match = /\[(0x\d+)\]/.match(input.not_nil!).not_nil! - puts hex_str @gba.bus[match[1].to_i(base: 16, prefix: true)] - else - puts "Available commands:" - puts " Resume execution:" - puts " ^D" - puts " exit" - puts " continue" - puts " Stepping:" - puts " step" - puts " next" - puts " tick" - puts " Listing breakpoints:" - puts " list" - puts " Adding breakpoints:" - puts " b 0x08000000" - puts " break 0x08000000" - puts " Removing breakpoints:" - puts " clear" - puts " clear 0x1234" - puts " Memory regions:" - puts " bios" - puts " ewram" - puts " iwram" - puts " pram" - puts " vram" - puts " oam" - puts " rom" - puts " sram" - puts " Reading memory:" - puts " [0x1234]" - puts " [0x1234], word" - puts " [0x1234], half" - puts " [0x1234], byte" - end - puts - end - end - - private def less(string : String) : Nil - file = File.new("/tmp/crab", "w") - file.puts string - system "less /tmp/crab" - file.delete - end - - private def print_breakpoints : Nil - print "Breakpoints: " - breakpoints.sort.each { |b| print "#{hex_str b}, " } - puts - end -end diff --git a/src/crab/display.cr b/src/crab/display.cr deleted file mode 100644 index e48ae51..0000000 --- a/src/crab/display.cr +++ /dev/null @@ -1,120 +0,0 @@ -require "lib_gl" - -class Display - WIDTH = 240 - HEIGHT = 160 - SCALE = 4 - - @microseconds = 0 - @frames = 0 - @last_time = Time.utc - @seconds : Int32 = Time.utc.second - - @blend : Bool = false - - def initialize - @window = SDL::Window.new(window_title(59.7), WIDTH * SCALE, HEIGHT * SCALE, flags: SDL::Window::Flags::OPENGL) - setup_gl - end - - def draw(framebuffer : Slice(UInt16)) : Nil - LibGL.tex_image_2d(LibGL::TEXTURE_2D, 0, LibGL::RGB5, 240, 160, 0, LibGL::RGBA, LibGL::UNSIGNED_SHORT_1_5_5_5_REV, framebuffer) - LibGL.draw_arrays(LibGL::TRIANGLE_STRIP, 0, 4) - LibSDL.gl_swap_window(@window) - update_draw_count - end - - def toggle_blending : Nil - if @blend - LibGL.disable(LibGL::BLEND) - else - LibGL.enable(LibGL::BLEND) - end - @blend = !@blend - end - - private def window_title(fps : Float) : String - "crab - #{fps.round(1)} fps" - end - - private def update_draw_count : Nil - current_time = Time.utc - @microseconds += (current_time - @last_time).microseconds - @last_time = current_time - @frames += 1 - if current_time.second != @seconds - fps = @frames * 1_000_000 / @microseconds - @window.title = window_title(fps) - @microseconds = 0 - @frames = 0 - @seconds = current_time.second - end - end - - private def compile_shader(source : String, type : UInt32) : UInt32 - source_ptr = source.to_unsafe - shader = LibGL.create_shader(type) - LibGL.shader_source(shader, 1, pointerof(source_ptr), nil) - LibGL.compile_shader(shader) - shader_compiled = 0 - LibGL.get_shader_iv(shader, LibGL::COMPILE_STATUS, pointerof(shader_compiled)) - if shader_compiled != LibGL::TRUE - log_length = 0 - LibGL.get_shader_iv(shader, LibGL::INFO_LOG_LENGTH, pointerof(log_length)) - s = " " * log_length - LibGL.get_shader_info_log(shader, log_length, pointerof(log_length), s) if log_length > 0 - abort "Error compiling shader: #{s}" - end - shader - end - - private def setup_gl : Nil - {% if flag?(:darwin) %} - LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_FLAGS, LibSDL::GLcontextFlag::FORWARD_COMPATIBLE_FLAG) - {% end %} - LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_PROFILE_MASK, LibSDL::GLprofile::PROFILE_CORE) - LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_MAJOR_VERSION, 3) - LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_MINOR_VERSION, 3) - - {% unless flag?(:darwin) %} - # todo: proper debug messages for mac - LibGL.enable(LibGL::DEBUG_OUTPUT) - LibGL.enable(LibGL::DEBUG_OUTPUT_SYNCHRONOUS) - LibGL.debug_message_callback(->Display.callback, nil) - {% end %} - - LibSDL.gl_create_context @window - LibSDL.gl_set_swap_interval(0) # disable vsync - shader_program = LibGL.create_program - - puts "OpenGL version: #{String.new(LibGL.get_string(LibGL::VERSION))}" - puts "Shader language version: #{String.new(LibGL.get_string(LibGL::SHADING_LANGUAGE_VERSION))}" - - LibGL.blend_func(LibGL::SRC_ALPHA, LibGL::ONE_MINUS_SRC_ALPHA) - - vert_shader_id = compile_shader(File.read("src/crab/shaders/gba_colors.vert"), LibGL::VERTEX_SHADER) - frag_shader_id = compile_shader(File.read("src/crab/shaders/gba_colors.frag"), LibGL::FRAGMENT_SHADER) - - frame_buffer = 0_u32 - LibGL.gen_textures(1, pointerof(frame_buffer)) - LibGL.active_texture(LibGL::TEXTURE0) - LibGL.bind_texture(LibGL::TEXTURE_2D, frame_buffer) - LibGL.attach_shader(shader_program, vert_shader_id) - LibGL.attach_shader(shader_program, frag_shader_id) - LibGL.link_program(shader_program) - LibGL.validate_program(shader_program) - a = [LibGL::BLUE, LibGL::GREEN, LibGL::RED, LibGL::ONE] # flip the rgba to bgra where a is always 1 - a_ptr = pointerof(a).as(Int32*) - LibGL.tex_parameter_iv(LibGL::TEXTURE_2D, LibGL::TEXTURE_SWIZZLE_RGBA, a_ptr) - LibGL.tex_parameter_i(LibGL::TEXTURE_2D, LibGL::TEXTURE_MIN_FILTER, LibGL::NEAREST) - LibGL.tex_parameter_i(LibGL::TEXTURE_2D, LibGL::TEXTURE_MAG_FILTER, LibGL::NEAREST) - LibGL.use_program(shader_program) - vao = 0_u32 # required even if not used in modern opengl - LibGL.gen_vertex_arrays(1, pointerof(vao)) - LibGL.bind_vertex_array(vao) - end - - protected def self.callback(source : UInt32, type : UInt32, id : UInt32, severity : UInt32, length : Int32, message : Pointer(UInt8), userParam : Pointer(Void)) : Nil - puts "OpenGL debug message: #{String.new message}" - end -end diff --git a/src/crab/dma.cr b/src/crab/dma.cr deleted file mode 100644 index e5ef70f..0000000 --- a/src/crab/dma.cr +++ /dev/null @@ -1,154 +0,0 @@ -class DMA - enum StartTiming - Immediate = 0 - VBlank = 1 - HBlank = 2 - Special = 3 - end - - enum AddressControl - Increment = 0 - Decrement = 1 - Fixed = 2 - IncrementReload = 3 - - def delta : Int - case self - in Increment, IncrementReload then 1 - in Decrement then -1 - in Fixed then 0 - end - end - end - - @interrupt_flags : Array(Proc(Nil)) - - def initialize(@gba : GBA) - @dmasad = Array(UInt32).new 4, 0 - @dmadad = Array(UInt32).new 4, 0 - @dmacnt_l = Array(UInt16).new 4, 0 - @dmacnt_h = Array(Reg::DMACNT).new 4 { Reg::DMACNT.new 0 } - @src = Array(UInt32).new 4, 0 - @dst = Array(UInt32).new 4, 0 - @interrupt_flags = [->{ @gba.interrupts.reg_if.dma0 = true }, ->{ @gba.interrupts.reg_if.dma1 = true }, - ->{ @gba.interrupts.reg_if.dma2 = true }, ->{ @gba.interrupts.reg_if.dma3 = true }] - @src_mask = [0x07FFFFFF, 0x0FFFFFFF, 0x0FFFFFFF, 0x0FFFFFFF] - @dst_mask = [0x07FFFFFF, 0x07FFFFFF, 0x07FFFFFF, 0x0FFFFFFF] - @len_mask = [0x3FFF, 0x3FFF, 0x3FFF, 0xFFFF] - end - - def read_io(io_addr : Int) : UInt8 - return 0_u8 if io_addr >= 0xE0 # todo: OOB read - channel = (io_addr - 0xB0) // 12 - reg = (io_addr - 0xB0) % 12 - case reg - when 0, 1, 2, 3 # dmasad - (@dmasad[channel] >> 8 * reg).to_u8! - when 4, 5, 6, 7 # dmadad - (@dmadad[channel] >> 8 * (reg - 4)).to_u8! - when 8, 9 # dmacnt_l - (@dmacnt_l[channel] >> 8 * (reg - 8)).to_u8! - when 10, 11 # dmacnt_h - (@dmacnt_h[channel].value >> 8 * (reg - 10)).to_u8! - else abort "Unmapped DMA read ~ addr:#{hex_str io_addr.to_u8}" - end - end - - def write_io(io_addr : Int, value : UInt8) : Nil - return if io_addr >= 0xE0 # todo: OOB write - channel = (io_addr - 0xB0) // 12 - reg = (io_addr - 0xB0) % 12 - case reg - when 0, 1, 2, 3 # dmasad - mask = 0xFF_u32 << (8 * reg) - value = value.to_u32 << (8 * reg) - dmasad = @dmasad[channel] - @dmasad[channel] = ((dmasad & ~mask) | value) & @src_mask[channel] - when 4, 5, 6, 7 # dmadad - reg -= 4 - mask = 0xFF_u32 << (8 * reg) - value = value.to_u32 << (8 * reg) - dmadad = @dmadad[channel] - @dmadad[channel] = ((dmadad & ~mask) | value) & @dst_mask[channel] - when 8, 9 # dmacnt_l - reg -= 8 - mask = 0xFF_u32 << (8 * reg) - value = value.to_u16 << (8 * reg) - dmacnt_l = @dmacnt_l[channel] - @dmacnt_l[channel] = ((dmacnt_l & ~mask) | value) & @len_mask[channel] - when 10, 11 # dmacnt_h - reg -= 10 - mask = 0xFF_u32 << (8 * reg) - value = value.to_u16 << (8 * reg) - dmacnt_h = @dmacnt_h[channel] - enabled = dmacnt_h.enable - dmacnt_h.value = (dmacnt_h.value & ~mask) | value - if dmacnt_h.enable && !enabled - @src[channel], @dst[channel] = @dmasad[channel], @dmadad[channel] - trigger channel if dmacnt_h.start_timing == StartTiming::Immediate.value - end - else abort "Unmapped DMA write ~ addr:#{hex_str io_addr.to_u8}, val:#{value}".colorize(:yellow) - end - end - - def trigger_hdma : Nil - 4.times do |channel| - dmacnt_h = @dmacnt_h[channel] - trigger channel if dmacnt_h.enable && dmacnt_h.start_timing == StartTiming::HBlank.value - end - end - - def trigger_vdma : Nil - 4.times do |channel| - dmacnt_h = @dmacnt_h[channel] - trigger channel if dmacnt_h.enable && dmacnt_h.start_timing == StartTiming::VBlank.value - end - end - - # todo: maybe abstract these various triggers - def trigger_fifo(fifo_channel : Int) : Nil - dmacnt_h = @dmacnt_h[fifo_channel + 1] - trigger fifo_channel + 1 if dmacnt_h.enable && dmacnt_h.start_timing == StartTiming::Special.value - end - - def trigger(channel : Int) : Nil - dmacnt_h = @dmacnt_h[channel] - - start_timing = StartTiming.from_value(dmacnt_h.start_timing) - source_control = AddressControl.from_value(dmacnt_h.source_control) - dest_control = AddressControl.from_value(dmacnt_h.dest_control) - word_size = 2 << dmacnt_h.type # 2 or 4 bytes - - len = @dmacnt_l[channel] - - puts "Prohibited source address control".colorize.fore(:yellow) if source_control == AddressControl::IncrementReload - - if start_timing == StartTiming::Special - if channel == 1 || channel == 2 # fifo - len = 4 - word_size = 4 - dest_control = AddressControl::Fixed - elsif channel == 3 # video capture - puts "todo: video capture dma" - else # prohibited - puts "Prohibited special dma".colorize.fore(:yellow) - end - end - - delta_source = word_size * source_control.delta - delta_dest = word_size * dest_control.delta - - len.times do |idx| - @gba.bus[@dst[channel]] = word_size == 4 ? @gba.bus.read_word(@src[channel]) : @gba.bus.read_half(@src[channel]) - @src[channel] &+= delta_source - @dst[channel] &+= delta_dest - end - - @dst[channel] = @dmadad[channel] if dest_control == AddressControl::IncrementReload - dmacnt_h.enable = false unless dmacnt_h.repeat && start_timing != StartTiming::Immediate - if dmacnt_h.irq_enable - @interrupt_flags[channel].call - @gba.interrupts.schedule_interrupt_check - end - end -end diff --git a/src/crab/gb/apu.cr b/src/crab/gb/apu.cr new file mode 100644 index 0000000..f493b28 --- /dev/null +++ b/src/crab/gb/apu.cr @@ -0,0 +1,179 @@ +require "./audio/abstract_channels" # so that channels don't need to all import +require "./audio/*" + +module GB + class APU + CHANNELS = 2 # Left / Right + BUFFER_SIZE = 1024 + SAMPLE_RATE = 65536 # Hz + SAMPLE_PERIOD = CPU::CLOCK_SPEED // SAMPLE_RATE + + FRAME_SEQUENCER_RATE = 512 # Hz + FRAME_SEQUENCER_PERIOD = CPU::CLOCK_SPEED // FRAME_SEQUENCER_RATE + + @sound_enabled : Bool = false + + @buffer = Slice(Float32).new BUFFER_SIZE + @buffer_pos = 0 + + @frame_sequencer_stage = 0 + getter first_half_of_length_period = false + + @left_enable = false + @left_volume = 0_u8 + @right_enable = false + @right_volume = 0_u8 + + @nr51 : UInt8 = 0x00 + + @audiospec : LibSDL::AudioSpec + @obtained_spec : LibSDL::AudioSpec + + setter sync : Bool + + def initialize(@gb : GB, headless : Bool, @sync : Bool) + @sync = false if headless + + @audiospec = LibSDL::AudioSpec.new + @audiospec.freq = SAMPLE_RATE + @audiospec.format = LibSDL::AUDIO_F32SYS + @audiospec.channels = CHANNELS + @audiospec.samples = BUFFER_SIZE + @audiospec.callback = nil + @audiospec.userdata = nil + + @obtained_spec = LibSDL::AudioSpec.new + + @channel1 = Channel1.new @gb + @channel2 = Channel2.new @gb + @channel3 = Channel3.new @gb + @channel4 = Channel4.new @gb + + tick_frame_sequencer + get_sample + + raise "Failed to open audio" if LibSDL.open_audio(pointerof(@audiospec), pointerof(@obtained_spec)) > 0 + + LibSDL.pause_audio 0 unless headless + end + + def tick_frame_sequencer : Nil + @first_half_of_length_period = @frame_sequencer_stage & 1 == 0 + case @frame_sequencer_stage + when 0 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + when 1 then nil + when 2 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + @channel1.sweep_step + when 3 then nil + when 4 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + when 5 then nil + when 6 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + @channel1.sweep_step + when 7 + @channel1.volume_step + @channel2.volume_step + @channel4.volume_step + else nil + end + @frame_sequencer_stage = 0 if (@frame_sequencer_stage += 1) > 7 + @gb.scheduler.schedule FRAME_SEQUENCER_PERIOD, Scheduler::EventType::APU, ->tick_frame_sequencer + end + + def get_sample : Nil + channel1_amp = @channel1.get_amplitude + channel2_amp = @channel2.get_amplitude + channel3_amp = @channel3.get_amplitude + channel4_amp = @channel4.get_amplitude + @buffer[@buffer_pos] = (@left_volume / 7).to_f32 * + ((@nr51 & 0x80 > 0 ? channel4_amp : 0) + + (@nr51 & 0x40 > 0 ? channel3_amp : 0) + + (@nr51 & 0x20 > 0 ? channel2_amp : 0) + + (@nr51 & 0x10 > 0 ? channel1_amp : 0)) / 4 + @buffer[@buffer_pos + 1] = (@right_volume / 7).to_f32 * + ((@nr51 & 0x08 > 0 ? channel4_amp : 0) + + (@nr51 & 0x04 > 0 ? channel3_amp : 0) + + (@nr51 & 0x02 > 0 ? channel2_amp : 0) + + (@nr51 & 0x01 > 0 ? channel1_amp : 0)) / 4 + @buffer_pos += 2 + + # push to SDL if buffer is full + if @buffer_pos >= BUFFER_SIZE + LibSDL.clear_queued_audio 1 unless @sync + while LibSDL.get_queued_audio_size(1) > BUFFER_SIZE * sizeof(Float32) * 2 + LibSDL.delay(1) + end + LibSDL.queue_audio 1, @buffer, BUFFER_SIZE * sizeof(Float32) + @buffer_pos = 0 + end + + @gb.scheduler.schedule SAMPLE_PERIOD, Scheduler::EventType::APU, ->get_sample + end + + # read from apu memory + def [](index : Int) : UInt8 + case index + when @channel1 then @channel1[index] + when @channel2 then @channel2[index] + when @channel3 then @channel3[index] + when @channel4 then @channel4[index] + when 0xFF24 + ((@left_enable ? 0b10000000 : 0) | (@left_volume << 4) | + (@right_enable ? 0b00001000 : 0) | @right_volume).to_u8 + when 0xFF25 then @nr51 + when 0xFF26 + 0x70 | + (@sound_enabled ? 0x80 : 0) | + (@channel4.enabled ? 0b1000 : 0) | + (@channel3.enabled ? 0b0100 : 0) | + (@channel2.enabled ? 0b0010 : 0) | + (@channel1.enabled ? 0b0001 : 0) + else 0xFF + end.to_u8 + end + + # write to apu memory + def []=(index : Int, value : UInt8) : Nil + return unless @sound_enabled || index == 0xFF26 || Channel3::WAVE_RAM_RANGE.includes?(index) + case index + when @channel1 then @channel1[index] = value + when @channel2 then @channel2[index] = value + when @channel3 then @channel3[index] = value + when @channel4 then @channel4[index] = value + when 0xFF24 + @left_enable = value & 0b10000000 > 0 + @left_volume = (value & 0b01110000) >> 4 + @right_enable = value & 0b00001000 > 0 + @right_volume = value & 0b00000111 + when 0xFF25 then @nr51 = value + when 0xFF26 + if value & 0x80 == 0 && @sound_enabled + (0xFF10..0xFF25).each { |addr| self[addr] = 0x00 } + @sound_enabled = false + elsif value & 0x80 > 0 && !@sound_enabled + @sound_enabled = true + @frame_sequencer_stage = 0 + @channel1.length_counter = 0 + @channel2.length_counter = 0 + @channel3.length_counter = 0 + @channel4.length_counter = 0 + end + end + end + end +end diff --git a/src/crab/gb/audio/abstract_channels.cr b/src/crab/gb/audio/abstract_channels.cr new file mode 100644 index 0000000..1cd8a96 --- /dev/null +++ b/src/crab/gb/audio/abstract_channels.cr @@ -0,0 +1,100 @@ +# All of the channels were developed using the following guide on gbdev +# https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware + +module GB + abstract class SoundChannel + property enabled : Bool = false + @dac_enabled : Bool = false + + # NRx1 + property length_counter = 0 + + # NRx4 + @length_enable : Bool = false + + def initialize(@gb : GB) + end + + # Step the channel, calling helpers to reload the period and step the wave generation + def step : Nil + step_wave_generation + schedule_reload frequency_timer + end + + # Step the length, disabling the channel if the length counter expires + def length_step : Nil + if @length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + end + + # Used so that channels can be matched with case..when statements + abstract def ===(value) + + # Calculate the frequency timer + abstract def frequency_timer : UInt32 + + abstract def schedule_reload(frequency_timer : UInt32) : Nil + + # Called when @period reaches 0 + abstract def step_wave_generation : Nil + + abstract def get_amplitude : Float32 + + abstract def [](index : Int) : UInt8 + abstract def []=(index : Int, value : UInt8) : Nil + end + + abstract class VolumeEnvelopeChannel < SoundChannel + # NRx2 + @starting_volume : UInt8 = 0x00 + @envelope_add_mode : Bool = false + @period : UInt8 = 0x00 + + @volume_envelope_timer : UInt8 = 0x00 + @current_volume : UInt8 = 0x00 + + @volume_envelope_is_updating = false + + def volume_step : Nil + if @period != 0 + @volume_envelope_timer -= 1 if @volume_envelope_timer > 0 + if @volume_envelope_timer == 0 + @volume_envelope_timer = @period + if (@current_volume < 0xF && @envelope_add_mode) || (@current_volume > 0 && !@envelope_add_mode) + @current_volume += (@envelope_add_mode ? 1 : -1) + else + @volume_envelope_is_updating = false + end + end + end + end + + def init_volume_envelope : Nil + @volume_envelope_timer = @period + @current_volume = @starting_volume + @volume_envelope_is_updating = true + end + + def read_NRx2 : UInt8 + @starting_volume << 4 | (@envelope_add_mode ? 0x08 : 0) | @period + end + + def write_NRx2(value : UInt8) : Nil + envelope_add_mode = value & 0x08 > 0 + if @enabled # Zombie mode glitch + @current_volume += 1 if (@period == 0 && @volume_envelope_is_updating) || !@envelope_add_mode + @current_volume = 0x10_u8 - @current_volume if (envelope_add_mode != @envelope_add_mode) + @current_volume &= 0x0F + end + + @starting_volume = value >> 4 + @envelope_add_mode = envelope_add_mode + @period = value & 0x07 + # Internal values + @dac_enabled = value & 0xF8 > 0 + @enabled = false if !@dac_enabled + end + end +end diff --git a/src/crab/gb/audio/channel1.cr b/src/crab/gb/audio/channel1.cr new file mode 100644 index 0000000..26c2761 --- /dev/null +++ b/src/crab/gb/audio/channel1.cr @@ -0,0 +1,150 @@ +module GB + class Channel1 < VolumeEnvelopeChannel + WAVE_DUTY = [ + [0, 0, 0, 0, 0, 0, 0, 1], # 12.5% + [1, 0, 0, 0, 0, 0, 0, 1], # 25% + [1, 0, 0, 0, 0, 1, 1, 1], # 50% + [0, 1, 1, 1, 1, 1, 1, 0], # 75% + ] + + RANGE = 0xFF10..0xFF14 + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + @wave_duty_position = 0 + + # NR10 + @sweep_period : UInt8 = 0x00 + @negate : Bool = false + @shift : UInt8 = 0x00 + + @sweep_timer : UInt8 = 0x00 + @frequency_shadow : UInt16 = 0x0000 + @sweep_enabled : Bool = false + @negate_has_been_used : Bool = false + + # NR11 + @duty : UInt8 = 0x00 + @length_load : UInt8 = 0x00 + + # NR13 / NR14 + @frequency : UInt16 = 0x00 + + def step_wave_generation : Nil + @wave_duty_position = (@wave_duty_position + 1) & 7 + end + + def frequency_timer : UInt32 + (0x800_u32 - @frequency) * 4 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gb.scheduler.schedule frequency_timer, Scheduler::EventType::APUChannel1, ->step + end + + def sweep_step : Nil + @sweep_timer -= 1 if @sweep_timer > 0 + if @sweep_timer == 0 + @sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8 + if @sweep_enabled && @sweep_period > 0 + calculated = frequency_calculation + if calculated <= 0x07FF && @shift > 0 + @frequency_shadow = @frequency = calculated + frequency_calculation + end + end + end + end + + def get_amplitude : Float32 + if @enabled && @dac_enabled + dac_input = WAVE_DUTY[@duty][@wave_duty_position] * @current_volume + dac_output = (dac_input / 7.5) - 1 + dac_output + else + 0 + end.to_f32 + end + + # Calculate the new shadow frequency, disable channel if overflow 11 bits + # https://gist.github.com/drhelius/3652407#file-game-boy-sound-operation-L243-L250 + def frequency_calculation : UInt16 + calculated = @frequency_shadow >> @shift + calculated = @frequency_shadow + (@negate ? -1 : 1) * calculated + @negate_has_been_used = true if @negate + @enabled = false if calculated > 0x07FF + calculated + end + + def [](index : Int) : UInt8 + case index + when 0xFF10 then 0x80 | @sweep_period << 4 | (@negate ? 0x08 : 0) | @shift + when 0xFF11 then 0x3F | @duty << 6 + when 0xFF12 then read_NRx2 + when 0xFF13 then 0xFF # write-only + when 0xFF14 then 0xBF | (@length_enable ? 0x40 : 0) + else raise "Reading from invalid Channel1 register: #{hex_str index.to_u16}" + end.to_u8 + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0xFF10 + @sweep_period = (value & 0x70) >> 4 + @negate = value & 0x08 > 0 + @shift = value & 0x07 + # Internal values + @enabled = false if !@negate && @negate_has_been_used + when 0xFF11 + @duty = (value & 0xC0) >> 6 + @length_load = value & 0x3F + # Internal values + @length_counter = 0x40 - @length_load + when 0xFF12 + write_NRx2 value + when 0xFF13 + @frequency = (@frequency & 0x0700) | value + when 0xFF14 + @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gb.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + log "triggered" + log " NR10: sweep_period:#{@sweep_period}, negate:#{@negate}, shift:#{@shift}" + log " NR11: duty:#{@duty}, length_load:#{@length_load}" + log " NR12: starting_volume:#{@starting_volume}, envelope_add_mode:#{@envelope_add_mode}, period:#{@period}" + log " NR13/NR14: frequency:#{@frequency}, length_enable:#{@length_enable}" + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x40 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gb.apu.first_half_of_length_period + end + # Init frequency + @gb.scheduler.clear Scheduler::EventType::APUChannel1 + schedule_reload frequency_timer + # Init volume envelope + init_volume_envelope + # Init sweep + @frequency_shadow = @frequency + @sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8 + @sweep_enabled = @sweep_period > 0 || @shift > 0 + @negate_has_been_used = false + if @shift > 0 # If sweep shift is non-zero, frequency calculation and overflow check are performed immediately + frequency_calculation + end + end + else raise "Writing to invalid Channel1 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gb/audio/channel2.cr b/src/crab/gb/audio/channel2.cr new file mode 100644 index 0000000..9cd4e82 --- /dev/null +++ b/src/crab/gb/audio/channel2.cr @@ -0,0 +1,100 @@ +module GB + class Channel2 < VolumeEnvelopeChannel + WAVE_DUTY = [ + [0, 0, 0, 0, 0, 0, 0, 1], # 12.5% + [1, 0, 0, 0, 0, 0, 0, 1], # 25% + [1, 0, 0, 0, 0, 1, 1, 1], # 50% + [0, 1, 1, 1, 1, 1, 1, 0], # 75% + ] + + RANGE = 0xFF16..0xFF19 + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + @wave_duty_position = 0 + + # NR21 + @duty : UInt8 = 0x00 + @length_load : UInt8 = 0x00 + + # NR23 / NR24 + @frequency : UInt16 = 0x00 + + def step_wave_generation : Nil + @wave_duty_position = (@wave_duty_position + 1) & 7 + end + + def frequency_timer : UInt32 + (0x800_u32 - @frequency) * 4 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gb.scheduler.schedule frequency_timer, Scheduler::EventType::APUChannel2, ->step + end + + def get_amplitude : Float32 + if @enabled && @dac_enabled + dac_input = WAVE_DUTY[@duty][@wave_duty_position] * @current_volume + dac_output = (dac_input / 7.5) - 1 + dac_output + else + 0 + end.to_f32 + end + + def [](index : Int) : UInt8 + case index + when 0xFF16 then 0x3F | @duty << 6 + when 0xFF17 then read_NRx2 + when 0xFF18 then 0xFF # write-only + when 0xFF19 then 0xBF | (@length_enable ? 0x40 : 0) + else raise "Reading from invalid Channel2 register: #{hex_str index.to_u16}" + end.to_u8 + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0xFF16 + @duty = (value & 0xC0) >> 6 + @length_load = value & 0x3F + # Internal values + @length_counter = 0x40 - @length_load + when 0xFF17 + write_NRx2 value + when 0xFF18 + @frequency = (@frequency & 0x0700) | value + when 0xFF19 + @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gb.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + log "triggered" + log " NR16: duty:#{@duty}, length_load:#{@length_load}" + log " NR17: starting_volume:#{@starting_volume}, envelope_add_mode:#{@envelope_add_mode}, period:#{@period}" + log " NR18/NR19: frequency:#{@frequency}, length_enable:#{@length_enable}" + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x40 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gb.apu.first_half_of_length_period + end + # Init frequency + @gb.scheduler.clear Scheduler::EventType::APUChannel2 + schedule_reload frequency_timer + # Init volume envelope + init_volume_envelope + end + else raise "Writing to invalid Channel2 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gb/audio/channel3.cr b/src/crab/gb/audio/channel3.cr new file mode 100644 index 0000000..e7ec883 --- /dev/null +++ b/src/crab/gb/audio/channel3.cr @@ -0,0 +1,130 @@ +module GB + class Channel3 < SoundChannel + RANGE = 0xFF1A..0xFF1E + WAVE_RAM_RANGE = 0xFF30..0xFF3F + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) || WAVE_RAM_RANGE.includes?(value) + end + + @wave_ram = Bytes.new(WAVE_RAM_RANGE.size) { |idx| idx & 1 == 0 ? 0x00_u8 : 0xFF_u8 } + @wave_ram_position : UInt8 = 0 + @wave_ram_sample_buffer : UInt8 = 0x00 + + # NR31 + @length_load : UInt8 = 0x00 + + # NR32 + @volume_code : UInt8 = 0x00 + + @volume_code_shift : UInt8 = 0 + + # NR33 / NR34 + @frequency : UInt16 = 0x00 + + def step_wave_generation : Nil + @wave_ram_position = (@wave_ram_position + 1) % (WAVE_RAM_RANGE.size * 2) + @wave_ram_sample_buffer = @wave_ram[@wave_ram_position // 2] + end + + def frequency_timer : UInt32 + (0x800_u32 - @frequency) * 2 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gb.scheduler.schedule frequency_timer, Scheduler::EventType::APUChannel3, ->step + end + + def get_amplitude : Float32 + if @enabled && @dac_enabled + dac_input = ((@wave_ram_sample_buffer >> (@wave_ram_position & 1 == 0 ? 4 : 0)) & 0x0F) >> @volume_code_shift + dac_output = (dac_input / 7.5) - 1 + dac_output + else + 0 + end.to_f32 + end + + def [](index : Int) : UInt8 + case index + when 0xFF1A then 0x7F | (@dac_enabled ? 0x80 : 0) + when 0xFF1B then 0xFF + when 0xFF1C then 0x9F | @volume_code << 5 + when 0xFF1D then 0xFF + when 0xFF1E then 0xBF | (@length_enable ? 0x40 : 0) + when WAVE_RAM_RANGE + if @enabled + @wave_ram[@wave_ram_position // 2] + else + @wave_ram[index - WAVE_RAM_RANGE.begin] + end + else raise "Reading from invalid Channel3 register: #{hex_str index.to_u16}" + end.to_u8 + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0xFF1A + @dac_enabled = value & 0x80 > 0 + @enabled = false if !@dac_enabled + when 0xFF1B + @length_load = value + # Internal values + @length_counter = 0x100 - @length_load + when 0xFF1C + @volume_code = (value & 0x60) >> 5 + # Internal values + @volume_code_shift = case @volume_code + when 0b00 then 4 + when 0b01 then 0 + when 0b10 then 1 + when 0b11 then 2 + else raise "Impossible volume code #{@volume_code}" + end.to_u8 + when 0xFF1D + @frequency = (@frequency & 0x0700) | value + when 0xFF1E + @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gb.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + log "triggered" + log " NR30: dac_enabled:#{@dac_enabled}" + log " NR31: length_load:#{@length_load}" + log " NR32: volume_code:#{@volume_code}" + log " NR33/NR34: frequency:#{@frequency}, length_enable:#{@length_enable}" + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x100 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gb.apu.first_half_of_length_period + end + # Init frequency + # todo: I'm patching in an extra 6 T-cycles here with the `+ 6`. This is specifically + # to get blargg's "09-wave read while on.s" to pass. I'm _not_ refilling the + # frequency timer with this extra cycles when it reaches 0. For now, I'm letting + # this be in order to work on other audio behavior. Note that this is pretty + # brittle in it's current state though... + @gb.scheduler.clear Scheduler::EventType::APUChannel3 + schedule_reload frequency_timer + 6 + # Init wave ram position + @wave_ram_position = 0 + end + when WAVE_RAM_RANGE + if @enabled + @wave_ram[@wave_ram_position // 2] = value + else + @wave_ram[index - WAVE_RAM_RANGE.begin] = value + end + else raise "Writing to invalid Channel3 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gb/audio/channel4.cr b/src/crab/gb/audio/channel4.cr new file mode 100644 index 0000000..6d2e193 --- /dev/null +++ b/src/crab/gb/audio/channel4.cr @@ -0,0 +1,103 @@ +module GB + class Channel4 < VolumeEnvelopeChannel + RANGE = 0xFF20..0xFF23 + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + @lfsr : UInt16 = 0x0000 + + # NR41 + @length_load : UInt8 = 0x00 + + # NR43 + @clock_shift : UInt8 = 0x00 + @width_mode : UInt8 = 0x00 + @divisor_code : UInt8 = 0x00 + + def step_wave_generation : Nil + new_bit = (@lfsr & 0b01) ^ ((@lfsr & 0b10) >> 1) + @lfsr >>= 1 + @lfsr |= new_bit << 14 + if @width_mode != 0 + @lfsr &= ~(1 << 6) + @lfsr |= new_bit << 6 + end + end + + def frequency_timer : UInt32 + (@divisor_code == 0 ? 8_u32 : @divisor_code.to_u32 << 4) << @clock_shift + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gb.scheduler.schedule frequency_timer, Scheduler::EventType::APUChannel4, ->step + end + + def get_amplitude : Float32 + if @enabled && @dac_enabled + dac_input = (~@lfsr & 1) * @current_volume + dac_output = (dac_input / 7.5) - 1 + dac_output + else + 0 + end.to_f32 + end + + def [](index : Int) : UInt8 + case index + when 0xFF20 then 0xFF + when 0xFF21 then read_NRx2 + when 0xFF22 then @clock_shift << 4 | @width_mode << 3 | @divisor_code + when 0xFF23 then 0xBF | (@length_enable ? 0x40 : 0) + else raise "Reading from invalid Channel4 register: #{hex_str index.to_u16}" + end.to_u8 + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0xFF20 + @length_load = value & 0x3F + # Internal values + @length_counter = 0x40 - @length_load + when 0xFF21 + write_NRx2 value + when 0xFF22 + @clock_shift = value >> 4 + @width_mode = (value & 0x08) >> 3 + @divisor_code = value & 0x07 + when 0xFF23 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gb.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + log "triggered" + log " NR41: length_load:#{@length_load}" + log " NR42: starting_volume:#{@starting_volume}, envelope_add_mode:#{@envelope_add_mode}, period:#{@period}" + log " NR43: clock_shift:#{@clock_shift}, width_mode:#{@width_mode}, divisor_code:#{@divisor_code}" + log " NR44: length_enable:#{@length_enable}" + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x40 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gb.apu.first_half_of_length_period + end + # Init frequency + @gb.scheduler.clear Scheduler::EventType::APUChannel4 + schedule_reload frequency_timer + # Init volume envelope + init_volume_envelope + # Init lfsr + @lfsr = 0x7FFF + end + else raise "Writing to invalid Channel4 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gb/cartridge.cr b/src/crab/gb/cartridge.cr new file mode 100644 index 0000000..a1068bc --- /dev/null +++ b/src/crab/gb/cartridge.cr @@ -0,0 +1,110 @@ +module GB + abstract class Cartridge + enum CGB + EXCLUSIVE + SUPPORT + NONE + end + + @rom : Bytes = Bytes.new 0 + @ram : Bytes = Bytes.new 0 + property sav_file_path : String = "" + + getter title : String { + io = IO::Memory.new + io.write @rom[0x0134...0x13F] + io.to_s.gsub(/[^[:print:]]/i, "").strip + } + + getter rom_size : UInt32 { + 0x8000_u32 << @rom[0x0148] + } + + getter ram_size : Int32 { + case @rom[0x0149] + when 0x01 then 0x0800 + when 0x02 then 0x2000 + when 0x03 then 0x2000 << 2 + when 0x04 then 0x2000 << 4 + when 0x05 then 0x2000 << 3 + else 0x0000 + end + } + + getter cgb : CGB { + case @rom[0x0143] + when 0x80 then CGB::SUPPORT + when 0xC0 then CGB::EXCLUSIVE + else CGB::NONE + end + } + + # open rom, determine MBC type, and initialize the correct cartridge + def self.new(rom_path : String) : Cartridge + rom = File.open rom_path do |file| + rom_size = file.read_at(0x0148, 1) { |io| 0x8000 << io.read_byte.not_nil! } + file.pos = 0 + bytes = Bytes.new rom_size.not_nil! + file.read bytes + bytes + end + + cartridge_type = rom[0x0147] + cartridge = case cartridge_type + when 0x00, 0x08, 0x09 then ROM.new rom + when 0x01, 0x02, 0x03 then MBC1.new rom + when 0x05, 0x06 then MBC2.new rom + when 0x0F, 0x10, 0x11, + 0x12, 0x13 then MBC3.new rom + when 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E then MBC5.new rom + else raise "Unimplemented cartridge type: #{hex_str cartridge_type}" + end + + cartridge.sav_file_path = rom_path.rpartition('.')[0] + ".sav" + cartridge.load_game if File.exists?(cartridge.sav_file_path) + cartridge + end + + # create a new Cartridge with the given bytes as rom + def self.new(rom : Bytes) : Cartridge + ROM.new rom + end + + # save the game to a .sav file + def save_game : Nil + File.write(@sav_file_path.not_nil!, @ram) + end + + # load the game from a .sav file + def load_game : Nil + File.open @sav_file_path do |file| + file.read @ram + end + end + + # the offset of the given bank number in rom + def rom_bank_offset(bank_number : Int) : Int + (bank_number.to_u32 * Memory::ROM_BANK_N.size) % rom_size + end + + # adjust the index for local rom + def rom_offset(index : Int) : Int + index - Memory::ROM_BANK_N.begin + end + + # the offset of the given bank number in ram + def ram_bank_offset(bank_number : Int) : Int + (bank_number.to_u32 * Memory::EXTERNAL_RAM.size) % ram_size + end + + # adjust the index for local ram + def ram_offset(index : Int) : Int + index - Memory::EXTERNAL_RAM.begin + end + + # read from cartridge memory + abstract def [](index : Int) : UInt8 + abstract def []=(index : Int, value : UInt8) : Nil + end +end diff --git a/src/crab/gb/cpu.cr b/src/crab/gb/cpu.cr new file mode 100644 index 0000000..517a8df --- /dev/null +++ b/src/crab/gb/cpu.cr @@ -0,0 +1,173 @@ +module GB + class CPU + CLOCK_SPEED = 4194304 + + macro register(upper, lower, mask = nil) + @{{upper.id}} : UInt8 = 0_u8 + @{{lower.id}} : UInt8 = 0_u8 + + def {{upper.id}} : UInt8 + @{{upper.id}} {% if mask %} & ({{mask.id}} >> 8) {% end %} + end + + def {{upper.id}}=(value : UInt8) : UInt8 + @{{upper.id}} = value {% if mask %} & ({{mask.id}} >> 8) {% end %} + end + + def {{lower.id}} : UInt8 + @{{lower.id}} {% if mask %} & {{mask.id}} {% end %} + end + + def {{lower.id}}=(value : UInt8) : UInt8 + @{{lower.id}} = value {% if mask %} & {{mask.id}} {% end %} + end + + def {{upper.id}}{{lower.id}} : UInt16 + (self.{{upper}}.to_u16 << 8 | self.{{lower}}.to_u16).not_nil! + end + + def {{upper.id}}{{lower.id}}=(value : UInt16) : UInt16 + self.{{upper.id}} = (value >> 8).to_u8 + self.{{lower.id}} = (value & 0xFF).to_u8 + self.{{upper.id}}{{lower.id}} + end + + def {{upper.id}}{{lower.id}}=(value : UInt8) : UInt16 + self.{{upper.id}} = 0_u8 + self.{{lower.id}} = value + self.{{upper.id}}{{lower.id}} + end + end + + macro flag(name, mask) + def f_{{name.id}}=(on : Int | Bool) + if on == false || on == 0 + self.f &= ~{{mask}} + else + self.f |= {{mask.id}} + end + end + + def f_{{name.id}} : Bool + self.f & {{mask.id}} == {{mask.id}} + end + + def f_n{{name.id}} : Bool + !f_{{name.id}} + end + end + + register a, f, mask: 0xFFF0 + register b, c + register d, e + register h, l + + flag z, 0b10000000 + flag n, 0b01000000 + flag h, 0b00100000 + flag c, 0b00010000 + + property pc : UInt16 = 0x0000 + property sp : UInt16 = 0x0000 + property memory : Memory + property scheduler : Scheduler + property ime : Bool = false + @halted : Bool = false + @halt_bug : Bool = false + + # hl reads are cached for each instruction + # this is tracked here to reduce complications in the codegen + @cached_hl_read : UInt8? = nil + + def initialize(@gb : GB) + @memory = gb.memory + @scheduler = gb.scheduler + end + + def skip_boot : Nil + @pc = 0x0100_u16 + @sp = 0xFFFE_u16 + self.af = 0x1180_u16 + self.bc = 0x0000_u16 + if @gb.cgb_ptr.value + self.de = 0xFF56_u16 + self.hl = 0x000D_u16 + else + self.de = 0x0008_u16 + self.hl = 0x007C_u16 + end + end + + # service all interrupts + def handle_interrupts + if @gb.interrupts.interrupt_ready? + @halted = false + if @ime + @ime = false + @sp &-= 1 + @memory[@sp] = (@pc >> 8).to_u8 + interrupt = @gb.interrupts.highest_priority + @sp &-= 1 + @memory[@sp] = @pc.to_u8! + @pc = interrupt.value + @gb.interrupts.clear interrupt + @memory.tick_extra 20 + end + end + end + + def memory_at_hl : UInt8 + @cached_hl_read ||= @memory[self.hl] + @cached_hl_read.not_nil! + end + + def memory_at_hl=(val : UInt8) : Nil + @cached_hl_read = val + @memory[self.hl] = val + end + + def print_state(op : String? = nil) : Nil + puts "AF:#{hex_str self.af} BC:#{hex_str self.bc} DE:#{hex_str self.de} HL:#{hex_str self.hl} | SP:#{hex_str @sp} | PC:#{hex_str @pc} | OP:#{hex_str @memory.read_byte @pc} | IME:#{@ime}#{" | #{op}" if op}" + end + + # Handle regular and obscure halting behavior + def halt : Nil + if !@ime && @gb.interrupts.interrupt_ready? + @halt_bug = true + @halted = false + else + @halted = true + end + end + + # Increment PC unless the halt bug should cause it to fail to increment + def inc_pc : Nil + if @halt_bug + @halt_bug = false + else + @pc &+= 1 + end + end + + # Runs for the specified number of machine cycles. If no argument provided, + # runs only one instruction. Handles interrupts _after_ the instruction is + # executed. + def tick : Nil + if @halted + cycles_taken = 4 + else + opcode = @memory[@pc] + {% if flag? :graphics_test %} + if opcode == 0x40 + @gb.ppu.write_png + exit 0 + end + {% end %} + cycles_taken = Opcodes::UNPREFIXED[opcode].call self + end + @cached_hl_read = nil # clear hl read cache + @memory.tick_extra cycles_taken # tell memory component to tick extra cycles + handle_interrupts + end + end +end diff --git a/src/crab/gb/display.cr b/src/crab/gb/display.cr new file mode 100644 index 0000000..d0483fb --- /dev/null +++ b/src/crab/gb/display.cr @@ -0,0 +1,85 @@ +require "stumpy_png" + +module GB + class Display + WIDTH = 160 + HEIGHT = 144 + + PIXELFORMAT_RGB24 = (1 << 28) | (7 << 24) | (1 << 20) | (0 << 16) | (24 << 8) | (3 << 0) + TEXTUREACCESS_STREAMING = 1 + + @window : SDL::Window + @renderer : SDL::Renderer + @texture : Pointer(LibSDL::Texture) + + @title : String + + @fps = 30 + @seconds : Int32 = Time.utc.second + + def initialize(gb : GB, headless : Bool) + @title = gb.cartridge.title + flags = headless ? SDL::Window::Flags::HIDDEN : SDL::Window::Flags::SHOWN + @window = SDL::Window.new(window_title, WIDTH * DISPLAY_SCALE, HEIGHT * DISPLAY_SCALE, flags: flags) + @renderer = SDL::Renderer.new @window + @renderer.logical_size = {WIDTH, HEIGHT} + @texture = LibSDL.create_texture @renderer, PIXELFORMAT_RGB24, TEXTUREACCESS_STREAMING, WIDTH, HEIGHT + end + + def window_title : String + "CryBoy - #{@title} - #{@fps} fps" + end + + def draw(framebuffer : Array(RGB)) : Nil + LibSDL.update_texture @texture, nil, framebuffer, WIDTH * sizeof(RGB) + @renderer.clear + @renderer.copy @texture + @renderer.present + @fps += 1 + if Time.utc.second != @seconds + @window.title = window_title + @fps = 0 + @seconds = Time.utc.second + end + end + + def write_png(framebuffer : Array(RGB)) : Nil + canvas = StumpyPNG::Canvas.new WIDTH, HEIGHT + HEIGHT.times do |row| + WIDTH.times do |col| + rgb = framebuffer[row * WIDTH + col] + color = StumpyPNG::RGBA.from_rgb8(rgb.red, rgb.green, rgb.blue) + canvas[col, row] = color + end + end + StumpyPNG.write(canvas, "out.png") + end + end +end + +############################################################################### +# Method for drawing all tiles in vram + +# @all_tiles_window = SDL::Window.new("ALL TILES", 128 * scale, 192 * scale) +# @all_tiles_renderer = SDL::Renderer.new @all_tiles_window +# @all_tiles_renderer.logical_size = {128, 192} + +# # a method for showing all tiles in vram for debugging +# def draw_all_tiles(memory : Memory) +# (0...24).each do |y| +# (0...16).each do |x| +# tile_ptr = 0x8000 + (y * 16 * 16) + (x * 16) +# (0...8).each do |tile_row| +# byte_1 = memory[tile_ptr + 2 * tile_row] +# byte_2 = memory[tile_ptr + 2 * tile_row + 1] +# (0...8).each do |tile_col| +# lsb = (byte_1 >> (7 - tile_col)) & 0x1 +# msb = (byte_2 >> (7 - tile_col)) & 0x1 +# @all_tiles_renderer.draw_color = @colors[(msb << 1) | lsb] +# @all_tiles_renderer.draw_point((8 * x + tile_col), (8 * y + tile_row)) +# end +# end +# end +# end +# @all_tiles_renderer.present +# end diff --git a/src/crab/gb/fifo_ppu.cr b/src/crab/gb/fifo_ppu.cr new file mode 100644 index 0000000..2b9f2be --- /dev/null +++ b/src/crab/gb/fifo_ppu.cr @@ -0,0 +1,276 @@ +module GB + struct Pixel + property color : UInt8 # 0-3 + property palette : UInt8 # 0-7 + property oam_idx : UInt8 # OAM index for sprite + property obj_to_bg : UInt8 # OBJ-to_BG Priority bit + + def initialize(@color : UInt8, @palette : UInt8, @oam_idx : UInt8, @obj_to_bg : UInt8) + end + end + + class FifoPPU < PPU + @fifo = Deque(Pixel).new 8 + @fifo_sprite = Deque(Pixel).new 8 + + @fetch_counter = 0 + @fetch_counter_sprite = 0 + @fetcher_x = 0 + @lx : Int32 = 0 + @smooth_scroll_sampled = false + @dropped_first_fetch = false + @fetching_window = false + @fetching_sprite = false + + @tile_num : UInt8 = 0x00 + @tile_attrs : UInt8 = 0x00 + @tile_data_low : UInt8 = 0x00 + @tile_data_high : UInt8 = 0x00 + + @sprites = Array(Sprite).new + + @current_window_line = -1 + + enum FetchStage + GET_TILE + GET_TILE_DATA_LOW + GET_TILE_DATA_HIGH + PUSH_PIXEL + SLEEP + end + + FETCHER_ORDER = [ + FetchStage::SLEEP, FetchStage::GET_TILE, + FetchStage::SLEEP, FetchStage::GET_TILE_DATA_LOW, + FetchStage::SLEEP, FetchStage::GET_TILE_DATA_HIGH, + FetchStage::PUSH_PIXEL, + ] + + def sample_smooth_scrolling + @smooth_scroll_sampled = true + if @fetching_window + @lx = -Math.max(0, 7 - @wx) + else + @lx = -(7 & @scx) + end + end + + def reset_bg_fifo(fetching_window : Bool) : Nil + @fifo.clear + @fetcher_x = 0 + @fetch_counter = 0 + @fetching_window = fetching_window + @current_window_line += 1 if @fetching_window + end + + def reset_sprite_fifo : Nil + @fifo_sprite.clear + @fetch_counter_sprite = 0 + @fetching_sprite = false + end + + # get first 10 sprites on scanline, ordered + def get_sprites : Array(Sprite) + sprites = [] of Sprite + (0x00_u8..0x9F_u8).step 4 do |sprite_address| + sprite = Sprite.new @sprite_table, sprite_address + if sprite.on_line @ly, sprite_height + index = 0 + sprites.each do |sprite_elm| + break if sprite.x < sprite_elm.x + index += 1 + end + sprites.insert index, sprite + end + break if sprites.size >= 10 + end + sprites + end + + def tick_bg_fetcher : Nil + case FETCHER_ORDER[@fetch_counter] + in FetchStage::GET_TILE + if @fetching_window + map = window_tile_map == 0 ? 0x1800 : 0x1C00 # 0x9800 : 0x9C00 + offset = @fetcher_x + ((@current_window_line >> 3) * 32) + else + map = bg_tile_map == 0 ? 0x1800 : 0x1C00 # 0x9800 : 0x9C00 + offset = ((@fetcher_x + (@scx >> 3)) & 0x1F) + ((((@ly.to_u16 + @scy) >> 3) * 32) & 0x3FF) + end + @tile_num = @vram[0][map + offset] + @tile_attrs = @vram[1][map + offset] # vram[1] is all 0x00 if running in dmg mode + @fetch_counter += 1 + in FetchStage::GET_TILE_DATA_LOW, FetchStage::GET_TILE_DATA_HIGH + if bg_window_tile_data > 0 + tile_num = @tile_num + tile_data_table = 0x0000 # 0x8000 + else + tile_num = @tile_num.to_i8! + tile_data_table = 0x1000 # 0x9000 + end + tile_ptr = tile_data_table + 16 * tile_num + bank_num = (@tile_attrs & 0b00001000) >> 3 + tile_row = @fetching_window ? @current_window_line & 7 : (@ly.to_u16 + @scy) & 7 + tile_row = 7 - tile_row if @tile_attrs & 0b01000000 > 0 + if FETCHER_ORDER[@fetch_counter] == FetchStage::GET_TILE_DATA_LOW + @tile_data_low = @vram[bank_num][tile_ptr + tile_row * 2] + @fetch_counter += 1 + else + @tile_data_high = @vram[bank_num][tile_ptr + tile_row * 2 + 1] + @fetch_counter += 1 + unless @dropped_first_fetch + @dropped_first_fetch = true + @fetch_counter = 0 # drop first tile + end + end + in FetchStage::PUSH_PIXEL + if @fifo.size == 0 + bg_enabled = bg_display? || @cgb_ptr.value + @fetcher_x += 1 + 8.times do |col| + shift = @tile_attrs & 0b00100000 > 0 ? col : 7 - col + lsb = (@tile_data_low >> shift) & 0x1 + msb = (@tile_data_high >> shift) & 0x1 + color = (msb << 1) | lsb + @fifo.push Pixel.new(bg_enabled ? color : 0_u8, @tile_attrs & 0x7, 0, (@tile_attrs & 0x80) >> 7) + end + @fetch_counter += 1 + end + in FetchStage::SLEEP + @fetch_counter += 1 + end + @fetch_counter %= FETCHER_ORDER.size + end + + def tick_sprite_fetcher : Nil + case FETCHER_ORDER[@fetch_counter_sprite] + in FetchStage::GET_TILE + @fetch_counter_sprite += 1 + in FetchStage::GET_TILE_DATA_LOW + @fetch_counter_sprite += 1 + in FetchStage::GET_TILE_DATA_HIGH + sprite = @sprites.shift + bytes = sprite.bytes @ly, sprite_height + 8.times do |col| + shift = sprite.x_flip? ? col : 7 - col + lsb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[0]] >> shift) & 0x1 + msb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[1]] >> shift) & 0x1 + color = (msb << 1) | lsb + pixel = Pixel.new(color, @cgb_ptr.value ? sprite.cgb_palette_number : sprite.dmg_palette_number, sprite.oam_idx, sprite.priority) + if col + sprite.x - 8 >= @lx + if col >= @fifo_sprite.size + @fifo_sprite.push pixel + elsif @fifo_sprite[col].color == 0 || (@cgb_ptr.value && pixel.oam_idx <= @fifo_sprite[col].oam_idx && pixel.color != 0) + @fifo_sprite[col] = pixel + end + end + end + @fetching_sprite = !@sprites.empty? && @sprites[0].x == sprite.x + @fetch_counter_sprite += 1 + in FetchStage::PUSH_PIXEL + @fetch_counter_sprite += 1 + in FetchStage::SLEEP + @fetch_counter_sprite += 1 + end + @fetch_counter_sprite %= FETCHER_ORDER.size + end + + def sprite_wins?(bg_pixel : Pixel, sprite_pixel : Pixel) : Bool + if sprite_enabled? && sprite_pixel.color > 0 + if @cgb_ptr.value + !bg_display? || bg_pixel.color == 0 || (bg_pixel.obj_to_bg == 0 && sprite_pixel.obj_to_bg == 0) + else + sprite_pixel.obj_to_bg == 0 || bg_pixel.color == 0 + end + else + false + end + end + + def tick_shifter : Nil + if @fifo.size > 0 + bg_pixel = @fifo.shift + sprite_pixel = @fifo_sprite.shift if @fifo_sprite.size > 0 + sample_smooth_scrolling unless @smooth_scroll_sampled + if @lx >= 0 # otherwise drop pixel on floor + if !sprite_pixel.nil? && sprite_wins? bg_pixel, sprite_pixel + pixel = sprite_pixel + palette = sprite_pixel.palette == 0 ? @obp0 : @obp1 + palettes = @obj_palettes + else + pixel = bg_pixel + palette = @bgp + palettes = @palettes + end + color = @cgb_ptr.value ? pixel.color : palette[pixel.color] + @framebuffer[Display::WIDTH * @ly + @lx] = palettes[pixel.palette][color] + end + @lx += 1 + if @lx == Display::WIDTH + self.mode_flag = 0 + end + if window_enabled? && @ly >= @wy && @lx + 7 >= @wx && !@fetching_window && @window_trigger + reset_bg_fifo fetching_window: true + end + if sprite_enabled? && @sprites.size > 0 && @lx + 8 >= @sprites[0].x + @fetching_sprite = true + @fetch_counter_sprite = 0 + end + end + end + + # tick ppu forward by specified number of cycles + def tick(cycles : Int) : Nil + if lcd_enabled? + cycles.times do + case self.mode_flag + when 2 # OAM search + if @cycle_counter == 79 + self.mode_flag = 3 + @window_trigger = true if @ly == @wy + reset_bg_fifo fetching_window: window_enabled? && @ly >= @wy && @wx <= 7 && @window_trigger + reset_sprite_fifo + @lx = 0 + @smooth_scroll_sampled = false + @dropped_first_fetch = false + @sprites = get_sprites + end + when 3 # drawing + tick_bg_fetcher unless @fetching_sprite + tick_sprite_fetcher if @fetching_sprite + tick_shifter unless @fetching_sprite + when 0 # hblank + if @cycle_counter == 456 + @cycle_counter = 0 + @ly += 1 + if @ly == Display::HEIGHT # final row of screen complete + self.mode_flag = 1 # switch to vblank + @gb.interrupts.vblank_interrupt = true + @gb.display.draw @framebuffer # render at vblank + @current_window_line = -1 + else + self.mode_flag = 2 # switch to oam search + end + end + when 1 # vblank + if @cycle_counter == 456 + @cycle_counter = 0 + @ly += 1 if @ly != 0 + handle_stat_interrupt + if @ly == 0 # end of vblank reached (ly has already shortcut to 0) + self.mode_flag = 2 # switch to oam search + # todo: I think the timing here might be _just wrong_ + end + end + @ly = 0 if @ly == 153 && @cycle_counter > 4 # shortcut ly to from 153 to 0 after 4 cycles + end + @cycle_counter += 1 + end + else # lcd is disabled + @cycle_counter = 0 # reset cycle counter + self.mode_flag = 0 # reset to mode 0 + @ly = 0 # reset ly + end + end + end +end diff --git a/src/crab/gb/gb.cr b/src/crab/gb/gb.cr new file mode 100644 index 0000000..99add86 --- /dev/null +++ b/src/crab/gb/gb.cr @@ -0,0 +1,84 @@ +require "sdl" +require "./apu" +require "./cartridge" +require "./cpu" +require "./display" +require "./interrupts" +require "./joypad" +require "./mbc/*" +require "./memory" +require "./opcodes" +require "./ppu" +require "./scanline_ppu" +require "./fifo_ppu" +require "./scheduler" +require "./timer" + +DISPLAY_SCALE = {% unless flag? :graphics_test %} 4 {% else %} 1 {% end %} + +module GB + class GB + getter bootrom : String? + getter cgb_ptr : Pointer(Bool) { pointerof(@cgb_enabled) } + getter cartridge : Cartridge + + getter! apu : APU + getter! cpu : CPU + getter! display : Display + getter! interrupts : Interrupts + getter! joypad : Joypad + getter! memory : Memory + getter! ppu : PPU + getter! scheduler : Scheduler + getter! timer : Timer + + def initialize(@bootrom : String?, rom_path : String, @fifo : Bool, @sync : Bool, @headless : Bool) + @cartridge = Cartridge.new rom_path + @cgb_enabled = !(bootrom.nil? && @cartridge.cgb == Cartridge::CGB::NONE) + + SDL.init(SDL::Init::VIDEO | SDL::Init::AUDIO | SDL::Init::JOYSTICK) + LibSDL.joystick_open 0 + at_exit { SDL.quit } + end + + def post_init : Nil + @scheduler = Scheduler.new + @interrupts = Interrupts.new + @apu = APU.new self, @headless, @sync + @display = Display.new self, @headless + @joypad = Joypad.new self + @ppu = @fifo ? FifoPPU.new self : ScanlinePPU.new self + @timer = Timer.new self + @memory = Memory.new self + @cpu = CPU.new self + skip_boot if @bootrom.nil? + end + + private def skip_boot : Nil + cpu.skip_boot + memory.skip_boot + ppu.skip_boot + timer.skip_boot + end + + def handle_events : Nil + while event = SDL::Event.poll + case event + when SDL::Event::Quit then exit 0 + when SDL::Event::Keyboard, + SDL::Event::JoyHat, + SDL::Event::JoyButton then joypad.handle_joypad_event event + else nil + end + end + scheduler.schedule 70224, Scheduler::EventType::HandleInput, ->handle_events + end + + def run : Nil + handle_events + loop do + cpu.tick + end + end + end +end diff --git a/src/crab/gb/interrupts.cr b/src/crab/gb/interrupts.cr new file mode 100644 index 0000000..acd007d --- /dev/null +++ b/src/crab/gb/interrupts.cr @@ -0,0 +1,98 @@ +module GB + class Interrupts + enum InterruptLine : UInt16 + VBLANK = 0x0040 + STAT = 0x0048 + TIMER = 0x0050 + SERIAL = 0x0058 + JOYPAD = 0x0060 + NONE = 0x0000 + end + + @top_3_ie_bits : UInt8 = 0x00 # they're writable for some reason + + property vblank_interrupt = false + property lcd_stat_interrupt = false + property timer_interrupt = false + property serial_interrupt = false + property joypad_interrupt = false + + property vblank_enabled = false + property lcd_stat_enabled = false + property timer_enabled = false + property serial_enabled = false + property joypad_enabled = false + + def highest_priority : InterruptLine + if vblank_interrupt && vblank_enabled + InterruptLine::VBLANK + elsif lcd_stat_interrupt && lcd_stat_enabled + InterruptLine::STAT + elsif timer_interrupt && timer_enabled + InterruptLine::TIMER + elsif serial_interrupt && serial_enabled + InterruptLine::SERIAL + elsif joypad_interrupt && joypad_enabled + InterruptLine::JOYPAD + else + InterruptLine::NONE + end + end + + def clear(interrupt_line : InterruptLine) : Nil + case interrupt_line + in InterruptLine::VBLANK then @vblank_interrupt = false + in InterruptLine::STAT then @lcd_stat_interrupt = false + in InterruptLine::TIMER then @timer_interrupt = false + in InterruptLine::SERIAL then @serial_interrupt = false + in InterruptLine::JOYPAD then @joypad_interrupt = false + in InterruptLine::NONE then nil + end + end + + def interrupt_ready? : Bool + self[0xFF0F] & self[0xFFFF] & 0x1F > 0 + end + + # read from interrupts memory + def [](index : Int) : UInt8 + case index + when 0xFF0F + 0xE0_u8 | + (@joypad_interrupt ? (0x1 << 4) : 0) | + (@serial_interrupt ? (0x1 << 3) : 0) | + (@timer_interrupt ? (0x1 << 2) : 0) | + (@lcd_stat_interrupt ? (0x1 << 1) : 0) | + (@vblank_interrupt ? (0x1 << 0) : 0) + when 0xFFFF + @top_3_ie_bits | + (@joypad_enabled ? (0x1 << 4) : 0) | + (@serial_enabled ? (0x1 << 3) : 0) | + (@timer_enabled ? (0x1 << 2) : 0) | + (@lcd_stat_enabled ? (0x1 << 1) : 0) | + (@vblank_enabled ? (0x1 << 0) : 0) + else raise "Reading from invalid interrupts register: #{hex_str index.to_u16!}" + end + end + + # write to interrupts memory + def []=(index : Int, value : UInt8) : Nil + case index + when 0xFF0F + @vblank_interrupt = value & (0x1 << 0) > 0 + @lcd_stat_interrupt = value & (0x1 << 1) > 0 + @timer_interrupt = value & (0x1 << 2) > 0 + @serial_interrupt = value & (0x1 << 3) > 0 + @joypad_interrupt = value & (0x1 << 4) > 0 + when 0xFFFF + @top_3_ie_bits = value & 0xE0 + @vblank_enabled = value & (0x1 << 0) > 0 + @lcd_stat_enabled = value & (0x1 << 1) > 0 + @timer_enabled = value & (0x1 << 2) > 0 + @serial_enabled = value & (0x1 << 3) > 0 + @joypad_enabled = value & (0x1 << 4) > 0 + else raise "Writing to invalid interrupts register: #{hex_str index.to_u16!}" + end + end + end +end diff --git a/src/crab/gb/joypad.cr b/src/crab/gb/joypad.cr new file mode 100644 index 0000000..fbaa909 --- /dev/null +++ b/src/crab/gb/joypad.cr @@ -0,0 +1,81 @@ +module GB + class Joypad + property button_keys = false + property direction_keys = false + + # describes if a button is CURRENTLY PRESSED + property down = false + property up = false + property left = false + property right = false + property start = false + property :select # select is a keyword + @select = false + property b = false + property a = false + + def initialize(@gb : GB) + end + + def read : UInt8 + array_to_uint8 [ + 1, + 1, + !@button_keys, + !@direction_keys, + !((@down && @direction_keys) || (@start && @button_keys)), + !((@up && @direction_keys) || (@select && @button_keys)), + !((@left && @direction_keys) || (@b && @button_keys)), + !((@right && @direction_keys) || (@a && @button_keys)), + ] + end + + def write(value : UInt8) : Nil + @button_keys = (value >> 5) & 0x1 == 0 + @direction_keys = (value >> 4) & 0x1 == 0 + end + + def handle_joypad_event(event : SDL::Event) : Nil + case event + when SDL::Event::Keyboard + case event.sym + when .down?, .d? then @down = event.pressed? + when .up?, .e? then @up = event.pressed? + when .left?, .s? then @left = event.pressed? + when .right?, .f? then @right = event.pressed? + when .semicolon? then @start = event.pressed? + when .l? then @select = event.pressed? + when .b?, .j? then @b = event.pressed? + when .a?, .k? then @a = event.pressed? + when .tab? then @gb.apu.sync = !event.pressed? + else nil + end + when SDL::Event::JoyHat + @down = false + @up = false + @left = false + @right = false + case event.value + when LibSDL::HAT_DOWN then @down = true + when LibSDL::HAT_UP then @up = true + when LibSDL::HAT_LEFT then @left = true + when LibSDL::HAT_RIGHT then @right = true + when LibSDL::HAT_LEFTUP then @left = @up = true + when LibSDL::HAT_LEFTDOWN then @left = @down = true + when LibSDL::HAT_RIGHTUP then @right = @up = true + when LibSDL::HAT_RIGHTDOWN then @right = @down = true + else nil + end + when SDL::Event::JoyButton + case event.button + when 7 then @start = event.pressed? + when 6 then @select = event.pressed? + when 0 then @b = event.pressed? + when 1 then @a = event.pressed? + else nil + end + else nil + end + end + end +end diff --git a/src/crab/gb/mbc/mbc1.cr b/src/crab/gb/mbc/mbc1.cr new file mode 100644 index 0000000..825bf66 --- /dev/null +++ b/src/crab/gb/mbc/mbc1.cr @@ -0,0 +1,68 @@ +module GB + class MBC1 < Cartridge + def initialize(@rom : Bytes) + if ram_size == 0 && (@rom[0x0147] == 0x02 || @rom[0x0147] == 0x03) + STDERR.puts "MBC1 cartridge has ram, but `ram_size` was reported as 0. Ignoring `ram_size` and using 8kB of ram." + @ram = Bytes.new 0x2000 + else + @ram = Bytes.new ram_size + end + @ram_enabled = false + @mode = 0_u8 + @reg1 = 1_u8 # main rom banking register + @reg2 = 0_u8 # secondary banking register + end + + def [](index : Int) : UInt8 + case index + when Memory::ROM_BANK_0 + if @mode == 0 + @rom[index] + else + # can contain banks 20/40/60 in mode 1 + bank_number = (@reg2 << 5) + @rom[rom_bank_offset(bank_number) + index] + end + when Memory::ROM_BANK_N + bank_number = ((@reg2 << 5) | @reg1) + @rom[rom_bank_offset(bank_number) + rom_offset(index)] + when Memory::EXTERNAL_RAM + if @ram_enabled && @ram.size > 0 + if @mode == 0 + @ram[ram_offset index] + else + @ram[ram_bank_offset(@reg2) + ram_offset(index)] + end + else + 0xFF_u8 + end + else raise "Reading from invalid cartridge register #{hex_str index.to_u16!}" + end + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0x0000..0x1FFF + enabling = value & 0x0F == 0x0A + save_game if @ram_enabled && !enabling + @ram_enabled = enabling + when 0x2000..0x3FFF + @reg1 = value & 0b00011111 # select 5 bits + @reg1 += 1 if @reg1 == 0 # translate 0 to 1 + when 0x4000..0x5FFF + @reg2 = value & 0b00000011 # select 2 bits + when 0x6000..0x7FFF + @mode = value & 0x1 + when Memory::EXTERNAL_RAM + if @ram_enabled && @ram.size > 0 + if @mode == 0 + @ram[ram_offset index] = value + else + @ram[ram_bank_offset(@reg2) + ram_offset(index)] = value + end + end + else raise "Writing to invalid cartridge register: #{hex_str index.to_u16!}" + end + end + end +end diff --git a/src/crab/gb/mbc/mbc2.cr b/src/crab/gb/mbc/mbc2.cr new file mode 100644 index 0000000..4c0e978 --- /dev/null +++ b/src/crab/gb/mbc/mbc2.cr @@ -0,0 +1,45 @@ +module GB + class MBC2 < Cartridge + getter ram_size : Int32 { 0x0200 } + + def initialize(@rom : Bytes) + @ram = Bytes.new ram_size + @ram_enabled = false + @rom_bank = 1_u8 + end + + def [](index : Int) : UInt8 + case index + when Memory::ROM_BANK_0 + @rom[index] + when Memory::ROM_BANK_N + @rom[rom_bank_offset(@rom_bank) + rom_offset(index)] + when Memory::EXTERNAL_RAM + if @ram_enabled + @ram[ram_offset(index) % ram_size] | 0xF0 + else + 0xFF_u8 + end + else raise "Reading from invalid cartridge register #{hex_str index.to_u16!}" + end + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0x0000..0x3FFF + if index & 0x0100 == 0 # RAMG + enabling = value & 0x0F == 0b1010 + save_game if @ram_enabled && !enabling + @ram_enabled = enabling + else # ROMB + @rom_bank = value & 0x0F + @rom_bank += 1 if @rom_bank == 0 + end + when Memory::EXTERNAL_RAM + if @ram_enabled + @ram[ram_offset(index) % ram_size] = value & 0x0F + end + end + end + end +end diff --git a/src/crab/gb/mbc/mbc3.cr b/src/crab/gb/mbc/mbc3.cr new file mode 100644 index 0000000..d1dfd58 --- /dev/null +++ b/src/crab/gb/mbc/mbc3.cr @@ -0,0 +1,54 @@ +module GB + class MBC3 < Cartridge + def initialize(@rom : Bytes) + @ram = Bytes.new ram_size + @ram_enabled = false + @rom_bank_number = 1_u8 # 7-bit register + @ram_bank_number = 0_u8 # 4-bit register + end + + def [](index : Int) : UInt8 + case index + when Memory::ROM_BANK_0 + @rom[index] + when Memory::ROM_BANK_N + @rom[rom_bank_offset(@rom_bank_number) + rom_offset(index)] + when Memory::EXTERNAL_RAM + if @ram_bank_number <= 3 + @ram_enabled ? @ram[ram_bank_offset(@ram_bank_number) + ram_offset(index)] : 0xFF_u8 + elsif @ram_bank_number <= 0x0C + # puts "reading clock: #{hex_str @ram_bank_number}" + 0xFF_u8 + else + raise "Invalid RAM/RTC bank register read: #{hex_str @ram_bank_number}" + end + else raise "Reading from invalid cartridge register #{hex_str index.to_u16!}" + end + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0x0000..0x1FFF + enabling = value & 0x0F == 0x0A + save_game if @ram_enabled && !enabling + @ram_enabled = enabling + when 0x2000..0x3FFF + @rom_bank_number = value & 0x7F + @rom_bank_number += 1 if @rom_bank_number == 0 + when 0x4000..0x5FFF + @ram_bank_number = value + when 0x6000..0x7FFF + # puts "latch clock: #{hex_str value}" + when Memory::EXTERNAL_RAM + if @ram_bank_number <= 0x03 + @ram[ram_bank_offset(@ram_bank_number) + ram_offset(index)] = value if @ram_enabled + elsif @ram_bank_number <= 0x0C + # puts "writing to clock: #{hex_str @ram_bank_number} -> #{hex_str value}" + else + raise "Invalid RAM/RTC bank register write: #{hex_str @ram_bank_number}" + end + else raise "Writing to invalid cartridge register: #{hex_str index.to_u16!}" + end + end + end +end diff --git a/src/crab/gb/mbc/mbc5.cr b/src/crab/gb/mbc/mbc5.cr new file mode 100644 index 0000000..a455e2e --- /dev/null +++ b/src/crab/gb/mbc/mbc5.cr @@ -0,0 +1,42 @@ +module GB + class MBC5 < Cartridge + def initialize(@rom : Bytes) + @ram = Bytes.new ram_size + @ram_enabled = false + @rom_bank_number = 1_u16 # 9-bit register + @ram_bank_number = 0_u8 # 4-bit register + end + + def [](index : Int) : UInt8 + case index + when Memory::ROM_BANK_0 + @rom[index] + when Memory::ROM_BANK_N + @rom[rom_bank_offset(@rom_bank_number) + rom_offset(index)] + when Memory::EXTERNAL_RAM + @ram_enabled ? @ram[ram_bank_offset(@ram_bank_number) + ram_offset(index)] : 0xFF_u8 + else raise "Reading from invalid cartridge register #{hex_str index.to_u16!}" + end + end + + def []=(index : Int, value : UInt8) : Nil + case index + when 0x0000..0x1FFF # different than mbc1, now 8-bit register + enabling = value & 0xFF == 0x0A + save_game if @ram_enabled && !enabling + @ram_enabled = enabling + when 0x2000..0x2FFF # select lower 8 bits + @rom_bank_number = (@rom_bank_number & 0x0100) | value + when 0x3000..0x3FFF # select upper 1 bit + @rom_bank_number = (@rom_bank_number & 0x00FF) | (value.to_u16 & 1) << 8 + when 0x4000..0x5FFF + @ram_bank_number = value & 0b00001111 + when 0x6000..0x7FFF + # unmapped write + when Memory::EXTERNAL_RAM + @ram[ram_bank_offset(@ram_bank_number) + ram_offset(index)] = value if @ram_enabled + else raise "Writing to invalid cartridge register: #{hex_str index.to_u16!}" + end + end + end +end diff --git a/src/crab/gb/mbc/rom.cr b/src/crab/gb/mbc/rom.cr new file mode 100644 index 0000000..5ceb4fe --- /dev/null +++ b/src/crab/gb/mbc/rom.cr @@ -0,0 +1,33 @@ +module GB + class ROM < Cartridge + def initialize(@rom : Bytes) + @ram = Bytes.new ram_size + end + + def [](index : Int) : UInt8 + case index + when Memory::ROM_BANK_0 then @rom[index] + when Memory::ROM_BANK_N then @rom[index] + when Memory::EXTERNAL_RAM + if index - Memory::EXTERNAL_RAM.begin < @ram.size + @ram[index - Memory::EXTERNAL_RAM.begin] + else + 0xFF_u8 + end + else raise "Reading from invalid cartridge register #{hex_str index.to_u16!}" + end + end + + def []=(index : Int, value : UInt8) : Nil + case index + when Memory::ROM_BANK_0 then nil + when Memory::ROM_BANK_N then nil + when Memory::EXTERNAL_RAM + if index - Memory::EXTERNAL_RAM.begin < @ram.size + @ram[index - Memory::EXTERNAL_RAM.begin] = value + end + else raise "Writing to invalid cartridge register: #{hex_str index.to_u16!}" + end + end + end +end diff --git a/src/crab/gb/memory.cr b/src/crab/gb/memory.cr new file mode 100644 index 0000000..220b29e --- /dev/null +++ b/src/crab/gb/memory.cr @@ -0,0 +1,300 @@ +module GB + class Memory + ROM_BANK_0 = 0x0000..0x3FFF + ROM_BANK_N = 0x4000..0x7FFF + VRAM = 0x8000..0x9FFF + EXTERNAL_RAM = 0xA000..0xBFFF + WORK_RAM_0 = 0xC000..0xCFFF + WORK_RAM_N = 0xD000..0xDFFF + ECHO = 0xE000..0xFDFF + OAM = 0xFE00..0xFE9F + NOT_USABLE = 0xFEA0..0xFEFF + IO_PORTS = 0xFF00..0xFF7F + HRAM = 0xFF80..0xFFFE + INTERRUPT_REG = 0xFFFF + + @cartridge : Cartridge + @interrupts : Interrupts + @ppu : PPU + @apu : APU + @timer : Timer + @joypad : Joypad + @scheduler : Scheduler + @cgb_ptr : Pointer(Bool) + + @wram = Array(Bytes).new 8 { Bytes.new GB::Memory::WORK_RAM_N.size } + @wram_bank : UInt8 = 1 + @hram = Bytes.new HRAM.size + @ff72 : UInt8 = 0x00 + @ff73 : UInt8 = 0x00 + @ff74 : UInt8 = 0x00 + @ff75 : UInt8 = 0x00 + @ff76 : UInt8 = 0x00 + @ff77 : UInt8 = 0x00 + property bootrom = Bytes.new 0 + @cycle_tick_count = 0 + + # From I conversation I had with gekkio on the EmuDev Discord: (todo) + + # the DMA controller takes over the source bus, which is either the external bus or the video ram bus + # and obviously the OAM itself since it's the target + # nothing else is affected by DMA + # in other words: + # * if the external bus is the source bus, accessing these lead to conflict situations: work RAM, anything on the cartridge. Everything else (including video RAM) doesn't lead to conflicts + # * if the video RAM is the source bus, accessing it leads to a conflict situation. Everything else (including work RAM, and the cartridge) doesn't lead to conflicts + # + # if the DMA source bus is read, you always get the current byte read by the DMA controller + # accessing the target bus (= OAM) works differently, and returning 0xff is probably reasonable until more information is gathered...I haven't yet studied OAM very much so I don't yet know the right answers + + # As of right now, my DMA implementation gets the timing correct and block + # access to OAM during DMA. It does not properly emulate collisions in the + # DMA source, as described above. + @dma : UInt8 = 0x00 + @current_dma_source : UInt16 = 0x0000 + @internal_dma_timer = 0 + @dma_position : UInt8 = 0xA0 + @requested_oam_dma_transfer : Bool = false + @next_dma_counter : UInt8 = 0x00 + + @requested_speed_switch : Bool = false + @current_speed : UInt8 = 0 # 0 (single) or 1 (double) + + def stop_instr : Nil + if @requested_speed_switch && @cgb_ptr.value + @requested_speed_switch = false + @current_speed ^= 1 # toggle between 0 and 1 + @scheduler.speed_mode = @current_speed + end + end + + # keep other components in sync with memory, usually before memory access + def tick_components(cycles = 4, from_cpu = true, ignore_speed = false) : Nil + @cycle_tick_count += cycles if from_cpu + @scheduler.tick cycles + @ppu.tick ignore_speed ? cycles : cycles >> @current_speed + @timer.tick cycles + dma_tick cycles + end + + def reset_cycle_count : Nil + @cycle_tick_count = 0 + end + + # tick remainder of expected cycles, then reset counter + def tick_extra(total_expected_cycles : Int) : Nil + raise "Operation took #{@cycle_tick_count} cycles, but only expected #{total_expected_cycles}" if @cycle_tick_count > total_expected_cycles + remaining = total_expected_cycles - @cycle_tick_count + tick_components remaining if remaining > 0 + reset_cycle_count + end + + def initialize(@gb : GB) + @cartridge = gb.cartridge + @interrupts = gb.interrupts + @ppu = gb.ppu + @apu = gb.apu + @timer = gb.timer + @joypad = gb.joypad + @scheduler = gb.scheduler + @cgb_ptr = gb.cgb_ptr + bootrom = gb.bootrom + unless bootrom.nil? + File.open bootrom do |file| + @bootrom = Bytes.new file.size + file.read @bootrom + end + end + end + + def skip_boot : Nil + write_byte 0xFF10, 0x80_u8 # NR10 + write_byte 0xFF11, 0xBF_u8 # NR11 + write_byte 0xFF12, 0xF3_u8 # NR12 + write_byte 0xFF14, 0xBF_u8 # NR14 + write_byte 0xFF16, 0x3F_u8 # NR21 + write_byte 0xFF17, 0x00_u8 # NR22 + write_byte 0xFF19, 0xBF_u8 # NR24 + write_byte 0xFF1A, 0x7F_u8 # NR30 + write_byte 0xFF1B, 0xFF_u8 # NR31 + write_byte 0xFF1C, 0x9F_u8 # NR32 + write_byte 0xFF1E, 0xBF_u8 # NR33 + write_byte 0xFF20, 0xFF_u8 # NR41 + write_byte 0xFF21, 0x00_u8 # NR42 + write_byte 0xFF22, 0x00_u8 # NR43 + write_byte 0xFF23, 0xBF_u8 # NR44 + write_byte 0xFF24, 0x77_u8 # NR50 + write_byte 0xFF25, 0xF3_u8 # NR51 + write_byte 0xFF26, 0xF1_u8 # NR52 + write_byte 0xFF40, 0x91_u8 # LCDC + write_byte 0xFF42, 0x00_u8 # SCY + write_byte 0xFF43, 0x00_u8 # SCX + write_byte 0xFF45, 0x00_u8 # LYC + write_byte 0xFF47, 0xFC_u8 # BGP + write_byte 0xFF48, 0xFF_u8 # OBP0 + write_byte 0xFF49, 0xFF_u8 # OBP1 + write_byte 0xFF4A, 0x00_u8 # WY + write_byte 0xFF4B, 0x00_u8 # WX + write_byte 0xFFFF, 0x00_u8 # IE + end + + # read 8 bits from memory (doesn't tick components) + def read_byte(index : Int) : UInt8 + return @bootrom[index] if @bootrom.size > 0 && (0x000 <= index < 0x100 || 0x200 <= index < 0x900) + case index + when ROM_BANK_0 then @cartridge[index] + when ROM_BANK_N then @cartridge[index] + when VRAM then @ppu[index] + when EXTERNAL_RAM then @cartridge[index] + when WORK_RAM_0 then @wram[0][index - WORK_RAM_0.begin] + when WORK_RAM_N then @wram[@wram_bank][index - WORK_RAM_N.begin] + when ECHO then read_byte index - 0x2000 + when OAM then @ppu[index] + when NOT_USABLE then 0_u8 + when IO_PORTS + case index + when 0xFF00 then @joypad.read + when 0xFF04..0xFF07 then @timer[index] + when 0xFF0F then @interrupts[index] + when 0xFF10..0xFF3F then @apu[index] + when 0xFF46 then @dma + when 0xFF40..0xFF4B then @ppu[index] + when 0xFF4D + if @cgb_ptr.value + 0x7E_u8 | @current_speed << 7 | (@requested_speed_switch ? 1 : 0) + else + 0xFF_u8 + end + when 0xFF4F then @ppu[index] + when 0xFF51..0xFF55 then @ppu[index] + when 0xFF68..0xFF6B then @ppu[index] + when 0xFF70 then @cgb_ptr.value ? 0xF8_u8 | @wram_bank : 0xFF_u8 + when 0xFF72 then @ff72 # (todo) undocumented register + when 0xFF73 then @ff73 # (todo) undocumented register + when 0xFF74 then @cgb_ptr.value ? @ff74 : 0xFF_u8 # (todo) undocumented register + when 0xFF75 then @ff75 # (todo) undocumented register + when 0xFF76 then 0x00_u8 # (todo) lower bits should have apu channel 1/2 PCM amp + when 0xFF77 then 0x00_u8 # (todo) lower bits should have apu channel 3/4 PCM amp + else 0xFF_u8 + end + when HRAM then @hram[index - HRAM.begin] + when INTERRUPT_REG then @interrupts[index] + else raise "FAILED TO GET INDEX #{index}" + end + end + + # read 8 bits from memory and tick other components + def [](index : Int) : UInt8 + # todo: not all of these registers are used. unused registers _should_ return 0xFF + # - sound doesn't take all of 0xFF10..0xFF3F + tick_components + return 0xFF_u8 if (0 < @dma_position <= 0xA0) && OAM.includes?(index) + read_byte index + end + + # write a 8 bits to memory (doesn't tick components) + def write_byte(index : Int, value : UInt8) : Nil + if index == 0xFF50 && value == 0x11 + @bootrom = Bytes.new 0 + @cgb_ptr.value = @cartridge.cgb != Cartridge::CGB::NONE + end + case index + when ROM_BANK_0 then @cartridge[index] = value + when ROM_BANK_N then @cartridge[index] = value + when VRAM then @ppu[index] = value + when EXTERNAL_RAM then @cartridge[index] = value + when WORK_RAM_0 then @wram[0][index - WORK_RAM_0.begin] = value + when WORK_RAM_N then @wram[@wram_bank][index - WORK_RAM_N.begin] = value + when ECHO then write_byte index - 0x2000, value + when OAM then @ppu[index] = value + when NOT_USABLE then nil + when IO_PORTS + case index + when 0xFF00 then @joypad.write value + when 0xFF01 + {% if flag? :print_serial %} + print value + STDOUT.flush + {% elsif flag? :print_serial_ascii %} + print value.chr + STDOUT.flush + {% end %} + when 0xFF04..0xFF07 then @timer[index] = value + when 0xFF0F then @interrupts[index] = value + when 0xFF10..0xFF3F then @apu[index] = value + when 0xFF46 then dma_transfer value + when 0xFF40..0xFF4B then @ppu[index] = value + when 0xFF4D then @requested_speed_switch = value & 0x1 > 0 if @cgb_ptr.value + when 0xFF4F then @ppu[index] = value + when 0xFF51..0xFF55 then @ppu[index] = value + when 0xFF68..0xFF6B then @ppu[index] = value + when 0xFF70 + if @cgb_ptr.value + @wram_bank = value & 0x7 + @wram_bank += 1 if @wram_bank == 0 + end + when 0xFF72 then @ff72 = value + when 0xFF73 then @ff73 = value + when 0xFF74 then @ff74 = value if @cgb_ptr.value + when 0xFF75 then @ff75 = value | 0x8F + else nil + end + when HRAM then @hram[index - HRAM.begin] = value + when INTERRUPT_REG then @interrupts[index] = value + else raise "FAILED TO SET INDEX #{index}" + end + end + + # write 8 bits to memory and tick other components + def []=(index : Int, value : UInt8) : Nil + tick_components + return if (0 < @dma_position <= 0xA0) && OAM.includes?(index) + write_byte index, value + end + + # write 16 bits to memory + def []=(index : Int, value : UInt16) : Nil + self[index + 1] = (value >> 8).to_u8 + self[index] = (value & 0xFF).to_u8 + end + + # read 16 bits from memory + def read_word(index : Int) : UInt16 + self[index].to_u16 | (self[index + 1].to_u16 << 8) + end + + def dma_transfer(source : UInt8) : Nil + @dma = source + @requested_oam_dma_transfer = true + @next_dma_counter = 0 + end + + # DMA should start 8 T-cycles after a write to 0xFF46. That's what + # `@requested_oam_dma_transfer` and `@next_dma_counter` are for. After that, + # memory is still blocked for an additional 4 T-cycles, which is why I + # increment `@dma_position` past 0xA0, even though it only transfers 0xA0 + # bytes. I just use it as an indicator of when memory should unlock again. + # Note: According to a comment in gekkio's oam_dma_start test, if DMA is + # restarted while it has not yet completed, the 8 T-cycles should + # be spent continuing the first DMA rather than jumping to the new one. + def dma_tick(cycles : Int) : Nil + cycles.times do + if @requested_oam_dma_transfer + @next_dma_counter += 1 + if @next_dma_counter == 8 + @requested_oam_dma_transfer = false + @current_dma_source = @dma.to_u16 << 8 + @dma_position = 0 + @internal_dma_timer = 0 + end + end + if @dma_position <= 0xA0 + if @internal_dma_timer & 3 == 0 + write_byte 0xFE00 + @dma_position, read_byte @current_dma_source + @dma_position if @dma_position < 0xA0 + @dma_position += 1 + end + @internal_dma_timer += 1 + end + end + end + end +end diff --git a/src/crab/gb/opcodes.cr b/src/crab/gb/opcodes.cr new file mode 100644 index 0000000..8edba0d --- /dev/null +++ b/src/crab/gb/opcodes.cr @@ -0,0 +1,4100 @@ +module GB + class Opcodes + UNPREFIXED = [ + # 0x00 NOP + ->(cpu : CPU) { + cpu.inc_pc + return 4 + }, + # 0x01 LD BC,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.bc = u16 + return 12 + }, + # 0x02 LD (BC),A + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory[cpu.bc] = cpu.a + return 8 + }, + # 0x03 INC BC + ->(cpu : CPU) { + cpu.inc_pc + cpu.bc &+= 1 + return 8 + }, + # 0x04 INC B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.b & 0x0F == 0x0F + cpu.b &+= 1 + cpu.f_z = cpu.b == 0 + cpu.f_n = false + return 4 + }, + # 0x05 DEC B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &-= 1 + cpu.f_z = cpu.b == 0 + cpu.f_h = cpu.b & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x06 LD B,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.b = u8 + return 8 + }, + # 0x07 RLCA + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = (cpu.a << 1) + (cpu.a >> 7) + cpu.f_c = cpu.a & 0x01 + cpu.f_z = false + cpu.f_n = false + cpu.f_h = false + return 4 + }, + # 0x08 LD (u16),SP + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.memory[u16] = cpu.sp + return 20 + }, + # 0x09 ADD HL,BC + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.hl & 0x0FFF).to_u32 + (cpu.bc & 0x0FFF) > 0x0FFF + cpu.hl &+= cpu.bc + cpu.f_c = cpu.hl < cpu.bc + cpu.f_n = false + return 8 + }, + # 0x0A LD A,(BC) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.memory[cpu.bc] + return 8 + }, + # 0x0B DEC BC + ->(cpu : CPU) { + cpu.inc_pc + cpu.bc &-= 1 + return 8 + }, + # 0x0C INC C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.c & 0x0F == 0x0F + cpu.c &+= 1 + cpu.f_z = cpu.c == 0 + cpu.f_n = false + return 4 + }, + # 0x0D DEC C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &-= 1 + cpu.f_z = cpu.c == 0 + cpu.f_h = cpu.c & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x0E LD C,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.c = u8 + return 8 + }, + # 0x0F RRCA + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = (cpu.a >> 1) + (cpu.a << 7) + cpu.f_c = cpu.a & 0x80 + cpu.f_z = false + cpu.f_n = false + cpu.f_h = false + return 4 + }, + # 0x10 STOP + ->(cpu : CPU) { + cpu.inc_pc + # todo: see if something more needs to happen here... + cpu.inc_pc + cpu.memory.stop_instr + return 4 + }, + # 0x11 LD DE,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.de = u16 + return 12 + }, + # 0x12 LD (DE),A + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory[cpu.de] = cpu.a + return 8 + }, + # 0x13 INC DE + ->(cpu : CPU) { + cpu.inc_pc + cpu.de &+= 1 + return 8 + }, + # 0x14 INC D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.d & 0x0F == 0x0F + cpu.d &+= 1 + cpu.f_z = cpu.d == 0 + cpu.f_n = false + return 4 + }, + # 0x15 DEC D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &-= 1 + cpu.f_z = cpu.d == 0 + cpu.f_h = cpu.d & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x16 LD D,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.d = u8 + return 8 + }, + # 0x17 RLA + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.a & 0x80 + cpu.a = (cpu.a << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = carry + cpu.f_z = false + cpu.f_n = false + cpu.f_h = false + return 4 + }, + # 0x18 JR i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + cpu.pc &+= i8 + return 12 + }, + # 0x19 ADD HL,DE + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.hl & 0x0FFF).to_u32 + (cpu.de & 0x0FFF) > 0x0FFF + cpu.hl &+= cpu.de + cpu.f_c = cpu.hl < cpu.de + cpu.f_n = false + return 8 + }, + # 0x1A LD A,(DE) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.memory[cpu.de] + return 8 + }, + # 0x1B DEC DE + ->(cpu : CPU) { + cpu.inc_pc + cpu.de &-= 1 + return 8 + }, + # 0x1C INC E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.e & 0x0F == 0x0F + cpu.e &+= 1 + cpu.f_z = cpu.e == 0 + cpu.f_n = false + return 4 + }, + # 0x1D DEC E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &-= 1 + cpu.f_z = cpu.e == 0 + cpu.f_h = cpu.e & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x1E LD E,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.e = u8 + return 8 + }, + # 0x1F RRA + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.a & 0x01 + cpu.a = (cpu.a >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_c = carry + cpu.f_z = false + cpu.f_n = false + cpu.f_h = false + return 4 + }, + # 0x20 JR NZ,i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + if cpu.f_nz + cpu.pc &+= i8 + return 12 + end + return 8 + }, + # 0x21 LD HL,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.hl = u16 + return 12 + }, + # 0x22 LD (HL+),A + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory[((cpu.hl &+= 1) &- 1)] = cpu.a + return 8 + }, + # 0x23 INC HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.hl &+= 1 + return 8 + }, + # 0x24 INC H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.h & 0x0F == 0x0F + cpu.h &+= 1 + cpu.f_z = cpu.h == 0 + cpu.f_n = false + return 4 + }, + # 0x25 DEC H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &-= 1 + cpu.f_z = cpu.h == 0 + cpu.f_h = cpu.h & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x26 LD H,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.h = u8 + return 8 + }, + # 0x27 DAA + ->(cpu : CPU) { + cpu.inc_pc + if cpu.f_n # last op was a subtraction + cpu.a &-= 0x60 if cpu.f_c + cpu.a &-= 0x06 if cpu.f_h + else # last op was an addition + if cpu.f_c || cpu.a > 0x99 + cpu.a &+= 0x60 + cpu.f_c = true + end + if cpu.f_h || cpu.a & 0x0F > 0x09 + cpu.a &+= 0x06 + end + end + cpu.f_z = cpu.a == 0 + cpu.f_h = false + return 4 + }, + # 0x28 JR Z,i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + if cpu.f_z + cpu.pc &+= i8 + return 12 + end + return 8 + }, + # 0x29 ADD HL,HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.hl & 0x0FFF).to_u32 + (cpu.hl & 0x0FFF) > 0x0FFF + cpu.f_c = cpu.hl > 0x7FFF + cpu.hl &+= cpu.hl + cpu.f_n = false + return 8 + }, + # 0x2A LD A,(HL+) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.memory[((cpu.hl &+= 1) &- 1)] + return 8 + }, + # 0x2B DEC HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.hl &-= 1 + return 8 + }, + # 0x2C INC L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.l & 0x0F == 0x0F + cpu.l &+= 1 + cpu.f_z = cpu.l == 0 + cpu.f_n = false + return 4 + }, + # 0x2D DEC L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &-= 1 + cpu.f_z = cpu.l == 0 + cpu.f_h = cpu.l & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x2E LD L,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.l = u8 + return 8 + }, + # 0x2F CPL + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = ~cpu.a + cpu.f_n = true + cpu.f_h = true + return 4 + }, + # 0x30 JR NC,i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + if cpu.f_nc + cpu.pc &+= i8 + return 12 + end + return 8 + }, + # 0x31 LD SP,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.sp = u16 + return 12 + }, + # 0x32 LD (HL-),A + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory[((cpu.hl &-= 1) &+ 1)] = cpu.a + return 8 + }, + # 0x33 INC SP + ->(cpu : CPU) { + cpu.inc_pc + cpu.sp &+= 1 + return 8 + }, + # 0x34 INC (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.memory_at_hl & 0x0F == 0x0F + cpu.memory_at_hl &+= 1 + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_n = false + return 12 + }, + # 0x35 DEC (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &-= 1 + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_h = cpu.memory_at_hl & 0x0F == 0x0F + cpu.f_n = true + return 12 + }, + # 0x36 LD (HL),u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.memory_at_hl = u8 + return 12 + }, + # 0x37 SCF + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_n = false + cpu.f_h = false + cpu.f_c = true + return 4 + }, + # 0x38 JR C,i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + if cpu.f_c + cpu.pc &+= i8 + return 12 + end + return 8 + }, + # 0x39 ADD HL,SP + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.hl & 0x0FFF).to_u32 + (cpu.sp & 0x0FFF) > 0x0FFF + cpu.hl &+= cpu.sp + cpu.f_c = cpu.hl < cpu.sp + cpu.f_n = false + return 8 + }, + # 0x3A LD A,(HL-) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.memory[((cpu.hl &-= 1) &+ 1)] + return 8 + }, + # 0x3B DEC SP + ->(cpu : CPU) { + cpu.inc_pc + cpu.sp &-= 1 + return 8 + }, + # 0x3C INC A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F == 0x0F + cpu.a &+= 1 + cpu.f_z = cpu.a == 0 + cpu.f_n = false + return 4 + }, + # 0x3D DEC A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &-= 1 + cpu.f_z = cpu.a == 0 + cpu.f_h = cpu.a & 0x0F == 0x0F + cpu.f_n = true + return 4 + }, + # 0x3E LD A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.a = u8 + return 8 + }, + # 0x3F CCF + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = !cpu.f_c + cpu.f_n = false + cpu.f_h = false + return 4 + }, + # 0x40 LD B,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.b + return 4 + }, + # 0x41 LD B,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.c + return 4 + }, + # 0x42 LD B,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.d + return 4 + }, + # 0x43 LD B,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.e + return 4 + }, + # 0x44 LD B,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.h + return 4 + }, + # 0x45 LD B,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.l + return 4 + }, + # 0x46 LD B,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.memory_at_hl + return 8 + }, + # 0x47 LD B,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = cpu.a + return 4 + }, + # 0x48 LD C,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.b + return 4 + }, + # 0x49 LD C,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.c + return 4 + }, + # 0x4A LD C,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.d + return 4 + }, + # 0x4B LD C,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.e + return 4 + }, + # 0x4C LD C,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.h + return 4 + }, + # 0x4D LD C,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.l + return 4 + }, + # 0x4E LD C,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.memory_at_hl + return 8 + }, + # 0x4F LD C,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = cpu.a + return 4 + }, + # 0x50 LD D,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.b + return 4 + }, + # 0x51 LD D,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.c + return 4 + }, + # 0x52 LD D,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.d + return 4 + }, + # 0x53 LD D,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.e + return 4 + }, + # 0x54 LD D,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.h + return 4 + }, + # 0x55 LD D,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.l + return 4 + }, + # 0x56 LD D,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.memory_at_hl + return 8 + }, + # 0x57 LD D,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = cpu.a + return 4 + }, + # 0x58 LD E,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.b + return 4 + }, + # 0x59 LD E,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.c + return 4 + }, + # 0x5A LD E,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.d + return 4 + }, + # 0x5B LD E,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.e + return 4 + }, + # 0x5C LD E,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.h + return 4 + }, + # 0x5D LD E,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.l + return 4 + }, + # 0x5E LD E,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.memory_at_hl + return 8 + }, + # 0x5F LD E,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = cpu.a + return 4 + }, + # 0x60 LD H,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.b + return 4 + }, + # 0x61 LD H,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.c + return 4 + }, + # 0x62 LD H,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.d + return 4 + }, + # 0x63 LD H,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.e + return 4 + }, + # 0x64 LD H,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.h + return 4 + }, + # 0x65 LD H,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.l + return 4 + }, + # 0x66 LD H,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.memory_at_hl + return 8 + }, + # 0x67 LD H,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = cpu.a + return 4 + }, + # 0x68 LD L,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.b + return 4 + }, + # 0x69 LD L,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.c + return 4 + }, + # 0x6A LD L,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.d + return 4 + }, + # 0x6B LD L,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.e + return 4 + }, + # 0x6C LD L,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.h + return 4 + }, + # 0x6D LD L,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.l + return 4 + }, + # 0x6E LD L,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.memory_at_hl + return 8 + }, + # 0x6F LD L,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = cpu.a + return 4 + }, + # 0x70 LD (HL),B + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.b + return 8 + }, + # 0x71 LD (HL),C + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.c + return 8 + }, + # 0x72 LD (HL),D + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.d + return 8 + }, + # 0x73 LD (HL),E + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.e + return 8 + }, + # 0x74 LD (HL),H + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.h + return 8 + }, + # 0x75 LD (HL),L + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.l + return 8 + }, + # 0x76 HALT + ->(cpu : CPU) { + cpu.inc_pc + cpu.halt + return 4 + }, + # 0x77 LD (HL),A + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = cpu.a + return 8 + }, + # 0x78 LD A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.b + return 4 + }, + # 0x79 LD A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.c + return 4 + }, + # 0x7A LD A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.d + return 4 + }, + # 0x7B LD A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.e + return 4 + }, + # 0x7C LD A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.h + return 4 + }, + # 0x7D LD A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.l + return 4 + }, + # 0x7E LD A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.memory_at_hl + return 8 + }, + # 0x7F LD A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.a + return 4 + }, + # 0x80 ADD A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.b & 0x0F) > 0x0F + cpu.a &+= cpu.b + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.b + cpu.f_n = false + return 4 + }, + # 0x81 ADD A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.c & 0x0F) > 0x0F + cpu.a &+= cpu.c + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.c + cpu.f_n = false + return 4 + }, + # 0x82 ADD A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.d & 0x0F) > 0x0F + cpu.a &+= cpu.d + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.d + cpu.f_n = false + return 4 + }, + # 0x83 ADD A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.e & 0x0F) > 0x0F + cpu.a &+= cpu.e + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.e + cpu.f_n = false + return 4 + }, + # 0x84 ADD A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.h & 0x0F) > 0x0F + cpu.a &+= cpu.h + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.h + cpu.f_n = false + return 4 + }, + # 0x85 ADD A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.l & 0x0F) > 0x0F + cpu.a &+= cpu.l + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.l + cpu.f_n = false + return 4 + }, + # 0x86 ADD A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.memory_at_hl & 0x0F) > 0x0F + cpu.a &+= cpu.memory_at_hl + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.memory_at_hl + cpu.f_n = false + return 8 + }, + # 0x87 ADD A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (cpu.a & 0x0F) > 0x0F + cpu.f_c = cpu.a > 0x7F + cpu.a &+= cpu.a + cpu.f_z = cpu.a == 0 + cpu.f_n = false + return 4 + }, + # 0x88 ADC A,B + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.b & 0x0F) + carry > 0x0F + cpu.a &+= cpu.b &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.b.to_u16 + carry + cpu.f_n = false + return 4 + }, + # 0x89 ADC A,C + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.c & 0x0F) + carry > 0x0F + cpu.a &+= cpu.c &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.c.to_u16 + carry + cpu.f_n = false + return 4 + }, + # 0x8A ADC A,D + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.d & 0x0F) + carry > 0x0F + cpu.a &+= cpu.d &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.d.to_u16 + carry + cpu.f_n = false + return 4 + }, + # 0x8B ADC A,E + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.e & 0x0F) + carry > 0x0F + cpu.a &+= cpu.e &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.e.to_u16 + carry + cpu.f_n = false + return 4 + }, + # 0x8C ADC A,H + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.h & 0x0F) + carry > 0x0F + cpu.a &+= cpu.h &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.h.to_u16 + carry + cpu.f_n = false + return 4 + }, + # 0x8D ADC A,L + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.l & 0x0F) + carry > 0x0F + cpu.a &+= cpu.l &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.l.to_u16 + carry + cpu.f_n = false + return 4 + }, + # 0x8E ADC A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.memory_at_hl & 0x0F) + carry > 0x0F + cpu.a &+= cpu.memory_at_hl &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < cpu.memory_at_hl.to_u16 + carry + cpu.f_n = false + return 8 + }, + # 0x8F ADC A,A + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (cpu.a & 0x0F) + carry > 0x0F + cpu.f_c = cpu.a > 0x7F + cpu.a &+= cpu.a &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_n = false + return 4 + }, + # 0x90 SUB A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.b & 0x0F + cpu.f_c = cpu.a < cpu.b + cpu.a &-= cpu.b + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x91 SUB A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.c & 0x0F + cpu.f_c = cpu.a < cpu.c + cpu.a &-= cpu.c + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x92 SUB A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.d & 0x0F + cpu.f_c = cpu.a < cpu.d + cpu.a &-= cpu.d + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x93 SUB A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.e & 0x0F + cpu.f_c = cpu.a < cpu.e + cpu.a &-= cpu.e + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x94 SUB A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.h & 0x0F + cpu.f_c = cpu.a < cpu.h + cpu.a &-= cpu.h + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x95 SUB A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.l & 0x0F + cpu.f_c = cpu.a < cpu.l + cpu.a &-= cpu.l + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x96 SUB A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.memory_at_hl & 0x0F + cpu.f_c = cpu.a < cpu.memory_at_hl + cpu.a &-= cpu.memory_at_hl + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 8 + }, + # 0x97 SUB A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < cpu.a & 0x0F + cpu.f_c = cpu.a < cpu.a + cpu.a &-= cpu.a + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x98 SBC A,B + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.b.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.b & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x99 SBC A,C + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.c.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.c & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x9A SBC A,D + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.d.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.d & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x9B SBC A,E + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.e.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.e & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x9C SBC A,H + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.h.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.h & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x9D SBC A,L + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.l.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.l & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0x9E SBC A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.memory_at_hl.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.memory_at_hl & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 8 + }, + # 0x9F SBC A,A + ->(cpu : CPU) { + cpu.inc_pc + to_sub = cpu.a.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (cpu.a & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 4 + }, + # 0xA0 AND A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.b + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA1 AND A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.c + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA2 AND A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.d + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA3 AND A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.e + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA4 AND A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.h + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA5 AND A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.l + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA6 AND A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.memory_at_hl + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 8 + }, + # 0xA7 AND A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= cpu.a + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 4 + }, + # 0xA8 XOR A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.b + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xA9 XOR A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.c + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xAA XOR A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.d + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xAB XOR A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.e + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xAC XOR A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.h + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xAD XOR A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.l + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xAE XOR A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.memory_at_hl + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0xAF XOR A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a ^= cpu.a + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB0 OR A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.b + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB1 OR A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.c + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB2 OR A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.d + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB3 OR A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.e + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB4 OR A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.h + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB5 OR A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.l + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB6 OR A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.memory_at_hl + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0xB7 OR A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= cpu.a + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 4 + }, + # 0xB8 CP A,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.b == 0 + cpu.f_h = cpu.a & 0xF < cpu.b & 0xF + cpu.f_c = cpu.a < cpu.b + cpu.f_n = true + return 4 + }, + # 0xB9 CP A,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.c == 0 + cpu.f_h = cpu.a & 0xF < cpu.c & 0xF + cpu.f_c = cpu.a < cpu.c + cpu.f_n = true + return 4 + }, + # 0xBA CP A,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.d == 0 + cpu.f_h = cpu.a & 0xF < cpu.d & 0xF + cpu.f_c = cpu.a < cpu.d + cpu.f_n = true + return 4 + }, + # 0xBB CP A,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.e == 0 + cpu.f_h = cpu.a & 0xF < cpu.e & 0xF + cpu.f_c = cpu.a < cpu.e + cpu.f_n = true + return 4 + }, + # 0xBC CP A,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.h == 0 + cpu.f_h = cpu.a & 0xF < cpu.h & 0xF + cpu.f_c = cpu.a < cpu.h + cpu.f_n = true + return 4 + }, + # 0xBD CP A,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.l == 0 + cpu.f_h = cpu.a & 0xF < cpu.l & 0xF + cpu.f_c = cpu.a < cpu.l + cpu.f_n = true + return 4 + }, + # 0xBE CP A,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.memory_at_hl == 0 + cpu.f_h = cpu.a & 0xF < cpu.memory_at_hl & 0xF + cpu.f_c = cpu.a < cpu.memory_at_hl + cpu.f_n = true + return 8 + }, + # 0xBF CP A,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a &- cpu.a == 0 + cpu.f_h = cpu.a & 0xF < cpu.a & 0xF + cpu.f_c = cpu.a < cpu.a + cpu.f_n = true + return 4 + }, + # 0xC0 RET NZ + ->(cpu : CPU) { + cpu.inc_pc + if cpu.f_nz + cpu.memory.tick_components + cpu.pc = cpu.memory.read_word cpu.sp + cpu.sp += 2 + return 20 + end + return 8 + }, + # 0xC1 POP BC + ->(cpu : CPU) { + cpu.inc_pc + cpu.bc = cpu.memory.read_word (cpu.sp += 2) - 2 + return 12 + }, + # 0xC2 JP NZ,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_nz + cpu.pc = u16 + return 16 + end + return 12 + }, + # 0xC3 JP u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.pc = u16 + return 16 + }, + # 0xC4 CALL NZ,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_nz + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = u16 + return 24 + end + return 12 + }, + # 0xC5 PUSH BC + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.bc + return 16 + }, + # 0xC6 ADD A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.f_h = (cpu.a & 0x0F) + (u8 & 0x0F) > 0x0F + cpu.a &+= u8 + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < u8 + cpu.f_n = false + return 8 + }, + # 0xC7 RST 00h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x00_u16 + return 16 + }, + # 0xC8 RET Z + ->(cpu : CPU) { + cpu.inc_pc + if cpu.f_z + cpu.memory.tick_components + cpu.pc = cpu.memory.read_word cpu.sp + cpu.sp += 2 + return 20 + end + return 8 + }, + # 0xC9 RET + ->(cpu : CPU) { + cpu.inc_pc + cpu.pc = cpu.memory.read_word cpu.sp + cpu.sp += 2 + return 16 + }, + # 0xCA JP Z,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_z + cpu.pc = u16 + return 16 + end + return 12 + }, + # 0xCB PREFIX CB + ->(cpu : CPU) { + cpu.inc_pc + # todo: This should operate as a seperate instruction, but can't be interrupted. + # This will require a restructure where the CPU leads the timing, rather than the PPU. + # https://discordapp.com/channels/465585922579103744/465586075830845475/712358911151177818 + # https://discordapp.com/channels/465585922579103744/465586075830845475/712359253255520328 + cycles = Opcodes::PREFIXED[cpu.memory[cpu.pc]].call cpu + return cycles + return 4 + }, + # 0xCC CALL Z,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_z + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = u16 + return 24 + end + return 12 + }, + # 0xCD CALL u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = u16 + return 24 + }, + # 0xCE ADC A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + carry = cpu.f_c ? 0x01 : 0x00 + cpu.f_h = (cpu.a & 0x0F) + (u8 & 0x0F) + carry > 0x0F + cpu.a &+= u8 &+ carry + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a < u8.to_u16 + carry + cpu.f_n = false + return 8 + }, + # 0xCF RST 08h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x08_u16 + return 16 + }, + # 0xD0 RET NC + ->(cpu : CPU) { + cpu.inc_pc + if cpu.f_nc + cpu.memory.tick_components + cpu.pc = cpu.memory.read_word cpu.sp + cpu.sp += 2 + return 20 + end + return 8 + }, + # 0xD1 POP DE + ->(cpu : CPU) { + cpu.inc_pc + cpu.de = cpu.memory.read_word (cpu.sp += 2) - 2 + return 12 + }, + # 0xD2 JP NC,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_nc + cpu.pc = u16 + return 16 + end + return 12 + }, + # 0xD3 UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xD4 CALL NC,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_nc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = u16 + return 24 + end + return 12 + }, + # 0xD5 PUSH DE + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.de + return 16 + }, + # 0xD6 SUB A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.f_h = cpu.a & 0x0F < u8 & 0x0F + cpu.f_c = cpu.a < u8 + cpu.a &-= u8 + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 8 + }, + # 0xD7 RST 10h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x10_u16 + return 16 + }, + # 0xD8 RET C + ->(cpu : CPU) { + cpu.inc_pc + if cpu.f_c + cpu.memory.tick_components + cpu.pc = cpu.memory.read_word cpu.sp + cpu.sp += 2 + return 20 + end + return 8 + }, + # 0xD9 RETI + ->(cpu : CPU) { + cpu.inc_pc + cpu.ime = true + cpu.pc = cpu.memory.read_word cpu.sp + cpu.sp += 0x02 + return 16 + }, + # 0xDA JP C,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_c + cpu.pc = u16 + return 16 + end + return 12 + }, + # 0xDB UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xDC CALL C,u16 + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + if cpu.f_c + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = u16 + return 24 + end + return 12 + }, + # 0xDD UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xDE SBC A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + to_sub = u8.to_u16 + (cpu.f_c ? 0x01 : 0x00) + cpu.f_h = (cpu.a & 0x0F) < (u8 & 0x0F) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_c = cpu.a < to_sub + cpu.a &-= to_sub + cpu.f_z = cpu.a == 0 + cpu.f_n = true + return 8 + }, + # 0xDF RST 18h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x18_u16 + return 16 + }, + # 0xE0 LD (FF00+u8),A + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.memory[0xFF00 &+ u8] = cpu.a + return 12 + }, + # 0xE1 POP HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.hl = cpu.memory.read_word (cpu.sp += 2) - 2 + return 12 + }, + # 0xE2 LD (FF00+C),A + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory[0xFF00 &+ cpu.c] = cpu.a + return 8 + }, + # 0xE3 UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xE4 UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xE5 PUSH HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.hl + return 16 + }, + # 0xE6 AND A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.a &= u8 + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = true + cpu.f_c = false + return 8 + }, + # 0xE7 RST 20h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x20_u16 + return 16 + }, + # 0xE8 ADD SP,i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + r = cpu.sp &+ i8 + cpu.f_h = (cpu.sp ^ i8 ^ r) & 0x0010 != 0 + cpu.f_c = (cpu.sp ^ i8 ^ r) & 0x0100 != 0 + cpu.sp = r + cpu.f_z = false + cpu.f_n = false + return 16 + }, + # 0xE9 JP HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.pc = cpu.hl + return 4 + }, + # 0xEA LD (u16),A + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.memory[u16] = cpu.a + return 16 + }, + # 0xEB UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xEC UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xED UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xEE XOR A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.a ^= u8 + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0xEF RST 28h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x28_u16 + return 16 + }, + # 0xF0 LD A,(FF00+u8) + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.a = cpu.memory[0xFF00 &+ u8] + return 12 + }, + # 0xF1 POP AF + ->(cpu : CPU) { + cpu.inc_pc + cpu.af = cpu.memory.read_word (cpu.sp += 2) - 2 + cpu.f_z = cpu.af & (0x1 << 7) + cpu.f_n = cpu.af & (0x1 << 6) + cpu.f_h = cpu.af & (0x1 << 5) + cpu.f_c = cpu.af & (0x1 << 4) + return 12 + }, + # 0xF2 LD A,(FF00+C) + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = cpu.memory[0xFF00 &+ cpu.c] + return 8 + }, + # 0xF3 DI + ->(cpu : CPU) { + cpu.inc_pc + cpu.ime = false + return 4 + }, + # 0xF4 UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xF5 PUSH AF + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.af + return 16 + }, + # 0xF6 OR A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.a |= u8 + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0xF7 RST 30h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x30_u16 + return 16 + }, + # 0xF8 LD HL,SP+i8 + ->(cpu : CPU) { + cpu.inc_pc + i8 = cpu.memory[cpu.pc].to_i8! + cpu.inc_pc + cpu.hl = cpu.sp &+ i8 + cpu.f_h = (cpu.sp ^ i8 ^ cpu.hl) & 0x0010 != 0 + cpu.f_c = (cpu.sp ^ i8 ^ cpu.hl) & 0x0100 != 0 + cpu.f_z = false + cpu.f_n = false + return 12 + }, + # 0xF9 LD SP,HL + ->(cpu : CPU) { + cpu.inc_pc + cpu.sp = cpu.hl + return 8 + }, + # 0xFA LD A,(u16) + ->(cpu : CPU) { + cpu.inc_pc + u16 = cpu.memory[cpu.pc].to_u16 + cpu.inc_pc + u16 |= cpu.memory[cpu.pc].to_u16 << 8 + cpu.inc_pc + cpu.a = cpu.memory[u16] + return 16 + }, + # 0xFB EI + ->(cpu : CPU) { + cpu.inc_pc + cpu.scheduler.schedule(4, Scheduler::EventType::IME) { cpu.ime = true } + return 4 + }, + # 0xFC UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xFD UNUSED + ->(cpu : CPU) { + cpu.inc_pc + # unused opcode + return 0 + }, + # 0xFE CP A,u8 + ->(cpu : CPU) { + cpu.inc_pc + u8 = cpu.memory[cpu.pc] + cpu.inc_pc + cpu.f_z = cpu.a &- u8 == 0 + cpu.f_h = cpu.a & 0xF < u8 & 0xF + cpu.f_c = cpu.a < u8 + cpu.f_n = true + return 8 + }, + # 0xFF RST 38h + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory.tick_components + cpu.memory[cpu.sp -= 2] = cpu.pc + cpu.pc = 0x38_u16 + return 16 + }, + ] + PREFIXED = [ + # 0x00 RLC B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = (cpu.b << 1) + (cpu.b >> 7) + cpu.f_z = cpu.b == 0 + cpu.f_c = cpu.b & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x01 RLC C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = (cpu.c << 1) + (cpu.c >> 7) + cpu.f_z = cpu.c == 0 + cpu.f_c = cpu.c & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x02 RLC D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = (cpu.d << 1) + (cpu.d >> 7) + cpu.f_z = cpu.d == 0 + cpu.f_c = cpu.d & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x03 RLC E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = (cpu.e << 1) + (cpu.e >> 7) + cpu.f_z = cpu.e == 0 + cpu.f_c = cpu.e & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x04 RLC H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = (cpu.h << 1) + (cpu.h >> 7) + cpu.f_z = cpu.h == 0 + cpu.f_c = cpu.h & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x05 RLC L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = (cpu.l << 1) + (cpu.l >> 7) + cpu.f_z = cpu.l == 0 + cpu.f_c = cpu.l & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x06 RLC (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = (cpu.memory_at_hl << 1) + (cpu.memory_at_hl >> 7) + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_c = cpu.memory_at_hl & 0x01 + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x07 RLC A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = (cpu.a << 1) + (cpu.a >> 7) + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a & 0x01 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x08 RRC B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = (cpu.b >> 1) + (cpu.b << 7) + cpu.f_z = cpu.b == 0 + cpu.f_c = cpu.b & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x09 RRC C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = (cpu.c >> 1) + (cpu.c << 7) + cpu.f_z = cpu.c == 0 + cpu.f_c = cpu.c & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x0A RRC D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = (cpu.d >> 1) + (cpu.d << 7) + cpu.f_z = cpu.d == 0 + cpu.f_c = cpu.d & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x0B RRC E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = (cpu.e >> 1) + (cpu.e << 7) + cpu.f_z = cpu.e == 0 + cpu.f_c = cpu.e & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x0C RRC H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = (cpu.h >> 1) + (cpu.h << 7) + cpu.f_z = cpu.h == 0 + cpu.f_c = cpu.h & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x0D RRC L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = (cpu.l >> 1) + (cpu.l << 7) + cpu.f_z = cpu.l == 0 + cpu.f_c = cpu.l & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x0E RRC (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = (cpu.memory_at_hl >> 1) + (cpu.memory_at_hl << 7) + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_c = cpu.memory_at_hl & 0x80 + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x0F RRC A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = (cpu.a >> 1) + (cpu.a << 7) + cpu.f_z = cpu.a == 0 + cpu.f_c = cpu.a & 0x80 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x10 RL B + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.b & 0x80 + cpu.b = (cpu.b << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.b == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x11 RL C + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.c & 0x80 + cpu.c = (cpu.c << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.c == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x12 RL D + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.d & 0x80 + cpu.d = (cpu.d << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.d == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x13 RL E + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.e & 0x80 + cpu.e = (cpu.e << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.e == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x14 RL H + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.h & 0x80 + cpu.h = (cpu.h << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.h == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x15 RL L + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.l & 0x80 + cpu.l = (cpu.l << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.l == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x16 RL (HL) + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.memory_at_hl & 0x80 + cpu.memory_at_hl = (cpu.memory_at_hl << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x17 RL A + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.a & 0x80 + cpu.a = (cpu.a << 1) + (cpu.f_c ? 0x01 : 0x00) + cpu.f_z = cpu.a == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x18 RR B + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.b & 0x01 + cpu.b = (cpu.b >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.b == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x19 RR C + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.c & 0x01 + cpu.c = (cpu.c >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.c == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x1A RR D + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.d & 0x01 + cpu.d = (cpu.d >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.d == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x1B RR E + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.e & 0x01 + cpu.e = (cpu.e >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.e == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x1C RR H + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.h & 0x01 + cpu.h = (cpu.h >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.h == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x1D RR L + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.l & 0x01 + cpu.l = (cpu.l >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.l == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x1E RR (HL) + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.memory_at_hl & 0x01 + cpu.memory_at_hl = (cpu.memory_at_hl >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x1F RR A + ->(cpu : CPU) { + cpu.inc_pc + carry = cpu.a & 0x01 + cpu.a = (cpu.a >> 1) + (cpu.f_c ? 0x80 : 0x00) + cpu.f_z = cpu.a == 0 + cpu.f_c = carry + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x20 SLA B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.b & 0x80 + cpu.b <<= 1 + cpu.f_z = cpu.b == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x21 SLA C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.c & 0x80 + cpu.c <<= 1 + cpu.f_z = cpu.c == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x22 SLA D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.d & 0x80 + cpu.d <<= 1 + cpu.f_z = cpu.d == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x23 SLA E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.e & 0x80 + cpu.e <<= 1 + cpu.f_z = cpu.e == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x24 SLA H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.h & 0x80 + cpu.h <<= 1 + cpu.f_z = cpu.h == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x25 SLA L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.l & 0x80 + cpu.l <<= 1 + cpu.f_z = cpu.l == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x26 SLA (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.memory_at_hl & 0x80 + cpu.memory_at_hl <<= 1 + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x27 SLA A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.a & 0x80 + cpu.a <<= 1 + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x28 SRA B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.b & 0x01 + cpu.b = (cpu.b >> 1) + (cpu.b & 0x80) + cpu.f_z = cpu.b == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x29 SRA C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.c & 0x01 + cpu.c = (cpu.c >> 1) + (cpu.c & 0x80) + cpu.f_z = cpu.c == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x2A SRA D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.d & 0x01 + cpu.d = (cpu.d >> 1) + (cpu.d & 0x80) + cpu.f_z = cpu.d == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x2B SRA E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.e & 0x01 + cpu.e = (cpu.e >> 1) + (cpu.e & 0x80) + cpu.f_z = cpu.e == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x2C SRA H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.h & 0x01 + cpu.h = (cpu.h >> 1) + (cpu.h & 0x80) + cpu.f_z = cpu.h == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x2D SRA L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.l & 0x01 + cpu.l = (cpu.l >> 1) + (cpu.l & 0x80) + cpu.f_z = cpu.l == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x2E SRA (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.memory_at_hl & 0x01 + cpu.memory_at_hl = (cpu.memory_at_hl >> 1) + (cpu.memory_at_hl & 0x80) + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x2F SRA A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.a & 0x01 + cpu.a = (cpu.a >> 1) + (cpu.a & 0x80) + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x30 SWAP B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b = (cpu.b << 4) + (cpu.b >> 4) + cpu.f_z = cpu.b == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x31 SWAP C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c = (cpu.c << 4) + (cpu.c >> 4) + cpu.f_z = cpu.c == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x32 SWAP D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d = (cpu.d << 4) + (cpu.d >> 4) + cpu.f_z = cpu.d == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x33 SWAP E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e = (cpu.e << 4) + (cpu.e >> 4) + cpu.f_z = cpu.e == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x34 SWAP H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h = (cpu.h << 4) + (cpu.h >> 4) + cpu.f_z = cpu.h == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x35 SWAP L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l = (cpu.l << 4) + (cpu.l >> 4) + cpu.f_z = cpu.l == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x36 SWAP (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl = (cpu.memory_at_hl << 4) + (cpu.memory_at_hl >> 4) + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 16 + }, + # 0x37 SWAP A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a = (cpu.a << 4) + (cpu.a >> 4) + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + cpu.f_c = false + return 8 + }, + # 0x38 SRL B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.b & 0x1 + cpu.b >>= 1 + cpu.f_z = cpu.b == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x39 SRL C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.c & 0x1 + cpu.c >>= 1 + cpu.f_z = cpu.c == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x3A SRL D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.d & 0x1 + cpu.d >>= 1 + cpu.f_z = cpu.d == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x3B SRL E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.e & 0x1 + cpu.e >>= 1 + cpu.f_z = cpu.e == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x3C SRL H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.h & 0x1 + cpu.h >>= 1 + cpu.f_z = cpu.h == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x3D SRL L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.l & 0x1 + cpu.l >>= 1 + cpu.f_z = cpu.l == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x3E SRL (HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.memory_at_hl & 0x1 + cpu.memory_at_hl >>= 1 + cpu.f_z = cpu.memory_at_hl == 0 + cpu.f_n = false + cpu.f_h = false + return 16 + }, + # 0x3F SRL A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_c = cpu.a & 0x1 + cpu.a >>= 1 + cpu.f_z = cpu.a == 0 + cpu.f_n = false + cpu.f_h = false + return 8 + }, + # 0x40 BIT 0,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x41 BIT 0,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x42 BIT 0,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x43 BIT 0,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x44 BIT 0,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x45 BIT 0,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x46 BIT 0,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x47 BIT 0,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 0) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x48 BIT 1,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x49 BIT 1,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x4A BIT 1,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x4B BIT 1,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x4C BIT 1,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x4D BIT 1,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x4E BIT 1,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x4F BIT 1,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 1) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x50 BIT 2,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x51 BIT 2,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x52 BIT 2,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x53 BIT 2,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x54 BIT 2,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x55 BIT 2,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x56 BIT 2,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x57 BIT 2,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 2) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x58 BIT 3,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x59 BIT 3,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x5A BIT 3,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x5B BIT 3,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x5C BIT 3,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x5D BIT 3,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x5E BIT 3,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x5F BIT 3,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 3) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x60 BIT 4,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x61 BIT 4,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x62 BIT 4,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x63 BIT 4,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x64 BIT 4,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x65 BIT 4,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x66 BIT 4,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x67 BIT 4,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 4) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x68 BIT 5,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x69 BIT 5,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x6A BIT 5,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x6B BIT 5,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x6C BIT 5,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x6D BIT 5,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x6E BIT 5,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x6F BIT 5,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 5) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x70 BIT 6,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x71 BIT 6,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x72 BIT 6,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x73 BIT 6,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x74 BIT 6,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x75 BIT 6,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x76 BIT 6,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x77 BIT 6,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 6) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x78 BIT 7,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.b & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x79 BIT 7,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.c & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x7A BIT 7,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.d & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x7B BIT 7,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.e & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x7C BIT 7,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.h & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x7D BIT 7,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.l & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x7E BIT 7,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.memory_at_hl & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 12 + }, + # 0x7F BIT 7,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.f_z = cpu.a & (0x1 << 7) == 0 + cpu.f_n = false + cpu.f_h = true + return 8 + }, + # 0x80 RES 0,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 0) + return 8 + }, + # 0x81 RES 0,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 0) + return 8 + }, + # 0x82 RES 0,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 0) + return 8 + }, + # 0x83 RES 0,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 0) + return 8 + }, + # 0x84 RES 0,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 0) + return 8 + }, + # 0x85 RES 0,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 0) + return 8 + }, + # 0x86 RES 0,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 0) + return 16 + }, + # 0x87 RES 0,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 0) + return 8 + }, + # 0x88 RES 1,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 1) + return 8 + }, + # 0x89 RES 1,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 1) + return 8 + }, + # 0x8A RES 1,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 1) + return 8 + }, + # 0x8B RES 1,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 1) + return 8 + }, + # 0x8C RES 1,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 1) + return 8 + }, + # 0x8D RES 1,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 1) + return 8 + }, + # 0x8E RES 1,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 1) + return 16 + }, + # 0x8F RES 1,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 1) + return 8 + }, + # 0x90 RES 2,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 2) + return 8 + }, + # 0x91 RES 2,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 2) + return 8 + }, + # 0x92 RES 2,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 2) + return 8 + }, + # 0x93 RES 2,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 2) + return 8 + }, + # 0x94 RES 2,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 2) + return 8 + }, + # 0x95 RES 2,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 2) + return 8 + }, + # 0x96 RES 2,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 2) + return 16 + }, + # 0x97 RES 2,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 2) + return 8 + }, + # 0x98 RES 3,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 3) + return 8 + }, + # 0x99 RES 3,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 3) + return 8 + }, + # 0x9A RES 3,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 3) + return 8 + }, + # 0x9B RES 3,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 3) + return 8 + }, + # 0x9C RES 3,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 3) + return 8 + }, + # 0x9D RES 3,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 3) + return 8 + }, + # 0x9E RES 3,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 3) + return 16 + }, + # 0x9F RES 3,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 3) + return 8 + }, + # 0xA0 RES 4,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 4) + return 8 + }, + # 0xA1 RES 4,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 4) + return 8 + }, + # 0xA2 RES 4,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 4) + return 8 + }, + # 0xA3 RES 4,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 4) + return 8 + }, + # 0xA4 RES 4,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 4) + return 8 + }, + # 0xA5 RES 4,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 4) + return 8 + }, + # 0xA6 RES 4,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 4) + return 16 + }, + # 0xA7 RES 4,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 4) + return 8 + }, + # 0xA8 RES 5,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 5) + return 8 + }, + # 0xA9 RES 5,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 5) + return 8 + }, + # 0xAA RES 5,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 5) + return 8 + }, + # 0xAB RES 5,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 5) + return 8 + }, + # 0xAC RES 5,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 5) + return 8 + }, + # 0xAD RES 5,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 5) + return 8 + }, + # 0xAE RES 5,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 5) + return 16 + }, + # 0xAF RES 5,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 5) + return 8 + }, + # 0xB0 RES 6,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 6) + return 8 + }, + # 0xB1 RES 6,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 6) + return 8 + }, + # 0xB2 RES 6,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 6) + return 8 + }, + # 0xB3 RES 6,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 6) + return 8 + }, + # 0xB4 RES 6,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 6) + return 8 + }, + # 0xB5 RES 6,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 6) + return 8 + }, + # 0xB6 RES 6,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 6) + return 16 + }, + # 0xB7 RES 6,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 6) + return 8 + }, + # 0xB8 RES 7,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b &= ~(0x1 << 7) + return 8 + }, + # 0xB9 RES 7,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c &= ~(0x1 << 7) + return 8 + }, + # 0xBA RES 7,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d &= ~(0x1 << 7) + return 8 + }, + # 0xBB RES 7,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e &= ~(0x1 << 7) + return 8 + }, + # 0xBC RES 7,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h &= ~(0x1 << 7) + return 8 + }, + # 0xBD RES 7,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l &= ~(0x1 << 7) + return 8 + }, + # 0xBE RES 7,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl &= ~(0x1 << 7) + return 16 + }, + # 0xBF RES 7,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a &= ~(0x1 << 7) + return 8 + }, + # 0xC0 SET 0,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 0) + return 8 + }, + # 0xC1 SET 0,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 0) + return 8 + }, + # 0xC2 SET 0,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 0) + return 8 + }, + # 0xC3 SET 0,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 0) + return 8 + }, + # 0xC4 SET 0,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 0) + return 8 + }, + # 0xC5 SET 0,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 0) + return 8 + }, + # 0xC6 SET 0,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 0) + return 16 + }, + # 0xC7 SET 0,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 0) + return 8 + }, + # 0xC8 SET 1,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 1) + return 8 + }, + # 0xC9 SET 1,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 1) + return 8 + }, + # 0xCA SET 1,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 1) + return 8 + }, + # 0xCB SET 1,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 1) + return 8 + }, + # 0xCC SET 1,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 1) + return 8 + }, + # 0xCD SET 1,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 1) + return 8 + }, + # 0xCE SET 1,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 1) + return 16 + }, + # 0xCF SET 1,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 1) + return 8 + }, + # 0xD0 SET 2,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 2) + return 8 + }, + # 0xD1 SET 2,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 2) + return 8 + }, + # 0xD2 SET 2,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 2) + return 8 + }, + # 0xD3 SET 2,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 2) + return 8 + }, + # 0xD4 SET 2,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 2) + return 8 + }, + # 0xD5 SET 2,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 2) + return 8 + }, + # 0xD6 SET 2,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 2) + return 16 + }, + # 0xD7 SET 2,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 2) + return 8 + }, + # 0xD8 SET 3,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 3) + return 8 + }, + # 0xD9 SET 3,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 3) + return 8 + }, + # 0xDA SET 3,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 3) + return 8 + }, + # 0xDB SET 3,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 3) + return 8 + }, + # 0xDC SET 3,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 3) + return 8 + }, + # 0xDD SET 3,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 3) + return 8 + }, + # 0xDE SET 3,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 3) + return 16 + }, + # 0xDF SET 3,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 3) + return 8 + }, + # 0xE0 SET 4,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 4) + return 8 + }, + # 0xE1 SET 4,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 4) + return 8 + }, + # 0xE2 SET 4,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 4) + return 8 + }, + # 0xE3 SET 4,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 4) + return 8 + }, + # 0xE4 SET 4,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 4) + return 8 + }, + # 0xE5 SET 4,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 4) + return 8 + }, + # 0xE6 SET 4,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 4) + return 16 + }, + # 0xE7 SET 4,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 4) + return 8 + }, + # 0xE8 SET 5,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 5) + return 8 + }, + # 0xE9 SET 5,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 5) + return 8 + }, + # 0xEA SET 5,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 5) + return 8 + }, + # 0xEB SET 5,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 5) + return 8 + }, + # 0xEC SET 5,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 5) + return 8 + }, + # 0xED SET 5,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 5) + return 8 + }, + # 0xEE SET 5,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 5) + return 16 + }, + # 0xEF SET 5,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 5) + return 8 + }, + # 0xF0 SET 6,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 6) + return 8 + }, + # 0xF1 SET 6,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 6) + return 8 + }, + # 0xF2 SET 6,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 6) + return 8 + }, + # 0xF3 SET 6,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 6) + return 8 + }, + # 0xF4 SET 6,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 6) + return 8 + }, + # 0xF5 SET 6,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 6) + return 8 + }, + # 0xF6 SET 6,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 6) + return 16 + }, + # 0xF7 SET 6,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 6) + return 8 + }, + # 0xF8 SET 7,B + ->(cpu : CPU) { + cpu.inc_pc + cpu.b |= (0x1 << 7) + return 8 + }, + # 0xF9 SET 7,C + ->(cpu : CPU) { + cpu.inc_pc + cpu.c |= (0x1 << 7) + return 8 + }, + # 0xFA SET 7,D + ->(cpu : CPU) { + cpu.inc_pc + cpu.d |= (0x1 << 7) + return 8 + }, + # 0xFB SET 7,E + ->(cpu : CPU) { + cpu.inc_pc + cpu.e |= (0x1 << 7) + return 8 + }, + # 0xFC SET 7,H + ->(cpu : CPU) { + cpu.inc_pc + cpu.h |= (0x1 << 7) + return 8 + }, + # 0xFD SET 7,L + ->(cpu : CPU) { + cpu.inc_pc + cpu.l |= (0x1 << 7) + return 8 + }, + # 0xFE SET 7,(HL) + ->(cpu : CPU) { + cpu.inc_pc + cpu.memory_at_hl |= (0x1 << 7) + return 16 + }, + # 0xFF SET 7,A + ->(cpu : CPU) { + cpu.inc_pc + cpu.a |= (0x1 << 7) + return 8 + }, + ] + end +end diff --git a/src/crab/gb/ppu.cr b/src/crab/gb/ppu.cr new file mode 100644 index 0000000..955d9dd --- /dev/null +++ b/src/crab/gb/ppu.cr @@ -0,0 +1,469 @@ + # This file is simply designed to hold shared features of the scanline and FIFO + # renderers while the FIFO renderer is in active development. The purpose of + # this file is solely to reduce common changes to both renderers. + + module GB + struct Sprite + getter oam_idx : UInt8 = 0_u8 + + def initialize(@y : UInt8, @x : UInt8, @tile_num : UInt8, @attributes : UInt8) + end + + def initialize(oam : Bytes, @oam_idx : UInt8) + initialize oam[oam_idx], oam[oam_idx + 1], oam[oam_idx + 2], oam[oam_idx + 3] + end + + def to_s(io : IO) + io << "Sprite(y:#{@y}, x:#{@x}, tile_num:#{@tile_num}, tile_ptr: #{hex_str tile_ptr}, visible:#{visible?}, priority:#{priority}, y_flip:#{y_flip?}, x_flip:#{x_flip?}, dmg_palette_number:#{dmg_palette_numpalette_number}" + end + + def on_line(line : Int, sprite_height = 8) : Bool + y <= line + 16 < y + sprite_height + end + + # behavior is undefined if sprite is not on given line + def bytes(line : Int, sprite_height = 8) : Tuple(UInt16, UInt16) + actual_y = -16 + y + if sprite_height == 8 + tile_ptr = 16_u16 * @tile_num + else # 8x16 tile + if (actual_y + 8 <= line) ^ y_flip? + tile_ptr = 16_u16 * (@tile_num | 0x01) + else + tile_ptr = 16_u16 * (@tile_num & 0xFE) + end + end + sprite_row = (line.to_i16 - actual_y) & 7 + if y_flip? + {tile_ptr + (7 - sprite_row) * 2, tile_ptr + (7 - sprite_row) * 2 + 1} + else + {tile_ptr + sprite_row * 2, tile_ptr + sprite_row * 2 + 1} + end + end + + def visible? : Bool + ((1...160).includes? y) && ((1...168).includes? x) + end + + def y : UInt8 + @y + end + + def x : UInt8 + @x + end + + def priority : UInt8 + (@attributes >> 7) & 0x1 + end + + def y_flip? : Bool + (@attributes >> 6) & 0x1 == 1 + end + + def x_flip? : Bool + (@attributes >> 5) & 0x1 == 1 + end + + def dmg_palette_number : UInt8 + (@attributes >> 4) & 0x1 + end + + def bank_num : UInt8 + (@attributes >> 3) & 0x1 + end + + def cgb_palette_number : UInt8 + @attributes & 0b111 + end + end + + struct RGB + property red, green, blue + + def initialize(@red : UInt8, @green : UInt8, @blue : UInt8) + end + + def initialize(grey : UInt8) + @red = @green = @blue = grey + end + + def self.from_bgr16(bgr : UInt16, should_convert : Bool) : RGB + color = RGB.new( + 0x1F_u8 & bgr, + 0x1F_u8 & (bgr >> 5), + 0x1F_u8 & (bgr >> 10) + ) + should_convert ? color.convert_from_cgb : color + end + + def convert_from_cgb : RGB + {% unless flag? :graphics_test %} + # correction algorithm from: https://byuu.net/video/color-emulation + RGB.new( + Math.min(240, (26_u32 * @red + 4_u32 * @green + 2_u32 * @blue) >> 2).to_u8, + Math.min(240, (24_u32 * @green + 8_u32 * @blue) >> 2).to_u8, + Math.min(240, (6_u32 * @red + 4_u32 * @green + 22_u32 * @blue) >> 2).to_u8 + ) + {% else %} + # documented in https://github.com/mattcurrie/mealybug-tearoom-tests + RGB.new( + @red << 3 | @red >> 2, + @green << 3 | @green >> 2, + @blue << 3 | @blue >> 2 + ) + {% end %} + end + end + + POST_BOOT_VRAM = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0xF0, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, + 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCF, 0x00, 0xCF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC3, 0x00, 0xC3, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x3C, 0x00, 0x3C, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, + 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFC, 0x00, + 0xFC, 0x00, 0xFC, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF3, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xCF, 0x00, 0xC3, 0x00, 0xC3, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0xFC, 0x00, 0xFC, 0x00, + 0x3C, 0x00, 0x42, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0xB9, 0x00, 0xA5, 0x00, 0x42, 0x00, 0x3C, 0x00, + ] + + abstract class PPU + @ran_bios : Bool # determine if colors should be adjusted for cgb + @cgb_ptr : Pointer(Bool) + + @framebuffer = Array(RGB).new Display::WIDTH * Display::HEIGHT, RGB.new(0, 0, 0) + + @pram = Bytes.new 64 + @palettes = Array(Array(RGB)).new 8 { Array(GB::RGB).new 4, GB::RGB.new(0, 0, 0) } + @palette_index : UInt8 = 0 + @auto_increment = false + + @obj_pram = Bytes.new 64 + @obj_palettes = Array(Array(RGB)).new 8 { Array(GB::RGB).new 4, GB::RGB.new(0, 0, 0) } + @obj_palette_index : UInt8 = 0 + @obj_auto_increment = false + + @vram = Array(Bytes).new 2 { Bytes.new GB::Memory::VRAM.size } # 0x8000..0x9FFF + @vram_bank : UInt8 = 0 # track which bank is active + @sprite_table = Bytes.new Memory::OAM.size # 0xFE00..0xFE9F + @lcd_control : UInt8 = 0x00_u8 # 0xFF40 + @lcd_status : UInt8 = 0x80_u8 # 0xFF41 + @scy : UInt8 = 0x00_u8 # 0xFF42 + @scx : UInt8 = 0x00_u8 # 0xFF43 + @ly : UInt8 = 0x00_u8 # 0xFF44 + @lyc : UInt8 = 0x00_u8 # 0xFF45 + @dma : UInt8 = 0x00_u8 # 0xFF46 + @bgp : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF47 + @obp0 : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF48 + @obp1 : Array(UInt8) = Array(UInt8).new 4, 0 # 0xFF49 + @wy : UInt8 = 0x00_u8 # 0xFF4A + @wx : UInt8 = 0x00_u8 # 0xFF4B + + # At some point, wy _must_ equal ly to enable the window + @window_trigger = false + @current_window_line = 0 + + @old_stat_flag = false + + @hdma1 : UInt8 = 0xFF + @hdma2 : UInt8 = 0xFF + @hdma3 : UInt8 = 0xFF + @hdma4 : UInt8 = 0xFF + @hdma5 : UInt8 = 0xFF + @hdma_src : UInt16 = 0x0000 + @hdma_dst : UInt16 = 0x8000 + @hdma_pos : UInt16 = 0x0000 + @hdma_active : Bool = false + + @first_line = true + + # count number of cycles into current line on fifo, or the number of cycles into the current mode on scanline + @cycle_counter : Int32 = 0 + + def initialize(@gb : GB) + @cgb_ptr = gb.cgb_ptr + unless @cgb_ptr.value # fill default color palettes + {% if flag? :pink %} + @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [ + RGB.new(0xFF, 0xF6, 0xD3), RGB.new(0xF9, 0xA8, 0x75), + RGB.new(0xEB, 0x6B, 0x6F), RGB.new(0x7C, 0x3F, 0x58), + ] + {% elsif flag? :graphics_test %} + @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [ + RGB.new(0xFF, 0xFF, 0xFF), RGB.new(0xAA, 0xAA, 0xAA), + RGB.new(0x55, 0x55, 0x55), RGB.new(0x00, 0x00, 0x00), + ] + {% else %} + @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [ + RGB.new(0xE0, 0xF8, 0xCF), RGB.new(0x86, 0xC0, 0x6C), + RGB.new(0x30, 0x68, 0x50), RGB.new(0x07, 0x17, 0x20), + ] + {% end %} + end + @ran_bios = @cgb_ptr.value + end + + def skip_boot : Nil + POST_BOOT_VRAM.each_with_index do |byte, idx| + @vram[0][idx] = byte.to_u8 + end + end + + # handle stat interrupts + # stat interrupts are only requested on the rising edge + def handle_stat_interrupt : Nil + self.coincidence_flag = @ly == @lyc + stat_flag = (coincidence_flag && coincidence_interrupt_enabled) || + (mode_flag == 2 && oam_interrupt_enabled) || + (mode_flag == 0 && hblank_interrupt_enabled) || + (mode_flag == 1 && vblank_interrupt_enabled) + if !@old_stat_flag && stat_flag + @gb.interrupts.lcd_stat_interrupt = true + end + @old_stat_flag = stat_flag + end + + # Copy 16-byte block from hdma_src to hdma_dst, then decrement value in hdma5 + def copy_hdma_block(block_number : Int) : Nil + 0x10.times do |byte| + offset = 0x10 * block_number + byte + @gb.memory.write_byte @hdma_dst &+ offset, @gb.memory.read_byte @hdma_src &+ offset + @gb.memory.tick_components 2, from_cpu: false, ignore_speed: true + end + @hdma5 &-= 1 + end + + def start_hdma(value : UInt8) : Nil + @hdma_src = ((@hdma1.to_u16 << 8) | @hdma2) & 0xFFF0 + @hdma_dst = 0x8000_u16 + (((@hdma3.to_u16 << 8) | @hdma4) & 0x1FF0) + @hdma5 = value & 0x7F + if value & 0x80 > 0 # hdma + @hdma_active = true + @hdma_pos = 0 + else # gdma + unless @hdma_active + (@hdma5 + 1).times do |block_num| + copy_hdma_block block_num + end + end + @hdma_active = false + end + end + + def step_hdma : Nil + copy_hdma_block @hdma_pos + @hdma_pos += 1 + @hdma_active = false if @hdma5 == 0xFF + end + + # read from ppu memory + def [](index : Int) : UInt8 + case index + when Memory::VRAM then @vram[@vram_bank][index - Memory::VRAM.begin] + when Memory::OAM then @sprite_table[index - Memory::OAM.begin] + when 0xFF40 then @lcd_control + when 0xFF41 + if @first_line && mode_flag == 2 + @lcd_status & 0b11111100 + else + @lcd_status + end + when 0xFF42 then @scy + when 0xFF43 then @scx + when 0xFF44 then @ly + when 0xFF45 then @lyc + when 0xFF46 then @dma + when 0xFF47 then palette_from_array @bgp + when 0xFF48 then palette_from_array @obp0 + when 0xFF49 then palette_from_array @obp1 + when 0xFF4A then @wy + when 0xFF4B then @wx + when 0xFF4F then @cgb_ptr.value ? 0xFE_u8 | @vram_bank : 0xFF_u8 + when 0xFF51 then @cgb_ptr.value ? @hdma1 : 0xFF_u8 + when 0xFF52 then @cgb_ptr.value ? @hdma2 : 0xFF_u8 + when 0xFF53 then @cgb_ptr.value ? @hdma3 : 0xFF_u8 + when 0xFF54 then @cgb_ptr.value ? @hdma4 : 0xFF_u8 + when 0xFF55 then @cgb_ptr.value ? @hdma5 : 0xFF_u8 + when 0xFF68 then @cgb_ptr.value ? 0x40_u8 | (@auto_increment ? 0x80 : 0) | @palette_index : 0xFF_u8 + when 0xFF69 then @cgb_ptr.value ? @pram[@palette_index] : 0xFF_u8 + when 0xFF6A then @cgb_ptr.value ? 0x40_u8 | (@obj_auto_increment ? 0x80 : 0) | @obj_palette_index : 0xFF_u8 + when 0xFF6B then @cgb_ptr.value ? @obj_pram[@obj_palette_index] : 0xFF_u8 + else raise "Reading from invalid ppu register: #{hex_str index.to_u16!}" + end + end + + # write to ppu memory + def []=(index : Int, value : UInt8) : Nil + case index + when Memory::VRAM then @vram[@vram_bank][index - Memory::VRAM.begin] = value + when Memory::OAM then @sprite_table[index - Memory::OAM.begin] = value + when 0xFF40 + if value & 0x80 > 0 && !lcd_enabled? + @ly = 0 + self.mode_flag = 2 + @first_line = true + end + @lcd_control = value + handle_stat_interrupt + when 0xFF41 + @lcd_status = (@lcd_status & 0b10000111) | (value & 0b01111000) + handle_stat_interrupt + when 0xFF42 then @scy = value + when 0xFF43 then @scx = value + when 0xFF44 then nil # read only + when 0xFF45 + @lyc = value + handle_stat_interrupt + when 0xFF46 then @dma = value + when 0xFF47 then @bgp = palette_to_array value + when 0xFF48 then @obp0 = palette_to_array value + when 0xFF49 then @obp1 = palette_to_array value + when 0xFF4A then @wy = value + when 0xFF4B then @wx = value + when 0xFF4F then @vram_bank = value & 1 if @cgb_ptr.value + when 0xFF51 then @hdma1 = value if @cgb_ptr.value + when 0xFF52 then @hdma2 = value if @cgb_ptr.value + when 0xFF53 then @hdma3 = value if @cgb_ptr.value + when 0xFF54 then @hdma4 = value if @cgb_ptr.value + when 0xFF55 then start_hdma value if @cgb_ptr.value + when 0xFF68 + if @cgb_ptr.value + @palette_index = value & 0x3F + @auto_increment = value & 0x80 > 0 + end + when 0xFF69 + if @cgb_ptr.value + @pram[@palette_index] = value + bgr16 = @pram[@palette_index | 1].to_u16 << 8 | @pram[@palette_index & ~1] + palette_number = @palette_index >> 3 + color_number = (@palette_index & 7) >> 1 + @palettes[palette_number][color_number] = RGB.from_bgr16 bgr16, @ran_bios + @palette_index += 1 if @auto_increment + @palette_index &= 0x3F + end + when 0xFF6A + if @cgb_ptr.value + @obj_palette_index = value & 0x3F + @obj_auto_increment = value & 0x80 > 0 + end + when 0xFF6B + if @cgb_ptr.value + @obj_pram[@obj_palette_index] = value + bgr16 = @obj_pram[@obj_palette_index | 1].to_u16 << 8 | @obj_pram[@obj_palette_index & ~1] + palette_number = @obj_palette_index >> 3 + color_number = (@obj_palette_index & 7) >> 1 + @obj_palettes[palette_number][color_number] = RGB.from_bgr16 bgr16, @ran_bios + @obj_palette_index += 1 if @obj_auto_increment + @obj_palette_index &= 0x3F + end + else raise "Writing to invalid ppu register: #{hex_str index.to_u16!}" + end + end + + # LCD Control Register + + def lcd_enabled? : Bool + @lcd_control & (0x1 << 7) != 0 + end + + def window_tile_map : UInt8 + @lcd_control & (0x1 << 6) + end + + def window_enabled? : Bool + @lcd_control & (0x1 << 5) != 0 + end + + def bg_window_tile_data : UInt8 + @lcd_control & (0x1 << 4) + end + + def bg_tile_map : UInt8 + @lcd_control & (0x1 << 3) + end + + def sprite_height + @lcd_control & (0x1 << 2) != 0 ? 16 : 8 + end + + def sprite_enabled? : Bool + @lcd_control & (0x1 << 1) != 0 + end + + def bg_display? : Bool + @lcd_control & 0x1 != 0 + end + + # LCD Status Register + + def coincidence_interrupt_enabled : Bool + @lcd_status & (0x1 << 6) != 0 + end + + def oam_interrupt_enabled : Bool + @lcd_status & (0x1 << 5) != 0 + end + + def vblank_interrupt_enabled : Bool + @lcd_status & (0x1 << 4) != 0 + end + + def hblank_interrupt_enabled : Bool + @lcd_status & (0x1 << 3) != 0 + end + + def coincidence_flag : Bool + @lcd_status & (0x1 << 2) != 0 + end + + def coincidence_flag=(on : Bool) : Nil + @lcd_status = (@lcd_status & ~(0x1 << 2)) | (on ? (0x1 << 2) : 0) + end + + def mode_flag : UInt8 + @lcd_status & 0x3 + end + + def mode_flag=(mode : UInt8) + step_hdma if mode == 0 && @hdma_active + @first_line = false if @first_line && mode_flag == 0 && mode == 2 + @window_trigger = false if mode == 1 + @lcd_status = (@lcd_status & 0b11111100) | mode + handle_stat_interrupt + end + + # palettes + + def palette_to_array(palette : UInt8) : Array(UInt8) + [palette & 0x3, (palette >> 2) & 0x3, (palette >> 4) & 0x3, (palette >> 6) & 0x3] + end + + def palette_from_array(palette_array : Array(UInt8)) : UInt8 + palette_array.each_with_index.reduce(0x00_u8) do |palette, (color, idx)| + palette | color << (idx * 2) + end + end + + def write_png : Nil + @gb.display.write_png @framebuffer + end + end + end diff --git a/src/crab/gb/scanline_ppu.cr b/src/crab/gb/scanline_ppu.cr new file mode 100644 index 0000000..fb67403 --- /dev/null +++ b/src/crab/gb/scanline_ppu.cr @@ -0,0 +1,177 @@ +module GB + class ScanlinePPU < PPU + # get first 10 sprites on scanline, ordered + # the order dictates how sprites should render, with the first ones on the bottom + def get_sprites : Array(Sprite) + sprites = [] of Sprite + (0x00..0x9F).step 4 do |sprite_address| + sprite = Sprite.new @sprite_table[sprite_address], @sprite_table[sprite_address + 1], @sprite_table[sprite_address + 2], @sprite_table[sprite_address + 3] + if sprite.on_line @ly, sprite_height + index = 0 + if !@cgb_ptr.value + sprites.each do |sprite_elm| + break if sprite.x >= sprite_elm.x + index += 1 + end + end + sprites.insert index, sprite + end + break if sprites.size >= 10 + end + sprites + end + + # color idx, BG-to-OAM priority bit + @scanline_color_vals = Array(Tuple(UInt8, Bool)).new Display::WIDTH, {0_u8, false} + + def scanline + @current_window_line = 0 if @ly == 0 + should_increment_window_line = false + window_map = window_tile_map == 0_u8 ? 0x1800 : 0x1C00 # 0x9800 : 0x9C00 + background_map = bg_tile_map == 0_u8 ? 0x1800 : 0x1C00 # 0x9800 : 0x9C00 + tile_data_table = bg_window_tile_data == 0 ? 0x1000 : 0x0000 # 0x9000 : 0x8000 + tile_row_window = @current_window_line & 7 + tile_row = (@ly.to_u16 + @scy) & 7 + Display::WIDTH.times do |x| + if window_enabled? && @ly >= @wy && x + 7 >= @wx && @window_trigger + should_increment_window_line = true + tile_num_addr = window_map + ((x + 7 - @wx) >> 3) + ((@current_window_line >> 3) * 32) + tile_num = @vram[0][tile_num_addr] + tile_num = tile_num.to_i8! if bg_window_tile_data == 0 + tile_ptr = tile_data_table + 16 * tile_num + bank_num = @cgb_ptr.value ? (@vram[1][tile_num_addr] & 0b00001000) >> 3 : 0 + if @cgb_ptr.value && @vram[1][tile_num_addr] & 0b01000000 > 0 + byte_1 = @vram[bank_num][tile_ptr + (7 - tile_row_window) * 2] + byte_2 = @vram[bank_num][tile_ptr + (7 - tile_row_window) * 2 + 1] + else + byte_1 = @vram[bank_num][tile_ptr + tile_row_window * 2] + byte_2 = @vram[bank_num][tile_ptr + tile_row_window * 2 + 1] + end + if @cgb_ptr.value && @vram[1][tile_num_addr] & 0b00100000 > 0 + lsb = (byte_1 >> ((x + 7 - @wx) & 7)) & 0x1 + msb = (byte_2 >> ((x + 7 - @wx) & 7)) & 0x1 + else + lsb = (byte_1 >> (7 - ((x + 7 - @wx) & 7))) & 0x1 + msb = (byte_2 >> (7 - ((x + 7 - @wx) & 7))) & 0x1 + end + color = (msb << 1) | lsb + @scanline_color_vals[x] = {color, @vram[1][tile_num_addr] & 0x80 > 0} + if @cgb_ptr.value + @framebuffer[Display::WIDTH * @ly + x] = @palettes[@vram[1][tile_num_addr] & 0b111][color] + else + @framebuffer[Display::WIDTH * @ly + x] = @palettes[0][@bgp[color]] + end + elsif bg_display? || @cgb_ptr.value + tile_num_addr = background_map + (((x + @scx) >> 3) & 0x1F) + ((((@ly.to_u16 + @scy) >> 3) * 32) & 0x3FF) + tile_num = @vram[0][tile_num_addr] + tile_num = tile_num.to_i8! if bg_window_tile_data == 0 + tile_ptr = tile_data_table + 16 * tile_num + bank_num = @cgb_ptr.value ? (@vram[1][tile_num_addr] & 0b00001000) >> 3 : 0 + if @cgb_ptr.value && @vram[1][tile_num_addr] & 0b01000000 > 0 + byte_1 = @vram[bank_num][tile_ptr + (7 - tile_row) * 2] + byte_2 = @vram[bank_num][tile_ptr + (7 - tile_row) * 2 + 1] + else + byte_1 = @vram[bank_num][tile_ptr + tile_row * 2] + byte_2 = @vram[bank_num][tile_ptr + tile_row * 2 + 1] + end + if @cgb_ptr.value && @vram[1][tile_num_addr] & 0b00100000 > 0 + lsb = (byte_1 >> ((x + @scx) & 7)) & 0x1 + msb = (byte_2 >> ((x + @scx) & 7)) & 0x1 + else + lsb = (byte_1 >> (7 - ((x + @scx) & 7))) & 0x1 + msb = (byte_2 >> (7 - ((x + @scx) & 7))) & 0x1 + end + color = (msb << 1) | lsb + @scanline_color_vals[x] = {color, @vram[1][tile_num_addr] & 0x80 > 0} + if @cgb_ptr.value + @framebuffer[Display::WIDTH * @ly + x] = @palettes[@vram[1][tile_num_addr] & 0b111][color] + else + @framebuffer[Display::WIDTH * @ly + x] = @palettes[0][@bgp[color]] + end + end + end + @current_window_line += 1 if should_increment_window_line + + if sprite_enabled? + get_sprites.each do |sprite| + bytes = sprite.bytes @ly, sprite_height + 8.times do |col| + x = col + sprite.x - 8 + next unless 0 <= x < Display::WIDTH # only render sprites on screen + if sprite.x_flip? + lsb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[0]] >> col) & 0x1 + msb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[1]] >> col) & 0x1 + else + lsb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[0]] >> (7 - col)) & 0x1 + msb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[1]] >> (7 - col)) & 0x1 + end + color = (msb << 1) | lsb + if color > 0 # color 0 is transparent + if @cgb_ptr.value + # if !bg_display, then objects are always on top in cgb mode + # objects are always on top of bg/window color 0 + # objects are on top of bg/window colors 1-3 if bg_priority and object priority are both unset + if !bg_display? || @scanline_color_vals[x][0] == 0 || (!@scanline_color_vals[x][1] && sprite.priority == 0) + @framebuffer[Display::WIDTH * @ly + x] = @obj_palettes[sprite.cgb_palette_number][color] + end + else + if sprite.priority == 0 || @scanline_color_vals[x][0] == 0 + palette = sprite.dmg_palette_number == 0 ? @obp0 : @obp1 + @framebuffer[Display::WIDTH * @ly + x] = @obj_palettes[0][palette[color]] + end + end + end + end + end + end + end + + # tick ppu forward by specified number of cycles + def tick(cycles : Int) : Nil + @cycle_counter += cycles + if lcd_enabled? + if self.mode_flag == 2 # oam search + if @cycle_counter >= 80 # end of oam search reached + @cycle_counter -= 80 # reset cycle_counter, saving extra cycles + self.mode_flag = 3 # switch to drawing + @window_trigger = true if @ly == @wy + end + elsif self.mode_flag == 3 # drawing + if @cycle_counter >= 172 # end of drawing reached + @cycle_counter -= 172 # reset cycle_counter, saving extra cycles + self.mode_flag = 0 # switch to hblank + scanline # store scanline data + end + elsif self.mode_flag == 0 # hblank + if @cycle_counter >= 204 # end of hblank reached + @cycle_counter -= 204 # reset cycle_counter, saving extra cycles + @ly += 1 + if @ly == Display::HEIGHT # final row of screen complete + self.mode_flag = 1 # switch to vblank + @gb.interrupts.vblank_interrupt = true + @gb.display.draw @framebuffer # render at vblank + else + self.mode_flag = 2 # switch to oam search + end + end + elsif self.mode_flag == 1 # vblank + if @cycle_counter >= 456 # end of line reached + @cycle_counter -= 456 # reset cycle_counter, saving extra cycles + @ly += 1 if @ly != 0 + handle_stat_interrupt + if @ly == 0 # end of vblank reached (ly has already shortcut to 0) + self.mode_flag = 2 # switch to oam search + end + end + @ly = 0 if @ly == 153 && @cycle_counter > 4 # shortcut ly to from 153 to 0 after 4 cycles + else + raise "Invalid mode #{self.mode_flag}" + end + else # lcd is disabled + @cycle_counter = 0 # reset cycle cycle_counter + self.mode_flag = 0 # reset to mode 0 + @ly = 0 # reset ly + end + end + end +end diff --git a/src/crab/gb/scheduler.cr b/src/crab/gb/scheduler.cr new file mode 100644 index 0000000..0ec4f51 --- /dev/null +++ b/src/crab/gb/scheduler.cr @@ -0,0 +1,90 @@ +module GB + class Scheduler + enum EventType + APU + APUChannel1 + APUChannel2 + APUChannel3 + APUChannel4 + IME + HandleInput + end + + private record Event, cycles : UInt64, type : EventType, proc : Proc(Void) do + def to_s(io) + io << "Event(cycles: #{cycles}, type: #{type}, proc: #{proc})" + end + end + + @events : Deque(Event) = Deque(Event).new 10 + @cycles : UInt64 = 0 + @next_event : UInt64 = UInt64::MAX + + @current_speed : UInt8 = 0 + + def schedule(cycles : Int, type : EventType, proc : Proc(Void)) : Nil + cycles = cycles << @current_speed unless type == EventType::IME + self << Event.new @cycles + cycles, type, proc + end + + def schedule(cycles : Int, type : EventType, &block : ->) + cycles = cycles << @current_speed unless type == EventType::IME + self << Event.new @cycles + cycles, type, block + end + + def clear(type : EventType) : Nil + @events.reject! { |event| event.type == type } + end + + # Set the current speed to 1x (0) or 2x (1) + def speed_mode=(speed : UInt8) : Nil + @current_speed = speed + @events.each_with_index do |event, idx| + unless event.type == EventType::IME + remaining_cycles = event.cycles - @cycles + # divide by two if entering single speed, else multiply by two + offset = remaining_cycles >> (@current_speed - speed) + @events[idx] = event.copy_with cycles: @cycles + offset + end + end + end + + def <<(event : Event) : Nil + idx = @events.bsearch_index { |e, i| e.cycles > event.cycles } + unless idx.nil? + @events.insert(idx, event) + else + @events << event + end + @next_event = @events[0].cycles + end + + def tick(cycles : Int) : Nil + if @cycles + cycles < @next_event + @cycles += cycles + else + cycles.times do + @cycles += 1 + call_current + end + end + end + + def call_current : Nil + loop do + event = @events.first? + if event && @cycles >= event.cycles + event.proc.call + @events.shift + else + if event + @next_event = event.cycles + else + @next_event = UInt64::MAX + end + return + end + end + end + end +end diff --git a/src/crab/gb/timer.cr b/src/crab/gb/timer.cr new file mode 100644 index 0000000..9c8daa8 --- /dev/null +++ b/src/crab/gb/timer.cr @@ -0,0 +1,94 @@ +module GB + class Timer + @div : UInt16 = 0x0000 # 16-bit divider register + @tima : UInt8 = 0x00 # 8-bit timer register + @tma : UInt8 = 0x00 # value to load when tima overflows + @enabled : Bool = false # if timer is enabled + @clock_select : UInt8 = 0x00 # frequency flag determining when to increment tima + @bit_for_tima = 9 # bit to detect falling edge for tima increments + + @previous_bit = false # used to detect falling edge + @countdown = -1 # load tma and set interrupt flag when countdown is 0 + + def initialize(@gb : GB) + end + + def skip_boot : Nil + @div = 0x2674_u16 + end + + # tick timer forward by specified number of cycles + def tick(cycles : Int) : Nil + cycles.times do + @countdown -= 1 if @countdown > -1 + reload_tima if @countdown == 0 + @div &+= 1 + check_edge + end + end + + # read from timer memory + def [](index : Int) : UInt8 + case index + when 0xFF04 then (@div >> 8).to_u8 + when 0xFF05 then @tima + when 0xFF06 then @tma + when 0xFF07 then 0xF8_u8 | (@enabled ? 0b100 : 0) | @clock_select + else raise "Reading from invalid timer register: #{hex_str index.to_u16!}" + end + end + + # write to timer memory + def []=(index : Int, value : UInt8) : Nil + case index + when 0xFF04 + @div = 0x0000_u16 + check_edge on_write: true + when 0xFF05 + if @countdown != 0 # ignore writes on cycle that tma is loaded + @tima = value + @countdown = -1 # abort interrupt and tma load + end + when 0xFF06 + @tma = value + @tima = @tma if @countdown == 0 # write to tima on cycle that tma is loaded + when 0xFF07 + @enabled = value & 0b100 != 0 + @clock_select = value & 0b011 + @bit_for_tima = case @clock_select + when 0b00 then 9 + when 0b01 then 3 + when 0b10 then 5 + when 0b11 then 7 + else raise "Selecting bit for TIMA. Will never be reached." + end + check_edge on_write: true + else raise "Writing to invalid timer register: #{hex_str index.to_u16!}" + end + end + + private def reload_tima : Nil + @gb.interrupts.timer_interrupt = true + @tima = @tma + end + + # Check the falling edge in div counter + # Note: reload and interrupt flag happen immediately on write + # This isn't actually entirely true. For more details on how this _actually_ works, read from gekkio + # starting here: https://discord.com/channels/465585922579103744/465586075830845475/793581987512188961 + private def check_edge(on_write = false) : Nil + current_bit = @enabled && (@div & (1 << @bit_for_tima) != 0) + if @previous_bit && !current_bit + @tima &+= 1 + if @tima == 0 + if on_write + reload_tima + else + @countdown = 4 + end + end + end + @previous_bit = current_bit + end + end +end diff --git a/src/crab/gb/update_opcodes.cr b/src/crab/gb/update_opcodes.cr new file mode 100644 index 0000000..f1af171 --- /dev/null +++ b/src/crab/gb/update_opcodes.cr @@ -0,0 +1,505 @@ +require "compiler/crystal/formatter" +require "compiler/crystal/command/format" +require "http/client" +require "json" + +OPCODE_JSON_URL = "https://raw.githubusercontent.com/izik1/gbops/master/dmgops.json" +FILE_PATH = "src/cryboy/opcodes.cr" + +module GB + module DmgOps + enum FlagOp + ZERO + ONE + UNCHANGED + DEFAULT + end + + class Flags + include JSON::Serializable + + @[JSON::Field(key: "Z")] + @z : String + @[JSON::Field(key: "N")] + @n : String + @[JSON::Field(key: "H")] + @h : String + @[JSON::Field(key: "C")] + @c : String + + def str_to_flagop(s : String) : FlagOp + case s + when "0" then FlagOp::ZERO + when "1" then FlagOp::ONE + when "-" then FlagOp::UNCHANGED + else FlagOp::DEFAULT + end + end + + def z : FlagOp + str_to_flagop @z + end + + def n : FlagOp + str_to_flagop @n + end + + def h : FlagOp + str_to_flagop @h + end + + def c : FlagOp + str_to_flagop @c + end + end + + enum Group + X8_LSM + X16_LSM + X8_ALU + X16_ALU + X8_RSB + CONTROL_BR + CONTROL_MISC + end + + class Operation + include JSON::Serializable + + @[JSON::Field(key: "Name")] + property name : String + @[JSON::Field(key: "Group")] + @group : String + @[JSON::Field(key: "TCyclesNoBranch")] + property cycles : UInt8 + @[JSON::Field(key: "TCyclesBranch")] + property cycles_branch : UInt8 + @[JSON::Field(key: "Length")] + property length : UInt8 + @[JSON::Field(key: "Flags")] + property flags : Flags + + # read the operation type from the name + def type : String + @name.split.first + end + + # read the operation operands from the name + def operands : Array(String) + split = name.split(limit: 2) + split.size <= 1 ? [] of String : split[1].split(',').map { |operand| normalize_operand operand } + end + + # read the group as a Group enum + def group : Group + case @group + when "x8/lsm" then Group::X8_LSM + when "x16/lsm" then Group::X16_LSM + when "x8/alu" then Group::X8_ALU + when "x16/alu" then Group::X16_ALU + when "x8/rsb" then Group::X8_RSB + when "control/br" then Group::CONTROL_BR + when "control/misc" then Group::CONTROL_MISC + else raise "Failed to match group #{@group}" + end + end + + # normalize an operand to work with the existing cpu methods/fields + def normalize_operand(operand : String) : String + operand = operand.downcase + operand = operand.sub "(", "cpu.memory[" + operand = operand.sub ")", "]" + operand = operand.sub "hl+", "((hl &+= 1) &- 1)" + operand = operand.sub "hl-", "((hl &-= 1) &+ 1)" + operand = operand.sub "ff00+", "0xFF00 &+ " + operand = operand.sub "sp+i8", "sp &+ i8" + operand = operand.sub /(\d\d)h/, "0x\\1_u16" + if group == Group::CONTROL_BR || group == Group::CONTROL_MISC + # distinguish between "flag c" and "register z" + operand = operand.sub /\bz\b/, "cpu.f_z" + operand = operand.sub /\bnz\b/, "cpu.f_nz" + operand = operand.sub /\bc\b/, "cpu.f_c" + operand = operand.sub /\bnc\b/, "cpu.f_nc" + end + operand = operand.sub "pc", "cpu.pc" + operand = operand.sub "sp", "cpu.sp" + operand = operand.sub "af", "cpu.af" + operand = operand.sub "bc", "cpu.bc" + operand = operand.sub "de", "cpu.de" + operand = operand.sub "hl", "cpu.hl" + operand = operand.sub /\ba\b/, "cpu.a" + operand = operand.sub /\bf\b/, "cpu.f" + operand = operand.sub /\bb\b/, "cpu.b" + operand = operand.sub /\bc\b/, "cpu.c" + operand = operand.sub /\bd\b/, "cpu.d" + operand = operand.sub /\be\b/, "cpu.e" + operand = operand.sub /\bh\b/, "cpu.h" + operand = operand.sub /\bl\b/, "cpu.l" + operand = operand.sub "cpu.memory[cpu.hl]", "cpu.memory_at_hl" + operand + end + + # set u8, u16, and i8 if necessary + def assign_extra_integers : Array(String) + if name.includes? "u8" + return ["u8 = cpu.memory[cpu.pc]", "cpu.inc_pc"] + elsif name.includes? "u16" + return ["u16 = cpu.memory[cpu.pc].to_u16", "cpu.inc_pc", "u16 |= cpu.memory[cpu.pc].to_u16 << 8", "cpu.inc_pc"] + elsif name.includes? "i8" + return ["i8 = cpu.memory[cpu.pc].to_i8!", "cpu.inc_pc"] + end + [] of String + end + + # create a branch condition + def branch(cond : String, body : Array(String)) : Array(String) + ["if #{cond}"] + body + set_reset_flags + ["return #{cycles_branch}", "end"] + end + + # set flag z to the given value if specified by this operation + def set_flag_z(o : Object) : Array(String) + flags.z == FlagOp::DEFAULT ? set_flag_z! o : [] of String + end + + # set flag z to the given value + def set_flag_z!(o : Object) : Array(String) + ["cpu.f_z = #{o}"] + end + + # set flag n to the given value if specified by this operation + def set_flag_n(o : Object) : Array(String) + flags.n == FlagOp::DEFAULT ? set_flag_n! o : [] of String + end + + # set flag n to the given value + def set_flag_n!(o : Object) : Array(String) + ["cpu.f_n = #{o}"] + end + + # set flag h to the given value if specified by this operation + def set_flag_h(o : Object) : Array(String) + flags.h == FlagOp::DEFAULT ? set_flag_h! o : [] of String + end + + # set flag h to the given value + def set_flag_h!(o : Object) : Array(String) + ["cpu.f_h = #{o}"] + end + + # set flag c to the given value if specified by this operation + def set_flag_c(o : Object) : Array(String) + flags.c == FlagOp::DEFAULT ? set_flag_c! o : [] of String + end + + # set flag c to the given value + def set_flag_c!(o : Object) : Array(String) + ["cpu.f_c = #{o}"] + end + + # generate code to set/reset flags if necessary + def set_reset_flags : Array(String) + (flags.z == FlagOp::ZERO ? set_flag_z! false : [] of String) + + (flags.z == FlagOp::ONE ? set_flag_z! true : [] of String) + + (flags.n == FlagOp::ZERO ? set_flag_n! false : [] of String) + + (flags.n == FlagOp::ONE ? set_flag_n! true : [] of String) + + (flags.h == FlagOp::ZERO ? set_flag_h! false : [] of String) + + (flags.h == FlagOp::ONE ? set_flag_h! true : [] of String) + + (flags.c == FlagOp::ZERO ? set_flag_c! false : [] of String) + + (flags.c == FlagOp::ONE ? set_flag_c! true : [] of String) + end + + # switch over operation type and generate code + private def codegen_help : Array(String) + case type + when "ADC" + to, from = operands + if to == from + ["carry = cpu.f_c ? 0x01 : 0x00"] + + set_flag_h("(#{to} & 0x0F) + (#{from} & 0x0F) + carry > 0x0F") + + set_flag_c("#{to} > 0x7F") + + ["#{to} &+= #{from} &+ carry"] + + set_flag_z("#{to} == 0") + else + ["carry = cpu.f_c ? 0x01 : 0x00"] + + set_flag_h("(#{to} & 0x0F) + (#{from} & 0x0F) + carry > 0x0F") + + ["#{to} &+= #{from} &+ carry"] + + set_flag_z("#{to} == 0") + + set_flag_c("#{to} < #{from}.to_u16 + carry") + end + when "ADD" + to, from = operands + if group == Group::X8_ALU + if to == from + set_flag_h("(#{to} & 0x0F) + (#{from} & 0x0F) > 0x0F") + + set_flag_c("#{to} > 0x7F") + + ["#{to} &+= #{from}"] + + set_flag_z("#{to} == 0") + else + set_flag_h("(#{to} & 0x0F) + (#{from} & 0x0F) > 0x0F") + + ["#{to} &+= #{from}"] + + set_flag_z("#{to} == 0") + + set_flag_c("#{to} < #{from}") + end + elsif group == Group::X16_ALU + if from == "i8" + ["r = cpu.sp &+ i8"] + + set_flag_h("(cpu.sp ^ i8 ^ r) & 0x0010 != 0") + + set_flag_c("(cpu.sp ^ i8 ^ r) & 0x0100 != 0") + + ["cpu.sp = r"] + elsif to == from + set_flag_h("(#{to} & 0x0FFF).to_u32 + (#{from} & 0x0FFF) > 0x0FFF") + + set_flag_c("#{to} > 0x7FFF") + + ["#{to} &+= #{from}"] + else + set_flag_h("(#{to} & 0x0FFF).to_u32 + (#{from} & 0x0FFF) > 0x0FFF") + + ["#{to} &+= #{from}"] + + set_flag_c("#{to} < #{from}") + end + else + raise "Invalid group #{group} for ADD." + end + when "AND" + to, from = operands + ["#{to} &= #{from}"] + + set_flag_z("#{to} == 0") + when "BIT" + bit, reg = operands + set_flag_z("#{reg} & (0x1 << #{bit}) == 0") + when "CALL" + instr = ["cpu.memory.tick_components", "cpu.memory[cpu.sp -= 2] = cpu.pc", "cpu.pc = u16"] + if operands.size == 1 + instr + else + cond, _ = operands + branch(cond, instr) + end + when "CCF" + set_flag_c("!cpu.f_c") + when "CP" + to, from = operands + set_flag_z("#{to} &- #{from} == 0") + + set_flag_h("#{to} & 0xF < #{from} & 0xF") + + set_flag_c("#{to} < #{from}") + when "CPL" + ["cpu.a = ~cpu.a"] + when "DAA" + [ + "if cpu.f_n # last op was a subtraction", + " cpu.a &-= 0x60 if cpu.f_c", + " cpu.a &-= 0x06 if cpu.f_h", + "else # last op was an addition", + " if cpu.f_c || cpu.a > 0x99", + " cpu.a &+= 0x60", + " cpu.f_c = true", + " end", + " if cpu.f_h || cpu.a & 0x0F > 0x09", + " cpu.a &+= 0x06", + " end", + "end", + ] + + set_flag_z("cpu.a == 0") + when "DEC" + to = operands[0] + ["#{to} &-= 1"] + + set_flag_z("#{to} == 0") + + set_flag_h("#{to} & 0x0F == 0x0F") + when "DI" + ["cpu.ime = false"] + when "EI" + ["cpu.scheduler.schedule(4, Scheduler::EventType::IME) { cpu.ime = true }"] + when "HALT" + ["cpu.halt"] + when "INC" + to = operands[0] + set_flag_h("#{to} & 0x0F == 0x0F") + + ["#{to} &+= 1"] + + set_flag_z("#{to} == 0") + when "JP" + if operands.size == 1 + ["cpu.pc = #{operands[0]}"] + else + cond, loc = operands + branch(cond, ["cpu.pc = #{loc}"]) + end + when "JR" + instr = ["cpu.pc &+= i8"] + if operands.size == 1 + instr + else + cond, _ = operands + branch(cond, instr) + end + when "LD" + to, from = operands + ["#{to} = #{from}"] + + # the following flags _only_ apply to `LD HL, SP + i8` + set_flag_h("(cpu.sp ^ i8 ^ cpu.hl) & 0x0010 != 0") + + set_flag_c("(cpu.sp ^ i8 ^ cpu.hl) & 0x0100 != 0") + when "NOP" + [] of String + when "OR" + to, from = operands + ["#{to} |= #{from}"] + + set_flag_z("#{to} == 0") + when "POP" + reg = operands[0] + ["#{reg} = cpu.memory.read_word (cpu.sp += 2) - 2"] + + set_flag_z("#{reg} & (0x1 << 7)") + + set_flag_n("#{reg} & (0x1 << 6)") + + set_flag_h("#{reg} & (0x1 << 5)") + + set_flag_c("#{reg} & (0x1 << 4)") + when "PREFIX" + [ + "# todo: This should operate as a seperate instruction, but can't be interrupted.", + "# This will require a restructure where the CPU leads the timing, rather than the PPU.", + "# https://discordapp.com/channels/465585922579103744/465586075830845475/712358911151177818", + "# https://discordapp.com/channels/465585922579103744/465586075830845475/712359253255520328", + "cycles = Opcodes::PREFIXED[cpu.memory[cpu.pc]].call cpu", + "return cycles", + ] + when "PUSH" + [ + "cpu.memory.tick_components", + "cpu.memory[cpu.sp -= 2] = #{operands[0]}", + ] + when "RES" + bit, reg = operands + ["#{reg} &= ~(0x1 << #{bit})"] + when "RET" + instr = ["cpu.pc = cpu.memory.read_word cpu.sp", "cpu.sp += 2"] + if operands.size == 0 + instr + else + cond = operands[0] + branch(cond, ["cpu.memory.tick_components"] + instr) + end + when "RETI" + ["cpu.ime = true", "cpu.pc = cpu.memory.read_word cpu.sp", "cpu.sp += 0x02"] + when "RL" + reg = operands[0] + ["carry = #{reg} & 0x80", "#{reg} = (#{reg} << 1) + (cpu.f_c ? 0x01 : 0x00)"] + + set_flag_z("#{reg} == 0") + + set_flag_c("carry") + when "RLA" + ["carry = cpu.a & 0x80", "cpu.a = (cpu.a << 1) + (cpu.f_c ? 0x01 : 0x00)"] + + set_flag_c("carry") + when "RLC" + reg = operands[0] + ["#{reg} = (#{reg} << 1) + (#{reg} >> 7)"] + + set_flag_z("#{reg} == 0") + + set_flag_c("#{reg} & 0x01") + when "RLCA" + ["cpu.a = (cpu.a << 1) + (cpu.a >> 7)"] + + set_flag_c("cpu.a & 0x01") + when "RR" + reg = operands[0] + ["carry = #{reg} & 0x01", "#{reg} = (#{reg} >> 1) + (cpu.f_c ? 0x80 : 0x00)"] + + set_flag_z("#{reg} == 0") + + set_flag_c("carry") + when "RRA" + ["carry = cpu.a & 0x01", "cpu.a = (cpu.a >> 1) + (cpu.f_c ? 0x80 : 0x00)"] + + set_flag_c("carry") + when "RRC" + reg = operands[0] + ["#{reg} = (#{reg} >> 1) + (#{reg} << 7)"] + + set_flag_z("#{reg} == 0") + + set_flag_c("#{reg} & 0x80") + when "RRCA" + ["cpu.a = (cpu.a >> 1) + (cpu.a << 7)"] + + set_flag_c("cpu.a & 0x80") + when "RST" + ["cpu.memory.tick_components", "cpu.memory[cpu.sp -= 2] = cpu.pc", "cpu.pc = #{operands[0]}"] + when "SBC" + to, from = operands + ["to_sub = #{from}.to_u16 + (cpu.f_c ? 0x01 : 0x00)"] + + set_flag_h("(#{to} & 0x0F) < (#{from} & 0x0F) + (cpu.f_c ? 0x01 : 0x00)") + + set_flag_c("#{to} < to_sub") + + ["#{to} &-= to_sub"] + + set_flag_z("#{to} == 0") + when "SCF" + # should already be covered by `set_reset_flags` + [] of String + when "SET" + bit, reg = operands + ["#{reg} |= (0x1 << #{bit})"] + when "SLA" + reg = operands[0] + set_flag_c("#{reg} & 0x80") + + ["#{reg} <<= 1"] + + set_flag_z("#{reg} == 0") + when "SRA" + reg = operands[0] + set_flag_c("#{reg} & 0x01") + + ["#{reg} = (#{reg} >> 1) + (#{reg} & 0x80)"] + + set_flag_z("#{reg} == 0") + when "SRL" + reg = operands[0] + set_flag_c("#{reg} & 0x1") + + ["#{reg} >>= 1"] + + set_flag_z("#{reg} == 0") + when "STOP" + ["# todo: see if something more needs to happen here...", "cpu.inc_pc", "cpu.memory.stop_instr"] + when "SUB" + to, from = operands + set_flag_h("#{to} & 0x0F < #{from} & 0x0F") + + set_flag_c("#{to} < #{from}") + + ["#{to} &-= #{from}"] + + set_flag_z("#{to} == 0") + when "SWAP" + reg = operands[0] + ["#{reg} = (#{reg} << 4) + (#{reg} >> 4)"] + + set_flag_z("#{reg} == 0") + when "UNUSED" + ["# unused opcode"] + when "XOR" + to, from = operands + ["#{to} ^= #{from}"] + + set_flag_z("#{to} == 0") + else ["raise \"Not currently supporting #{name}\""] + end + end + + # generate the code required to process this operation + def codegen : Array(String) + # ["cpu.print_state \"#{name}\""] + + ["cpu.inc_pc"] + + assign_extra_integers + + codegen_help + + set_reset_flags + + ["return #{cycles}"] + end + end + + class Response + include JSON::Serializable + + @[JSON::Field(key: "Unprefixed")] + @operations : Array(Operation) + @[JSON::Field(key: "CBPrefixed")] + @cb_operations : Array(Operation) + + def codegen : Array(String) + (["class Opcodes", "UNPREFIXED = ["] + + @operations.map_with_index { |operation, index| + ["# 0x#{index.to_s(16).rjust(2, '0').upcase} #{operation.name}", "->(cpu : CPU) {"] + + operation.codegen + + ["},"] + } + + ["]", "PREFIXED = ["] + + @cb_operations.map_with_index { |operation, index| + ["# 0x#{index.to_s(16).rjust(2, '0').upcase} #{operation.name}", "->(cpu : CPU) {"] + + operation.codegen + + ["},"] + } + + ["]", "end"]).flatten + end + end + end + + HTTP::Client.get OPCODE_JSON_URL do |response| + parsed = DmgOps::Response.from_json(response.body_io) + codegen = parsed.codegen.join("\n") + File.write FILE_PATH, codegen + Crystal::Command::FormatCommand.new([FILE_PATH]).run + end +end diff --git a/src/crab/gb/util.cr b/src/crab/gb/util.cr new file mode 100644 index 0000000..3ba4428 --- /dev/null +++ b/src/crab/gb/util.cr @@ -0,0 +1,43 @@ +module GB + def hex_str(n : UInt8 | UInt16 | UInt32 | UInt64) : String + "0x#{n.to_s(16).rjust(sizeof(typeof(n)) * 2, '0').upcase}" + end + + def array_to_uint8(array : Array(Bool | Int)) : UInt8 + raise "Array needs to have a length of 8" if array.size != 8 + value = 0_u8 + array.each_with_index do |bit, index| + value |= (bit == false || bit == 0 ? 0 : 1) << (7 - index) + end + value + end + + def array_to_uint16(array : Array(Bool | Int)) : UInt16 + raise "Array needs to have a length of 16" if array.size != 16 + value = 0_u16 + array.each_with_index do |bit, index| + value |= (bit == false || bit == 0 ? 0 : 1) << (15 - index) + end + value + end + + macro trace(value, newline = true) + {% if flag? :trace %} + {% if newline %} + puts {{value}} + {% else %} + print {{value}} + {% end %} + {% end %} + end + + macro log(value, newline = true) + {% if flag? :log %} + {% if newline %} + puts {{value}} + {% else %} + print {{value}} + {% end %} + {% end %} + end +end diff --git a/src/crab/gba.cr b/src/crab/gba.cr deleted file mode 100644 index 4ea8076..0000000 --- a/src/crab/gba.cr +++ /dev/null @@ -1,86 +0,0 @@ -require "./types" -require "./reg" -require "./util" -require "./scheduler" -require "./cartridge" -require "./storage" -require "./storage/*" -require "./mmio" -require "./timer" -require "./keypad" -require "./bus" -require "./interrupts" -require "./cpu" -require "./display" -require "./ppu" -require "./apu" -require "./dma" -require "./debugger" - -class GBA - getter! scheduler : Scheduler - getter! cartridge : Cartridge - getter! storage : Storage - getter! mmio : MMIO - getter! timer : Timer - getter! keypad : Keypad - getter! bus : Bus - getter! interrupts : Interrupts - getter! cpu : CPU - getter! display : Display - getter! ppu : PPU - getter! apu : APU - getter! dma : DMA - getter! debugger : Debugger - - def initialize(@bios_path : String, rom_path : String) - @scheduler = Scheduler.new - @cartridge = Cartridge.new rom_path - @storage = Storage.new rom_path - handle_events - handle_saves - - SDL.init(SDL::Init::VIDEO | SDL::Init::AUDIO | SDL::Init::JOYSTICK) - LibSDL.joystick_open 0 - at_exit { SDL.quit } - end - - def post_init : Nil - @mmio = MMIO.new self - @timer = Timer.new self - @keypad = Keypad.new self - @bus = Bus.new self, @bios_path - @interrupts = Interrupts.new self - @cpu = CPU.new self - @display = Display.new - @ppu = PPU.new self - @apu = APU.new self - @dma = DMA.new self - @debugger = Debugger.new self - end - - def handle_events : Nil - scheduler.schedule 280896, ->handle_events - while event = SDL::Event.poll - case event - when SDL::Event::Quit then exit 0 - when SDL::Event::Keyboard, - SDL::Event::JoyHat, - SDL::Event::JoyButton then keypad.handle_keypad_event event - else nil - end - end - end - - def handle_saves : Nil - scheduler.schedule 280896, ->handle_saves - storage.write_save - end - - def run : Nil - loop do - {% if flag? :debugger %} debugger.check_debug {% end %} - cpu.tick - end - end -end diff --git a/src/crab/gba/apu.cr b/src/crab/gba/apu.cr new file mode 100644 index 0000000..33293a7 --- /dev/null +++ b/src/crab/gba/apu.cr @@ -0,0 +1,199 @@ +require "./apu/abstract_channels" # so that channels don't need to all import +require "./apu/*" + +module GBA + class APU + CHANNELS = 2 # Left / Right + BUFFER_SIZE = 1024 + SAMPLE_RATE = 32768 # Hz + SAMPLE_PERIOD = CPU::CLOCK_SPEED // SAMPLE_RATE + + FRAME_SEQUENCER_RATE = 512 # Hz + FRAME_SEQUENCER_PERIOD = CPU::CLOCK_SPEED // FRAME_SEQUENCER_RATE + + @soundcnt_l = Reg::SOUNDCNT_L.new 0 + getter soundcnt_h = Reg::SOUNDCNT_H.new 0 + @sound_enabled : Bool = false + @soundbias = Reg::SOUNDBIAS.new 0x3FE + + @buffer = Slice(Int16).new BUFFER_SIZE + @buffer_pos = 0 + @frame_sequencer_stage = 0 + getter first_half_of_length_period = false + + @audiospec : LibSDL::AudioSpec + @obtained_spec : LibSDL::AudioSpec + + @sync : Bool = true + + def initialize(@gba : GBA) + @audiospec = LibSDL::AudioSpec.new + @audiospec.freq = SAMPLE_RATE + @audiospec.format = LibSDL::AUDIO_S16 + @audiospec.channels = CHANNELS + @audiospec.samples = BUFFER_SIZE + @audiospec.callback = nil + @audiospec.userdata = nil + + @obtained_spec = LibSDL::AudioSpec.new + + @channel1 = Channel1.new @gba + @channel2 = Channel2.new @gba + @channel3 = Channel3.new @gba + @channel4 = Channel4.new @gba + @dma_channels = DMAChannels.new @gba, @soundcnt_h + + tick_frame_sequencer + get_sample + + raise "Failed to open audio" if LibSDL.open_audio(pointerof(@audiospec), pointerof(@obtained_spec)) > 0 + + LibSDL.pause_audio 0 + end + + def toggle_sync + @sync = !@sync + end + + def tick_frame_sequencer : Nil + @first_half_of_length_period = @frame_sequencer_stage & 1 == 0 + case @frame_sequencer_stage + when 0 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + when 1 then nil + when 2 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + @channel1.sweep_step + when 3 then nil + when 4 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + when 5 then nil + when 6 + @channel1.length_step + @channel2.length_step + @channel3.length_step + @channel4.length_step + @channel1.sweep_step + when 7 + @channel1.volume_step + @channel2.volume_step + @channel4.volume_step + else nil + end + @frame_sequencer_stage = 0 if (@frame_sequencer_stage += 1) > 7 + @gba.scheduler.schedule FRAME_SEQUENCER_PERIOD, ->tick_frame_sequencer + end + + def get_sample : Nil + abort "Prohibited sound 1-4 volume #{@soundcnt_h.sound_volume}" if @soundcnt_h.sound_volume >= 3 + # Gets PSGs on scale of -0x80..0x80 each + psg_sound = ((@channel1.get_amplitude * @soundcnt_l.channel_1_left) + + (@channel2.get_amplitude * @soundcnt_l.channel_2_left) + + (@channel3.get_amplitude * @soundcnt_l.channel_3_left) + + (@channel4.get_amplitude * @soundcnt_l.channel_4_left)) + # Keep PSGs on scale of -0x200...0x200 (shift by `5 - vol` to account for `*8` from left/right vol) + psg_left = (psg_sound * @soundcnt_l.left_volume) >> (5 - @soundcnt_h.sound_volume) + psg_right = (psg_sound * @soundcnt_l.right_volume) >> (5 - @soundcnt_h.sound_volume) + + # Gets DMAs on scale of -0x100...0x100 + dma_a, dma_b = @dma_channels.get_amplitude + # Puts DMAs on scale of -0x200...0x200 + dma_a <<= @soundcnt_h.dma_sound_a_volume + dma_b <<= @soundcnt_h.dma_sound_b_volume + dma_left = dma_a * @soundcnt_h.dma_sound_a_left + dma_b * @soundcnt_h.dma_sound_b_left + dma_right = dma_a * @soundcnt_h.dma_sound_a_right + dma_b * @soundcnt_h.dma_sound_b_right + + total_left = (psg_left + dma_left + @soundbias.bias_level).clamp(0_i16..0x3FF_i16) - @soundbias.bias_level + total_right = (psg_right + dma_right + @soundbias.bias_level).clamp(0_i16..0x3FF_i16) - @soundbias.bias_level + + @buffer[@buffer_pos] = total_left * 32 + @buffer[@buffer_pos + 1] = total_right * 32 + @buffer_pos += 2 + + # push to SDL if buffer is full + if @buffer_pos >= BUFFER_SIZE + LibSDL.clear_queued_audio 1 unless @sync + while LibSDL.get_queued_audio_size(1) > BUFFER_SIZE * sizeof(Int16) * 2 + LibSDL.delay(1) + end + LibSDL.queue_audio 1, @buffer, BUFFER_SIZE * sizeof(Int16) + @buffer_pos = 0 + end + + @gba.scheduler.schedule SAMPLE_PERIOD, ->get_sample + end + + def timer_overflow(timer : Int) : Nil + @dma_channels.timer_overflow timer + end + + def read_io(io_addr : Int) : UInt8 + case io_addr + when @channel1 then @channel1.read_io io_addr + when @channel2 then @channel2.read_io io_addr + when @channel3 then @channel3.read_io io_addr + when @channel4 then @channel4.read_io io_addr + when @dma_channels then @dma_channels.read_io io_addr + when 0x80 then @soundcnt_l.value.to_u8! + when 0x81 then (@soundcnt_l.value >> 8).to_u8! + when 0x82 then @soundcnt_h.value.to_u8! + when 0x83 then (@soundcnt_h.value >> 8).to_u8! + when 0x84 + 0x70_u8 | + (@sound_enabled ? 0x80 : 0) | + (@channel4.enabled ? 0b1000 : 0) | + (@channel3.enabled ? 0b0100 : 0) | + (@channel2.enabled ? 0b0010 : 0) | + (@channel1.enabled ? 0b0001 : 0) + when 0x85 then 0_u8 # unused + when 0x88 then @soundbias.value.to_u8! + when 0x89 then (@soundbias.value >> 8).to_u8! + else puts "Unmapped APU read ~ addr:#{hex_str io_addr.to_u8}".colorize.fore(:red); 0_u8 # todo: open bus + + + end + end + + # write to apu memory + def write_io(io_addr : Int, value : UInt8) : Nil + return unless @sound_enabled || 0x82 <= io_addr <= 0x89 || Channel3::WAVE_RAM_RANGE.includes?(io_addr) + case io_addr + when @channel1 then @channel1.write_io io_addr, value + when @channel2 then @channel2.write_io io_addr, value + when @channel3 then @channel3.write_io io_addr, value + when @channel4 then @channel4.write_io io_addr, value + when @dma_channels then @dma_channels.write_io io_addr, value + when 0x80 then @soundcnt_l.value = (@soundcnt_l.value & 0xFF00) | value + when 0x81 then @soundcnt_l.value = (@soundcnt_l.value & 0x00FF) | value.to_u16 << 8 + when 0x82 then @soundcnt_h.value = (@soundcnt_h.value & 0xFF00) | value + when 0x83 then @soundcnt_h.value = (@soundcnt_h.value & 0x00FF) | value.to_u16 << 8 + when 0x84 + if value & 0x80 == 0 && @sound_enabled + (0x60..0x81).each { |addr| self.write_io addr, 0x00 } + @sound_enabled = false + elsif value & 0x80 > 0 && !@sound_enabled + @sound_enabled = true + @frame_sequencer_stage = 0 + @channel1.length_counter = 0 + @channel2.length_counter = 0 + @channel3.length_counter = 0 + @channel4.length_counter = 0 + end + when 0x85 # unused + when 0x88 then @soundbias.value = (@soundbias.value & 0xFF00) | value + when 0x89 then @soundbias.value = (@soundbias.value & 0x00FF) | value.to_u16 << 8 + when 0xA8..0xAF # unused + else puts "Unmapped APU write ~ addr:#{hex_str io_addr.to_u8}, val:#{value}".colorize(:yellow) + end + end + end +end diff --git a/src/crab/gba/apu/abstract_channels.cr b/src/crab/gba/apu/abstract_channels.cr new file mode 100644 index 0000000..99be785 --- /dev/null +++ b/src/crab/gba/apu/abstract_channels.cr @@ -0,0 +1,100 @@ + # All of the channels were developed using the following guide on gbdev + # https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware + + module GBA + abstract class SoundChannel + property enabled : Bool = false + @dac_enabled : Bool = false + + # NRx1 + property length_counter = 0 + + # NRx4 + @length_enable : Bool = false + + def initialize(@gba : GBA) + end + + # Step the channel, calling helpers to reload the period and step the wave generation + def step : Nil + step_wave_generation + schedule_reload frequency_timer + end + + # Step the length, disabling the channel if the length counter expires + def length_step : Nil + if @length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + end + + # Used so that channels can be matched with case..when statements + abstract def ===(value) + + # Calculate the frequency timer + abstract def frequency_timer : UInt32 + + abstract def schedule_reload(frequency_timer : UInt32) : Nil + + # Called when @period reaches 0 + abstract def step_wave_generation : Nil + + abstract def get_amplitude : Int16 + + abstract def read_io(index : Int) : UInt8 + abstract def write_io(index : Int, value : UInt8) : Nil + end + + abstract class VolumeEnvelopeChannel < SoundChannel + # NRx2 + @starting_volume : UInt8 = 0x00 + @envelope_add_mode : Bool = false + @period : UInt8 = 0x00 + + @volume_envelope_timer : UInt8 = 0x00 + @current_volume : UInt8 = 0x00 + + @volume_envelope_is_updating = false + + def volume_step : Nil + if @period != 0 + @volume_envelope_timer -= 1 if @volume_envelope_timer > 0 + if @volume_envelope_timer == 0 + @volume_envelope_timer = @period + if (@current_volume < 0xF && @envelope_add_mode) || (@current_volume > 0 && !@envelope_add_mode) + @current_volume += (@envelope_add_mode ? 1 : -1) + else + @volume_envelope_is_updating = false + end + end + end + end + + def init_volume_envelope : Nil + @volume_envelope_timer = @period + @current_volume = @starting_volume + @volume_envelope_is_updating = true + end + + def read_NRx2 : UInt8 + @starting_volume << 4 | (@envelope_add_mode ? 0x08 : 0) | @period + end + + def write_NRx2(value : UInt8) : Nil + envelope_add_mode = value & 0x08 > 0 + if @enabled # Zombie mode glitch + @current_volume += 1 if (@period == 0 && @volume_envelope_is_updating) || !@envelope_add_mode + @current_volume = 0x10_u8 - @current_volume if (envelope_add_mode != @envelope_add_mode) + @current_volume &= 0x0F + end + + @starting_volume = value >> 4 + @envelope_add_mode = envelope_add_mode + @period = value & 0x07 + # Internal values + @dac_enabled = value & 0xF8 > 0 + @enabled = false if !@dac_enabled + end + end + end diff --git a/src/crab/gba/apu/channel1.cr b/src/crab/gba/apu/channel1.cr new file mode 100644 index 0000000..7dc4caa --- /dev/null +++ b/src/crab/gba/apu/channel1.cr @@ -0,0 +1,149 @@ +module GBA + class Channel1 < VolumeEnvelopeChannel + WAVE_DUTY = [ + [-8, -8, -8, -8, -8, -8, -8, +8], # 12.5% + [+8, -8, -8, -8, -8, -8, -8, +8], # 25% + [+8, -8, -8, -8, -8, +8, +8, +8], # 50% + [-8, +8, +8, +8, +8, +8, +8, -8], # 75% + ] + + RANGE = 0x60..0x67 + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + @wave_duty_position = 0 + + # NR10 + @sweep_period : UInt8 = 0x00 + @negate : Bool = false + @shift : UInt8 = 0x00 + + @sweep_timer : UInt8 = 0x00 + @frequency_shadow : UInt16 = 0x0000 + @sweep_enabled : Bool = false + @negate_has_been_used : Bool = false + + # NR11 + @duty : UInt8 = 0x00 + @length_load : UInt8 = 0x00 + + # NR13 / NR14 + @frequency : UInt16 = 0x00 + + def step_wave_generation : Nil + @wave_duty_position = (@wave_duty_position + 1) & 7 + end + + def frequency_timer : UInt32 + (0x800_u32 - @frequency) * 4 * 4 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel1 + end + + def sweep_step : Nil + @sweep_timer -= 1 if @sweep_timer > 0 + if @sweep_timer == 0 + @sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8 + if @sweep_enabled && @sweep_period > 0 + calculated = frequency_calculation + if calculated <= 0x07FF && @shift > 0 + @frequency_shadow = @frequency = calculated + frequency_calculation + end + end + end + end + + # Outputs a value -0x80..0x80 + def get_amplitude : Int16 + if @enabled && @dac_enabled + WAVE_DUTY[@duty][@wave_duty_position].to_i16 * @current_volume + else + 0_i16 + end + end + + # Calculate the new shadow frequency, disable channel if overflow 11 bits + # https://gist.github.com/drhelius/3652407#file-game-boy-sound-operation-L243-L250 + def frequency_calculation : UInt16 + calculated = @frequency_shadow >> @shift + calculated = @frequency_shadow + (@negate ? -1 : 1) * calculated + @negate_has_been_used = true if @negate + @enabled = false if calculated > 0x07FF + calculated + end + + def read_io(index : Int) : UInt8 + case index + when 0x60 then 0x80_u8 | @sweep_period << 4 | (@negate ? 0x08 : 0) | @shift + when 0x62 then 0x3F_u8 | @duty << 6 + when 0x63 then read_NRx2 + when 0x64 then 0xFF_u8 # write-only + when 0x65 then 0xBF_u8 | (@length_enable ? 0x40 : 0) + else puts "Reading from invalid Channel1 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus + + + end + end + + def write_io(index : Int, value : UInt8) : Nil + case index + when 0x60 + @sweep_period = (value & 0x70) >> 4 + @negate = value & 0x08 > 0 + @shift = value & 0x07 + # Internal values + @enabled = false if !@negate && @negate_has_been_used + when 0x61 # not used + when 0x62 + @duty = (value & 0xC0) >> 6 + @length_load = value & 0x3F + # Internal values + @length_counter = 0x40 - @length_load + when 0x63 + write_NRx2 value + when 0x64 + @frequency = (@frequency & 0x0700) | value + when 0x65 + @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x40 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period + end + # Init frequency + @gba.scheduler.clear Scheduler::EventType::APUChannel1 + schedule_reload frequency_timer + # Init volume envelope + init_volume_envelope + # Init sweep + @frequency_shadow = @frequency + @sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8 + @sweep_enabled = @sweep_period > 0 || @shift > 0 + @negate_has_been_used = false + if @shift > 0 # If sweep shift is non-zero, frequency calculation and overflow check are performed immediately + frequency_calculation + end + end + when 0x66 # not used + when 0x67 # not used + else raise "Writing to invalid Channel1 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gba/apu/channel2.cr b/src/crab/gba/apu/channel2.cr new file mode 100644 index 0000000..755c08d --- /dev/null +++ b/src/crab/gba/apu/channel2.cr @@ -0,0 +1,101 @@ +module GBA + class Channel2 < VolumeEnvelopeChannel + WAVE_DUTY = [ + [-8, -8, -8, -8, -8, -8, -8, +8], # 12.5% + [+8, -8, -8, -8, -8, -8, -8, +8], # 25% + [+8, -8, -8, -8, -8, +8, +8, +8], # 50% + [-8, +8, +8, +8, +8, +8, +8, -8], # 75% + ] + + RANGE = 0x68..0x6F + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + @wave_duty_position = 0 + + # NR21 + @duty : UInt8 = 0x00 + @length_load : UInt8 = 0x00 + + # NR23 / NR24 + @frequency : UInt16 = 0x00 + + def step_wave_generation : Nil + @wave_duty_position = (@wave_duty_position + 1) & 7 + end + + def frequency_timer : UInt32 + (0x800_u32 - @frequency) * 4 * 4 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel2 + end + + # Outputs a value -0x80..0x80 + def get_amplitude : Int16 + if @enabled && @dac_enabled + WAVE_DUTY[@duty][@wave_duty_position].to_i16 * @current_volume + else + 0_i16 + end + end + + def read_io(index : Int) : UInt8 + case index + when 0x68 then 0x3F_u8 | @duty << 6 + when 0x69 then read_NRx2 + when 0x6C then 0xFF_u8 # write-only + when 0x6D then 0xBF_u8 | (@length_enable ? 0x40 : 0) + else puts "Reading from invalid Channel2 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus + + + end + end + + def write_io(index : Int, value : UInt8) : Nil + case index + when 0x68 + @duty = (value & 0xC0) >> 6 + @length_load = value & 0x3F + # Internal values + @length_counter = 0x40 - @length_load + when 0x69 + write_NRx2 value + when 0x6A # not used + when 0x6B # not used + when 0x6C + @frequency = (@frequency & 0x0700) | value + when 0x6D + @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x40 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period + end + # Init frequency + @gba.scheduler.clear Scheduler::EventType::APUChannel2 + schedule_reload frequency_timer + # Init volume envelope + init_volume_envelope + end + when 0x6E # not used + when 0x6F # not used + else raise "Writing to invalid Channel2 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gba/apu/channel3.cr b/src/crab/gba/apu/channel3.cr new file mode 100644 index 0000000..68a6e7e --- /dev/null +++ b/src/crab/gba/apu/channel3.cr @@ -0,0 +1,129 @@ +module GBA + class Channel3 < SoundChannel + RANGE = 0x70..0x77 + WAVE_RAM_RANGE = 0x90..0x9F + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) || WAVE_RAM_RANGE.includes?(value) + end + + @wave_ram = Array(Bytes).new 2, Bytes.new(WAVE_RAM_RANGE.size) { |idx| idx & 1 == 0 ? 0x00_u8 : 0xFF_u8 } + @wave_ram_position : UInt8 = 0 + @wave_ram_sample_buffer : UInt8 = 0x00 + + # NR30 + @wave_ram_dimension : Bool = false + @wave_ram_bank : UInt8 = 0 + + # NR31 + @length_load : UInt8 = 0x00 + + # NR32 + @volume_code : UInt8 = 0x00 + @volume_force : Bool = false + + # NR33 / NR34 + @frequency : UInt16 = 0x00 + + def step_wave_generation : Nil + @wave_ram_position = (@wave_ram_position + 1) % (WAVE_RAM_RANGE.size * 2) + @wave_ram_bank ^= 1 if @wave_ram_position == 0 && @wave_ram_dimension + full_sample = @wave_ram[@wave_ram_bank][@wave_ram_position // 2] + @wave_ram_sample_buffer = (full_sample >> (@wave_ram_position & 1 == 0 ? 4 : 0)) & 0xF + end + + def frequency_timer : UInt32 + (0x800_u32 - @frequency) * 2 * 4 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel3 + end + + # Outputs a value -0x80..0x80 + def get_amplitude : Int16 + if @enabled && @dac_enabled + (@wave_ram_sample_buffer.to_i16 - 8) * 4 * (@volume_force ? 3 : {0, 4, 2, 1}[@volume_code]) + else + 0_i16 + end + end + + def read_io(index : Int) : UInt8 + case index + when 0x70 then 0x7F | (@dac_enabled ? 0x80 : 0) + when 0x72 then 0xFF + when 0x73 then 0x9F | @volume_code << 5 + when 0x74 then 0xFF + when 0x75 then 0xBF | (@length_enable ? 0x40 : 0) + when WAVE_RAM_RANGE + if @enabled + @wave_ram[@wave_ram_bank][@wave_ram_position // 2] + else + @wave_ram[@wave_ram_bank][index - WAVE_RAM_RANGE.begin] + end + else puts "Reading from invalid Channel3 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus + + + end.to_u8 + end + + def write_io(index : Int, value : UInt8) : Nil + case index + when 0x70 + @dac_enabled = value & 0x80 > 0 + @enabled = false if !@dac_enabled + @wave_ram_dimension = bit?(value, 5) + @wave_ram_bank = bits(value, 6..6) + when 0x71 # not used + when 0x72 + @length_load = value + # Internal values + @length_counter = 0x100 - @length_load + when 0x73 + @volume_code = (value & 0x60) >> 5 + @volume_force = bit?(value, 7) + when 0x74 + @frequency = (@frequency & 0x0700) | value + when 0x75 + @frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8 + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x100 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period + end + # Init frequency + # todo: I'm patching in an extra 6 T-cycles here with the `+ 6`. This is specifically + # to get blargg's "09-wave read while on.s" to pass. I'm _not_ refilling the + # frequency timer with this extra cycles when it reaches 0. For now, I'm letting + # this be in order to work on other audio behavior. Note that this is pretty + # brittle in it's current state though... + @gba.scheduler.clear Scheduler::EventType::APUChannel3 + schedule_reload frequency_timer + 6 + # Init wave ram position + @wave_ram_position = 0 + end + when 0x76 # not used + when 0x77 # not used + when WAVE_RAM_RANGE + if @enabled + @wave_ram[@wave_ram_bank][@wave_ram_position // 2] = value + else + @wave_ram[@wave_ram_bank][index - WAVE_RAM_RANGE.begin] = value + end + else raise "Writing to invalid Channel3 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gba/apu/channel4.cr b/src/crab/gba/apu/channel4.cr new file mode 100644 index 0000000..fd5ff5e --- /dev/null +++ b/src/crab/gba/apu/channel4.cr @@ -0,0 +1,103 @@ +module GBA + class Channel4 < VolumeEnvelopeChannel + RANGE = 0x78..0x7F + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + @lfsr : UInt16 = 0x0000 + + # NR41 + @length_load : UInt8 = 0x00 + + # NR43 + @clock_shift : UInt8 = 0x00 + @width_mode : UInt8 = 0x00 + @divisor_code : UInt8 = 0x00 + + def step_wave_generation : Nil + new_bit = (@lfsr & 0b01) ^ ((@lfsr & 0b10) >> 1) + @lfsr >>= 1 + @lfsr |= new_bit << 14 + if @width_mode != 0 + @lfsr &= ~(1 << 6) + @lfsr |= new_bit << 6 + end + end + + def frequency_timer : UInt32 + ((@divisor_code == 0 ? 8_u32 : @divisor_code.to_u32 << 4) << @clock_shift) * 4 + end + + def schedule_reload(frequency_timer : UInt32) : Nil + @gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel4 + end + + # Outputs a value -0x80..0x80 + def get_amplitude : Int16 + if @enabled && @dac_enabled + ((~@lfsr & 1).to_i16 * 16 - 8) * @current_volume + else + 0_i16 + end + end + + def read_io(index : Int) : UInt8 + case index + when 0x78 then 0xFF + when 0x79 then read_NRx2 + when 0x7C then @clock_shift << 4 | @width_mode << 3 | @divisor_code + when 0x7D then 0xBF | (@length_enable ? 0x40 : 0) + else puts "Reading from invalid Channel4 register: #{hex_str index.to_u16}".colorize.fore(:red); 0_u8 # todo: open bus + + + end.to_u8 + end + + def write_io(index : Int, value : UInt8) : Nil + case index + when 0x78 + @length_load = value & 0x3F + # Internal values + @length_counter = 0x40 - @length_load + when 0x79 + write_NRx2 value + when 0x7A # not used + when 0x7B # not used + when 0x7C + @clock_shift = value >> 4 + @width_mode = (value & 0x08) >> 3 + @divisor_code = value & 0x07 + when 0x7D + length_enable = value & 0x40 > 0 + # Obscure length counter behavior #1 + if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0 + @length_counter -= 1 + @enabled = false if @length_counter == 0 + end + @length_enable = length_enable + trigger = value & 0x80 > 0 + if trigger + @enabled = true if @dac_enabled + # Init length + if @length_counter == 0 + @length_counter = 0x40 + # Obscure length counter behavior #2 + @length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period + end + # Init frequency + @gba.scheduler.clear Scheduler::EventType::APUChannel4 + schedule_reload frequency_timer + # Init volume envelope + init_volume_envelope + # Init lfsr + @lfsr = 0x7FFF + end + when 0x7E # not used + when 0x7F # not used + else raise "Writing to invalid Channel4 register: #{hex_str index.to_u16}" + end + end + end +end diff --git a/src/crab/gba/apu/dma_channels.cr b/src/crab/gba/apu/dma_channels.cr new file mode 100644 index 0000000..f8021c0 --- /dev/null +++ b/src/crab/gba/apu/dma_channels.cr @@ -0,0 +1,58 @@ +module GBA + class DMAChannels + RANGE = 0xA0..0xA7 + + @fifos = Array(Array(Int8)).new 2 { Array(Int8).new 32, 0 } + @positions = Array(Int32).new 2, 0 + @sizes = Array(Int32).new 2, 0 + @timers : Array(Proc(UInt16)) + @latches = Array(Int16).new 2, 0 + + def ===(value) : Bool + value.is_a?(Int) && RANGE.includes?(value) + end + + def initialize(@gba : GBA, @control : Reg::SOUNDCNT_H) + @timers = [ + ->{ @control.dma_sound_a_timer }, + ->{ @control.dma_sound_b_timer }, + ] + end + + def read_io(index : Int) : UInt8 + 0_u8 + end + + def write_io(index : Int, value : Byte) : Nil + channel = bit?(index, 2).to_unsafe + if @sizes[channel] < 32 + @fifos[channel][(@positions[channel] + @sizes[channel]) % 32] = value.to_i8! + @sizes[channel] += 1 + else + log "Writing #{hex_str value} to fifo #{(channel + 65).chr}, but it's already full".colorize.fore(:red) + end + end + + def timer_overflow(timer : Int) : Nil + 2.times do |channel| + if timer == @timers[channel].call + if @sizes[channel] > 0 + log "Timer overflow good; channel:#{channel}, timer:#{timer}".colorize.fore(:yellow) + @latches[channel] = @fifos[channel][@positions[channel]].to_i16 << 1 # put on scale of -0x100..0x100 + @positions[channel] = (@positions[channel] + 1) % 32 + @sizes[channel] -= 1 + else + log "Timer overflow but empty; channel:#{channel}, timer:#{timer}".colorize.fore(:yellow) + @latches[channel] = 0 + end + end + @gba.dma.trigger_fifo(channel) if @sizes[channel] < 16 + end + end + + # Outputs a value -0x100...0x100 + def get_amplitude : Tuple(Int16, Int16) + {@latches[0], @latches[1]} + end + end +end diff --git a/src/crab/gba/arm/arm.cr b/src/crab/gba/arm/arm.cr new file mode 100644 index 0000000..2b66193 --- /dev/null +++ b/src/crab/gba/arm/arm.cr @@ -0,0 +1,100 @@ +module GBA + # The `private defs` here are effectively meaningless since I only run ARM + # functions from the CPU where I include it, but I'm just using it as an + # indicator that the functions should not be called directly outside of the + # module. + + module ARM + def arm_execute(instr : Word) : Nil + if check_cond bits(instr, 28..31) + hash = hash_instr instr + lut[hash].call instr + else + log "Skipping instruction, cond: #{hex_str instr >> 28}" + step_arm + end + end + + private def hash_instr(instr : Word) : Word + (instr >> 16 & 0x0FF0) | (instr >> 4 & 0xF) + end + + def fill_lut : Slice(Proc(Word, Nil)) + lut = Slice(Proc(Word, Nil)).new 4096, ->arm_unimplemented(Word) + 4096.times do |idx| + if idx & 0b111100000000 == 0b111100000000 + lut[idx] = ->arm_software_interrupt(Word) + elsif idx & 0b111100000001 == 0b111000000001 + # coprocessor register transfer + elsif idx & 0b111100000001 == 0b111000000001 + # coprocessor data operation + elsif idx & 0b111000000000 == 0b110000000000 + # coprocessor data transfer + elsif idx & 0b111000000000 == 0b101000000000 + lut[idx] = ->arm_branch(Word) + elsif idx & 0b111000000000 == 0b100000000000 + lut[idx] = ->arm_block_data_transfer(Word) + elsif idx & 0b111000000001 == 0b011000000001 + # undefined + elsif idx & 0b110000000000 == 0b010000000000 + lut[idx] = ->arm_single_data_transfer(Word) + elsif idx & 0b111111111111 == 0b000100100001 + lut[idx] = ->arm_branch_exchange(Word) + elsif idx & 0b111110111111 == 0b000100001001 + lut[idx] = ->arm_single_data_swap(Word) + elsif idx & 0b111110001111 == 0b000010001001 + lut[idx] = ->arm_multiply_long(Word) + elsif idx & 0b111111001111 == 0b000000001001 + lut[idx] = ->arm_multiply(Word) + elsif idx & 0b111001001001 == 0b000001001001 + lut[idx] = ->arm_halfword_data_transfer_immediate(Word) + elsif idx & 0b111001001001 == 0b000000001001 + lut[idx] = ->arm_halfword_data_transfer_register(Word) + elsif idx & 0b110110010000 == 0b000100000000 + lut[idx] = ->arm_psr_transfer(Word) + elsif idx & 0b110000000000 == 0b000000000000 + lut[idx] = ->arm_data_processing(Word) + else + lut[idx] = ->arm_unused(Word) + end + end + lut + end + + def arm_unimplemented(instr : Word) : Nil + puts "Unimplemented instruction: #{hex_str instr}" + exit 1 + end + + def arm_unused(instr : Word) : Nil + puts "Unused instruction: #{hex_str instr}" + end + + def rotate_register(instr : Word, carry_out : Pointer(Bool), allow_register_shifts : Bool) : Word + reg = bits(instr, 0..3) + shift_type = bits(instr, 5..6) + immediate = !(allow_register_shifts && bit?(instr, 4)) + if immediate + shift_amount = bits(instr, 7..11) + else + shift_register = bits(instr, 8..11) + # todo weird logic if bottom byte of reg > 31 + shift_amount = @r[shift_register] & 0xFF + end + case shift_type + when 0b00 then lsl(@r[reg], shift_amount, carry_out) + when 0b01 then lsr(@r[reg], shift_amount, immediate, carry_out) + when 0b10 then asr(@r[reg], shift_amount, immediate, carry_out) + when 0b11 then ror(@r[reg], shift_amount, immediate, carry_out) + else raise "Impossible shift type: #{hex_str shift_type}" + end + end + + def immediate_offset(instr : Word, carry_out : Pointer(Bool)) : Word + rotate = bits(instr, 8..11) + imm = bits(instr, 0..7) + # todo putting "false" here causes the gba-suite tests to pass, but _why_ + ror(imm, 2 * rotate, false, carry_out) + end + end +end diff --git a/src/crab/gba/arm/block_data_transfer.cr b/src/crab/gba/arm/block_data_transfer.cr new file mode 100644 index 0000000..52eac6c --- /dev/null +++ b/src/crab/gba/arm/block_data_transfer.cr @@ -0,0 +1,53 @@ +module GBA + module ARM + def arm_block_data_transfer(instr : Word) : Nil + pre_index = bit?(instr, 24) + add = bit?(instr, 23) + s_bit = bit?(instr, 22) + write_back = bit?(instr, 21) + load = bit?(instr, 20) + rn = bits(instr, 16..19) + list = bits(instr, 0..15) + + if s_bit + abort "todo: handle cases with r15 in list" if bit?(list, 15) + mode = @cpsr.mode + switch_mode CPU::Mode::USR + end + + address = @r[rn] + bits_set = count_set_bits(list) + if bits_set == 0 # odd behavior on empty list, tested in gba-suite + bits_set = 16 + list = 0x8000 + end + final_addr = address + bits_set * (add ? 4 : -4) + if add + address += 4 if pre_index + else + address = final_addr + address += 4 unless pre_index + end + first_transfer = false + 16.times do |idx| # always transfered to/from incrementing addresses + if bit?(list, idx) + if load + set_reg(idx, @gba.bus.read_word(address)) + else + @gba.bus[address] = @r[idx] + @gba.bus[address] &+= 4 if idx == 15 # pc reads 12 ahead instead of 8 + end + address += 4 # can always do these post since the address was accounted for up front + set_reg(rn, final_addr) if write_back && !first_transfer && !(load && bit?(list, rn)) + first_transfer = true # writeback happens on second cycle of the instruction + end + end + + if s_bit + switch_mode CPU::Mode.from_value mode.not_nil! + end + + step_arm unless load && bit?(list, 15) + end + end +end diff --git a/src/crab/gba/arm/branch.cr b/src/crab/gba/arm/branch.cr new file mode 100644 index 0000000..6cbcbfb --- /dev/null +++ b/src/crab/gba/arm/branch.cr @@ -0,0 +1,10 @@ +module GBA + module ARM + def arm_branch(instr : Word) : Nil + link = bit?(instr, 24) + offset = (bits(instr, 0..23) << 8).to_i32! >> 6 + set_reg(14, @r[15] - 4) if link + set_reg(15, @r[15] &+ offset) + end + end +end diff --git a/src/crab/gba/arm/branch_exchange.cr b/src/crab/gba/arm/branch_exchange.cr new file mode 100644 index 0000000..f68d410 --- /dev/null +++ b/src/crab/gba/arm/branch_exchange.cr @@ -0,0 +1,13 @@ +module GBA + module ARM + def arm_branch_exchange(instr : Word) : Nil + rn = bits(instr, 0..3) + if bit?(@r[rn], 0) + @cpsr.thumb = true + set_reg(15, @r[rn]) + else + set_reg(15, @r[rn]) + end + end + end +end diff --git a/src/crab/gba/arm/data_processing.cr b/src/crab/gba/arm/data_processing.cr new file mode 100644 index 0000000..6530920 --- /dev/null +++ b/src/crab/gba/arm/data_processing.cr @@ -0,0 +1,108 @@ +module GBA + module ARM + def arm_data_processing(instr : Word) : Nil + imm_flag = bit?(instr, 25) + opcode = bits(instr, 21..24) + set_conditions = bit?(instr, 20) + rn = bits(instr, 16..19) + rd = bits(instr, 12..15) + # The PC value will be the address of the instruction, plus 8 or 12 bytes due to instruction + # prefetching. If the shift amount is specified in the instruction, the PC will be 8 bytes + # ahead. If a register is used to specify the shift amount the PC will be 12 bytes ahead. + pc_reads_12_ahead = !imm_flag && bit?(instr, 4) + @r[15] &+= 4 if pc_reads_12_ahead + barrel_shifter_carry_out = @cpsr.carry + operand_2 = if imm_flag # Operand 2 is an immediate + immediate_offset bits(instr, 0..11), pointerof(barrel_shifter_carry_out) + else # Operand 2 is a register + rotate_register bits(instr, 0..11), pointerof(barrel_shifter_carry_out), allow_register_shifts: true + end + case opcode + when 0b0000 # AND + set_reg(rd, @r[rn] & operand_2) + if set_conditions + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = barrel_shifter_carry_out + end + step_arm unless rd == 15 + when 0b0001 # EOR + set_reg(rd, @r[rn] ^ operand_2) + if set_conditions + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = barrel_shifter_carry_out + end + step_arm unless rd == 15 + when 0b0010 # SUB + set_reg(rd, sub(@r[rn], operand_2, set_conditions)) + step_arm unless rd == 15 + when 0b0011 # RSB + set_reg(rd, sub(operand_2, @r[rn], set_conditions)) + step_arm unless rd == 15 + when 0b0100 # ADD + set_reg(rd, add(@r[rn], operand_2, set_conditions)) + step_arm unless rd == 15 + when 0b0101 # ADC + set_reg(rd, adc(@r[rn], operand_2, set_conditions)) + step_arm unless rd == 15 + when 0b0110 # SBC + set_reg(rd, sbc(@r[rn], operand_2, set_conditions)) + step_arm unless rd == 15 + when 0b0111 # RSC + set_reg(rd, sbc(operand_2, @r[rn], set_conditions)) + step_arm unless rd == 15 + when 0b1000 # TST + set_neg_and_zero_flags(@r[rn] & operand_2) + @cpsr.carry = barrel_shifter_carry_out + step_arm + when 0b1001 # TEQ + set_neg_and_zero_flags(@r[rn] ^ operand_2) + @cpsr.carry = barrel_shifter_carry_out + step_arm + when 0b1010 # CMP + sub(@r[rn], operand_2, set_conditions) + step_arm + when 0b1011 # CMN + add(@r[rn], operand_2, set_conditions) + step_arm + when 0b1100 # ORR + set_reg(rd, @r[rn] | operand_2) + if set_conditions + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = barrel_shifter_carry_out + end + step_arm unless rd == 15 + when 0b1101 # MOV + set_reg(rd, operand_2) + if set_conditions + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = barrel_shifter_carry_out + end + step_arm unless rd == 15 + when 0b1110 # BIC + set_reg(rd, @r[rn] & ~operand_2) + if set_conditions + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = barrel_shifter_carry_out + end + step_arm unless rd == 15 + when 0b1111 # MVN + set_reg(rd, ~operand_2) + if set_conditions + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = barrel_shifter_carry_out + end + step_arm unless rd == 15 + else raise "Unimplemented execution of data processing opcode: #{hex_str opcode}" + end + @r[15] &-= 4 if pc_reads_12_ahead # todo: this is probably borked if there's a write to r15, but it needs some more thought.. + if rd == 15 && set_conditions + @r[15] &-= 4 if @spsr.thumb # writing to r15 will have already cleared the pipeline and bumped r15 for arm mode + old_spsr = @spsr.value + new_mode = CPU::Mode.from_value(@spsr.mode) + switch_mode new_mode + @cpsr.value = old_spsr + @spsr.value = new_mode.bank == 0 ? @cpsr.value : @spsr_banks[new_mode.bank] + end + end + end +end diff --git a/src/crab/gba/arm/halfword_data_transfer_imm.cr b/src/crab/gba/arm/halfword_data_transfer_imm.cr new file mode 100644 index 0000000..bb06634 --- /dev/null +++ b/src/crab/gba/arm/halfword_data_transfer_imm.cr @@ -0,0 +1,59 @@ +module GBA + module ARM + def arm_halfword_data_transfer_immediate(instr : Word) : Nil + pre_index = bit?(instr, 24) + add = bit?(instr, 23) + write_back = bit?(instr, 21) + load = bit?(instr, 20) + rn = bits(instr, 16..19) + rd = bits(instr, 12..15) + offset_high = bits(instr, 8..11) + sh = bits(instr, 5..6) + offset_low = bits(instr, 0..3) + + address = @r[rn] + offset = offset_high << 4 | offset_low + + if pre_index + if add + address &+= offset + else + address &-= offset + end + end + + case sh + when 0b00 # swp, no docs on this? + abort "HalfwordDataTransferReg swp #{hex_str instr}" + when 0b01 # ldrh/strh + if load + set_reg(rd, @gba.bus.read_half_rotate address) + else + @gba.bus[address] = 0xFFFF_u16 & @r[rd] + # When R15 is the source register (Rd) of a register store (STR) instruction, the stored + # value will be address of the instruction plus 12. + @gba.bus[address] &+= 4 if rd == 15 + end + when 0b10 # ldrsb + set_reg(rd, @gba.bus[address].to_i8!.to_u32!) + when 0b11 # ldrsh + set_reg(rd, @gba.bus.read_half_signed(address)) + else raise "Invalid halfword data transfer imm op: #{sh}" + end + + unless pre_index + if add + address &+= offset + else + address &-= offset + end + end + # In the case of post-indexed addressing, the write back bit is redundant and is always set to + # zero, since the old base value can be retained by setting the offset to zero. Therefore + # post-indexed data transfers always write back the modified base. + set_reg(rn, address) if (write_back || !pre_index) && (rd != rn || !load) + + step_arm unless load && rd == 15 + end + end +end diff --git a/src/crab/gba/arm/halfword_data_transfer_reg.cr b/src/crab/gba/arm/halfword_data_transfer_reg.cr new file mode 100644 index 0000000..e3761cc --- /dev/null +++ b/src/crab/gba/arm/halfword_data_transfer_reg.cr @@ -0,0 +1,58 @@ +module GBA + module ARM + def arm_halfword_data_transfer_register(instr : Word) : Nil + pre_index = bit?(instr, 24) + add = bit?(instr, 23) + write_back = bit?(instr, 21) + load = bit?(instr, 20) + rn = bits(instr, 16..19) + rd = bits(instr, 12..15) + sh = bits(instr, 5..6) + rm = bits(instr, 0..3) + + address = @r[rn] + offset = @r[rm] + + if pre_index + if add + address &+= offset + else + address &-= offset + end + end + + case sh + when 0b00 # swp, no docs on this? + abort "HalfwordDataTransferReg swp #{hex_str instr}" + when 0b01 # ldrh/strh + if load + set_reg(rd, @gba.bus.read_half_rotate address) + else + @gba.bus[address] = 0xFFFF_u16 & @r[rd] + # When R15 is the source register (Rd) of a register store (STR) instruction, the stored + # value will be address of the instruction plus 12. + @gba.bus[address] &+= 4 if rd == 15 + end + when 0b10 # ldrsb + set_reg(rd, @gba.bus[address].to_i8!.to_u32!) + when 0b11 # ldrsh + set_reg(rd, @gba.bus.read_half_signed(address)) + else raise "Invalid halfword data transfer imm op: #{sh}" + end + + unless pre_index + if add + address &+= offset + else + address &-= offset + end + end + # In the case of post-indexed addressing, the write back bit is redundant and is always set to + # zero, since the old base value can be retained by setting the offset to zero. Therefore + # post-indexed data transfers always write back the modified base. + set_reg(rn, address) if (write_back || !pre_index) && (rd != rn || !load) + + step_arm unless load && rd == 15 + end + end +end diff --git a/src/crab/gba/arm/multiply.cr b/src/crab/gba/arm/multiply.cr new file mode 100644 index 0000000..59811e2 --- /dev/null +++ b/src/crab/gba/arm/multiply.cr @@ -0,0 +1,17 @@ +module GBA + module ARM + def arm_multiply(instr : Word) : Nil + accumulate = bit?(instr, 21) + set_conditions = bit?(instr, 20) + rd = bits(instr, 16..19) + rn = bits(instr, 12..15) + rs = bits(instr, 8..11) + rm = bits(instr, 0..3) + + set_reg(rd, @r[rm] &* @r[rs] &+ (accumulate ? @r[rn] : 0)) + set_neg_and_zero_flags(@r[rd]) if set_conditions + + step_arm unless rd == 15 + end + end +end diff --git a/src/crab/gba/arm/multiply_long.cr b/src/crab/gba/arm/multiply_long.cr new file mode 100644 index 0000000..ff22996 --- /dev/null +++ b/src/crab/gba/arm/multiply_long.cr @@ -0,0 +1,29 @@ +module GBA + module ARM + def arm_multiply_long(instr : Word) : Nil + signed = bit?(instr, 22) + accumulate = bit?(instr, 21) + set_conditions = bit?(instr, 20) + rdhi = bits(instr, 16..19) + rdlo = bits(instr, 12..15) + rs = bits(instr, 8..11) + rm = bits(instr, 0..3) + + res = if signed + @r[rm].to_i32!.to_i64 &* @r[rs].to_i32! + else + @r[rm].to_u64 &* @r[rs] + end + res &+= @r[rdhi].to_u64 << 32 | @r[rdlo] if accumulate + + set_reg(rdhi, (res >> 32).to_u32!) + set_reg(rdlo, res.to_u32!) + if set_conditions + @cpsr.negative = bit?(@r[rdhi], 31) + @cpsr.zero = res == 0 + end + + step_arm unless rdhi == 15 || rdlo == 15 + end + end +end diff --git a/src/crab/gba/arm/psr_transfer.cr b/src/crab/gba/arm/psr_transfer.cr new file mode 100644 index 0000000..6ca2548 --- /dev/null +++ b/src/crab/gba/arm/psr_transfer.cr @@ -0,0 +1,45 @@ +module GBA + module ARM + def arm_psr_transfer(instr : Word) : Nil + spsr = bit?(instr, 22) + mode = CPU::Mode.from_value @cpsr.mode + has_spsr = mode != CPU::Mode::USR && mode != CPU::Mode::SYS + + if bit?(instr, 21) # MSR + mask = 0_u32 + mask |= 0xFF000000 if bit?(instr, 19) # f (aka _flg) + mask |= 0x00FF0000 if bit?(instr, 18) # s + mask |= 0x0000FF00 if bit?(instr, 17) # x + mask |= 0x000000FF if bit?(instr, 16) # c (aka _ctl) + + if bit?(instr, 25) # immediate + barrel_shifter_carry_out = false # unused, doesn't matter + value = immediate_offset bits(instr, 0..11), pointerof(barrel_shifter_carry_out) + else # register value + value = @r[bits(instr, 0..3)] + end + + value &= mask + if spsr + if has_spsr + @spsr.value = (@spsr.value & ~mask) | value + end + else + thumb = @cpsr.thumb + switch_mode CPU::Mode.from_value value & 0x1F if mask & 0xFF > 0 + @cpsr.value = (@cpsr.value & ~mask) | value + @cpsr.thumb = thumb + end + else # MRS + rd = bits(instr, 12..15) + if spsr && has_spsr + set_reg(rd, @spsr.value) + else + set_reg(rd, @cpsr.value) + end + end + + step_arm unless !bit?(instr, 21) && bits(instr, 12..15) == 15 + end + end +end diff --git a/src/crab/gba/arm/single_data_swap.cr b/src/crab/gba/arm/single_data_swap.cr new file mode 100644 index 0000000..09cb8a8 --- /dev/null +++ b/src/crab/gba/arm/single_data_swap.cr @@ -0,0 +1,21 @@ +module GBA + module ARM + def arm_single_data_swap(instr : Word) : Nil + byte_quantity = bit?(instr, 22) + rn = bits(instr, 16..19) + rd = bits(instr, 12..15) + rm = bits(instr, 0..3) + if byte_quantity + tmp = @gba.bus[@r[rn]] + @gba.bus[@r[rn]] = @r[rm].to_u8! + set_reg(rd, tmp.to_u32) + else + tmp = @gba.bus.read_word_rotate @r[rn] + @gba.bus[@r[rn]] = @r[rm] + set_reg(rd, tmp) + end + + step_arm unless rd == 15 + end + end +end diff --git a/src/crab/gba/arm/single_data_transfer.cr b/src/crab/gba/arm/single_data_transfer.cr new file mode 100644 index 0000000..7880f13 --- /dev/null +++ b/src/crab/gba/arm/single_data_transfer.cr @@ -0,0 +1,62 @@ +module GBA + module ARM + def arm_single_data_transfer(instr : Word) : Nil + imm_flag = bit?(instr, 25) + pre_indexing = bit?(instr, 24) + add_offset = bit?(instr, 23) + byte_quantity = bit?(instr, 22) + write_back = bit?(instr, 21) + load = bit?(instr, 20) + rn = bits(instr, 16..19) + rd = bits(instr, 12..15) + + barrel_shifter_carry_out = false # unused, doesn't matter + offset = if imm_flag # Operand 2 is a register (opposite of data processing for some reason) + rotate_register bits(instr, 0..11), pointerof(barrel_shifter_carry_out), allow_register_shifts: false + else # Operand 2 is an immediate offset + bits(instr, 0..11) + end + + address = @r[rn] + + if pre_indexing + if add_offset + address &+= offset + else + address &-= offset + end + end + + if load + if byte_quantity + set_reg(rd, @gba.bus[address].to_u32) + else + set_reg(rd, @gba.bus.read_word_rotate address) + end + else + if byte_quantity + @gba.bus[address] = @r[rd].to_u8! + else + @gba.bus[address] = @r[rd] + end + # When R15 is the source register (Rd) of a register store (STR) instruction, the stored + # value will be address of the instruction plus 12. + @gba.bus[address] &+= 4 if rd == 15 + end + + unless pre_indexing + if add_offset + address &+= offset + else + address &-= offset + end + end + # In the case of post-indexed addressing, the write back bit is redundant and is always set to + # zero, since the old base value can be retained by setting the offset to zero. Therefore + # post-indexed data transfers always write back the modified base. + set_reg(rn, address) if (write_back || !pre_indexing) && (rd != rn || !load) + + step_arm unless load && rd == 15 + end + end +end diff --git a/src/crab/gba/arm/software_interrupt.cr b/src/crab/gba/arm/software_interrupt.cr new file mode 100644 index 0000000..b651153 --- /dev/null +++ b/src/crab/gba/arm/software_interrupt.cr @@ -0,0 +1,11 @@ +module GBA + module ARM + def arm_software_interrupt(instr : Word) : Nil + lr = @r[15] - 4 + switch_mode CPU::Mode::SVC + set_reg(14, lr) + @cpsr.irq_disable = true + set_reg(15, 0x08) + end + end +end diff --git a/src/crab/gba/bus.cr b/src/crab/gba/bus.cr new file mode 100644 index 0000000..75320df --- /dev/null +++ b/src/crab/gba/bus.cr @@ -0,0 +1,177 @@ +module GBA + class Bus + getter bios = Bytes.new 0x4000 + getter wram_board = Bytes.new 0x40000 + getter wram_chip = Bytes.new 0x08000 + + def initialize(@gba : GBA, bios_path : String) + File.open(bios_path) { |file| file.read @bios } + end + + def [](index : Int) : Byte + case bits(index, 24..27) + when 0x0 then @bios[index & 0x3FFF] + when 0x1 then 0_u8 # todo: open bus + when 0x2 then @wram_board[index & 0x3FFFF] + when 0x3 then @wram_chip[index & 0x7FFF] + when 0x4 then @gba.mmio[index] + when 0x5 then @gba.ppu.pram[index & 0x3FF] + when 0x6 + address = 0x1FFFF_u32 & index + address -= 0x8000 if address > 0x17FFF + @gba.ppu.vram[address] + when 0x7 then @gba.ppu.oam[index & 0x3FF] + when 0x8, 0x9, + 0xA, 0xB, + 0xC, 0xD then @gba.cartridge.rom[index & 0x01FFFFFF] + when 0xE, 0xF then @gba.storage[index] + else abort "Unmapped read: #{hex_str index.to_u32}" + end + end + + def read_half(index : Int) : HalfWord + index &= ~1 + case bits(index, 24..27) + when 0x0 then (@bios.to_unsafe + (index & 0x3FFF)).as(HalfWord*).value + when 0x1 then 0_u16 # todo: open bus + when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(HalfWord*).value + when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(HalfWord*).value + when 0x4 then read_half_slow(index) + when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(HalfWord*).value + when 0x6 + address = 0x1FFFF_u32 & index + address -= 0x8000 if address > 0x17FFF + (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value + when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value + when 0x8, 0x9, + 0xA, 0xB, + 0xC, 0xD then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value + when 0xE, 0xF then @gba.storage.read_half(index) + else abort "Unmapped read: #{hex_str index.to_u32}" + end + end + + def read_half_rotate(index : Int) : Word + half = read_half(index).to_u32! + bits = (index & 1) << 3 + half >> bits | half << (32 - bits) + end + + # On ARM7 aka ARMv4 aka NDS7/GBA: + # LDRH Rd,[odd] --> LDRH Rd,[odd-1] ROR 8 ;read to bit0-7 and bit24-31 + # LDRSH Rd,[odd] --> LDRSB Rd,[odd] ;sign-expand BYTE value + def read_half_signed(index : Int) : Word + if bit?(index, 0) + self[index].to_i8!.to_u32! + else + read_half(index).to_i16!.to_u32! + end + end + + def read_word(index : Int) : Word + index &= ~3 + case bits(index, 24..27) + when 0x0 then (@bios.to_unsafe + (index & 0x3FFF)).as(Word*).value + when 0x1 then 0_u32 # todo: open bus + when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(Word*).value + when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(Word*).value + when 0x4 then read_word_slow(index) + when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(Word*).value + when 0x6 + address = 0x1FFFF_u32 & index + address -= 0x8000 if address > 0x17FFF + (@gba.ppu.vram.to_unsafe + address).as(Word*).value + when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value + when 0x8, 0x9, + 0xA, 0xB, + 0xC, 0xD then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(Word*).value + when 0xE, 0xF then @gba.storage.read_word(index) + else abort "Unmapped read: #{hex_str index.to_u32}" + end + end + + def read_word_rotate(index : Int) : Word + word = read_word index + bits = (index & 3) << 3 + word >> bits | word << (32 - bits) + end + + def []=(index : Int, value : Byte) : Nil + return if bits(index, 28..31) > 0 + @gba.cpu.fill_pipeline if index <= @gba.cpu.r[15] && index >= @gba.cpu.r[15] &- 4 # detect writes near pc + case bits(index, 24..27) + when 0x2 then @wram_board[index & 0x3FFFF] = value + when 0x3 then @wram_chip[index & 0x7FFF] = value + when 0x4 then @gba.mmio[index] = value + when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FE)).as(HalfWord*).value = 0x0101_u16 * value + when 0x6 + address = 0x1FFFE_u32 & index # todo ignored range is different when in bitmap mode + (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = 0x0101_u16 * value if address <= 0x0FFFF + when 0xE, 0xF then @gba.storage[index] = value + else log "Unmapped write: #{hex_str index.to_u32}" + end + end + + def []=(index : Int, value : HalfWord) : Nil + return if bits(index, 28..31) > 0 + index &= ~1 + @gba.cpu.fill_pipeline if index <= @gba.cpu.r[15] && index >= @gba.cpu.r[15] &- 4 # detect writes near pc + case bits(index, 24..27) + when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(HalfWord*).value = value + when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(HalfWord*).value = value + when 0x4 then write_half_slow(index, value) + when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(HalfWord*).value = value + when 0x6 + address = 0x1FFFF_u32 & index + address -= 0x8000 if address > 0x17FFF + (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = value + when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value = value + when 0xE, 0xF then write_half_slow(index, value) + else log "Unmapped write: #{hex_str index.to_u32}" + end + end + + def []=(index : Int, value : Word) : Nil + return if bits(index, 28..31) > 0 + index &= ~3 + @gba.cpu.fill_pipeline if index <= @gba.cpu.r[15] && index >= @gba.cpu.r[15] &- 4 # detect writes near pc + case bits(index, 24..27) + when 0x2 then (@wram_board.to_unsafe + (index & 0x3FFFF)).as(Word*).value = value + when 0x3 then (@wram_chip.to_unsafe + (index & 0x7FFF)).as(Word*).value = value + when 0x4 then write_word_slow(index, value) + when 0x5 then (@gba.ppu.pram.to_unsafe + (index & 0x3FF)).as(Word*).value = value + when 0x6 + address = 0x1FFFF_u32 & index + address -= 0x8000 if address > 0x17FFF + (@gba.ppu.vram.to_unsafe + address).as(Word*).value = value + when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value = value + when 0xE, 0xF then write_word_slow(index, value) + else log "Unmapped write: #{hex_str index.to_u32}" + end + end + + private def read_half_slow(index : Int) : HalfWord + self[index].to_u16! | + (self[index + 1].to_u16! << 8) + end + + private def read_word_slow(index : Int) : Word + self[index].to_u32! | + (self[index + 1].to_u32! << 8) | + (self[index + 2].to_u32! << 16) | + (self[index + 3].to_u32! << 24) + end + + private def write_half_slow(index : Int, value : HalfWord) : Nil + self[index] = value.to_u8! + self[index + 1] = (value >> 8).to_u8! + end + + private def write_word_slow(index : Int, value : Word) : Nil + self[index] = value.to_u8! + self[index + 1] = (value >> 8).to_u8! + self[index + 2] = (value >> 16).to_u8! + self[index + 3] = (value >> 24).to_u8! + end + end +end diff --git a/src/crab/gba/cartridge.cr b/src/crab/gba/cartridge.cr new file mode 100644 index 0000000..c420af6 --- /dev/null +++ b/src/crab/gba/cartridge.cr @@ -0,0 +1,34 @@ +module GBA + class Cartridge + getter rom : Bytes + + getter title : String { + io = IO::Memory.new + io.write @rom[0x0A0...0x0AC] + io.to_s + } + + @rom = Bytes.new 0x02000000 do |addr| + oob = 0xFFFF & (addr >> 1) + (oob >> (8 * (addr & 1))).to_u8! + end + + def initialize(rom_path : String) + File.open(rom_path) { |file| file.read @rom } + # The following logic accounts for improperly dumped ROMs or bad homebrews. + # All proper ROMs should have a power-of-two size. The handling here is + # really pretty arbitrary. mGBA chooses to fill the entire ROM address + # space with zeros in this case, although gba-suite/unsafe tests that there + # are zeros up to the next power of two. I've chosen to just make that test + # pass, although there's an argument to be made that it's better to match + # mGBA behavior instead. Either way, if a ROM relies on this behavior, it's + # a buggy ROM. This is just an attempt to match the some expected behavior. + size = File.size(rom_path) + if count_set_bits(size) != 1 + last_bit = last_set_bit(size) + next_power = 2 ** (last_bit + 1) + (size...next_power).each { |i| @rom[i] = 0 } + end + end + end +end diff --git a/src/crab/gba/cpu.cr b/src/crab/gba/cpu.cr new file mode 100644 index 0000000..bc30b7e --- /dev/null +++ b/src/crab/gba/cpu.cr @@ -0,0 +1,300 @@ +require "./arm/*" +require "./thumb/*" +require "./pipeline" + +module GBA + class CPU + include ARM + include THUMB + + CLOCK_SPEED = 2**24 + + enum Mode : UInt32 + USR = 0b10000 + FIQ = 0b10001 + IRQ = 0b10010 + SVC = 0b10011 + ABT = 0b10111 + UND = 0b11011 + SYS = 0b11111 + + def bank : Int + case self + in Mode::USR, Mode::SYS then 0 + in Mode::FIQ then 1 + in Mode::IRQ then 2 + in Mode::SVC then 3 + in Mode::ABT then 4 + in Mode::UND then 5 + end + end + end + + class PSR < BitField(UInt32) + bool negative + bool zero + bool carry + bool overflow + num reserved, 20 + bool irq_disable + bool fiq_disable + bool thumb + num mode, 5 + end + + getter r = Slice(Word).new 16 + @cpsr : PSR = PSR.new CPU::Mode::SYS.value + @spsr : PSR = PSR.new CPU::Mode::SYS.value + getter pipeline = Pipeline.new + getter lut : Slice(Proc(Word, Nil)) { fill_lut } + getter thumb_lut : Slice(Proc(Word, Nil)) { fill_thumb_lut } + @reg_banks = Array(Array(Word)).new 6 { Array(GBA::Word).new 7, 0 } + @spsr_banks = Array(Word).new 6, CPU::Mode::SYS.value # logically independent of typical register banks + property halted = false + + def initialize(@gba : GBA) + @reg_banks[Mode::USR.bank][5] = @r[13] = 0x03007F00 + @reg_banks[Mode::IRQ.bank][5] = 0x03007FA0 + @reg_banks[Mode::SVC.bank][5] = 0x03007FE0 + @r[15] = 0x08000000 + clear_pipeline + end + + def switch_mode(new_mode : Mode, caller = __FILE__) : Nil + old_mode = Mode.from_value @cpsr.mode + return if new_mode == old_mode + new_bank = new_mode.bank + old_bank = old_mode.bank + if new_mode == Mode::FIQ || old_mode == Mode::FIQ + 5.times do |idx| + @reg_banks[old_bank][idx] = @r[8 + idx] + @r[8 + idx] = @reg_banks[new_bank][idx] + end + end + # store old regs + @reg_banks[old_bank][5] = @r[13] + @reg_banks[old_bank][6] = @r[14] + @spsr_banks[old_bank] = @spsr.value + # load new regs + @r[13] = @reg_banks[new_bank][5] + @r[14] = @reg_banks[new_bank][6] + @spsr.value = @cpsr.value + @cpsr.mode = new_mode.value + end + + def irq : Nil + unless @cpsr.irq_disable + lr = @r[15] - (@cpsr.thumb ? 0 : 4) + switch_mode CPU::Mode::IRQ + @cpsr.thumb = false + @cpsr.irq_disable = true + set_reg(14, lr) + set_reg(15, 0x18) + end + end + + def fill_pipeline : Nil + if @cpsr.thumb + pc = @r[15] & ~1 + @pipeline.push @gba.bus.read_half(@r[15] &- 2).to_u32! if @pipeline.size == 0 + @pipeline.push @gba.bus.read_half(@r[15]).to_u32! if @pipeline.size == 1 + else + pc = @r[15] & ~3 + @pipeline.push @gba.bus.read_word(@r[15] &- 4) if @pipeline.size == 0 + @pipeline.push @gba.bus.read_word(@r[15]) if @pipeline.size == 1 + end + end + + def clear_pipeline : Nil + @pipeline.clear + if @cpsr.thumb + @r[15] &+= 4 + else + @r[15] &+= 8 + end + end + + def read_instr : Word + if @pipeline.size == 0 + if @cpsr.thumb + @r[15] &= ~1 + @gba.bus.read_half(@r[15] &- 4).to_u32! + else + @r[15] &= ~3 + @gba.bus.read_word(@r[15] &- 8) + end + else + @pipeline.shift + end + end + + def tick : Nil + unless @halted + instr = read_instr + {% if flag? :trace %} print_state instr {% end %} + if @cpsr.thumb + thumb_execute instr + else + arm_execute instr + end + @gba.scheduler.tick 1 + else + @gba.scheduler.fast_forward + end + end + + def check_cond(cond : Word) : Bool + case cond + when 0x0 then @cpsr.zero + when 0x1 then !@cpsr.zero + when 0x2 then @cpsr.carry + when 0x3 then !@cpsr.carry + when 0x4 then @cpsr.negative + when 0x5 then !@cpsr.negative + when 0x6 then @cpsr.overflow + when 0x7 then !@cpsr.overflow + when 0x8 then @cpsr.carry && !@cpsr.zero + when 0x9 then !@cpsr.carry || @cpsr.zero + when 0xA then @cpsr.negative == @cpsr.overflow + when 0xB then @cpsr.negative != @cpsr.overflow + when 0xC then !@cpsr.zero && @cpsr.negative == @cpsr.overflow + when 0xD then @cpsr.zero || @cpsr.negative != @cpsr.overflow + when 0xE then true + else raise "Cond 0xF is reserved" + end + end + + def step_arm : Nil + @r[15] &+= 4 + end + + def step_thumb : Nil + @r[15] &+= 2 + end + + @[AlwaysInline] + def set_reg(reg : Int, value : Int) : UInt32 + @r[reg] = value.to_u32! + clear_pipeline if reg == 15 + value.to_u32! + end + + @[AlwaysInline] + def set_neg_and_zero_flags(value : Int) : Nil + @cpsr.negative = bit?(value, 31) + @cpsr.zero = value == 0 + end + + # Logical shift left + def lsl(word : Word, bits : Int, carry_out : Pointer(Bool)) : Word + log "lsl - word:#{hex_str word}, bits:#{bits}" + return word if bits == 0 + carry_out.value = bit?(word, 32 - bits) + word << bits + end + + # Logical shift right + def lsr(word : Word, bits : Int, immediate : Bool, carry_out : Pointer(Bool)) : Word + log "lsr - word:#{hex_str word}, bits:#{bits}" + if bits == 0 + return word unless immediate + bits = 32 + end + carry_out.value = bit?(word, bits - 1) + word >> bits + end + + # Arithmetic shift right + def asr(word : Word, bits : Int, immediate : Bool, carry_out : Pointer(Bool)) : Word + log "asr - word:#{hex_str word}, bits:#{bits}" + if bits == 0 + return word unless immediate + bits = 32 + end + if bits <= 31 + carry_out.value = bit?(word, bits - 1) + word >> bits | (0xFFFFFFFF_u32 &* (word >> 31)) << (32 - bits) + else + # ASR by 32 or more has result filled with and carry out equal to bit 31 of Rm. + carry_out.value = bit?(word, 31) + 0xFFFFFFFF_u32 &* (word >> 31) + end + end + + # Rotate right + def ror(word : Word, bits : Int, immediate : Bool, carry_out : Pointer(Bool)) : Word + log "ror - word:#{hex_str word}, bits:#{bits}" + if bits == 0 # RRX #1 + return word unless immediate + res = (word >> 1) | (@cpsr.carry.to_unsafe << 31) + carry_out.value = bit?(word, 0) + res + else + bits &= 31 # ROR by n where n is greater than 32 will give the same result and carry out as ROR by n-32 + bits = 32 if bits == 0 # ROR by 32 has result equal to Rm, carry out equal to bit 31 of Rm. + carry_out.value = bit?(word, bits - 1) + word >> bits | word << (32 - bits) + end + end + + # Subtract two values + def sub(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word + log "sub - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" + res = operand_1 &- operand_2 + if set_conditions + set_neg_and_zero_flags(res) + @cpsr.carry = operand_1 >= operand_2 + @cpsr.overflow = bit?((operand_1 ^ operand_2) & (operand_1 ^ res), 31) + end + res + end + + # Subtract two values with carry + def sbc(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word + log "sbc - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" + res = operand_1 &- operand_2 &- 1 &+ @cpsr.carry.to_unsafe + if set_conditions + set_neg_and_zero_flags(res) + @cpsr.carry = operand_1 >= operand_2.to_u64 + 1 - @cpsr.carry.to_unsafe + @cpsr.overflow = bit?((operand_1 ^ operand_2) & (operand_1 ^ res), 31) + end + res + end + + # Add two values + def add(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word + log "add - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" + res = operand_1 &+ operand_2 + if set_conditions + set_neg_and_zero_flags(res) + @cpsr.carry = res < operand_1 + @cpsr.overflow = bit?(~(operand_1 ^ operand_2) & (operand_2 ^ res), 31) + end + res + end + + # Add two values with carry + def adc(operand_1 : Word, operand_2 : Word, set_conditions : Bool) : Word + log "adc - operand_1:#{hex_str operand_1}, operand_2:#{hex_str operand_2}" + res = operand_1 &+ operand_2 &+ @cpsr.carry.to_unsafe + if set_conditions + set_neg_and_zero_flags(res) + @cpsr.carry = res < operand_1.to_u64 + @cpsr.carry.to_unsafe + @cpsr.overflow = bit?(~(operand_1 ^ operand_2) & (operand_2 ^ res), 31) + end + res + end + + def print_state(instr : Word? = nil) : Nil + @r.each_with_index do |val, reg| + print "#{hex_str reg == 15 ? val - (@cpsr.thumb ? 2 : 4) : val, prefix: false} " + end + instr ||= @pipeline.peek + if @cpsr.thumb + puts "cpsr: #{hex_str @cpsr.value, prefix: false} | #{hex_str instr.to_u16, prefix: false}" + else + puts "cpsr: #{hex_str @cpsr.value, prefix: false} | #{hex_str instr, prefix: false}" + end + end + end +end diff --git a/src/crab/gba/debugger.cr b/src/crab/gba/debugger.cr new file mode 100644 index 0000000..8f0de89 --- /dev/null +++ b/src/crab/gba/debugger.cr @@ -0,0 +1,105 @@ +module GBA + class Debugger + getter breakpoints = [] of Word + + def initialize(@gba : GBA) + end + + def break_on(addr : Word) + {% if flag? :debugger %} breakpoints << addr {% end %} + end + + def check_debug : Nil + {% if flag? :debugger %} debug if breakpoints.includes? @gba.cpu.r[15] {% end %} + end + + private def debug : Nil + puts "#{"----- DEBUGGER -----".colorize.mode(:bold)} #{"`help` for list of commands".colorize.mode(:dim)}" + @gba.cpu.print_state + while true + input = gets + case input + when .nil?, "exit", "continue" then break + when "step", "next", "tick" + @gba.cpu.tick + @gba.cpu.print_state + when "bios" then less @gba.bus.bios.hexdump + when "ewram" then less @gba.bus.wram_board.hexdump + when "iwram" then less @gba.bus.wram_chip.hexdump + when "pram" then less @gba.ppu.pram.hexdump + when "vram" then less @gba.ppu.vram.hexdump + when "oam" then less @gba.ppu.oam.hexdump + when "rom" then less @gba.cartridge.rom.hexdump + when "sram" then less @gba.cartridge.sram.hexdump + when "list" then print_breakpoints + when /(b|break) (0x\d+)/ + match = /(b|break) (0x\d+)/.match(input.not_nil!).not_nil! + breakpoints << match[2].to_i(base: 16, prefix: true).to_u32! + print_breakpoints + when "clear" + breakpoints.clear + print_breakpoints + when /clear (0x\d+)/ + match = /clear (0x\d+)/.match(input.not_nil!).not_nil! + breakpoints.delete(match[1].to_i(base: 16, prefix: true)) + print_breakpoints + when /\[(0x\d+)\]$/, /\[(0x\d+)\], word/ + match = /\[(0x\d+)\]/.match(input.not_nil!).not_nil! + puts hex_str @gba.bus.read_word(match[1].to_i(base: 16, prefix: true)) + when /\[(0x\d+)\], half/ + match = /\[(0x\d+)\]/.match(input.not_nil!).not_nil! + puts hex_str @gba.bus.read_half(match[1].to_i(base: 16, prefix: true)) + when /\[(0x\d+)\], byte/ + match = /\[(0x\d+)\]/.match(input.not_nil!).not_nil! + puts hex_str @gba.bus[match[1].to_i(base: 16, prefix: true)] + else + puts "Available commands:" + puts " Resume execution:" + puts " ^D" + puts " exit" + puts " continue" + puts " Stepping:" + puts " step" + puts " next" + puts " tick" + puts " Listing breakpoints:" + puts " list" + puts " Adding breakpoints:" + puts " b 0x08000000" + puts " break 0x08000000" + puts " Removing breakpoints:" + puts " clear" + puts " clear 0x1234" + puts " Memory regions:" + puts " bios" + puts " ewram" + puts " iwram" + puts " pram" + puts " vram" + puts " oam" + puts " rom" + puts " sram" + puts " Reading memory:" + puts " [0x1234]" + puts " [0x1234], word" + puts " [0x1234], half" + puts " [0x1234], byte" + end + puts + end + end + + private def less(string : String) : Nil + file = File.new("/tmp/crab", "w") + file.puts string + system "less /tmp/crab" + file.delete + end + + private def print_breakpoints : Nil + print "Breakpoints: " + breakpoints.sort.each { |b| print "#{hex_str b}, " } + puts + end + end +end diff --git a/src/crab/gba/display.cr b/src/crab/gba/display.cr new file mode 100644 index 0000000..d8480bf --- /dev/null +++ b/src/crab/gba/display.cr @@ -0,0 +1,122 @@ +require "lib_gl" + +module GBA + class Display + WIDTH = 240 + HEIGHT = 160 + SCALE = 4 + + @microseconds = 0 + @frames = 0 + @last_time = Time.utc + @seconds : Int32 = Time.utc.second + + @blend : Bool = false + + def initialize + @window = SDL::Window.new(window_title(59.7), WIDTH * SCALE, HEIGHT * SCALE, flags: SDL::Window::Flags::OPENGL) + setup_gl + end + + def draw(framebuffer : Slice(UInt16)) : Nil + LibGL.tex_image_2d(LibGL::TEXTURE_2D, 0, LibGL::RGB5, 240, 160, 0, LibGL::RGBA, LibGL::UNSIGNED_SHORT_1_5_5_5_REV, framebuffer) + LibGL.draw_arrays(LibGL::TRIANGLE_STRIP, 0, 4) + LibSDL.gl_swap_window(@window) + update_draw_count + end + + def toggle_blending : Nil + if @blend + LibGL.disable(LibGL::BLEND) + else + LibGL.enable(LibGL::BLEND) + end + @blend = !@blend + end + + private def window_title(fps : Float) : String + "crab - #{fps.round(1)} fps" + end + + private def update_draw_count : Nil + current_time = Time.utc + @microseconds += (current_time - @last_time).microseconds + @last_time = current_time + @frames += 1 + if current_time.second != @seconds + fps = @frames * 1_000_000 / @microseconds + @window.title = window_title(fps) + @microseconds = 0 + @frames = 0 + @seconds = current_time.second + end + end + + private def compile_shader(source : String, type : UInt32) : UInt32 + source_ptr = source.to_unsafe + shader = LibGL.create_shader(type) + LibGL.shader_source(shader, 1, pointerof(source_ptr), nil) + LibGL.compile_shader(shader) + shader_compiled = 0 + LibGL.get_shader_iv(shader, LibGL::COMPILE_STATUS, pointerof(shader_compiled)) + if shader_compiled != LibGL::TRUE + log_length = 0 + LibGL.get_shader_iv(shader, LibGL::INFO_LOG_LENGTH, pointerof(log_length)) + s = " " * log_length + LibGL.get_shader_info_log(shader, log_length, pointerof(log_length), s) if log_length > 0 + abort "Error compiling shader: #{s}" + end + shader + end + + private def setup_gl : Nil + {% if flag?(:darwin) %} + LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_FLAGS, LibSDL::GLcontextFlag::FORWARD_COMPATIBLE_FLAG) + {% end %} + LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_PROFILE_MASK, LibSDL::GLprofile::PROFILE_CORE) + LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_MAJOR_VERSION, 3) + LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_MINOR_VERSION, 3) + + {% unless flag?(:darwin) %} + # todo: proper debug messages for mac + LibGL.enable(LibGL::DEBUG_OUTPUT) + LibGL.enable(LibGL::DEBUG_OUTPUT_SYNCHRONOUS) + LibGL.debug_message_callback(->Display.callback, nil) + {% end %} + + LibSDL.gl_create_context @window + LibSDL.gl_set_swap_interval(0) # disable vsync + shader_program = LibGL.create_program + + puts "OpenGL version: #{String.new(LibGL.get_string(LibGL::VERSION))}" + puts "Shader language version: #{String.new(LibGL.get_string(LibGL::SHADING_LANGUAGE_VERSION))}" + + LibGL.blend_func(LibGL::SRC_ALPHA, LibGL::ONE_MINUS_SRC_ALPHA) + + vert_shader_id = compile_shader(File.read("src/crab/gba/shaders/gba_colors.vert"), LibGL::VERTEX_SHADER) + frag_shader_id = compile_shader(File.read("src/crab/gba/shaders/gba_colors.frag"), LibGL::FRAGMENT_SHADER) + + frame_buffer = 0_u32 + LibGL.gen_textures(1, pointerof(frame_buffer)) + LibGL.active_texture(LibGL::TEXTURE0) + LibGL.bind_texture(LibGL::TEXTURE_2D, frame_buffer) + LibGL.attach_shader(shader_program, vert_shader_id) + LibGL.attach_shader(shader_program, frag_shader_id) + LibGL.link_program(shader_program) + LibGL.validate_program(shader_program) + a = [LibGL::BLUE, LibGL::GREEN, LibGL::RED, LibGL::ONE] # flip the rgba to bgra where a is always 1 + a_ptr = pointerof(a).as(Int32*) + LibGL.tex_parameter_iv(LibGL::TEXTURE_2D, LibGL::TEXTURE_SWIZZLE_RGBA, a_ptr) + LibGL.tex_parameter_i(LibGL::TEXTURE_2D, LibGL::TEXTURE_MIN_FILTER, LibGL::NEAREST) + LibGL.tex_parameter_i(LibGL::TEXTURE_2D, LibGL::TEXTURE_MAG_FILTER, LibGL::NEAREST) + LibGL.use_program(shader_program) + vao = 0_u32 # required even if not used in modern opengl + LibGL.gen_vertex_arrays(1, pointerof(vao)) + LibGL.bind_vertex_array(vao) + end + + protected def self.callback(source : UInt32, type : UInt32, id : UInt32, severity : UInt32, length : Int32, message : Pointer(UInt8), userParam : Pointer(Void)) : Nil + puts "OpenGL debug message: #{String.new message}" + end + end +end diff --git a/src/crab/gba/dma.cr b/src/crab/gba/dma.cr new file mode 100644 index 0000000..5938d5e --- /dev/null +++ b/src/crab/gba/dma.cr @@ -0,0 +1,156 @@ +module GBA + class DMA + enum StartTiming + Immediate = 0 + VBlank = 1 + HBlank = 2 + Special = 3 + end + + enum AddressControl + Increment = 0 + Decrement = 1 + Fixed = 2 + IncrementReload = 3 + + def delta : Int + case self + in Increment, IncrementReload then 1 + in Decrement then -1 + in Fixed then 0 + end + end + end + + @interrupt_flags : Array(Proc(Nil)) + + def initialize(@gba : GBA) + @dmasad = Array(UInt32).new 4, 0 + @dmadad = Array(UInt32).new 4, 0 + @dmacnt_l = Array(UInt16).new 4, 0 + @dmacnt_h = Array(Reg::DMACNT).new 4 { Reg::DMACNT.new 0 } + @src = Array(UInt32).new 4, 0 + @dst = Array(UInt32).new 4, 0 + @interrupt_flags = [->{ @gba.interrupts.reg_if.dma0 = true }, ->{ @gba.interrupts.reg_if.dma1 = true }, + ->{ @gba.interrupts.reg_if.dma2 = true }, ->{ @gba.interrupts.reg_if.dma3 = true }] + @src_mask = [0x07FFFFFF, 0x0FFFFFFF, 0x0FFFFFFF, 0x0FFFFFFF] + @dst_mask = [0x07FFFFFF, 0x07FFFFFF, 0x07FFFFFF, 0x0FFFFFFF] + @len_mask = [0x3FFF, 0x3FFF, 0x3FFF, 0xFFFF] + end + + def read_io(io_addr : Int) : UInt8 + return 0_u8 if io_addr >= 0xE0 # todo: OOB read + channel = (io_addr - 0xB0) // 12 + reg = (io_addr - 0xB0) % 12 + case reg + when 0, 1, 2, 3 # dmasad + (@dmasad[channel] >> 8 * reg).to_u8! + when 4, 5, 6, 7 # dmadad + (@dmadad[channel] >> 8 * (reg - 4)).to_u8! + when 8, 9 # dmacnt_l + (@dmacnt_l[channel] >> 8 * (reg - 8)).to_u8! + when 10, 11 # dmacnt_h + (@dmacnt_h[channel].value >> 8 * (reg - 10)).to_u8! + else abort "Unmapped DMA read ~ addr:#{hex_str io_addr.to_u8}" + end + end + + def write_io(io_addr : Int, value : UInt8) : Nil + return if io_addr >= 0xE0 # todo: OOB write + channel = (io_addr - 0xB0) // 12 + reg = (io_addr - 0xB0) % 12 + case reg + when 0, 1, 2, 3 # dmasad + mask = 0xFF_u32 << (8 * reg) + value = value.to_u32 << (8 * reg) + dmasad = @dmasad[channel] + @dmasad[channel] = ((dmasad & ~mask) | value) & @src_mask[channel] + when 4, 5, 6, 7 # dmadad + reg -= 4 + mask = 0xFF_u32 << (8 * reg) + value = value.to_u32 << (8 * reg) + dmadad = @dmadad[channel] + @dmadad[channel] = ((dmadad & ~mask) | value) & @dst_mask[channel] + when 8, 9 # dmacnt_l + reg -= 8 + mask = 0xFF_u32 << (8 * reg) + value = value.to_u16 << (8 * reg) + dmacnt_l = @dmacnt_l[channel] + @dmacnt_l[channel] = ((dmacnt_l & ~mask) | value) & @len_mask[channel] + when 10, 11 # dmacnt_h + reg -= 10 + mask = 0xFF_u32 << (8 * reg) + value = value.to_u16 << (8 * reg) + dmacnt_h = @dmacnt_h[channel] + enabled = dmacnt_h.enable + dmacnt_h.value = (dmacnt_h.value & ~mask) | value + if dmacnt_h.enable && !enabled + @src[channel], @dst[channel] = @dmasad[channel], @dmadad[channel] + trigger channel if dmacnt_h.start_timing == StartTiming::Immediate.value + end + else abort "Unmapped DMA write ~ addr:#{hex_str io_addr.to_u8}, val:#{value}".colorize(:yellow) + end + end + + def trigger_hdma : Nil + 4.times do |channel| + dmacnt_h = @dmacnt_h[channel] + trigger channel if dmacnt_h.enable && dmacnt_h.start_timing == StartTiming::HBlank.value + end + end + + def trigger_vdma : Nil + 4.times do |channel| + dmacnt_h = @dmacnt_h[channel] + trigger channel if dmacnt_h.enable && dmacnt_h.start_timing == StartTiming::VBlank.value + end + end + + # todo: maybe abstract these various triggers + def trigger_fifo(fifo_channel : Int) : Nil + dmacnt_h = @dmacnt_h[fifo_channel + 1] + trigger fifo_channel + 1 if dmacnt_h.enable && dmacnt_h.start_timing == StartTiming::Special.value + end + + def trigger(channel : Int) : Nil + dmacnt_h = @dmacnt_h[channel] + + start_timing = StartTiming.from_value(dmacnt_h.start_timing) + source_control = AddressControl.from_value(dmacnt_h.source_control) + dest_control = AddressControl.from_value(dmacnt_h.dest_control) + word_size = 2 << dmacnt_h.type # 2 or 4 bytes + + len = @dmacnt_l[channel] + + puts "Prohibited source address control".colorize.fore(:yellow) if source_control == AddressControl::IncrementReload + + if start_timing == StartTiming::Special + if channel == 1 || channel == 2 # fifo + len = 4 + word_size = 4 + dest_control = AddressControl::Fixed + elsif channel == 3 # video capture + puts "todo: video capture dma" + else # prohibited + puts "Prohibited special dma".colorize.fore(:yellow) + end + end + + delta_source = word_size * source_control.delta + delta_dest = word_size * dest_control.delta + + len.times do |idx| + @gba.bus[@dst[channel]] = word_size == 4 ? @gba.bus.read_word(@src[channel]) : @gba.bus.read_half(@src[channel]) + @src[channel] &+= delta_source + @dst[channel] &+= delta_dest + end + + @dst[channel] = @dmadad[channel] if dest_control == AddressControl::IncrementReload + dmacnt_h.enable = false unless dmacnt_h.repeat && start_timing != StartTiming::Immediate + if dmacnt_h.irq_enable + @interrupt_flags[channel].call + @gba.interrupts.schedule_interrupt_check + end + end + end +end diff --git a/src/crab/gba/gba.cr b/src/crab/gba/gba.cr new file mode 100644 index 0000000..e13a801 --- /dev/null +++ b/src/crab/gba/gba.cr @@ -0,0 +1,87 @@ +require "./types" +require "./reg" +require "./scheduler" +require "./cartridge" +require "./storage" +require "./storage/*" +require "./mmio" +require "./timer" +require "./keypad" +require "./bus" +require "./interrupts" +require "./cpu" +require "./display" +require "./ppu" +require "./apu" +require "./dma" +require "./debugger" + +module GBA + class GBA + getter! scheduler : Scheduler + getter! cartridge : Cartridge + getter! storage : Storage + getter! mmio : MMIO + getter! timer : Timer + getter! keypad : Keypad + getter! bus : Bus + getter! interrupts : Interrupts + getter! cpu : CPU + getter! display : Display + getter! ppu : PPU + getter! apu : APU + getter! dma : DMA + getter! debugger : Debugger + + def initialize(@bios_path : String, rom_path : String) + @scheduler = Scheduler.new + @cartridge = Cartridge.new rom_path + @storage = Storage.new rom_path + handle_events + handle_saves + + SDL.init(SDL::Init::VIDEO | SDL::Init::AUDIO | SDL::Init::JOYSTICK) + LibSDL.joystick_open 0 + at_exit { SDL.quit } + end + + def post_init : Nil + @mmio = MMIO.new self + @timer = Timer.new self + @keypad = Keypad.new self + @bus = Bus.new self, @bios_path + @interrupts = Interrupts.new self + @cpu = CPU.new self + @display = Display.new + @ppu = PPU.new self + @apu = APU.new self + @dma = DMA.new self + @debugger = Debugger.new self + end + + def handle_events : Nil + scheduler.schedule 280896, ->handle_events + while event = SDL::Event.poll + case event + when SDL::Event::Quit then exit 0 + when SDL::Event::Keyboard, + SDL::Event::JoyHat, + SDL::Event::JoyButton then keypad.handle_keypad_event event + else nil + end + end + end + + def handle_saves : Nil + scheduler.schedule 280896, ->handle_saves + storage.write_save + end + + def run : Nil + loop do + {% if flag? :debugger %} debugger.check_debug {% end %} + cpu.tick + end + end + end +end diff --git a/src/crab/gba/interrupts.cr b/src/crab/gba/interrupts.cr new file mode 100644 index 0000000..21c8813 --- /dev/null +++ b/src/crab/gba/interrupts.cr @@ -0,0 +1,65 @@ +module GBA + class Interrupts + class InterruptReg < BitField(UInt16) + num not_used, 2, lock: true + bool game_pak + bool keypad + bool dma3 + bool dma2 + bool dma1 + bool dma0 + bool serial + bool timer3 + bool timer2 + bool timer1 + bool timer0 + bool vcounter + bool hblank + bool vblank + end + + getter reg_ie : InterruptReg = InterruptReg.new 0 + getter reg_if : InterruptReg = InterruptReg.new 0 + getter ime : Bool = false + + def initialize(@gba : GBA) + end + + def read_io(io_addr : Int) : Byte + case io_addr + when 0x200 then 0xFF_u8 & @reg_ie.value + when 0x201 then 0xFF_u8 & @reg_ie.value >> 8 + when 0x202 then 0xFF_u8 & @reg_if.value + when 0x203 then 0xFF_u8 & @reg_if.value >> 8 + when 0x208 then @ime ? 1_u8 : 0_u8 + when 0x209 then 0_u8 + else raise "Unimplemented interrupts read ~ addr:#{hex_str io_addr.to_u8!}" + end + end + + def write_io(io_addr : Int, value : Byte) : Nil + case io_addr + when 0x200 then @reg_ie.value = (@reg_ie.value & 0xFF00) | value + when 0x201 then @reg_ie.value = (@reg_ie.value & 0x00FF) | value.to_u16 << 8 + when 0x202 then @reg_if.value &= ~value + when 0x203 then @reg_if.value &= ~(value.to_u16 << 8) + when 0x208 then @ime = bit?(value, 0) + when 0x209 # ignored + else raise "Unimplemented interrupts write ~ addr:#{hex_str io_addr.to_u8!}, val:#{value}" + end + schedule_interrupt_check + end + + def schedule_interrupt_check : Nil + @gba.scheduler.schedule 0, ->check_interrupts + end + + private def check_interrupts : Nil + if @reg_ie.value & @reg_if.value != 0 + @gba.cpu.halted = false + # puts "IE:#{hex_str @reg_ie.value} & IF:#{hex_str @reg_if.value} != 0" + @gba.cpu.irq if @ime + end + end + end +end diff --git a/src/crab/gba/keypad.cr b/src/crab/gba/keypad.cr new file mode 100644 index 0000000..462fc38 --- /dev/null +++ b/src/crab/gba/keypad.cr @@ -0,0 +1,105 @@ +module GBA + class Keypad + class KEYINPUT < BitField(UInt16) + num not_used, 6 + bool l + bool r + bool down + bool up + bool left + bool right + bool start + bool :select + bool b + bool a + end + + class KEYCNT < BitField(UInt16) + bool irq_condition + bool irq_enable + num not_used, 4 + bool l + bool r + bool down + bool up + bool left + bool right + bool start + bool :select + bool b + bool a + end + + @keyinput = KEYINPUT.new 0xFFFF_u16 + @keycnt = KEYCNT.new 0xFFFF_u16 + + def initialize(@gba : GBA) + end + + def read_io(io_addr : Int) : Byte + case io_addr + when 0x130 then 0xFF_u8 & @keyinput.value + when 0x131 then 0xFF_u8 & @keyinput.value >> 8 + when 0x132 then 0xFF_u8 & @keycnt.value + when 0x133 then 0xFF_u8 & @keycnt.value >> 8 + else raise "Unimplemented keypad read ~ addr:#{hex_str io_addr.to_u8!}" + end + end + + def write_io(io_addr : Int, value : Byte) : Nil + case io_addr + when 0x130 then nil + when 0x131 then nil + else raise "Unimplemented keypad write ~ addr:#{hex_str io_addr.to_u8!}, val:#{value}" + end + end + + def handle_keypad_event(event : SDL::Event) : Nil + case event + when SDL::Event::Keyboard + bit = !event.pressed? + case event.sym + when .down?, .d? then @keyinput.down = bit + when .up?, .e? then @keyinput.up = bit + when .left?, .s? then @keyinput.left = bit + when .right?, .f? then @keyinput.right = bit + when .semicolon? then @keyinput.start = bit + when .l? then @keyinput.select = bit + when .b?, .j? then @keyinput.b = bit + when .a?, .k? then @keyinput.a = bit + when .w? then @keyinput.l = bit + when .r? then @keyinput.r = bit + # Extras + when .tab? then @gba.apu.toggle_sync if event.pressed? + when .m? then @gba.display.toggle_blending if event.pressed? + else nil + end + when SDL::Event::JoyHat + @keyinput.value |= 0x00F0 + case event.value + when LibSDL::HAT_DOWN then @keyinput.down = false + when LibSDL::HAT_UP then @keyinput.up = false + when LibSDL::HAT_LEFT then @keyinput.left = false + when LibSDL::HAT_RIGHT then @keyinput.right = false + when LibSDL::HAT_LEFTUP then @keyinput.left = true; @keyinput.up = true + when LibSDL::HAT_LEFTDOWN then @keyinput.left = true; @keyinput.down = true + when LibSDL::HAT_RIGHTUP then @keyinput.right = true; @keyinput.up = true + when LibSDL::HAT_RIGHTDOWN then @keyinput.right = true; @keyinput.down = true + else nil + end + when SDL::Event::JoyButton + bit = !event.pressed? + case event.button + when 0 then @keyinput.b = bit + when 1 then @keyinput.a = bit + when 4 then @keyinput.l = bit + when 5 then @keyinput.r = bit + when 6 then @keyinput.select = bit + when 7 then @keyinput.start = bit + else nil + end + else nil + end + end + end +end diff --git a/src/crab/gba/mmio.cr b/src/crab/gba/mmio.cr new file mode 100644 index 0000000..a1594f9 --- /dev/null +++ b/src/crab/gba/mmio.cr @@ -0,0 +1,91 @@ +module GBA + class MMIO + class WAITCNT < BitField(UInt16) + bool gamepak_type, lock: true + bool gamepack_prefetch_buffer + bool not_used, lock: true + num phi_terminal_output, 2 + num wait_state_2_second_access, 1 + num wait_state_2_first_access, 2 + num wait_state_1_second_access, 1 + num wait_state_1_first_access, 2 + num wait_state_0_second_access, 1 + num wait_state_0_first_access, 2 + num sram_wait_control, 2 + end + + @waitcnt = WAITCNT.new 0 + + def initialize(@gba : GBA) + end + + def [](index : Int) : Byte + io_addr = 0x0FFF_u16 & index + if io_addr <= 0x05F + @gba.ppu.read_io io_addr + elsif io_addr <= 0xAF + @gba.apu.read_io io_addr + elsif io_addr <= 0xFF + @gba.dma.read_io io_addr + elsif 0x100 <= io_addr <= 0x10F + @gba.timer.read_io io_addr + elsif 0x130 <= io_addr <= 0x133 + @gba.keypad.read_io io_addr + elsif 0x120 <= io_addr <= 0x12F || 0x134 <= io_addr <= 0x1FF + # todo: serial + 0_u8 + elsif 0x200 <= io_addr <= 0x203 || 0x208 <= io_addr <= 0x209 + @gba.interrupts.read_io io_addr + elsif 0x204 <= io_addr <= 0x205 + (@waitcnt.value >> (8 * (io_addr & 1))).to_u8! + elsif not_used? io_addr + 0xFF_u8 # todo what is returned here? + else + # todo: oob reads + puts "Unmapped MMIO read: #{hex_str index.to_u32}".colorize(:red) + 0_u8 + end + end + + def []=(index : Int, value : Byte) : Nil + io_addr = 0x0FFF_u16 & index + if io_addr <= 0x05F + @gba.ppu.write_io io_addr, value + elsif io_addr <= 0xAF + @gba.apu.write_io io_addr, value + elsif io_addr <= 0xFF + @gba.dma.write_io io_addr, value + elsif 0x100 <= io_addr <= 0x10F + @gba.timer.write_io io_addr, value + elsif 0x130 <= io_addr <= 0x133 + @gba.keypad.read_io io_addr + elsif 0x120 <= io_addr <= 0x12F || 0x134 <= io_addr <= 0x1FF + # todo: serial + elsif 0x200 <= io_addr <= 0x203 || 0x208 <= io_addr <= 0x209 + @gba.interrupts.write_io io_addr, value + elsif 0x204 <= io_addr <= 0x205 + shift = 8 * (io_addr & 1) + mask = 0xFF00_u16 >> shift + @waitcnt.value = (@waitcnt.value & mask) | value.to_u16 << shift + elsif io_addr == 0x301 + if bit?(value, 7) + abort "Stopping not supported" + else + @gba.cpu.halted = true + end + elsif not_used? io_addr + else + puts "Unmapped MMIO write ~ addr:#{hex_str index.to_u32}, val:#{hex_str value}".colorize(:yellow) + end + end + + def not_used?(io_addr : Int) : Bool + (0x0E0..0x0FF).includes?(io_addr) || (0x110..0x11F).includes?(io_addr) || + (0x12C..0x12F).includes?(io_addr) || (0x138..0x13F).includes?(io_addr) || + (0x142..0x14F).includes?(io_addr) || (0x15A..0x1FF).includes?(io_addr) || + (0x206..0x207).includes?(io_addr) || (0x20A..0x2FF).includes?(io_addr) || + (0x302..0x40F).includes?(io_addr) || (0x441..0x7FF).includes?(io_addr) || + (0x804..0xFFFF).includes?(io_addr) + end + end +end diff --git a/src/crab/gba/pipeline.cr b/src/crab/gba/pipeline.cr new file mode 100644 index 0000000..8ae7920 --- /dev/null +++ b/src/crab/gba/pipeline.cr @@ -0,0 +1,35 @@ +module GBA + # A super minimalistic FIFO queue implementation optimized for + # use as an ARM instruction pipeline. + class Pipeline + @buffer = Slice(Word).new 2 + @pos = 0 + @size = 0 + + def push(instr : Word) : Nil + raise "Pushing to full pipeline" if @size == 2 + index = (@pos + @size) & 1 + @buffer[index] = instr + @size += 1 + end + + def shift : Word + @size -= 1 + val = @buffer[@pos] + @pos = (@pos + 1) & 1 + val + end + + def peek : Word + @buffer[@pos] + end + + def clear : Nil + @size = 0 + end + + def size : Int32 + @size + end + end +end diff --git a/src/crab/gba/ppu.cr b/src/crab/gba/ppu.cr new file mode 100644 index 0000000..3337459 --- /dev/null +++ b/src/crab/gba/ppu.cr @@ -0,0 +1,564 @@ +module GBA + class PPU + SPRITE_PIXEL = SpritePixel.new 4, 0, false, false # base sprite pixel to fill buffer with on each scanline + + @framebuffer : Slice(UInt16) = Slice(UInt16).new 0x9600 # framebuffer as 16-bit xBBBBBGGGGGRRRRR + @layer_palettes : Array(Bytes) = Array.new 4 { Bytes.new 240 } + @sprite_pixels : Slice(SpritePixel) = Slice(SpritePixel).new 240, SPRITE_PIXEL + + getter pram = Bytes.new 0x400 + getter vram = Bytes.new 0x18000 + getter oam = Bytes.new 0x400 + + @dispcnt = Reg::DISPCNT.new 0 + @dispstat = Reg::DISPSTAT.new 0 + @vcount : UInt16 = 0x0000_u16 + @bgcnt = Array(Reg::BGCNT).new 4 { GBA::Reg::BGCNT.new 0 } + @bghofs = Array(Reg::BGOFS).new 4 { GBA::Reg::BGOFS.new 0 } + @bgvofs = Array(Reg::BGOFS).new 4 { GBA::Reg::BGOFS.new 0 } + @bgaff = Array(Array(Reg::BGAFF)).new 2 { Array(GBA::Reg::BGAFF).new 4 { GBA::Reg::BGAFF.new 0 } } + @bgref = Array(Array(Reg::BGREF)).new 2 { Array(GBA::Reg::BGREF).new 2 { GBA::Reg::BGREF.new 0 } } + @bgref_int = Array(Array(Int32)).new 2 { Array(Int32).new 2, 0 } + @win0h = Reg::WINH.new 0 + @win1h = Reg::WINH.new 0 + @win0v = Reg::WINV.new 0 + @win1v = Reg::WINV.new 0 + @winin = Reg::WININ.new 0 + @winout = Reg::WINOUT.new 0 + @mosaic = Reg::MOSAIC.new 0 + @bldcnt = Reg::BLDCNT.new 0 + @bldalpha = Reg::BLDALPHA.new 0 + @bldy = Reg::BLDY.new 0 + + def initialize(@gba : GBA) + start_line + end + + def start_line : Nil + @gba.scheduler.schedule 960, ->start_hblank + end + + def start_hblank : Nil + @gba.scheduler.schedule 272, ->end_hblank + @dispstat.hblank = true + if @dispstat.hblank_irq_enable + @gba.interrupts.reg_if.hblank = true + @gba.interrupts.schedule_interrupt_check + end + if @vcount < 160 + scanline + @gba.dma.trigger_hdma + @bgref_int.each_with_index do |bgrefs, bg_num| + bgrefs[0] &+= @bgaff[bg_num][1].num # bgx += dmx + bgrefs[1] &+= @bgaff[bg_num][3].num # bgy += dmy + end + end + end + + def end_hblank : Nil + @gba.scheduler.schedule 0, ->start_line + @dispstat.hblank = false + @vcount = (@vcount + 1) % 228 + @dispstat.vcounter = @vcount == @dispstat.vcount_setting + @gba.interrupts.reg_if.vcounter = true if @dispstat.vcounter_irq_enable && @dispstat.vcounter + if @vcount == 0 + @dispstat.vblank = false + elsif @vcount == 160 + @dispstat.vblank = true + @gba.dma.trigger_vdma + @gba.interrupts.reg_if.vblank = true if @dispstat.vblank_irq_enable + @bgref.each_with_index { |bgrefs, bg_num| bgrefs.each_with_index { |bgref, ref_num| @bgref_int[bg_num][ref_num] = bgref.num } } + draw + end + @gba.interrupts.schedule_interrupt_check + end + + def draw : Nil + @gba.display.draw @framebuffer + end + + # Get the screen entry offset from the tile x, tile y, and background screen-size param using tonc algo + @[AlwaysInline] + def se_index(tx : Int, ty : Int, screen_size : Int) : Int + n = tx + ty * 32 + n += 0x03E0 if tx >= 32 + n += 0x0400 if ty >= 32 && screen_size == 0b11 + n + end + + def scanline : Nil + row = @vcount.to_u32 + row_base = 240 * row + scanline = @framebuffer + row_base + scanline.to_unsafe.clear(240) + @layer_palettes.each &.to_unsafe.clear 240 + @sprite_pixels.map! { SPRITE_PIXEL } + case @dispcnt.bg_mode + when 0 + render_reg_bg(0) + render_reg_bg(1) + render_reg_bg(2) + render_reg_bg(3) + render_sprites + composite(scanline) + when 1 + render_reg_bg(0) + render_reg_bg(1) + render_aff_bg(2) + render_sprites + composite(scanline) + when 2 + render_aff_bg(2) + render_aff_bg(3) + render_sprites + composite(scanline) + when 3 + 240.times { |col| scanline[col] = @vram.to_unsafe.as(UInt16*)[row_base + col] } + when 4 + base = @dispcnt.display_frame_select ? 0xA000 : 0 + 240.times do |col| + pal_idx = @vram[base + row_base + col] + scanline[col] = @pram.to_unsafe.as(UInt16*)[pal_idx] + end + when 5 + base = @dispcnt.display_frame_select ? 0xA000 : 0 + background_color = @pram.to_unsafe.as(UInt16*)[0] + if @vcount < 128 + 160.times do |col| + scanline[col] = (@vram + base).to_unsafe.as(UInt16*)[row * 160 + col] + end + 160.to 239 do |col| + scanline[col] = background_color + end + else + 240.times do |col| + scanline[col] = background_color + end + end + else abort "Invalid background mode: #{@dispcnt.bg_mode}" + end + end + + def render_reg_bg(bg : Int) : Nil + return unless bit?(@dispcnt.value, 8 + bg) + pal_buf = @layer_palettes[bg] + bgcnt = @bgcnt[bg] + bgvofs = @bgvofs[bg] + bghofs = @bghofs[bg] + + tw, th = case bgcnt.screen_size + when 0b00 then {0x0FF, 0x0FF} # 32x32 + when 0b01 then {0x1FF, 0x0FF} # 64x32 + when 0b10 then {0x0FF, 0x1FF} # 32x64 + when 0b11 then {0x1FF, 0x1FF} # 64x64 + else raise "Impossible bgcnt screen size: #{bgcnt.screen_size}" + end + + screen_base = 0x800_u32 * bgcnt.screen_base_block + character_base = bgcnt.character_base_block.to_u32 * 0x4000 + effective_row = (@vcount.to_u32 + bgvofs.offset) & th + ty = effective_row >> 3 + 240.times do |col| + effective_col = (col + bghofs.offset) & tw + tx = effective_col >> 3 + + se_idx = se_index(tx, ty, bgcnt.screen_size) + screen_entry = @vram[screen_base + se_idx * 2 + 1].to_u16 << 8 | @vram[screen_base + se_idx * 2] + + tile_id = bits(screen_entry, 0..9) + y = (effective_row & 7) ^ (7 * (screen_entry >> 11 & 1)) + x = (effective_col & 7) ^ (7 * (screen_entry >> 10 & 1)) + + if bgcnt.color_mode # 8bpp + pal_idx = @vram[character_base + tile_id * 0x40 + y * 8 + x] + else # 4bpp + palette_bank = bits(screen_entry, 12..15) + palettes = @vram[character_base + tile_id * 0x20 + y * 4 + (x >> 1)] + pal_idx = ((palettes >> ((x & 1) * 4)) & 0xF) + pal_idx = (palette_bank << 4) + pal_idx if pal_idx > 0 + end + pal_buf[col] = pal_idx.to_u8 + end + end + + def render_aff_bg(bg : Int) : Nil + return unless bit?(@dispcnt.value, 8 + bg) + pal_buf = @layer_palettes[bg] + row = @vcount.to_u32 + bgcnt = @bgcnt[bg] + + dx, _, dy, _ = @bgaff[bg - 2].map &.num + int_x, int_y = @bgref_int[bg - 2] + + size = 16 << bgcnt.screen_size # tiles, always a square + size_pixels = size << 3 + + screen_base = 0x800_u32 * bgcnt.screen_base_block + character_base = bgcnt.character_base_block.to_u32 * 0x4000 + 240.times do |col| + x = int_x >> 8 + y = int_y >> 8 + int_x += dx + int_y += dy + + if bgcnt.affine_wrap + x %= size_pixels + y %= size_pixels + end + next unless 0 <= x < size_pixels && 0 <= y < size_pixels + + # affine screen entries are merely one-byte tile indices + tile_id = @vram[screen_base + (y >> 3) * size + (x >> 3)] + pal_idx = @vram[character_base + 0x40 * tile_id + 8 * (y & 7) + (x & 7)] + pal_buf[col] = pal_idx + end + end + + def render_sprites : Nil + return unless @dispcnt.screen_display_obj + base = 0x10000_u32 + sprites = Slice(Sprite).new(@oam.to_unsafe.as(Sprite*), 128) + sprites.each do |sprite| + next if sprite.obj_shape == 3 # prohibited + next if sprite.affine_mode == 0b10 # sprite disabled + x_coord, y_coord = sprite.x_coord.to_i16, sprite.y_coord.to_i16 + x_coord -= 512 if x_coord > 239 + y_coord -= 256 if y_coord > 159 + orig_width, orig_height = SIZES[sprite.obj_shape][sprite.obj_size] + width, height = orig_width, orig_height + center_x, center_y = x_coord + width // 2, y_coord + height // 2 # off of center + if sprite.affine + oam_affine_entry = sprite.attr1_bits_9_13 + # signed 8.8 fixed-point numbers, need to shr 8 + pa = sprites[oam_affine_entry * 4].aff_param.to_i32 + pb = sprites[oam_affine_entry * 4 + 1].aff_param.to_i32 + pc = sprites[oam_affine_entry * 4 + 2].aff_param.to_i32 + pd = sprites[oam_affine_entry * 4 + 3].aff_param.to_i32 + if sprite.attr0_bit_9 # double-size (rotated sprites won't clip unless scaled) + center_x += width >> 1 + center_y += height >> 1 + width <<= 1 + height <<= 1 + end + else # identity matrix if sprite isn't affine (shifted left 8 to match the 8.8 fixed-point) + pa, pb, pc, pd = 0x100, 0, 0, 0x100 + end + if y_coord <= @vcount < y_coord + height + iy = @vcount.to_i16 - center_y + min_x, max_x = Math.max(0, x_coord), Math.min(240, x_coord + width) + (-(width // 2)...(width // 2)).each do |ix| + col = center_x + ix + next unless min_x <= col < max_x + # transform to texture coordinates + px = (pa * ix + pb * iy) >> 8 + py = (pc * ix + pd * iy) >> 8 + # bring origin back to top-left of the sprite + px += (orig_width // 2) + py += (orig_height // 2) + + next unless 0 <= px < orig_width && 0 <= py < orig_height + + px = orig_width - px - 1 if bit?(sprite.attr1, 12) && !sprite.affine + py = orig_height - py - 1 if bit?(sprite.attr1, 13) && !sprite.affine + + x = px & 7 + y = py & 7 + + tile_id = sprite.character_name + offset = py >> 3 + if @dispcnt.obj_character_vram_mapping + offset *= orig_width >> 3 + else + if sprite.color_mode + offset *= 0x10 + else + offset *= 0x20 + end + end + offset += px >> 3 + if sprite.color_mode # 8bpp + tile_id >>= 1 + tile_id += offset + pal_idx = @vram[base + tile_id * 0x40 + y * 8 + x] + else # 4bpp + tile_id += offset + palettes = @vram[base + tile_id * 0x20 + y * 4 + (x >> 1)] + pal_idx = ((palettes >> ((x & 1) * 4)) & 0xF) + pal_idx += (sprite.palette_number << 4) if pal_idx > 0 + end + + if sprite.obj_mode == 0b10 + @sprite_pixels[col] = @sprite_pixels[col].copy_with window: true if pal_idx > 0 + elsif sprite.priority < @sprite_pixels[col].priority || @sprite_pixels[col].palette == 0 + @sprite_pixels[col] = @sprite_pixels[col].copy_with priority: sprite.priority + @sprite_pixels[col] = @sprite_pixels[col].copy_with palette: pal_idx.to_u16, blends: sprite.obj_mode == 0b01 if pal_idx > 0 + end + end + end + end + end + + def calculate_color(col : Int) : UInt16 + enables, effects = if @dispcnt.window_0_display && @win0h.x1 <= col < @win0h.x2 && @win0v.y1 <= @vcount < @win0v.y2 # win0 + {bits(@winin.value, 0..4), @winin.window_0_color_special_effect} + elsif @dispcnt.window_1_display && @win1h.x1 <= col < @win1h.x2 && @win1v.y1 <= @vcount < @win1v.y2 # win1 + {bits(@winin.value, 8..12), @winin.window_1_color_special_effect} + elsif @dispcnt.obj_window_display && @sprite_pixels[col].window # obj win + {bits(@winout.value, 8..12), @winout.obj_window_color_special_effect} + elsif @dispcnt.window_0_display || @dispcnt.window_1_display || @dispcnt.obj_window_display # winout + {bits(@winout.value, 0..4), @winout.outside_color_special_effect} + else # no windows + {bits(@dispcnt.value, 8..12), true} + end + top_color = nil + 4.times do |priority| + if bit?(enables, 4) + sprite_pixel = @sprite_pixels[col] + if sprite_pixel.priority == priority && sprite_pixel.palette > 0 + selected_color = (@pram + 0x200).to_unsafe.as(UInt16*)[sprite_pixel.palette] + if top_color.nil? # todo: brightness for sprites + if !(sprite_pixel.blends || (@bldcnt.is_bg_target(4, target: 1) && effects)) + return selected_color + elsif @bldcnt.color_special_effect == 1 # alpha blending + top_color = selected_color + elsif @bldcnt.color_special_effect == 2 # brightness increase + bgr16 = BGR16.new(selected_color) + return (bgr16 + (BGR16.new(0xFFFF) - bgr16) * (Math.min(16, @bldy.evy_coefficient) / 16)).value + else # brightness decrease + bgr16 = BGR16.new(selected_color) + return (bgr16 - bgr16 * (Math.min(16, @bldy.evy_coefficient) / 16)).value + end + else + if @bldcnt.is_bg_target(4, target: 2) || sprite_pixel.blends + color = BGR16.new(top_color) * (Math.min(16, @bldalpha.eva_coefficient) / 16) + BGR16.new(selected_color) * (Math.min(16, @bldalpha.evb_coefficient) / 16) + return color.value + else + return top_color + end + end + end + end + 4.times do |bg| + if bit?(enables, bg) + if @bgcnt[bg].priority == priority + palette = @layer_palettes[bg][col] + next if palette == 0 + selected_color = @pram.to_unsafe.as(UInt16*)[palette] + if top_color.nil? + if @bldcnt.color_special_effect == 0 || !@bldcnt.is_bg_target(bg, target: 1) || !effects + return selected_color + elsif @bldcnt.color_special_effect == 1 # alpha blending + top_color = selected_color + elsif @bldcnt.color_special_effect == 2 # brightness increase + bgr16 = BGR16.new(selected_color) + return (bgr16 + (BGR16.new(0xFFFF) - bgr16) * (Math.min(16, @bldy.evy_coefficient) / 16)).value + else # brightness decrease + bgr16 = BGR16.new(selected_color) + return (bgr16 - bgr16 * (Math.min(16, @bldy.evy_coefficient) / 16)).value + end + else + if @bldcnt.is_bg_target(bg, target: 2) + color = BGR16.new(top_color) * (Math.min(16, @bldalpha.eva_coefficient) / 16) + BGR16.new(selected_color) * (Math.min(16, @bldalpha.evb_coefficient) / 16) + return color.value + else # second layer isn't set in bldcnt, don't blend + return top_color + end + end + end + end + end + end + top_color || @pram.to_unsafe.as(UInt16*)[0] + end + + def composite(scanline : Slice(UInt16)) : Nil + 240.times do |col| + scanline[col] = calculate_color(col) + end + end + + def read_io(io_addr : Int) : Byte + case io_addr + when 0x000..0x001 then @dispcnt.read_byte(io_addr & 1) + when 0x002..0x003 then 0_u8 # todo green swap + when 0x004..0x005 then @dispstat.read_byte(io_addr & 1) + when 0x006..0x007 then (@vcount >> (8 * (io_addr & 1))).to_u8! + when 0x008..0x00F then @bgcnt[(io_addr - 0x008) >> 1].read_byte(io_addr & 1) + when 0x010..0x01F + bg_num = (io_addr - 0x010) >> 2 + if bit?(io_addr, 1) + @bgvofs[bg_num].read_byte(io_addr & 1) + else + @bghofs[bg_num].read_byte(io_addr & 1) + end + when 0x020..0x03F + bg_num = (io_addr & 0x10) >> 4 # (bg 0/1 represents bg 2/3, since those are the only aff bgs) + offs = io_addr & 0xF + if offs >= 8 + offs -= 8 + @bgref[bg_num][offs >> 2].read_byte(offs & 3) + else + @bgaff[bg_num][offs >> 1].read_byte(offs & 1) + end + when 0x040 then 0xFF_u8 & @win0h.value + when 0x041 then 0xFF_u8 & @win0h.value >> 8 + when 0x042 then 0xFF_u8 & @win1h.value + when 0x043 then 0xFF_u8 & @win1h.value >> 8 + when 0x044 then 0xFF_u8 & @win0v.value + when 0x045 then 0xFF_u8 & @win0v.value >> 8 + when 0x046 then 0xFF_u8 & @win1v.value + when 0x047 then 0xFF_u8 & @win1v.value >> 8 + when 0x048 then 0xFF_u8 & @winin.value + when 0x049 then 0xFF_u8 & @winin.value >> 8 + when 0x04A then 0xFF_u8 & @winout.value + when 0x04B then 0xFF_u8 & @winout.value >> 8 + when 0x04C then 0xFF_u8 & @mosaic.value + when 0x04D then 0xFF_u8 & @mosaic.value >> 8 + when 0x050 then 0xFF_u8 & @bldcnt.value + when 0x051 then 0xFF_u8 & @bldcnt.value >> 8 + when 0x052 then 0xFF_u8 & @bldalpha.value + when 0x053 then 0xFF_u8 & @bldalpha.value >> 8 + when 0x054 then 0xFF_u8 & @bldy.value + when 0x055 then 0xFF_u8 & @bldy.value >> 8 + else log "Unmapped PPU read ~ addr:#{hex_str io_addr.to_u8}"; 0_u8 # todo: open bus + + + end + end + + def write_io(io_addr : Int, value : Byte) : Nil + case io_addr + when 0x000..0x001 then @dispcnt.write_byte(io_addr & 1, value) + when 0x002..0x003 # undocumented - green swap + when 0x004..0x005 then @dispstat.write_byte(io_addr & 1, value) + when 0x006..0x007 # vcount + when 0x008..0x00F then @bgcnt[(io_addr - 0x008) >> 1].write_byte(io_addr & 1, value) + when 0x010..0x01F + bg_num = (io_addr - 0x010) >> 2 + if bit?(io_addr, 1) + @bgvofs[bg_num].write_byte(io_addr & 1, value) + else + @bghofs[bg_num].write_byte(io_addr & 1, value) + end + when 0x020..0x03F + bg_num = (io_addr & 0x10) >> 4 # (bg 0/1 represents bg 2/3, since those are the only aff bgs) + offs = io_addr & 0xF + if offs >= 8 + offs -= 8 + @bgref[bg_num][offs >> 2].write_byte(offs & 3, value) + @bgref_int[bg_num][offs >> 2] = @bgref[bg_num][offs >> 2].num + else + @bgaff[bg_num][offs >> 1].write_byte(offs & 1, value) + end + when 0x040 then @win0h.value = (@win0h.value & 0xFF00) | value + when 0x041 then @win0h.value = (@win0h.value & 0x00FF) | value.to_u16 << 8 + when 0x042 then @win1h.value = (@win1h.value & 0xFF00) | value + when 0x043 then @win1h.value = (@win1h.value & 0x00FF) | value.to_u16 << 8 + when 0x044 then @win0v.value = (@win0v.value & 0xFF00) | value + when 0x045 then @win0v.value = (@win0v.value & 0x00FF) | value.to_u16 << 8 + when 0x046 then @win1v.value = (@win1v.value & 0xFF00) | value + when 0x047 then @win1v.value = (@win1v.value & 0x00FF) | value.to_u16 << 8 + when 0x048 then @winin.value = (@winin.value & 0xFF00) | value + when 0x049 then @winin.value = (@winin.value & 0x00FF) | value.to_u16 << 8 + when 0x04A then @winout.value = (@winout.value & 0xFF00) | value + when 0x04B then @winout.value = (@winout.value & 0x00FF) | value.to_u16 << 8 + when 0x04C then @mosaic.value = (@mosaic.value & 0xFF00) | value + when 0x04D then @mosaic.value = (@mosaic.value & 0x00FF) | value.to_u16 << 8 + when 0x050 then @bldcnt.value = (@bldcnt.value & 0xFF00) | value + when 0x051 then @bldcnt.value = (@bldcnt.value & 0x00FF) | value.to_u16 << 8 + when 0x052 then @bldalpha.value = (@bldalpha.value & 0xFF00) | value + when 0x053 then @bldalpha.value = (@bldalpha.value & 0x00FF) | value.to_u16 << 8 + when 0x054 then @bldy.value = (@bldy.value & 0xFF00) | value + when 0x055 then @bldy.value = (@bldy.value & 0x00FF) | value.to_u16 << 8 + end + end + end + + # SIZES[SHAPE][SIZE] + SIZES = [ + [ # square + {8, 8}, + {16, 16}, + {32, 32}, + {64, 64}, + ], + [ # horizontal rectangle + {16, 8}, + {32, 8}, + {32, 16}, + {64, 32}, + ], + [ # vertical rectangle + {8, 16}, + {8, 32}, + {16, 32}, + {32, 64}, + ], + ] + + record Sprite, attr0 : UInt16, attr1 : UInt16, attr2 : UInt16, aff_param : Int16 do + # OBJ Attribute 0 + + def obj_shape + bits(attr0, 14..15) + end + + def color_mode + bit?(attr0, 13) + end + + def obj_mosaic + bit?(attr0, 12) + end + + def obj_mode + bits(attr0, 10..11) + end + + def attr0_bit_9 + bit?(attr0, 9) + end + + def affine + bit?(attr0, 8) + end + + def affine_mode + bits(attr0, 8..9) + end + + def y_coord + bits(attr0, 0..7) + end + + # OBJ Attribute 1 + + def obj_size + bits(attr1, 14..15) + end + + def attr1_bits_9_13 + bits(attr1, 9..13) + end + + def x_coord + bits(attr1, 0..8) + end + + # OBJ Attribute 2 + + def character_name + bits(attr2, 0..9) + end + + def priority + bits(attr2, 10..11) + end + + def palette_number + bits(attr2, 12..15) + end + end + + record SpritePixel, priority : UInt16, palette : UInt16, blends : Bool, window : Bool +end diff --git a/src/crab/gba/reg.cr b/src/crab/gba/reg.cr new file mode 100644 index 0000000..5929b02 --- /dev/null +++ b/src/crab/gba/reg.cr @@ -0,0 +1,258 @@ +module GBA + module Reg + module Base16 + def read_byte(byte_num : Int) : Byte + (@value >> (8 * byte_num)).to_u8! + end + + def write_byte(byte_num : Int, byte : Byte) : Byte + shift = 8 * byte_num + mask = ~(0xFF_u16 << shift) + @value = (@value & mask) | byte.to_u16 << shift + byte + end + end + + module Base32 + def read_byte(byte_num : Int) : Byte + (@value >> (8 * byte_num)).to_u8! + end + + def write_byte(byte_num : Int, byte : Byte) : Byte + shift = 8 * byte_num + mask = ~(0xFF_u32 << shift) + @value = (@value & mask) | byte.to_u32 << shift + byte + end + end + + #################### + # APU + + class SOUNDCNT_L < BitField(UInt16) + num channel_4_left, 1 + num channel_3_left, 1 + num channel_2_left, 1 + num channel_1_left, 1 + num channel_4_right, 1 + num channel_3_right, 1 + num channel_2_right, 1 + num channel_1_right, 1 + bool not_used_1, lock: true + num left_volume, 3 + bool not_used_2, lock: true + num right_volume, 3 + end + + class SOUNDCNT_H < BitField(UInt16) + bool dma_sound_b_reset, lock: true + num dma_sound_b_timer, 1 + num dma_sound_b_left, 1 + num dma_sound_b_right, 1 + bool dma_sound_a_reset, lock: true + num dma_sound_a_timer, 1 + num dma_sound_a_left, 1 + num dma_sound_a_right, 1 + num not_used, 4, lock: true + num dma_sound_b_volume, 1 + num dma_sound_a_volume, 1 + num sound_volume, 2 + end + + class SOUNDBIAS < BitField(UInt16) + num amplitude_resolution, 2 + num not_used_1, 4 + num bias_level, 9 + bool not_used_2 + end + + #################### + # DMA + + class DMACNT < BitField(UInt16) + bool enable + bool irq_enable + num start_timing, 2 + bool game_pak + num type, 1 + bool repeat + num source_control, 2 + num dest_control, 2 + num not_used, 5 + + def to_s(io) + io << "enable:#{enable}, irq:#{irq_enable}, timing:#{start_timing}, game_pak:#{game_pak}, type:#{type}, repeat:#{repeat}, srcctl:#{source_control}, dstctl:#{dest_control}" + end + end + + #################### + # Timer + + class TMCNT < BitField(UInt16) + num not_used_1, 8, lock: true + bool enable + bool irq_enable + num not_used_2, 3, lock: true + bool cascade + num frequency, 2 + + def to_s(io) + io << "enable:#{enable}, irq:#{irq_enable}, cascade:#{cascade}, freq:#{frequency}" + end + end + + #################### + # PPU + + class DISPCNT < BitField(UInt16) + include Base16 + bool obj_window_display + bool window_1_display + bool window_0_display + bool screen_display_obj + bool screen_display_bg3 + bool screen_display_bg2 + bool screen_display_bg1 + bool screen_display_bg0 + bool forced_blank # (1=Allow access to VRAM,Palette,OAM) + bool obj_character_vram_mapping # (0=Two dimensional, 1=One dimensional) + bool hblank_interval_free # (1=Allow access to OAM during H-Blank) + bool display_frame_select # (0-1=Frame 0-1) (for BG Modes 4,5 only) + bool reserved_for_bios, lock: true + num bg_mode, 3 # (0-5=Video Mode 0-5, 6-7=Prohibited) + end + + class DISPSTAT < BitField(UInt16) + include Base16 + num vcount_setting, 8 + num not_used, 2 + bool vcounter_irq_enable + bool hblank_irq_enable + bool vblank_irq_enable + bool vcounter, lock: true + bool hblank, lock: true + bool vblank, lock: true + end + + class BGCNT < BitField(UInt16) + include Base16 + num screen_size, 2 + bool affine_wrap + num screen_base_block, 5 + bool color_mode + bool mosaic + num not_used, 2, lock: true + num character_base_block, 2 + num priority, 2 + end + + class BGOFS < BitField(UInt16) + include Base16 + num not_used, 7, lock: true + num offset, 9 + end + + class BGAFF < BitField(UInt16) + include Base16 + bool sign + num integer, 7 + num fraction, 8 + + def num : Int16 + value.to_i16! + end + end + + class BGREF < BitField(UInt32) + include Base32 + num not_used, 4, lock: true + bool sign + num integer, 19 + num fraction, 8 + + def num : Int32 + (value << 4).to_i32! >> 4 + end + end + + class WINH < BitField(UInt16) + include Base16 + num x1, 8 + num x2, 8 + end + + class WINV < BitField(UInt16) + include Base16 + num y1, 8 + num y2, 8 + end + + class WININ < BitField(UInt16) + include Base16 + num not_used_1, 2, lock: true + bool window_1_color_special_effect + bool window_1_obj_enable + num window_1_enable_bits, 4 + num not_used_0, 2, lock: true + bool window_0_color_special_effect + bool window_0_obj_enable + num window_0_enable_bits, 4 + end + + class WINOUT < BitField(UInt16) + include Base16 + num not_used_obj, 2, lock: true + bool obj_window_color_special_effect + bool obj_window_obj_enable + num obj_window_enable_bits, 4 + num not_used_outside, 2, lock: true + bool outside_color_special_effect + bool outside_obj_enable + num outside_enable_bits, 4 + end + + class MOSAIC < BitField(UInt16) + include Base16 + num obj_mosiac_v_size, 4 + num obj_mosiac_h_size, 4 + num bg_mosiac_v_size, 4 + num bg_mosiac_h_size, 4 + end + + class BLDCNT < BitField(UInt16) + include Base16 + num not_used, 2, lock: true + bool bd_2nd_target_pixel + bool obj_2nd_target_pixel + bool bg3_2nd_target_pixel + bool bg2_2nd_target_pixel + bool bg1_2nd_target_pixel + bool bg0_2nd_target_pixel + num color_special_effect, 2 + bool bd_1st_target_pixel + bool obj_1st_target_pixel + bool bg3_1st_target_pixel + bool bg2_1st_target_pixel + bool bg1_1st_target_pixel + bool bg0_1st_target_pixel + + def is_bg_target(bg : Int, target : Int) : Bool + bit?(value, bg + ((target - 1) * 8)) + end + end + + class BLDALPHA < BitField(UInt16) + include Base16 + num not_used_13_15, 3, lock: true + num evb_coefficient, 5 + num not_used_5_7, 3, lock: true + num eva_coefficient, 5 + end + + class BLDY < BitField(UInt16) + include Base16 + num not_used, 11, lock: true + num evy_coefficient, 5 + end + end +end diff --git a/src/crab/gba/scheduler.cr b/src/crab/gba/scheduler.cr new file mode 100644 index 0000000..ae22f48 --- /dev/null +++ b/src/crab/gba/scheduler.cr @@ -0,0 +1,73 @@ +module GBA + class Scheduler + enum EventType + DEFAULT + APUChannel1 + APUChannel2 + APUChannel3 + APUChannel4 + Timer0 + Timer1 + Timer2 + Timer3 + end + + private record Event, cycles : UInt64, proc : Proc(Void), type : EventType + + @events : Deque(Event) = Deque(Event).new 10 + getter cycles : UInt64 = 0 + @next_event : UInt64 = UInt64::MAX + + def schedule(cycles : Int, proc : Proc(Void), type = EventType::DEFAULT) : Nil + self << Event.new @cycles + cycles, proc, type + end + + @[AlwaysInline] + def <<(event : Event) : Nil + idx = @events.bsearch_index { |e| e.cycles > event.cycles } + unless idx.nil? + @events.insert(idx, event) + else + @events << event + end + @next_event = @events[0].cycles + end + + def clear(type : EventType) : Nil + @events.reject! { |event| event.type == type } + end + + def tick(cycles : Int) : Nil + if @cycles + cycles < @next_event + @cycles += cycles + else + cycles.times do + @cycles += 1 + call_current + end + end + end + + def call_current : Nil + loop do + event = @events.first? + if event && @cycles >= event.cycles + event.proc.call + @events.shift + else + if event + @next_event = event.cycles + else + @next_event = UInt64::MAX + end + return + end + end + end + + def fast_forward : Nil + @cycles = @next_event + call_current + end + end +end diff --git a/src/crab/shaders/gba_colors.frag b/src/crab/gba/shaders/gba_colors.frag similarity index 100% rename from src/crab/shaders/gba_colors.frag rename to src/crab/gba/shaders/gba_colors.frag diff --git a/src/crab/shaders/gba_colors.vert b/src/crab/gba/shaders/gba_colors.vert similarity index 100% rename from src/crab/shaders/gba_colors.vert rename to src/crab/gba/shaders/gba_colors.vert diff --git a/src/crab/gba/storage.cr b/src/crab/gba/storage.cr new file mode 100644 index 0000000..f9aa1e4 --- /dev/null +++ b/src/crab/gba/storage.cr @@ -0,0 +1,68 @@ +module GBA + abstract class Storage + enum Type + EEPROM + SRAM + FLASH + FLASH512 + FLASH1M + + def regex : Regex # don't rely on the 3 digits after this string + /#{self}_V/ + end + + def bytes : Int + case self + in EEPROM then abort "todo: Support EEPROM" + in SRAM then 0x08000 + in FLASH then 0x10000 + in FLASH512 then 0x10000 + in FLASH1M then 0x20000 + end + end + end + + @dirty = false + setter save_path : String = "" + getter memory : Bytes = Bytes.new 0 # implementing class needs to override + + def self.new(rom_path : String) : Storage + save_path = rom_path.rpartition('.')[0] + ".sav" + type = File.open(rom_path, "rb") { |file| find_type(file) } + puts "Backup type could not be identified.".colorize.fore(:red) unless type + puts "Backup type: #{type}, save path: #{save_path}" + storage = case type + in Type::EEPROM then abort "todo: Support EEPROM" + in Type::SRAM, nil then SRAM.new + in Type::FLASH, Type::FLASH512, Type::FLASH1M then Flash.new type + end + storage.save_path = save_path + File.open(save_path, &.read(storage.memory)) if File.exists?(save_path) + storage + end + + def write_save : Nil + if @dirty + File.write(@save_path, @memory) + @dirty = false + end + end + + abstract def [](index : Int) : Byte + + def read_half(index : Int) : HalfWord + 0x0101_u16 * self[index & ~1] + end + + def read_word(index : Int) : Word + 0x01010101_u32 * self[index & ~3] + end + + abstract def []=(index : Int, value : Byte) : Nil + + private def self.find_type(file : File) : Type? + str = file.gets_to_end + Type.each { |type| return type if type.regex.matches?(str) } + end + end +end diff --git a/src/crab/gba/storage/flash.cr b/src/crab/gba/storage/flash.cr new file mode 100644 index 0000000..05f4286 --- /dev/null +++ b/src/crab/gba/storage/flash.cr @@ -0,0 +1,90 @@ +module GBA + class Flash < Storage + @[Flags] + enum State + READY + CMD_1 + CMD_2 + IDENTIFICATION + PREPARE_WRITE + PREPARE_ERASE + SET_BANK + end + + enum Command : Byte + ENTER_IDENT = 0x90 + EXIT_IDENT = 0xF0 + PREPARE_ERASE = 0x80 + ERASE_ALL = 0x10 + ERASE_CHUNK = 0x30 + PREPARE_WRITE = 0xA0 + SET_BANK = 0xB0 + end + + @state = State::READY + @bank = 0_u8 + + def initialize(@type : Type) + @memory = Bytes.new(@type.bytes, 0xFF) + @id = case @type + when Type::FLASH1M then 0x1362 # Sanyo + else 0x1B32 # Panasonic + end + end + + def [](index : Int) : Byte + index &= 0xFFFF + if @state.includes?(State::IDENTIFICATION) && 0 <= index <= 1 + (@id >> (8 * index) & 0xFF).to_u8! + else + @memory[0x10000 * @bank + index] + end + end + + def []=(index : Int, value : Byte) : Nil + index &= 0xFFFF + case @state + when .includes? State::PREPARE_WRITE + @memory[0x10000 * @bank + index] &= value + @dirty = true + @state ^= State::PREPARE_WRITE + when .includes? State::SET_BANK + @bank = value & 1 + @state ^= State::SET_BANK + when .includes? State::READY + if index == 0x5555 && value == 0xAA + @state ^= State::READY + @state |= State::CMD_1 + end + when .includes? State::CMD_1 + if index == 0x2AAA && value == 0x55 + @state ^= State::CMD_1 + @state |= State::CMD_2 + end + when .includes? State::CMD_2 + if index == 0x5555 + case value + when Command::ENTER_IDENT.value then @state |= State::IDENTIFICATION + when Command::EXIT_IDENT.value then @state ^= State::IDENTIFICATION + when Command::PREPARE_ERASE.value then @state |= State::PREPARE_ERASE + when Command::ERASE_ALL.value + if @state.includes? State::PREPARE_ERASE + @memory.size.times { |i| @memory[i] = 0xFF } + @dirty = true + @state ^= State::PREPARE_ERASE + end + when Command::PREPARE_WRITE.value then @state |= State::PREPARE_WRITE + when Command::SET_BANK.value then @state |= State::SET_BANK if @type == Type::FLASH1M + else puts "Unsupported flash command #{hex_str value}" + end + elsif @state.includes?(State::PREPARE_ERASE) && index & 0x0FFF == 0 && value == Command::ERASE_CHUNK.value + 0x1000.times { |i| @memory[0x10000 * @bank + index + i] = 0xFF } + @dirty = true + @state ^= State::PREPARE_ERASE + end + @state ^= State::CMD_2 + @state |= State::READY + end + end + end +end diff --git a/src/crab/gba/storage/sram.cr b/src/crab/gba/storage/sram.cr new file mode 100644 index 0000000..0c6f3b8 --- /dev/null +++ b/src/crab/gba/storage/sram.cr @@ -0,0 +1,14 @@ +module GBA + class SRAM < Storage + @memory = Bytes.new(Type::SRAM.bytes, 0xFF) + + def [](index : Int) : Byte + @memory[index & 0x7FFF] + end + + def []=(index : Int, value : Byte) : Nil + @memory[index & 0x7FFF] = value + @dirty = true + end + end +end diff --git a/src/crab/gba/thumb/add_offset_to_stack_pointer.cr b/src/crab/gba/thumb/add_offset_to_stack_pointer.cr new file mode 100644 index 0000000..ae9f315 --- /dev/null +++ b/src/crab/gba/thumb/add_offset_to_stack_pointer.cr @@ -0,0 +1,15 @@ +module GBA + module THUMB + def thumb_add_offset_to_stack_pointer(instr : Word) : Nil + sign = bit?(instr, 7) + offset = bits(instr, 0..6) + if sign # negative + set_reg(13, @r[13] &- (offset << 2)) + else # positive + set_reg(13, @r[13] &+ (offset << 2)) + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/add_subtract.cr b/src/crab/gba/thumb/add_subtract.cr new file mode 100644 index 0000000..d7997f0 --- /dev/null +++ b/src/crab/gba/thumb/add_subtract.cr @@ -0,0 +1,23 @@ +module GBA + module THUMB + def thumb_add_subtract(instr : Word) : Nil + imm_flag = bit?(instr, 10) + sub = bit?(instr, 9) + imm = bits(instr, 6..8) + rs = bits(instr, 3..5) + rd = bits(instr, 0..2) + operand = if imm_flag + imm + else + @r[imm] + end + if sub + set_reg(rd, sub(@r[rs], operand, true)) + else + set_reg(rd, add(@r[rs], operand, true)) + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/alu_operations.cr b/src/crab/gba/thumb/alu_operations.cr new file mode 100644 index 0000000..caed2fe --- /dev/null +++ b/src/crab/gba/thumb/alu_operations.cr @@ -0,0 +1,40 @@ +module GBA + module THUMB + def thumb_alu_operations(instr : Word) : Nil + op = bits(instr, 6..9) + rs = bits(instr, 3..5) + rd = bits(instr, 0..2) + barrel_shifter_carry_out = @cpsr.carry + case op + when 0b0000 then res = set_reg(rd, @r[rd] & @r[rs]) + when 0b0001 then res = set_reg(rd, @r[rd] ^ @r[rs]) + when 0b0010 + res = set_reg(rd, lsl(@r[rd], @r[rs], pointerof(barrel_shifter_carry_out))) + @cpsr.carry = barrel_shifter_carry_out + when 0b0011 + res = set_reg(rd, lsr(@r[rd], @r[rs], false, pointerof(barrel_shifter_carry_out))) + @cpsr.carry = barrel_shifter_carry_out + when 0b0100 + res = set_reg(rd, asr(@r[rd], @r[rs], false, pointerof(barrel_shifter_carry_out))) + @cpsr.carry = barrel_shifter_carry_out + when 0b0101 then res = set_reg(rd, adc(@r[rd], @r[rs], set_conditions: true)) + when 0b0110 then res = set_reg(rd, sbc(@r[rd], @r[rs], set_conditions: true)) + when 0b0111 + res = set_reg(rd, ror(@r[rd], @r[rs], false, pointerof(barrel_shifter_carry_out))) + @cpsr.carry = barrel_shifter_carry_out + when 0b1000 then res = @r[rd] & @r[rs] + when 0b1001 then res = set_reg(rd, sub(0, @r[rs], set_conditions: true)) + when 0b1010 then res = sub(@r[rd], @r[rs], set_conditions: true) + when 0b1011 then res = add(@r[rd], @r[rs], set_conditions: true) + when 0b1100 then res = set_reg(rd, @r[rd] | @r[rs]) + when 0b1101 then res = set_reg(rd, @r[rs] &* @r[rd]) + when 0b1110 then res = set_reg(rd, @r[rd] & ~@r[rs]) + when 0b1111 then res = set_reg(rd, ~@r[rs]) + else raise "Invalid alu op: #{op}" + end + set_neg_and_zero_flags(res) + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/conditional_branch.cr b/src/crab/gba/thumb/conditional_branch.cr new file mode 100644 index 0000000..1693d93 --- /dev/null +++ b/src/crab/gba/thumb/conditional_branch.cr @@ -0,0 +1,13 @@ +module GBA + module THUMB + def thumb_conditional_branch(instr : Word) : Nil + cond = bits(instr, 8..11) + offset = bits(instr, 0..7).to_i8!.to_i32 + if check_cond cond + set_reg(15, @r[15] &+ (offset * 2)) + else + step_thumb + end + end + end +end diff --git a/src/crab/gba/thumb/hi_reg_branch_exchange.cr b/src/crab/gba/thumb/hi_reg_branch_exchange.cr new file mode 100644 index 0000000..68c7cff --- /dev/null +++ b/src/crab/gba/thumb/hi_reg_branch_exchange.cr @@ -0,0 +1,30 @@ +module GBA + module THUMB + def thumb_high_reg_branch_exchange(instr : Word) : Nil + op = bits(instr, 8..9) + h1 = bit?(instr, 7) + h2 = bit?(instr, 6) + rs = bits(instr, 3..5) + rd = bits(instr, 0..2) + + rd += 8 if h1 + rs += 8 if h2 + + # In this group only CMP (Op = 01) sets the CPSR condition codes. + case op + when 0b00 then set_reg(rd, add(@r[rd], @r[rs], false)) + when 0b01 then sub(@r[rd], @r[rs], true) + when 0b10 then set_reg(rd, @r[rs]) + when 0b11 + if bit?(@r[rs], 0) + set_reg(15, @r[rs]) + else + @cpsr.thumb = false + set_reg(15, @r[rs]) + end + end + + step_thumb unless rd == 15 || op == 0b11 + end + end +end diff --git a/src/crab/gba/thumb/load_address.cr b/src/crab/gba/thumb/load_address.cr new file mode 100644 index 0000000..b237f94 --- /dev/null +++ b/src/crab/gba/thumb/load_address.cr @@ -0,0 +1,14 @@ +module GBA + module THUMB + def thumb_load_address(instr : Word) : Nil + source = bit?(instr, 11) + rd = bits(instr, 8..10) + word = bits(instr, 0..7) + imm = word << 2 + # Where the PC is used as the source register (SP = 0), bit 1 of the PC is always read as 0. + set_reg(rd, (source ? @r[13] : @r[15] & ~2) &+ imm) + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/load_store_halfword.cr b/src/crab/gba/thumb/load_store_halfword.cr new file mode 100644 index 0000000..51adaac --- /dev/null +++ b/src/crab/gba/thumb/load_store_halfword.cr @@ -0,0 +1,18 @@ +module GBA + module THUMB + def thumb_load_store_halfword(instr : Word) : Nil + load = bit?(instr, 11) + offset = bits(instr, 6..10) + rb = bits(instr, 3..5) + rd = bits(instr, 0..2) + address = @r[rb] + (offset << 1) + if load + set_reg(rd, @gba.bus.read_half_rotate(address)) + else + @gba.bus[address] = @r[rd].to_u16! + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/load_store_immediate_offset.cr b/src/crab/gba/thumb/load_store_immediate_offset.cr new file mode 100644 index 0000000..bec14a7 --- /dev/null +++ b/src/crab/gba/thumb/load_store_immediate_offset.cr @@ -0,0 +1,19 @@ +module GBA + module THUMB + def thumb_load_store_immediate_offset(instr : Word) : Nil + byte_quantity_and_load = bits(instr, 11..12) + offset = bits(instr, 6..10) + rb = bits(instr, 3..5) + rd = bits(instr, 0..2) + base_address = @r[rb] + case byte_quantity_and_load + when 0b00 then @gba.bus[base_address &+ (offset << 2)] = @r[rd] # str + when 0b01 then set_reg(rd, @gba.bus.read_word_rotate(base_address &+ (offset << 2))) # ldr + when 0b10 then @gba.bus[base_address &+ offset] = @r[rd].to_u8! # strb + when 0b11 then set_reg(rd, @gba.bus[base_address &+ offset].to_u32) # ldrb + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/load_store_register_offset.cr b/src/crab/gba/thumb/load_store_register_offset.cr new file mode 100644 index 0000000..6329d10 --- /dev/null +++ b/src/crab/gba/thumb/load_store_register_offset.cr @@ -0,0 +1,19 @@ +module GBA + module THUMB + def thumb_load_store_register_offset(instr : Word) : Nil + load_and_byte_quantity = bits(instr, 10..11) + ro = bits(instr, 6..8) + rb = bits(instr, 3..5) + rd = bits(instr, 0..2) + address = @r[rb] &+ @r[ro] + case load_and_byte_quantity + when 0b00 then @gba.bus[address] = @r[rd] # str + when 0b01 then @gba.bus[address] = @r[rd].to_u8! # strb + when 0b10 then set_reg(rd, @gba.bus.read_word_rotate(address)) # ldr + when 0b11 then set_reg(rd, @gba.bus[address].to_u32!) # ldrb + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/load_store_sign_extended.cr b/src/crab/gba/thumb/load_store_sign_extended.cr new file mode 100644 index 0000000..c30509d --- /dev/null +++ b/src/crab/gba/thumb/load_store_sign_extended.cr @@ -0,0 +1,20 @@ +module GBA + module THUMB + def thumb_load_store_sign_extended(instr : Word) : Nil + hs = bits(instr, 10..11) + ro = bits(instr, 6..8) + rb = bits(instr, 3..5) + rd = bits(instr, 0..2) + address = @r[rb] &+ @r[ro] + case hs + when 0b00 then @gba.bus[address] = @r[rd].to_u16! # strh + when 0b01 then set_reg(rd, @gba.bus[address].to_i8!.to_u32!) # ldsb + when 0b10 then set_reg(rd, @gba.bus.read_half_rotate(address)) # ldrh + when 0b11 then set_reg(rd, @gba.bus.read_half_signed(address)) # ldsh + else raise "Invalid load/store signed extended: #{hs}" + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/long_branch_link.cr b/src/crab/gba/thumb/long_branch_link.cr new file mode 100644 index 0000000..66a9659 --- /dev/null +++ b/src/crab/gba/thumb/long_branch_link.cr @@ -0,0 +1,17 @@ +module GBA + module THUMB + def thumb_long_branch_link(instr : Word) : Nil + second_instr = bit?(instr, 11) + offset = bits(instr, 0..10) + if second_instr + temp = @r[15] &- 2 + set_reg(15, @r[14] &+ (offset << 1)) + set_reg(14, temp | 1) + else + offset = (offset << 5).to_i16! >> 5 + set_reg(14, @r[15] &+ (offset.to_u32! << 12)) + step_thumb + end + end + end +end diff --git a/src/crab/gba/thumb/move_compare_add_subtract.cr b/src/crab/gba/thumb/move_compare_add_subtract.cr new file mode 100644 index 0000000..6455ee6 --- /dev/null +++ b/src/crab/gba/thumb/move_compare_add_subtract.cr @@ -0,0 +1,20 @@ +module GBA + module THUMB + def thumb_move_compare_add_subtract(instr : Word) : Nil + op = bits(instr, 11..12) + rd = bits(instr, 8..10) + offset = bits(instr, 0..7) + case op + when 0b00 + set_reg(rd, offset) + set_neg_and_zero_flags(@r[rd]) + when 0b01 then sub(@r[rd], offset, true) + when 0b10 then set_reg(rd, add(@r[rd], offset, true)) + when 0b11 then set_reg(rd, sub(@r[rd], offset, true)) + else raise "Invalid move/compare/add/subtract op: #{op}" + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/move_shifted_register.cr b/src/crab/gba/thumb/move_shifted_register.cr new file mode 100644 index 0000000..f21d439 --- /dev/null +++ b/src/crab/gba/thumb/move_shifted_register.cr @@ -0,0 +1,21 @@ +module GBA + module THUMB + def thumb_move_shifted_register(instr : Word) : Nil + op = bits(instr, 11..12) + offset = bits(instr, 6..10) + rs = bits(instr, 3..5) + rd = bits(instr, 0..2) + carry_out = @cpsr.carry + case op + when 0b00 then set_reg(rd, lsl(@r[rs], offset, pointerof(carry_out))) + when 0b01 then set_reg(rd, lsr(@r[rs], offset, true, pointerof(carry_out))) + when 0b10 then set_reg(rd, asr(@r[rs], offset, true, pointerof(carry_out))) + else raise "Invalid shifted register op: #{op}" + end + set_neg_and_zero_flags(@r[rd]) + @cpsr.carry = carry_out + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/multiple_load_store.cr b/src/crab/gba/thumb/multiple_load_store.cr new file mode 100644 index 0000000..fc883cc --- /dev/null +++ b/src/crab/gba/thumb/multiple_load_store.cr @@ -0,0 +1,40 @@ +module GBA + module THUMB + def thumb_multiple_load_store(instr : Word) : Nil + load = bit?(instr, 11) + rb = bits(instr, 8..10) + list = bits(instr, 0..7) + address = @r[rb] + unless list == 0 + if load # ldmia + 8.times do |idx| + if bit?(list, idx) + set_reg(idx, @gba.bus.read_word(address)) + address &+= 4 + end + end + else # stmia + base_addr = nil + 8.times do |idx| + if bit?(list, idx) + @gba.bus[address] = @r[idx] + base_addr = address if rb == idx + address &+= 4 + end + end + @gba.bus[base_addr] = address if base_addr && first_set_bit(list) != rb # rb is written after first store + end + set_reg(rb, address) + else # https://github.com/jsmolka/gba-suite/blob/0e32e15c6241e6dc20851563ba88f4656ac50936/thumb/memory.asm#L459 + if load + set_reg(15, @gba.bus.read_word(address)) + else + @gba.bus[address] = @r[15] &+ 2 + end + set_reg(rb, address &+ 0x40) + end + + step_thumb unless list == 0 && load + end + end +end diff --git a/src/crab/gba/thumb/pc_relative_load.cr b/src/crab/gba/thumb/pc_relative_load.cr new file mode 100644 index 0000000..baeed02 --- /dev/null +++ b/src/crab/gba/thumb/pc_relative_load.cr @@ -0,0 +1,11 @@ +module GBA + module THUMB + def thumb_pc_relative_load(instr : Word) : Nil + imm = bits(instr, 0..7) + rd = bits(instr, 8..10) + set_reg(rd, @gba.bus.read_word((@r[15] & ~2) &+ (imm << 2))) + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/push_pop_registers.cr b/src/crab/gba/thumb/push_pop_registers.cr new file mode 100644 index 0000000..7c5bf47 --- /dev/null +++ b/src/crab/gba/thumb/push_pop_registers.cr @@ -0,0 +1,36 @@ +module GBA + module THUMB + def thumb_push_pop_registers(instr : Word) : Nil + pop = bit?(instr, 11) + pclr = bit?(instr, 8) + list = bits(instr, 0..7) + address = @r[13] + if pop + 8.times do |idx| + if bit?(list, idx) + set_reg(idx, @gba.bus.read_word(address)) + address &+= 4 + end + end + if pclr + set_reg(15, @gba.bus.read_word(address)) + address &+= 4 + end + else + if pclr + address &-= 4 + @gba.bus[address] = @r[14] + end + 7.downto(0).each do |idx| + if bit?(list, idx) + address &-= 4 + @gba.bus[address] = @r[idx] + end + end + end + set_reg(13, address) + + step_thumb unless pop && pclr + end + end +end diff --git a/src/crab/gba/thumb/software_interrupt.cr b/src/crab/gba/thumb/software_interrupt.cr new file mode 100644 index 0000000..7a2252c --- /dev/null +++ b/src/crab/gba/thumb/software_interrupt.cr @@ -0,0 +1,12 @@ +module GBA + module THUMB + def thumb_software_interrupt(instr : Word) : Nil + lr = @r[15] - 2 + switch_mode CPU::Mode::SVC + set_reg(14, lr) + @cpsr.irq_disable = true + @cpsr.thumb = false + set_reg(15, 0x08) + end + end +end diff --git a/src/crab/gba/thumb/sp_relative_load_store.cr b/src/crab/gba/thumb/sp_relative_load_store.cr new file mode 100644 index 0000000..c3cca2b --- /dev/null +++ b/src/crab/gba/thumb/sp_relative_load_store.cr @@ -0,0 +1,17 @@ +module GBA + module THUMB + def thumb_sp_relative_load_store(instr : Word) : Nil + load = bit?(instr, 11) + rd = bits(instr, 8..10) + word = bits(instr, 0..7) + address = @r[13] &+ (word << 2) + if load + set_reg(rd, @gba.bus.read_word_rotate(address)) + else + @gba.bus[address] = @r[rd] + end + + step_thumb + end + end +end diff --git a/src/crab/gba/thumb/thumb.cr b/src/crab/gba/thumb/thumb.cr new file mode 100644 index 0000000..d555bab --- /dev/null +++ b/src/crab/gba/thumb/thumb.cr @@ -0,0 +1,58 @@ +module GBA + module THUMB + def thumb_execute(instr : Word) : Nil + thumb_lut[instr >> 8].call instr + end + + def fill_thumb_lut + lut = Slice(Proc(Word, Nil)).new 256, ->thumb_unimplemented(Word) + 256.times do |idx| + if idx & 0b11110000 == 0b11110000 + lut[idx] = ->thumb_long_branch_link(Word) + elsif idx & 0b11111000 == 0b11100000 + lut[idx] = ->thumb_unconditional_branch(Word) + elsif idx & 0b11111111 == 0b11011111 + lut[idx] = ->thumb_software_interrupt(Word) + elsif idx & 0b11110000 == 0b11010000 + lut[idx] = ->thumb_conditional_branch(Word) + elsif idx & 0b11110000 == 0b11000000 + lut[idx] = ->thumb_multiple_load_store(Word) + elsif idx & 0b11110110 == 0b10110100 + lut[idx] = ->thumb_push_pop_registers(Word) + elsif idx & 0b11111111 == 0b10110000 + lut[idx] = ->thumb_add_offset_to_stack_pointer(Word) + elsif idx & 0b11110000 == 0b10100000 + lut[idx] = ->thumb_load_address(Word) + elsif idx & 0b11110000 == 0b10010000 + lut[idx] = ->thumb_sp_relative_load_store(Word) + elsif idx & 0b11110000 == 0b10000000 + lut[idx] = ->thumb_load_store_halfword(Word) + elsif idx & 0b11100000 == 0b01100000 + lut[idx] = ->thumb_load_store_immediate_offset(Word) + elsif idx & 0b11110010 == 0b01010010 + lut[idx] = ->thumb_load_store_sign_extended(Word) + elsif idx & 0b11110010 == 0b01010000 + lut[idx] = ->thumb_load_store_register_offset(Word) + elsif idx & 0b11111000 == 0b01001000 + lut[idx] = ->thumb_pc_relative_load(Word) + elsif idx & 0b11111100 == 0b01000100 + lut[idx] = ->thumb_high_reg_branch_exchange(Word) + elsif idx & 0b11111100 == 0b01000000 + lut[idx] = ->thumb_alu_operations(Word) + elsif idx & 0b11100000 == 0b00100000 + lut[idx] = ->thumb_move_compare_add_subtract(Word) + elsif idx & 0b11111000 == 0b00011000 + lut[idx] = ->thumb_add_subtract(Word) + elsif idx & 0b11100000 == 0b00000000 + lut[idx] = ->thumb_move_shifted_register(Word) + end + end + lut + end + + def thumb_unimplemented(instr : Word) : Nil + puts "Unimplemented instruction: #{hex_str instr.to_u16}" + exit 1 + end + end +end diff --git a/src/crab/gba/thumb/unconditional_branch.cr b/src/crab/gba/thumb/unconditional_branch.cr new file mode 100644 index 0000000..eda6c52 --- /dev/null +++ b/src/crab/gba/thumb/unconditional_branch.cr @@ -0,0 +1,9 @@ +module GBA + module THUMB + def thumb_unconditional_branch(instr : Word) : Nil + offset = bits(instr, 0..10) + offset = (offset << 5).to_i16! >> 4 + set_reg(15, @r[15] &+ offset) + end + end +end diff --git a/src/crab/gba/timer.cr b/src/crab/gba/timer.cr new file mode 100644 index 0000000..1813fe5 --- /dev/null +++ b/src/crab/gba/timer.cr @@ -0,0 +1,98 @@ +module GBA + class Timer + PERIODS = [1, 64, 256, 1024] + + @interrupt_flags : Array(Proc(Nil)) + + def initialize(@gba : GBA) + @tmcnt = Array(Reg::TMCNT).new 4 { Reg::TMCNT.new 0 } # control registers + @tmd = Array(UInt16).new 4, 0 # reload values + @tm = Array(UInt16).new 4, 0 # counted values + @cycle_enabled = Array(UInt64).new 4, 0 # cycle that the timer was enabled + @events = Array(Proc(Nil)).new 4 { |i| overflow i } # overflow closures for each timer + @event_types = [Scheduler::EventType::Timer0, Scheduler::EventType::Timer1, + Scheduler::EventType::Timer2, Scheduler::EventType::Timer3] + @interrupt_flags = [->{ @gba.interrupts.reg_if.timer0 = true }, ->{ @gba.interrupts.reg_if.timer1 = true }, + ->{ @gba.interrupts.reg_if.timer2 = true }, ->{ @gba.interrupts.reg_if.timer3 = true }] + end + + def overflow(num : Int) : Proc(Nil) + ->{ + @tm[num] = @tmd[num] + @cycle_enabled[num] = @gba.scheduler.cycles + if num < 3 && @tmcnt[num + 1].cascade && @tmcnt[num + 1].enable + @tm[num + 1] &+= 1 + @events[num + 1].call if @tm[num + 1] == 0 # call overflow handler if cascaded timer overflowed + end + @gba.apu.timer_overflow num if num <= 1 # alert apu of timer 0-1 overflow + if @tmcnt[num].irq_enable # set interupt flag for this timer + @interrupt_flags[num].call + @gba.interrupts.schedule_interrupt_check + end + @gba.scheduler.schedule cycles_until_overflow(num), @events[num], @event_types[num] unless @tmcnt[num].cascade + } + end + + def cycles_until_overflow(num : Int) : Int32 + PERIODS[@tmcnt[num].frequency] * (0x10000 - @tm[num]) + end + + def get_current_tm(num : Int) : UInt16 + if @tmcnt[num].enable && !@tmcnt[num].cascade + elapsed = @gba.scheduler.cycles - @cycle_enabled[num] + @tm[num] &+ elapsed // PERIODS[@tmcnt[num].frequency] + else + @tm[num] + end + end + + def update_tm(num : Int) : Nil + @tm[num] = get_current_tm(num) + @cycle_enabled[num] = @gba.scheduler.cycles + end + + def read_io(io_addr : Int) : UInt8 + num = (io_addr & 0xF) // 4 + tmcnt = @tmcnt[num] + value = if bit?(io_addr, 1) + tmcnt.value + else + get_current_tm(num) + end + value >>= 8 if bit?(io_addr, 0) + value.to_u8! + end + + def write_io(io_addr : Int, value : UInt8) : Nil + num = (io_addr & 0xF) // 4 + high = bit?(io_addr, 0) + mask = 0xFF00_u16 + if high + mask >>= 8 + value = value.to_u16 << 8 + end + if bit?(io_addr, 1) + unless high + update_tm(num) + tmcnt = @tmcnt[num] + was_enabled = tmcnt.enable + was_cascade = tmcnt.cascade + tmcnt.value = (tmcnt.value & mask) | value + if tmcnt.enable + if tmcnt.cascade + @gba.scheduler.clear @event_types[num] + elsif !was_enabled || was_cascade # enabled or no longer cascade + @cycle_enabled[num] = @gba.scheduler.cycles + @tm[num] = @tmd[num] if !was_enabled + @gba.scheduler.schedule cycles_until_overflow(num), @events[num], @event_types[num] + end + elsif was_enabled # disabled + @gba.scheduler.clear(@event_types[num]) + end + end + else + @tmd[num] = (@tmd[num] & mask) | value + end + end + end +end diff --git a/src/crab/gba/types.cr b/src/crab/gba/types.cr new file mode 100644 index 0000000..3985b23 --- /dev/null +++ b/src/crab/gba/types.cr @@ -0,0 +1,38 @@ +module GBA + alias Byte = UInt8 + alias HalfWord = UInt16 + alias Word = UInt32 + alias Words = Slice(UInt32) + record BGR16, value : UInt16 do # xBBBBBGGGGGRRRRR + # Create a new BGR16 struct with the given values. Trucates at 5 bits. + def initialize(blue : Number, green : Number, red : Number) + @value = (blue <= 0x1F ? blue.to_u16 : 0x1F_u16) << 10 | + (green <= 0x1F ? green.to_u16 : 0x1F_u16) << 5 | + (red <= 0x1F ? red.to_u16 : 0x1F_u16) + end + + def blue : UInt16 + bits(value, 0xA..0xE) + end + + def green : UInt16 + bits(value, 0x5..0x9) + end + + def red : UInt16 + bits(value, 0x0..0x4) + end + + def +(other : BGR16) : BGR16 + BGR16.new(blue + other.blue, green + other.green, red + other.red) + end + + def -(other : BGR16) : BGR16 + BGR16.new(blue.to_i - other.blue, green.to_i - other.green, red.to_i - other.red) + end + + def *(operand : Number) : BGR16 + BGR16.new(blue * operand, green * operand, red * operand) + end + end +end diff --git a/src/crab/gba/util.cr b/src/crab/gba/util.cr new file mode 100644 index 0000000..657cd07 --- /dev/null +++ b/src/crab/gba/util.cr @@ -0,0 +1,63 @@ +module GBA + def hex_str(n : UInt8 | UInt16 | UInt32 | UInt64, prefix = true) : String + (prefix ? "0x" : "") + "#{n.to_s(16).rjust(sizeof(typeof(n)) * 2, '0').upcase}" + end + + macro bit?(value, bit) + ({{value}} & (1 << {{bit}}) > 0) + end + + macro bits(value, range) + ({{value}} >> {{range.begin}} & (1 << {{range.to_a.size}}) - 1) + end + + macro set_bit(value, bit) + ({{value}} | 1 << {{bit}}) + end + + macro clear_bit(value, bit) + ({{value}} & ~(1 << {{bit}})) + end + + macro count_bits(value) + (8 * sizeof(typeof(n))) + end + + def count_set_bits(n : Int) : Int + count = 0 + count_bits(n).times { |idx| count += n >> idx & 1 } + count + end + + def first_set_bit(n : Int) : Int + count = count_bits(n) + count.times { |idx| return idx if bit?(n, idx) } + count + end + + def last_set_bit(n : Int) : Int + count = count_bits(n) + count.downto(0) { |idx| return idx if bit?(n, idx) } + count + end + + macro trace(value, newline = true) + {% if flag? :trace %} + {% if newline %} + puts {{value}} + {% else %} + print {{value}} + {% end %} + {% end %} + end + + macro log(value, newline = true) + {% if flag?(:log) %} + {% if newline %} + puts {{value}} + {% else %} + print {{value}} + {% end %} + {% end %} + end +end diff --git a/src/crab/interrupts.cr b/src/crab/interrupts.cr deleted file mode 100644 index 35d4bcd..0000000 --- a/src/crab/interrupts.cr +++ /dev/null @@ -1,63 +0,0 @@ -class Interrupts - class InterruptReg < BitField(UInt16) - num not_used, 2, lock: true - bool game_pak - bool keypad - bool dma3 - bool dma2 - bool dma1 - bool dma0 - bool serial - bool timer3 - bool timer2 - bool timer1 - bool timer0 - bool vcounter - bool hblank - bool vblank - end - - getter reg_ie : InterruptReg = InterruptReg.new 0 - getter reg_if : InterruptReg = InterruptReg.new 0 - getter ime : Bool = false - - def initialize(@gba : GBA) - end - - def read_io(io_addr : Int) : Byte - case io_addr - when 0x200 then 0xFF_u8 & @reg_ie.value - when 0x201 then 0xFF_u8 & @reg_ie.value >> 8 - when 0x202 then 0xFF_u8 & @reg_if.value - when 0x203 then 0xFF_u8 & @reg_if.value >> 8 - when 0x208 then @ime ? 1_u8 : 0_u8 - when 0x209 then 0_u8 - else raise "Unimplemented interrupts read ~ addr:#{hex_str io_addr.to_u8!}" - end - end - - def write_io(io_addr : Int, value : Byte) : Nil - case io_addr - when 0x200 then @reg_ie.value = (@reg_ie.value & 0xFF00) | value - when 0x201 then @reg_ie.value = (@reg_ie.value & 0x00FF) | value.to_u16 << 8 - when 0x202 then @reg_if.value &= ~value - when 0x203 then @reg_if.value &= ~(value.to_u16 << 8) - when 0x208 then @ime = bit?(value, 0) - when 0x209 # ignored - else raise "Unimplemented interrupts write ~ addr:#{hex_str io_addr.to_u8!}, val:#{value}" - end - schedule_interrupt_check - end - - def schedule_interrupt_check : Nil - @gba.scheduler.schedule 0, ->check_interrupts - end - - private def check_interrupts : Nil - if @reg_ie.value & @reg_if.value != 0 - @gba.cpu.halted = false - # puts "IE:#{hex_str @reg_ie.value} & IF:#{hex_str @reg_if.value} != 0" - @gba.cpu.irq if @ime - end - end -end diff --git a/src/crab/keypad.cr b/src/crab/keypad.cr deleted file mode 100644 index 55be1b7..0000000 --- a/src/crab/keypad.cr +++ /dev/null @@ -1,103 +0,0 @@ -class Keypad - class KEYINPUT < BitField(UInt16) - num not_used, 6 - bool l - bool r - bool down - bool up - bool left - bool right - bool start - bool :select - bool b - bool a - end - - class KEYCNT < BitField(UInt16) - bool irq_condition - bool irq_enable - num not_used, 4 - bool l - bool r - bool down - bool up - bool left - bool right - bool start - bool :select - bool b - bool a - end - - @keyinput = KEYINPUT.new 0xFFFF_u16 - @keycnt = KEYCNT.new 0xFFFF_u16 - - def initialize(@gba : GBA) - end - - def read_io(io_addr : Int) : Byte - case io_addr - when 0x130 then 0xFF_u8 & @keyinput.value - when 0x131 then 0xFF_u8 & @keyinput.value >> 8 - when 0x132 then 0xFF_u8 & @keycnt.value - when 0x133 then 0xFF_u8 & @keycnt.value >> 8 - else raise "Unimplemented keypad read ~ addr:#{hex_str io_addr.to_u8!}" - end - end - - def write_io(io_addr : Int, value : Byte) : Nil - case io_addr - when 0x130 then nil - when 0x131 then nil - else raise "Unimplemented keypad write ~ addr:#{hex_str io_addr.to_u8!}, val:#{value}" - end - end - - def handle_keypad_event(event : SDL::Event) : Nil - case event - when SDL::Event::Keyboard - bit = !event.pressed? - case event.sym - when .down?, .d? then @keyinput.down = bit - when .up?, .e? then @keyinput.up = bit - when .left?, .s? then @keyinput.left = bit - when .right?, .f? then @keyinput.right = bit - when .semicolon? then @keyinput.start = bit - when .l? then @keyinput.select = bit - when .b?, .j? then @keyinput.b = bit - when .a?, .k? then @keyinput.a = bit - when .w? then @keyinput.l = bit - when .r? then @keyinput.r = bit - # Extras - when .tab? then @gba.apu.toggle_sync if event.pressed? - when .m? then @gba.display.toggle_blending if event.pressed? - else nil - end - when SDL::Event::JoyHat - @keyinput.value |= 0x00F0 - case event.value - when LibSDL::HAT_DOWN then @keyinput.down = false - when LibSDL::HAT_UP then @keyinput.up = false - when LibSDL::HAT_LEFT then @keyinput.left = false - when LibSDL::HAT_RIGHT then @keyinput.right = false - when LibSDL::HAT_LEFTUP then @keyinput.left = true; @keyinput.up = true - when LibSDL::HAT_LEFTDOWN then @keyinput.left = true; @keyinput.down = true - when LibSDL::HAT_RIGHTUP then @keyinput.right = true; @keyinput.up = true - when LibSDL::HAT_RIGHTDOWN then @keyinput.right = true; @keyinput.down = true - else nil - end - when SDL::Event::JoyButton - bit = !event.pressed? - case event.button - when 0 then @keyinput.b = bit - when 1 then @keyinput.a = bit - when 4 then @keyinput.l = bit - when 5 then @keyinput.r = bit - when 6 then @keyinput.select = bit - when 7 then @keyinput.start = bit - else nil - end - else nil - end - end -end diff --git a/src/crab/mmio.cr b/src/crab/mmio.cr deleted file mode 100644 index 075257c..0000000 --- a/src/crab/mmio.cr +++ /dev/null @@ -1,89 +0,0 @@ -class MMIO - class WAITCNT < BitField(UInt16) - bool gamepak_type, lock: true - bool gamepack_prefetch_buffer - bool not_used, lock: true - num phi_terminal_output, 2 - num wait_state_2_second_access, 1 - num wait_state_2_first_access, 2 - num wait_state_1_second_access, 1 - num wait_state_1_first_access, 2 - num wait_state_0_second_access, 1 - num wait_state_0_first_access, 2 - num sram_wait_control, 2 - end - - @waitcnt = WAITCNT.new 0 - - def initialize(@gba : GBA) - end - - def [](index : Int) : Byte - io_addr = 0x0FFF_u16 & index - if io_addr <= 0x05F - @gba.ppu.read_io io_addr - elsif io_addr <= 0xAF - @gba.apu.read_io io_addr - elsif io_addr <= 0xFF - @gba.dma.read_io io_addr - elsif 0x100 <= io_addr <= 0x10F - @gba.timer.read_io io_addr - elsif 0x130 <= io_addr <= 0x133 - @gba.keypad.read_io io_addr - elsif 0x120 <= io_addr <= 0x12F || 0x134 <= io_addr <= 0x1FF - # todo: serial - 0_u8 - elsif 0x200 <= io_addr <= 0x203 || 0x208 <= io_addr <= 0x209 - @gba.interrupts.read_io io_addr - elsif 0x204 <= io_addr <= 0x205 - (@waitcnt.value >> (8 * (io_addr & 1))).to_u8! - elsif not_used? io_addr - 0xFF_u8 # todo what is returned here? - else - # todo: oob reads - puts "Unmapped MMIO read: #{hex_str index.to_u32}".colorize(:red) - 0_u8 - end - end - - def []=(index : Int, value : Byte) : Nil - io_addr = 0x0FFF_u16 & index - if io_addr <= 0x05F - @gba.ppu.write_io io_addr, value - elsif io_addr <= 0xAF - @gba.apu.write_io io_addr, value - elsif io_addr <= 0xFF - @gba.dma.write_io io_addr, value - elsif 0x100 <= io_addr <= 0x10F - @gba.timer.write_io io_addr, value - elsif 0x130 <= io_addr <= 0x133 - @gba.keypad.read_io io_addr - elsif 0x120 <= io_addr <= 0x12F || 0x134 <= io_addr <= 0x1FF - # todo: serial - elsif 0x200 <= io_addr <= 0x203 || 0x208 <= io_addr <= 0x209 - @gba.interrupts.write_io io_addr, value - elsif 0x204 <= io_addr <= 0x205 - shift = 8 * (io_addr & 1) - mask = 0xFF00_u16 >> shift - @waitcnt.value = (@waitcnt.value & mask) | value.to_u16 << shift - elsif io_addr == 0x301 - if bit?(value, 7) - abort "Stopping not supported" - else - @gba.cpu.halted = true - end - elsif not_used? io_addr - else - puts "Unmapped MMIO write ~ addr:#{hex_str index.to_u32}, val:#{hex_str value}".colorize(:yellow) - end - end - - def not_used?(io_addr : Int) : Bool - (0x0E0..0x0FF).includes?(io_addr) || (0x110..0x11F).includes?(io_addr) || - (0x12C..0x12F).includes?(io_addr) || (0x138..0x13F).includes?(io_addr) || - (0x142..0x14F).includes?(io_addr) || (0x15A..0x1FF).includes?(io_addr) || - (0x206..0x207).includes?(io_addr) || (0x20A..0x2FF).includes?(io_addr) || - (0x302..0x40F).includes?(io_addr) || (0x441..0x7FF).includes?(io_addr) || - (0x804..0xFFFF).includes?(io_addr) - end -end diff --git a/src/crab/pipeline.cr b/src/crab/pipeline.cr deleted file mode 100644 index d121c5b..0000000 --- a/src/crab/pipeline.cr +++ /dev/null @@ -1,33 +0,0 @@ -# A super minimalistic FIFO queue implementation optimized for -# use as an ARM instruction pipeline. -class Pipeline - @buffer = Slice(Word).new 2 - @pos = 0 - @size = 0 - - def push(instr : Word) : Nil - raise "Pushing to full pipeline" if @size == 2 - index = (@pos + @size) & 1 - @buffer[index] = instr - @size += 1 - end - - def shift : Word - @size -= 1 - val = @buffer[@pos] - @pos = (@pos + 1) & 1 - val - end - - def peek : Word - @buffer[@pos] - end - - def clear : Nil - @size = 0 - end - - def size : Int32 - @size - end -end diff --git a/src/crab/ppu.cr b/src/crab/ppu.cr deleted file mode 100644 index ebba0f0..0000000 --- a/src/crab/ppu.cr +++ /dev/null @@ -1,560 +0,0 @@ -class PPU - SPRITE_PIXEL = SpritePixel.new 4, 0, false, false # base sprite pixel to fill buffer with on each scanline - - @framebuffer : Slice(UInt16) = Slice(UInt16).new 0x9600 # framebuffer as 16-bit xBBBBBGGGGGRRRRR - @layer_palettes : Array(Bytes) = Array.new 4 { Bytes.new 240 } - @sprite_pixels : Slice(SpritePixel) = Slice(SpritePixel).new 240, SPRITE_PIXEL - - getter pram = Bytes.new 0x400 - getter vram = Bytes.new 0x18000 - getter oam = Bytes.new 0x400 - - getter dispcnt = Reg::DISPCNT.new 0 - getter dispstat = Reg::DISPSTAT.new 0 - getter vcount : UInt16 = 0x0000_u16 - getter bgcnt = Array(Reg::BGCNT).new 4 { Reg::BGCNT.new 0 } - getter bghofs = Array(Reg::BGOFS).new 4 { Reg::BGOFS.new 0 } - getter bgvofs = Array(Reg::BGOFS).new 4 { Reg::BGOFS.new 0 } - getter bgaff = Array(Array(Reg::BGAFF)).new 2 { Array(Reg::BGAFF).new 4 { Reg::BGAFF.new 0 } } - getter bgref = Array(Array(Reg::BGREF)).new 2 { Array(Reg::BGREF).new 2 { Reg::BGREF.new 0 } } - getter bgref_int = Array(Array(Int32)).new 2 { Array(Int32).new 2, 0 } - getter win0h = Reg::WINH.new 0 - getter win1h = Reg::WINH.new 0 - getter win0v = Reg::WINV.new 0 - getter win1v = Reg::WINV.new 0 - getter winin = Reg::WININ.new 0 - getter winout = Reg::WINOUT.new 0 - getter mosaic = Reg::MOSAIC.new 0 - getter bldcnt = Reg::BLDCNT.new 0 - getter bldalpha = Reg::BLDALPHA.new 0 - getter bldy = Reg::BLDY.new 0 - - def initialize(@gba : GBA) - start_line - end - - def start_line : Nil - @gba.scheduler.schedule 960, ->start_hblank - end - - def start_hblank : Nil - @gba.scheduler.schedule 272, ->end_hblank - @dispstat.hblank = true - if @dispstat.hblank_irq_enable - @gba.interrupts.reg_if.hblank = true - @gba.interrupts.schedule_interrupt_check - end - if @vcount < 160 - scanline - @gba.dma.trigger_hdma - @bgref_int.each_with_index do |bgrefs, bg_num| - bgrefs[0] &+= @bgaff[bg_num][1].num # bgx += dmx - bgrefs[1] &+= @bgaff[bg_num][3].num # bgy += dmy - end - end - end - - def end_hblank : Nil - @gba.scheduler.schedule 0, ->start_line - @dispstat.hblank = false - @vcount = (@vcount + 1) % 228 - @dispstat.vcounter = @vcount == @dispstat.vcount_setting - @gba.interrupts.reg_if.vcounter = true if @dispstat.vcounter_irq_enable && @dispstat.vcounter - if @vcount == 0 - @dispstat.vblank = false - elsif @vcount == 160 - @dispstat.vblank = true - @gba.dma.trigger_vdma - @gba.interrupts.reg_if.vblank = true if @dispstat.vblank_irq_enable - @bgref.each_with_index { |bgrefs, bg_num| bgrefs.each_with_index { |bgref, ref_num| @bgref_int[bg_num][ref_num] = bgref.num } } - draw - end - @gba.interrupts.schedule_interrupt_check - end - - def draw : Nil - @gba.display.draw @framebuffer - end - - # Get the screen entry offset from the tile x, tile y, and background screen-size param using tonc algo - @[AlwaysInline] - def se_index(tx : Int, ty : Int, screen_size : Int) : Int - n = tx + ty * 32 - n += 0x03E0 if tx >= 32 - n += 0x0400 if ty >= 32 && screen_size == 0b11 - n - end - - def scanline : Nil - row = @vcount.to_u32 - row_base = 240 * row - scanline = @framebuffer + row_base - scanline.to_unsafe.clear(240) - @layer_palettes.each &.to_unsafe.clear 240 - @sprite_pixels.map! { SPRITE_PIXEL } - case @dispcnt.bg_mode - when 0 - render_reg_bg(0) - render_reg_bg(1) - render_reg_bg(2) - render_reg_bg(3) - render_sprites - composite(scanline) - when 1 - render_reg_bg(0) - render_reg_bg(1) - render_aff_bg(2) - render_sprites - composite(scanline) - when 2 - render_aff_bg(2) - render_aff_bg(3) - render_sprites - composite(scanline) - when 3 - 240.times { |col| scanline[col] = @vram.to_unsafe.as(UInt16*)[row_base + col] } - when 4 - base = @dispcnt.display_frame_select ? 0xA000 : 0 - 240.times do |col| - pal_idx = @vram[base + row_base + col] - scanline[col] = @pram.to_unsafe.as(UInt16*)[pal_idx] - end - when 5 - base = @dispcnt.display_frame_select ? 0xA000 : 0 - background_color = @pram.to_unsafe.as(UInt16*)[0] - if @vcount < 128 - 160.times do |col| - scanline[col] = (@vram + base).to_unsafe.as(UInt16*)[row * 160 + col] - end - 160.to 239 do |col| - scanline[col] = background_color - end - else - 240.times do |col| - scanline[col] = background_color - end - end - else abort "Invalid background mode: #{@dispcnt.bg_mode}" - end - end - - def render_reg_bg(bg : Int) : Nil - return unless bit?(@dispcnt.value, 8 + bg) - pal_buf = @layer_palettes[bg] - bgcnt = @bgcnt[bg] - bgvofs = @bgvofs[bg] - bghofs = @bghofs[bg] - - tw, th = case bgcnt.screen_size - when 0b00 then {0x0FF, 0x0FF} # 32x32 - when 0b01 then {0x1FF, 0x0FF} # 64x32 - when 0b10 then {0x0FF, 0x1FF} # 32x64 - when 0b11 then {0x1FF, 0x1FF} # 64x64 - else raise "Impossible bgcnt screen size: #{bgcnt.screen_size}" - end - - screen_base = 0x800_u32 * bgcnt.screen_base_block - character_base = bgcnt.character_base_block.to_u32 * 0x4000 - effective_row = (@vcount.to_u32 + bgvofs.offset) & th - ty = effective_row >> 3 - 240.times do |col| - effective_col = (col + bghofs.offset) & tw - tx = effective_col >> 3 - - se_idx = se_index(tx, ty, bgcnt.screen_size) - screen_entry = @vram[screen_base + se_idx * 2 + 1].to_u16 << 8 | @vram[screen_base + se_idx * 2] - - tile_id = bits(screen_entry, 0..9) - y = (effective_row & 7) ^ (7 * (screen_entry >> 11 & 1)) - x = (effective_col & 7) ^ (7 * (screen_entry >> 10 & 1)) - - if bgcnt.color_mode # 8bpp - pal_idx = @vram[character_base + tile_id * 0x40 + y * 8 + x] - else # 4bpp - palette_bank = bits(screen_entry, 12..15) - palettes = @vram[character_base + tile_id * 0x20 + y * 4 + (x >> 1)] - pal_idx = ((palettes >> ((x & 1) * 4)) & 0xF) - pal_idx = (palette_bank << 4) + pal_idx if pal_idx > 0 - end - pal_buf[col] = pal_idx.to_u8 - end - end - - def render_aff_bg(bg : Int) : Nil - return unless bit?(@dispcnt.value, 8 + bg) - pal_buf = @layer_palettes[bg] - row = @vcount.to_u32 - bgcnt = @bgcnt[bg] - - dx, _, dy, _ = @bgaff[bg - 2].map &.num - int_x, int_y = @bgref_int[bg - 2] - - size = 16 << bgcnt.screen_size # tiles, always a square - size_pixels = size << 3 - - screen_base = 0x800_u32 * bgcnt.screen_base_block - character_base = bgcnt.character_base_block.to_u32 * 0x4000 - 240.times do |col| - x = int_x >> 8 - y = int_y >> 8 - int_x += dx - int_y += dy - - if bgcnt.affine_wrap - x %= size_pixels - y %= size_pixels - end - next unless 0 <= x < size_pixels && 0 <= y < size_pixels - - # affine screen entries are merely one-byte tile indices - tile_id = @vram[screen_base + (y >> 3) * size + (x >> 3)] - pal_idx = @vram[character_base + 0x40 * tile_id + 8 * (y & 7) + (x & 7)] - pal_buf[col] = pal_idx - end - end - - def render_sprites : Nil - return unless @dispcnt.screen_display_obj - base = 0x10000_u32 - sprites = Slice(Sprite).new(@oam.to_unsafe.as(Sprite*), 128) - sprites.each do |sprite| - next if sprite.obj_shape == 3 # prohibited - next if sprite.affine_mode == 0b10 # sprite disabled - x_coord, y_coord = sprite.x_coord.to_i16, sprite.y_coord.to_i16 - x_coord -= 512 if x_coord > 239 - y_coord -= 256 if y_coord > 159 - orig_width, orig_height = SIZES[sprite.obj_shape][sprite.obj_size] - width, height = orig_width, orig_height - center_x, center_y = x_coord + width // 2, y_coord + height // 2 # off of center - if sprite.affine - oam_affine_entry = sprite.attr1_bits_9_13 - # signed 8.8 fixed-point numbers, need to shr 8 - pa = sprites[oam_affine_entry * 4].aff_param.to_i32 - pb = sprites[oam_affine_entry * 4 + 1].aff_param.to_i32 - pc = sprites[oam_affine_entry * 4 + 2].aff_param.to_i32 - pd = sprites[oam_affine_entry * 4 + 3].aff_param.to_i32 - if sprite.attr0_bit_9 # double-size (rotated sprites won't clip unless scaled) - center_x += width >> 1 - center_y += height >> 1 - width <<= 1 - height <<= 1 - end - else # identity matrix if sprite isn't affine (shifted left 8 to match the 8.8 fixed-point) - pa, pb, pc, pd = 0x100, 0, 0, 0x100 - end - if y_coord <= @vcount < y_coord + height - iy = @vcount.to_i16 - center_y - min_x, max_x = Math.max(0, x_coord), Math.min(240, x_coord + width) - (-(width // 2)...(width // 2)).each do |ix| - col = center_x + ix - next unless min_x <= col < max_x - # transform to texture coordinates - px = (pa * ix + pb * iy) >> 8 - py = (pc * ix + pd * iy) >> 8 - # bring origin back to top-left of the sprite - px += (orig_width // 2) - py += (orig_height // 2) - - next unless 0 <= px < orig_width && 0 <= py < orig_height - - px = orig_width - px - 1 if bit?(sprite.attr1, 12) && !sprite.affine - py = orig_height - py - 1 if bit?(sprite.attr1, 13) && !sprite.affine - - x = px & 7 - y = py & 7 - - tile_id = sprite.character_name - offset = py >> 3 - if @dispcnt.obj_character_vram_mapping - offset *= orig_width >> 3 - else - if sprite.color_mode - offset *= 0x10 - else - offset *= 0x20 - end - end - offset += px >> 3 - if sprite.color_mode # 8bpp - tile_id >>= 1 - tile_id += offset - pal_idx = @vram[base + tile_id * 0x40 + y * 8 + x] - else # 4bpp - tile_id += offset - palettes = @vram[base + tile_id * 0x20 + y * 4 + (x >> 1)] - pal_idx = ((palettes >> ((x & 1) * 4)) & 0xF) - pal_idx += (sprite.palette_number << 4) if pal_idx > 0 - end - - if sprite.obj_mode == 0b10 - @sprite_pixels[col] = @sprite_pixels[col].copy_with window: true if pal_idx > 0 - elsif sprite.priority < @sprite_pixels[col].priority || @sprite_pixels[col].palette == 0 - @sprite_pixels[col] = @sprite_pixels[col].copy_with priority: sprite.priority - @sprite_pixels[col] = @sprite_pixels[col].copy_with palette: pal_idx.to_u16, blends: sprite.obj_mode == 0b01 if pal_idx > 0 - end - end - end - end - end - - def calculate_color(col : Int) : UInt16 - enables, effects = if @dispcnt.window_0_display && @win0h.x1 <= col < @win0h.x2 && @win0v.y1 <= @vcount < @win0v.y2 # win0 - {bits(@winin.value, 0..4), @winin.window_0_color_special_effect} - elsif @dispcnt.window_1_display && @win1h.x1 <= col < @win1h.x2 && @win1v.y1 <= @vcount < @win1v.y2 # win1 - {bits(@winin.value, 8..12), @winin.window_1_color_special_effect} - elsif @dispcnt.obj_window_display && @sprite_pixels[col].window # obj win - {bits(@winout.value, 8..12), @winout.obj_window_color_special_effect} - elsif @dispcnt.window_0_display || @dispcnt.window_1_display || @dispcnt.obj_window_display # winout - {bits(@winout.value, 0..4), @winout.outside_color_special_effect} - else # no windows - {bits(@dispcnt.value, 8..12), true} - end - top_color = nil - 4.times do |priority| - if bit?(enables, 4) - sprite_pixel = @sprite_pixels[col] - if sprite_pixel.priority == priority && sprite_pixel.palette > 0 - selected_color = (@pram + 0x200).to_unsafe.as(UInt16*)[sprite_pixel.palette] - if top_color.nil? # todo: brightness for sprites - if !(sprite_pixel.blends || (@bldcnt.is_bg_target(4, target: 1) && effects)) - return selected_color - elsif @bldcnt.color_special_effect == 1 # alpha blending - top_color = selected_color - elsif @bldcnt.color_special_effect == 2 # brightness increase - bgr16 = BGR16.new(selected_color) - return (bgr16 + (BGR16.new(0xFFFF) - bgr16) * (Math.min(16, @bldy.evy_coefficient) / 16)).value - else # brightness decrease - bgr16 = BGR16.new(selected_color) - return (bgr16 - bgr16 * (Math.min(16, @bldy.evy_coefficient) / 16)).value - end - else - if @bldcnt.is_bg_target(4, target: 2) || sprite_pixel.blends - color = BGR16.new(top_color) * (Math.min(16, @bldalpha.eva_coefficient) / 16) + BGR16.new(selected_color) * (Math.min(16, @bldalpha.evb_coefficient) / 16) - return color.value - else - return top_color - end - end - end - end - 4.times do |bg| - if bit?(enables, bg) - if @bgcnt[bg].priority == priority - palette = @layer_palettes[bg][col] - next if palette == 0 - selected_color = @pram.to_unsafe.as(UInt16*)[palette] - if top_color.nil? - if @bldcnt.color_special_effect == 0 || !@bldcnt.is_bg_target(bg, target: 1) || !effects - return selected_color - elsif @bldcnt.color_special_effect == 1 # alpha blending - top_color = selected_color - elsif @bldcnt.color_special_effect == 2 # brightness increase - bgr16 = BGR16.new(selected_color) - return (bgr16 + (BGR16.new(0xFFFF) - bgr16) * (Math.min(16, @bldy.evy_coefficient) / 16)).value - else # brightness decrease - bgr16 = BGR16.new(selected_color) - return (bgr16 - bgr16 * (Math.min(16, @bldy.evy_coefficient) / 16)).value - end - else - if @bldcnt.is_bg_target(bg, target: 2) - color = BGR16.new(top_color) * (Math.min(16, @bldalpha.eva_coefficient) / 16) + BGR16.new(selected_color) * (Math.min(16, @bldalpha.evb_coefficient) / 16) - return color.value - else # second layer isn't set in bldcnt, don't blend - return top_color - end - end - end - end - end - end - top_color || @pram.to_unsafe.as(UInt16*)[0] - end - - def composite(scanline : Slice(UInt16)) : Nil - 240.times do |col| - scanline[col] = calculate_color(col) - end - end - - def read_io(io_addr : Int) : Byte - case io_addr - when 0x000..0x001 then @dispcnt.read_byte(io_addr & 1) - when 0x002..0x003 then 0_u8 # todo green swap - when 0x004..0x005 then @dispstat.read_byte(io_addr & 1) - when 0x006..0x007 then (@vcount >> (8 * (io_addr & 1))).to_u8! - when 0x008..0x00F then @bgcnt[(io_addr - 0x008) >> 1].read_byte(io_addr & 1) - when 0x010..0x01F - bg_num = (io_addr - 0x010) >> 2 - if bit?(io_addr, 1) - @bgvofs[bg_num].read_byte(io_addr & 1) - else - @bghofs[bg_num].read_byte(io_addr & 1) - end - when 0x020..0x03F - bg_num = (io_addr & 0x10) >> 4 # (bg 0/1 represents bg 2/3, since those are the only aff bgs) - offs = io_addr & 0xF - if offs >= 8 - offs -= 8 - @bgref[bg_num][offs >> 2].read_byte(offs & 3) - else - @bgaff[bg_num][offs >> 1].read_byte(offs & 1) - end - when 0x040 then 0xFF_u8 & @win0h.value - when 0x041 then 0xFF_u8 & @win0h.value >> 8 - when 0x042 then 0xFF_u8 & @win1h.value - when 0x043 then 0xFF_u8 & @win1h.value >> 8 - when 0x044 then 0xFF_u8 & @win0v.value - when 0x045 then 0xFF_u8 & @win0v.value >> 8 - when 0x046 then 0xFF_u8 & @win1v.value - when 0x047 then 0xFF_u8 & @win1v.value >> 8 - when 0x048 then 0xFF_u8 & @winin.value - when 0x049 then 0xFF_u8 & @winin.value >> 8 - when 0x04A then 0xFF_u8 & @winout.value - when 0x04B then 0xFF_u8 & @winout.value >> 8 - when 0x04C then 0xFF_u8 & @mosaic.value - when 0x04D then 0xFF_u8 & @mosaic.value >> 8 - when 0x050 then 0xFF_u8 & @bldcnt.value - when 0x051 then 0xFF_u8 & @bldcnt.value >> 8 - when 0x052 then 0xFF_u8 & @bldalpha.value - when 0x053 then 0xFF_u8 & @bldalpha.value >> 8 - when 0x054 then 0xFF_u8 & @bldy.value - when 0x055 then 0xFF_u8 & @bldy.value >> 8 - else log "Unmapped PPU read ~ addr:#{hex_str io_addr.to_u8}"; 0_u8 # todo: open bus - end - end - - def write_io(io_addr : Int, value : Byte) : Nil - case io_addr - when 0x000..0x001 then @dispcnt.write_byte(io_addr & 1, value) - when 0x002..0x003 # undocumented - green swap - when 0x004..0x005 then @dispstat.write_byte(io_addr & 1, value) - when 0x006..0x007 # vcount - when 0x008..0x00F then @bgcnt[(io_addr - 0x008) >> 1].write_byte(io_addr & 1, value) - when 0x010..0x01F - bg_num = (io_addr - 0x010) >> 2 - if bit?(io_addr, 1) - @bgvofs[bg_num].write_byte(io_addr & 1, value) - else - @bghofs[bg_num].write_byte(io_addr & 1, value) - end - when 0x020..0x03F - bg_num = (io_addr & 0x10) >> 4 # (bg 0/1 represents bg 2/3, since those are the only aff bgs) - offs = io_addr & 0xF - if offs >= 8 - offs -= 8 - @bgref[bg_num][offs >> 2].write_byte(offs & 3, value) - @bgref_int[bg_num][offs >> 2] = @bgref[bg_num][offs >> 2].num - else - @bgaff[bg_num][offs >> 1].write_byte(offs & 1, value) - end - when 0x040 then @win0h.value = (@win0h.value & 0xFF00) | value - when 0x041 then @win0h.value = (@win0h.value & 0x00FF) | value.to_u16 << 8 - when 0x042 then @win1h.value = (@win1h.value & 0xFF00) | value - when 0x043 then @win1h.value = (@win1h.value & 0x00FF) | value.to_u16 << 8 - when 0x044 then @win0v.value = (@win0v.value & 0xFF00) | value - when 0x045 then @win0v.value = (@win0v.value & 0x00FF) | value.to_u16 << 8 - when 0x046 then @win1v.value = (@win1v.value & 0xFF00) | value - when 0x047 then @win1v.value = (@win1v.value & 0x00FF) | value.to_u16 << 8 - when 0x048 then @winin.value = (@winin.value & 0xFF00) | value - when 0x049 then @winin.value = (@winin.value & 0x00FF) | value.to_u16 << 8 - when 0x04A then @winout.value = (@winout.value & 0xFF00) | value - when 0x04B then @winout.value = (@winout.value & 0x00FF) | value.to_u16 << 8 - when 0x04C then @mosaic.value = (@mosaic.value & 0xFF00) | value - when 0x04D then @mosaic.value = (@mosaic.value & 0x00FF) | value.to_u16 << 8 - when 0x050 then @bldcnt.value = (@bldcnt.value & 0xFF00) | value - when 0x051 then @bldcnt.value = (@bldcnt.value & 0x00FF) | value.to_u16 << 8 - when 0x052 then @bldalpha.value = (@bldalpha.value & 0xFF00) | value - when 0x053 then @bldalpha.value = (@bldalpha.value & 0x00FF) | value.to_u16 << 8 - when 0x054 then @bldy.value = (@bldy.value & 0xFF00) | value - when 0x055 then @bldy.value = (@bldy.value & 0x00FF) | value.to_u16 << 8 - end - end -end - -# SIZES[SHAPE][SIZE] -SIZES = [ - [ # square - {8, 8}, - {16, 16}, - {32, 32}, - {64, 64}, - ], - [ # horizontal rectangle - {16, 8}, - {32, 8}, - {32, 16}, - {64, 32}, - ], - [ # vertical rectangle - {8, 16}, - {8, 32}, - {16, 32}, - {32, 64}, - ], -] - -record Sprite, attr0 : UInt16, attr1 : UInt16, attr2 : UInt16, aff_param : Int16 do - # OBJ Attribute 0 - - def obj_shape - bits(attr0, 14..15) - end - - def color_mode - bit?(attr0, 13) - end - - def obj_mosaic - bit?(attr0, 12) - end - - def obj_mode - bits(attr0, 10..11) - end - - def attr0_bit_9 - bit?(attr0, 9) - end - - def affine - bit?(attr0, 8) - end - - def affine_mode - bits(attr0, 8..9) - end - - def y_coord - bits(attr0, 0..7) - end - - # OBJ Attribute 1 - - def obj_size - bits(attr1, 14..15) - end - - def attr1_bits_9_13 - bits(attr1, 9..13) - end - - def x_coord - bits(attr1, 0..8) - end - - # OBJ Attribute 2 - - def character_name - bits(attr2, 0..9) - end - - def priority - bits(attr2, 10..11) - end - - def palette_number - bits(attr2, 12..15) - end -end - -record SpritePixel, priority : UInt16, palette : UInt16, blends : Bool, window : Bool diff --git a/src/crab/reg.cr b/src/crab/reg.cr deleted file mode 100644 index 5685096..0000000 --- a/src/crab/reg.cr +++ /dev/null @@ -1,256 +0,0 @@ -module Reg - module Base16 - def read_byte(byte_num : Int) : Byte - (@value >> (8 * byte_num)).to_u8! - end - - def write_byte(byte_num : Int, byte : Byte) : Byte - shift = 8 * byte_num - mask = ~(0xFF_u16 << shift) - @value = (@value & mask) | byte.to_u16 << shift - byte - end - end - - module Base32 - def read_byte(byte_num : Int) : Byte - (@value >> (8 * byte_num)).to_u8! - end - - def write_byte(byte_num : Int, byte : Byte) : Byte - shift = 8 * byte_num - mask = ~(0xFF_u32 << shift) - @value = (@value & mask) | byte.to_u32 << shift - byte - end - end - - #################### - # APU - - class SOUNDCNT_L < BitField(UInt16) - num channel_4_left, 1 - num channel_3_left, 1 - num channel_2_left, 1 - num channel_1_left, 1 - num channel_4_right, 1 - num channel_3_right, 1 - num channel_2_right, 1 - num channel_1_right, 1 - bool not_used_1, lock: true - num left_volume, 3 - bool not_used_2, lock: true - num right_volume, 3 - end - - class SOUNDCNT_H < BitField(UInt16) - bool dma_sound_b_reset, lock: true - num dma_sound_b_timer, 1 - num dma_sound_b_left, 1 - num dma_sound_b_right, 1 - bool dma_sound_a_reset, lock: true - num dma_sound_a_timer, 1 - num dma_sound_a_left, 1 - num dma_sound_a_right, 1 - num not_used, 4, lock: true - num dma_sound_b_volume, 1 - num dma_sound_a_volume, 1 - num sound_volume, 2 - end - - class SOUNDBIAS < BitField(UInt16) - num amplitude_resolution, 2 - num not_used_1, 4 - num bias_level, 9 - bool not_used_2 - end - - #################### - # DMA - - class DMACNT < BitField(UInt16) - bool enable - bool irq_enable - num start_timing, 2 - bool game_pak - num type, 1 - bool repeat - num source_control, 2 - num dest_control, 2 - num not_used, 5 - - def to_s(io) - io << "enable:#{enable}, irq:#{irq_enable}, timing:#{start_timing}, game_pak:#{game_pak}, type:#{type}, repeat:#{repeat}, srcctl:#{source_control}, dstctl:#{dest_control}" - end - end - - #################### - # Timer - - class TMCNT < BitField(UInt16) - num not_used_1, 8, lock: true - bool enable - bool irq_enable - num not_used_2, 3, lock: true - bool cascade - num frequency, 2 - - def to_s(io) - io << "enable:#{enable}, irq:#{irq_enable}, cascade:#{cascade}, freq:#{frequency}" - end - end - - #################### - # PPU - - class DISPCNT < BitField(UInt16) - include Base16 - bool obj_window_display - bool window_1_display - bool window_0_display - bool screen_display_obj - bool screen_display_bg3 - bool screen_display_bg2 - bool screen_display_bg1 - bool screen_display_bg0 - bool forced_blank # (1=Allow access to VRAM,Palette,OAM) - bool obj_character_vram_mapping # (0=Two dimensional, 1=One dimensional) - bool hblank_interval_free # (1=Allow access to OAM during H-Blank) - bool display_frame_select # (0-1=Frame 0-1) (for BG Modes 4,5 only) - bool reserved_for_bios, lock: true - num bg_mode, 3 # (0-5=Video Mode 0-5, 6-7=Prohibited) - end - - class DISPSTAT < BitField(UInt16) - include Base16 - num vcount_setting, 8 - num not_used, 2 - bool vcounter_irq_enable - bool hblank_irq_enable - bool vblank_irq_enable - bool vcounter, lock: true - bool hblank, lock: true - bool vblank, lock: true - end - - class BGCNT < BitField(UInt16) - include Base16 - num screen_size, 2 - bool affine_wrap - num screen_base_block, 5 - bool color_mode - bool mosaic - num not_used, 2, lock: true - num character_base_block, 2 - num priority, 2 - end - - class BGOFS < BitField(UInt16) - include Base16 - num not_used, 7, lock: true - num offset, 9 - end - - class BGAFF < BitField(UInt16) - include Base16 - bool sign - num integer, 7 - num fraction, 8 - - def num : Int16 - value.to_i16! - end - end - - class BGREF < BitField(UInt32) - include Base32 - num not_used, 4, lock: true - bool sign - num integer, 19 - num fraction, 8 - - def num : Int32 - (value << 4).to_i32! >> 4 - end - end - - class WINH < BitField(UInt16) - include Base16 - num x1, 8 - num x2, 8 - end - - class WINV < BitField(UInt16) - include Base16 - num y1, 8 - num y2, 8 - end - - class WININ < BitField(UInt16) - include Base16 - num not_used_1, 2, lock: true - bool window_1_color_special_effect - bool window_1_obj_enable - num window_1_enable_bits, 4 - num not_used_0, 2, lock: true - bool window_0_color_special_effect - bool window_0_obj_enable - num window_0_enable_bits, 4 - end - - class WINOUT < BitField(UInt16) - include Base16 - num not_used_obj, 2, lock: true - bool obj_window_color_special_effect - bool obj_window_obj_enable - num obj_window_enable_bits, 4 - num not_used_outside, 2, lock: true - bool outside_color_special_effect - bool outside_obj_enable - num outside_enable_bits, 4 - end - - class MOSAIC < BitField(UInt16) - include Base16 - num obj_mosiac_v_size, 4 - num obj_mosiac_h_size, 4 - num bg_mosiac_v_size, 4 - num bg_mosiac_h_size, 4 - end - - class BLDCNT < BitField(UInt16) - include Base16 - num not_used, 2, lock: true - bool bd_2nd_target_pixel - bool obj_2nd_target_pixel - bool bg3_2nd_target_pixel - bool bg2_2nd_target_pixel - bool bg1_2nd_target_pixel - bool bg0_2nd_target_pixel - num color_special_effect, 2 - bool bd_1st_target_pixel - bool obj_1st_target_pixel - bool bg3_1st_target_pixel - bool bg2_1st_target_pixel - bool bg1_1st_target_pixel - bool bg0_1st_target_pixel - - def is_bg_target(bg : Int, target : Int) : Bool - bit?(value, bg + ((target - 1) * 8)) - end - end - - class BLDALPHA < BitField(UInt16) - include Base16 - num not_used_13_15, 3, lock: true - num evb_coefficient, 5 - num not_used_5_7, 3, lock: true - num eva_coefficient, 5 - end - - class BLDY < BitField(UInt16) - include Base16 - num not_used, 11, lock: true - num evy_coefficient, 5 - end -end diff --git a/src/crab/scheduler.cr b/src/crab/scheduler.cr deleted file mode 100644 index 648918e..0000000 --- a/src/crab/scheduler.cr +++ /dev/null @@ -1,71 +0,0 @@ -class Scheduler - enum EventType - DEFAULT - APUChannel1 - APUChannel2 - APUChannel3 - APUChannel4 - Timer0 - Timer1 - Timer2 - Timer3 - end - - private record Event, cycles : UInt64, proc : Proc(Void), type : EventType - - @events : Deque(Event) = Deque(Event).new 10 - getter cycles : UInt64 = 0 - @next_event : UInt64 = UInt64::MAX - - def schedule(cycles : Int, proc : Proc(Void), type = EventType::DEFAULT) : Nil - self << Event.new @cycles + cycles, proc, type - end - - @[AlwaysInline] - def <<(event : Event) : Nil - idx = @events.bsearch_index { |e| e.cycles > event.cycles } - unless idx.nil? - @events.insert(idx, event) - else - @events << event - end - @next_event = @events[0].cycles - end - - def clear(type : EventType) : Nil - @events.reject! { |event| event.type == type } - end - - def tick(cycles : Int) : Nil - if @cycles + cycles < @next_event - @cycles += cycles - else - cycles.times do - @cycles += 1 - call_current - end - end - end - - def call_current : Nil - loop do - event = @events.first? - if event && @cycles >= event.cycles - event.proc.call - @events.shift - else - if event - @next_event = event.cycles - else - @next_event = UInt64::MAX - end - return - end - end - end - - def fast_forward : Nil - @cycles = @next_event - call_current - end -end diff --git a/src/crab/storage.cr b/src/crab/storage.cr deleted file mode 100644 index e3a30b3..0000000 --- a/src/crab/storage.cr +++ /dev/null @@ -1,66 +0,0 @@ -abstract class Storage - enum Type - EEPROM - SRAM - FLASH - FLASH512 - FLASH1M - - def regex : Regex # don't rely on the 3 digits after this string - /#{self}_V/ - end - - def bytes : Int - case self - in EEPROM then abort "todo: Support EEPROM" - in SRAM then 0x08000 - in FLASH then 0x10000 - in FLASH512 then 0x10000 - in FLASH1M then 0x20000 - end - end - end - - @dirty = false - setter save_path : String = "" - getter memory : Bytes = Bytes.new 0 # implementing class needs to override - - def self.new(rom_path : String) : Storage - save_path = rom_path.rpartition('.')[0] + ".sav" - type = File.open(rom_path, "rb") { |file| find_type(file) } - puts "Backup type could not be identified.".colorize.fore(:red) unless type - puts "Backup type: #{type}, save path: #{save_path}" - storage = case type - in Type::EEPROM then abort "todo: Support EEPROM" - in Type::SRAM, nil then SRAM.new - in Type::FLASH, Type::FLASH512, Type::FLASH1M then Flash.new type - end - storage.save_path = save_path - File.open(save_path, &.read(storage.memory)) if File.exists?(save_path) - storage - end - - def write_save : Nil - if @dirty - File.write(@save_path, @memory) - @dirty = false - end - end - - abstract def [](index : Int) : Byte - - def read_half(index : Int) : HalfWord - 0x0101_u16 * self[index & ~1] - end - - def read_word(index : Int) : Word - 0x01010101_u32 * self[index & ~3] - end - - abstract def []=(index : Int, value : Byte) : Nil - - private def self.find_type(file : File) : Type? - str = file.gets_to_end - Type.each { |type| return type if type.regex.matches?(str) } - end -end diff --git a/src/crab/storage/flash.cr b/src/crab/storage/flash.cr deleted file mode 100644 index a228b40..0000000 --- a/src/crab/storage/flash.cr +++ /dev/null @@ -1,88 +0,0 @@ -class Flash < Storage - @[Flags] - enum State - READY - CMD_1 - CMD_2 - IDENTIFICATION - PREPARE_WRITE - PREPARE_ERASE - SET_BANK - end - - enum Command : Byte - ENTER_IDENT = 0x90 - EXIT_IDENT = 0xF0 - PREPARE_ERASE = 0x80 - ERASE_ALL = 0x10 - ERASE_CHUNK = 0x30 - PREPARE_WRITE = 0xA0 - SET_BANK = 0xB0 - end - - @state = State::READY - @bank = 0_u8 - - def initialize(@type : Type) - @memory = Bytes.new(@type.bytes, 0xFF) - @id = case @type - when Type::FLASH1M then 0x1362 # Sanyo - else 0x1B32 # Panasonic - end - end - - def [](index : Int) : Byte - index &= 0xFFFF - if @state.includes?(State::IDENTIFICATION) && 0 <= index <= 1 - (@id >> (8 * index) & 0xFF).to_u8! - else - @memory[0x10000 * @bank + index] - end - end - - def []=(index : Int, value : Byte) : Nil - index &= 0xFFFF - case @state - when .includes? State::PREPARE_WRITE - @memory[0x10000 * @bank + index] &= value - @dirty = true - @state ^= State::PREPARE_WRITE - when .includes? State::SET_BANK - @bank = value & 1 - @state ^= State::SET_BANK - when .includes? State::READY - if index == 0x5555 && value == 0xAA - @state ^= State::READY - @state |= State::CMD_1 - end - when .includes? State::CMD_1 - if index == 0x2AAA && value == 0x55 - @state ^= State::CMD_1 - @state |= State::CMD_2 - end - when .includes? State::CMD_2 - if index == 0x5555 - case value - when Command::ENTER_IDENT.value then @state |= State::IDENTIFICATION - when Command::EXIT_IDENT.value then @state ^= State::IDENTIFICATION - when Command::PREPARE_ERASE.value then @state |= State::PREPARE_ERASE - when Command::ERASE_ALL.value - if @state.includes? State::PREPARE_ERASE - @memory.size.times { |i| @memory[i] = 0xFF } - @dirty = true - @state ^= State::PREPARE_ERASE - end - when Command::PREPARE_WRITE.value then @state |= State::PREPARE_WRITE - when Command::SET_BANK.value then @state |= State::SET_BANK if @type == Type::FLASH1M - else puts "Unsupported flash command #{hex_str value}" - end - elsif @state.includes?(State::PREPARE_ERASE) && index & 0x0FFF == 0 && value == Command::ERASE_CHUNK.value - 0x1000.times { |i| @memory[0x10000 * @bank + index + i] = 0xFF } - @dirty = true - @state ^= State::PREPARE_ERASE - end - @state ^= State::CMD_2 - @state |= State::READY - end - end -end diff --git a/src/crab/storage/sram.cr b/src/crab/storage/sram.cr deleted file mode 100644 index d04935f..0000000 --- a/src/crab/storage/sram.cr +++ /dev/null @@ -1,12 +0,0 @@ -class SRAM < Storage - @memory = Bytes.new(Type::SRAM.bytes, 0xFF) - - def [](index : Int) : Byte - @memory[index & 0x7FFF] - end - - def []=(index : Int, value : Byte) : Nil - @memory[index & 0x7FFF] = value - @dirty = true - end -end diff --git a/src/crab/thumb/add_offset_to_stack_pointer.cr b/src/crab/thumb/add_offset_to_stack_pointer.cr deleted file mode 100644 index f2d580e..0000000 --- a/src/crab/thumb/add_offset_to_stack_pointer.cr +++ /dev/null @@ -1,13 +0,0 @@ -module THUMB - def thumb_add_offset_to_stack_pointer(instr : Word) : Nil - sign = bit?(instr, 7) - offset = bits(instr, 0..6) - if sign # negative - set_reg(13, @r[13] &- (offset << 2)) - else # positive - set_reg(13, @r[13] &+ (offset << 2)) - end - - step_thumb - end -end diff --git a/src/crab/thumb/add_subtract.cr b/src/crab/thumb/add_subtract.cr deleted file mode 100644 index 380cf9e..0000000 --- a/src/crab/thumb/add_subtract.cr +++ /dev/null @@ -1,21 +0,0 @@ -module THUMB - def thumb_add_subtract(instr : Word) : Nil - imm_flag = bit?(instr, 10) - sub = bit?(instr, 9) - imm = bits(instr, 6..8) - rs = bits(instr, 3..5) - rd = bits(instr, 0..2) - operand = if imm_flag - imm - else - @r[imm] - end - if sub - set_reg(rd, sub(@r[rs], operand, true)) - else - set_reg(rd, add(@r[rs], operand, true)) - end - - step_thumb - end -end diff --git a/src/crab/thumb/alu_operations.cr b/src/crab/thumb/alu_operations.cr deleted file mode 100644 index 72c962b..0000000 --- a/src/crab/thumb/alu_operations.cr +++ /dev/null @@ -1,38 +0,0 @@ -module THUMB - def thumb_alu_operations(instr : Word) : Nil - op = bits(instr, 6..9) - rs = bits(instr, 3..5) - rd = bits(instr, 0..2) - barrel_shifter_carry_out = @cpsr.carry - case op - when 0b0000 then res = set_reg(rd, @r[rd] & @r[rs]) - when 0b0001 then res = set_reg(rd, @r[rd] ^ @r[rs]) - when 0b0010 - res = set_reg(rd, lsl(@r[rd], @r[rs], pointerof(barrel_shifter_carry_out))) - @cpsr.carry = barrel_shifter_carry_out - when 0b0011 - res = set_reg(rd, lsr(@r[rd], @r[rs], false, pointerof(barrel_shifter_carry_out))) - @cpsr.carry = barrel_shifter_carry_out - when 0b0100 - res = set_reg(rd, asr(@r[rd], @r[rs], false, pointerof(barrel_shifter_carry_out))) - @cpsr.carry = barrel_shifter_carry_out - when 0b0101 then res = set_reg(rd, adc(@r[rd], @r[rs], set_conditions: true)) - when 0b0110 then res = set_reg(rd, sbc(@r[rd], @r[rs], set_conditions: true)) - when 0b0111 - res = set_reg(rd, ror(@r[rd], @r[rs], false, pointerof(barrel_shifter_carry_out))) - @cpsr.carry = barrel_shifter_carry_out - when 0b1000 then res = @r[rd] & @r[rs] - when 0b1001 then res = set_reg(rd, sub(0, @r[rs], set_conditions: true)) - when 0b1010 then res = sub(@r[rd], @r[rs], set_conditions: true) - when 0b1011 then res = add(@r[rd], @r[rs], set_conditions: true) - when 0b1100 then res = set_reg(rd, @r[rd] | @r[rs]) - when 0b1101 then res = set_reg(rd, @r[rs] &* @r[rd]) - when 0b1110 then res = set_reg(rd, @r[rd] & ~@r[rs]) - when 0b1111 then res = set_reg(rd, ~@r[rs]) - else raise "Invalid alu op: #{op}" - end - set_neg_and_zero_flags(res) - - step_thumb - end -end diff --git a/src/crab/thumb/conditional_branch.cr b/src/crab/thumb/conditional_branch.cr deleted file mode 100644 index b37c3d3..0000000 --- a/src/crab/thumb/conditional_branch.cr +++ /dev/null @@ -1,11 +0,0 @@ -module THUMB - def thumb_conditional_branch(instr : Word) : Nil - cond = bits(instr, 8..11) - offset = bits(instr, 0..7).to_i8!.to_i32 - if check_cond cond - set_reg(15, @r[15] &+ (offset * 2)) - else - step_thumb - end - end -end diff --git a/src/crab/thumb/hi_reg_branch_exchange.cr b/src/crab/thumb/hi_reg_branch_exchange.cr deleted file mode 100644 index 1fd16d5..0000000 --- a/src/crab/thumb/hi_reg_branch_exchange.cr +++ /dev/null @@ -1,28 +0,0 @@ -module THUMB - def thumb_high_reg_branch_exchange(instr : Word) : Nil - op = bits(instr, 8..9) - h1 = bit?(instr, 7) - h2 = bit?(instr, 6) - rs = bits(instr, 3..5) - rd = bits(instr, 0..2) - - rd += 8 if h1 - rs += 8 if h2 - - # In this group only CMP (Op = 01) sets the CPSR condition codes. - case op - when 0b00 then set_reg(rd, add(@r[rd], @r[rs], false)) - when 0b01 then sub(@r[rd], @r[rs], true) - when 0b10 then set_reg(rd, @r[rs]) - when 0b11 - if bit?(@r[rs], 0) - set_reg(15, @r[rs]) - else - @cpsr.thumb = false - set_reg(15, @r[rs]) - end - end - - step_thumb unless rd == 15 || op == 0b11 - end -end diff --git a/src/crab/thumb/load_address.cr b/src/crab/thumb/load_address.cr deleted file mode 100644 index 703bf9f..0000000 --- a/src/crab/thumb/load_address.cr +++ /dev/null @@ -1,12 +0,0 @@ -module THUMB - def thumb_load_address(instr : Word) : Nil - source = bit?(instr, 11) - rd = bits(instr, 8..10) - word = bits(instr, 0..7) - imm = word << 2 - # Where the PC is used as the source register (SP = 0), bit 1 of the PC is always read as 0. - set_reg(rd, (source ? @r[13] : @r[15] & ~2) &+ imm) - - step_thumb - end -end diff --git a/src/crab/thumb/load_store_halfword.cr b/src/crab/thumb/load_store_halfword.cr deleted file mode 100644 index f9c7b87..0000000 --- a/src/crab/thumb/load_store_halfword.cr +++ /dev/null @@ -1,16 +0,0 @@ -module THUMB - def thumb_load_store_halfword(instr : Word) : Nil - load = bit?(instr, 11) - offset = bits(instr, 6..10) - rb = bits(instr, 3..5) - rd = bits(instr, 0..2) - address = @r[rb] + (offset << 1) - if load - set_reg(rd, @gba.bus.read_half_rotate(address)) - else - @gba.bus[address] = @r[rd].to_u16! - end - - step_thumb - end -end diff --git a/src/crab/thumb/load_store_immediate_offset.cr b/src/crab/thumb/load_store_immediate_offset.cr deleted file mode 100644 index 9ff31bb..0000000 --- a/src/crab/thumb/load_store_immediate_offset.cr +++ /dev/null @@ -1,17 +0,0 @@ -module THUMB - def thumb_load_store_immediate_offset(instr : Word) : Nil - byte_quantity_and_load = bits(instr, 11..12) - offset = bits(instr, 6..10) - rb = bits(instr, 3..5) - rd = bits(instr, 0..2) - base_address = @r[rb] - case byte_quantity_and_load - when 0b00 then @gba.bus[base_address &+ (offset << 2)] = @r[rd] # str - when 0b01 then set_reg(rd, @gba.bus.read_word_rotate(base_address &+ (offset << 2))) # ldr - when 0b10 then @gba.bus[base_address &+ offset] = @r[rd].to_u8! # strb - when 0b11 then set_reg(rd, @gba.bus[base_address &+ offset].to_u32) # ldrb - end - - step_thumb - end -end diff --git a/src/crab/thumb/load_store_register_offset.cr b/src/crab/thumb/load_store_register_offset.cr deleted file mode 100644 index 758a1e2..0000000 --- a/src/crab/thumb/load_store_register_offset.cr +++ /dev/null @@ -1,17 +0,0 @@ -module THUMB - def thumb_load_store_register_offset(instr : Word) : Nil - load_and_byte_quantity = bits(instr, 10..11) - ro = bits(instr, 6..8) - rb = bits(instr, 3..5) - rd = bits(instr, 0..2) - address = @r[rb] &+ @r[ro] - case load_and_byte_quantity - when 0b00 then @gba.bus[address] = @r[rd] # str - when 0b01 then @gba.bus[address] = @r[rd].to_u8! # strb - when 0b10 then set_reg(rd, @gba.bus.read_word_rotate(address)) # ldr - when 0b11 then set_reg(rd, @gba.bus[address].to_u32!) # ldrb - end - - step_thumb - end -end diff --git a/src/crab/thumb/load_store_sign_extended.cr b/src/crab/thumb/load_store_sign_extended.cr deleted file mode 100644 index 8bd6df8..0000000 --- a/src/crab/thumb/load_store_sign_extended.cr +++ /dev/null @@ -1,18 +0,0 @@ -module THUMB - def thumb_load_store_sign_extended(instr : Word) : Nil - hs = bits(instr, 10..11) - ro = bits(instr, 6..8) - rb = bits(instr, 3..5) - rd = bits(instr, 0..2) - address = @r[rb] &+ @r[ro] - case hs - when 0b00 then @gba.bus[address] = @r[rd].to_u16! # strh - when 0b01 then set_reg(rd, @gba.bus[address].to_i8!.to_u32!) # ldsb - when 0b10 then set_reg(rd, @gba.bus.read_half_rotate(address)) # ldrh - when 0b11 then set_reg(rd, @gba.bus.read_half_signed(address)) # ldsh - else raise "Invalid load/store signed extended: #{hs}" - end - - step_thumb - end -end diff --git a/src/crab/thumb/long_branch_link.cr b/src/crab/thumb/long_branch_link.cr deleted file mode 100644 index 1c2323e..0000000 --- a/src/crab/thumb/long_branch_link.cr +++ /dev/null @@ -1,15 +0,0 @@ -module THUMB - def thumb_long_branch_link(instr : Word) : Nil - second_instr = bit?(instr, 11) - offset = bits(instr, 0..10) - if second_instr - temp = @r[15] &- 2 - set_reg(15, @r[14] &+ (offset << 1)) - set_reg(14, temp | 1) - else - offset = (offset << 5).to_i16! >> 5 - set_reg(14, @r[15] &+ (offset.to_u32! << 12)) - step_thumb - end - end -end diff --git a/src/crab/thumb/move_compare_add_subtract.cr b/src/crab/thumb/move_compare_add_subtract.cr deleted file mode 100644 index 5ac7632..0000000 --- a/src/crab/thumb/move_compare_add_subtract.cr +++ /dev/null @@ -1,18 +0,0 @@ -module THUMB - def thumb_move_compare_add_subtract(instr : Word) : Nil - op = bits(instr, 11..12) - rd = bits(instr, 8..10) - offset = bits(instr, 0..7) - case op - when 0b00 - set_reg(rd, offset) - set_neg_and_zero_flags(@r[rd]) - when 0b01 then sub(@r[rd], offset, true) - when 0b10 then set_reg(rd, add(@r[rd], offset, true)) - when 0b11 then set_reg(rd, sub(@r[rd], offset, true)) - else raise "Invalid move/compare/add/subtract op: #{op}" - end - - step_thumb - end -end diff --git a/src/crab/thumb/move_shifted_register.cr b/src/crab/thumb/move_shifted_register.cr deleted file mode 100644 index e6ae27c..0000000 --- a/src/crab/thumb/move_shifted_register.cr +++ /dev/null @@ -1,19 +0,0 @@ -module THUMB - def thumb_move_shifted_register(instr : Word) : Nil - op = bits(instr, 11..12) - offset = bits(instr, 6..10) - rs = bits(instr, 3..5) - rd = bits(instr, 0..2) - carry_out = @cpsr.carry - case op - when 0b00 then set_reg(rd, lsl(@r[rs], offset, pointerof(carry_out))) - when 0b01 then set_reg(rd, lsr(@r[rs], offset, true, pointerof(carry_out))) - when 0b10 then set_reg(rd, asr(@r[rs], offset, true, pointerof(carry_out))) - else raise "Invalid shifted register op: #{op}" - end - set_neg_and_zero_flags(@r[rd]) - @cpsr.carry = carry_out - - step_thumb - end -end diff --git a/src/crab/thumb/multiple_load_store.cr b/src/crab/thumb/multiple_load_store.cr deleted file mode 100644 index 467ac80..0000000 --- a/src/crab/thumb/multiple_load_store.cr +++ /dev/null @@ -1,38 +0,0 @@ -module THUMB - def thumb_multiple_load_store(instr : Word) : Nil - load = bit?(instr, 11) - rb = bits(instr, 8..10) - list = bits(instr, 0..7) - address = @r[rb] - unless list == 0 - if load # ldmia - 8.times do |idx| - if bit?(list, idx) - set_reg(idx, @gba.bus.read_word(address)) - address &+= 4 - end - end - else # stmia - base_addr = nil - 8.times do |idx| - if bit?(list, idx) - @gba.bus[address] = @r[idx] - base_addr = address if rb == idx - address &+= 4 - end - end - @gba.bus[base_addr] = address if base_addr && first_set_bit(list) != rb # rb is written after first store - end - set_reg(rb, address) - else # https://github.com/jsmolka/gba-suite/blob/0e32e15c6241e6dc20851563ba88f4656ac50936/thumb/memory.asm#L459 - if load - set_reg(15, @gba.bus.read_word(address)) - else - @gba.bus[address] = @r[15] &+ 2 - end - set_reg(rb, address &+ 0x40) - end - - step_thumb unless list == 0 && load - end -end diff --git a/src/crab/thumb/pc_relative_load.cr b/src/crab/thumb/pc_relative_load.cr deleted file mode 100644 index 84d2d96..0000000 --- a/src/crab/thumb/pc_relative_load.cr +++ /dev/null @@ -1,9 +0,0 @@ -module THUMB - def thumb_pc_relative_load(instr : Word) : Nil - imm = bits(instr, 0..7) - rd = bits(instr, 8..10) - set_reg(rd, @gba.bus.read_word((@r[15] & ~2) &+ (imm << 2))) - - step_thumb - end -end diff --git a/src/crab/thumb/push_pop_registers.cr b/src/crab/thumb/push_pop_registers.cr deleted file mode 100644 index e43811a..0000000 --- a/src/crab/thumb/push_pop_registers.cr +++ /dev/null @@ -1,34 +0,0 @@ -module THUMB - def thumb_push_pop_registers(instr : Word) : Nil - pop = bit?(instr, 11) - pclr = bit?(instr, 8) - list = bits(instr, 0..7) - address = @r[13] - if pop - 8.times do |idx| - if bit?(list, idx) - set_reg(idx, @gba.bus.read_word(address)) - address &+= 4 - end - end - if pclr - set_reg(15, @gba.bus.read_word(address)) - address &+= 4 - end - else - if pclr - address &-= 4 - @gba.bus[address] = @r[14] - end - 7.downto(0).each do |idx| - if bit?(list, idx) - address &-= 4 - @gba.bus[address] = @r[idx] - end - end - end - set_reg(13, address) - - step_thumb unless pop && pclr - end -end diff --git a/src/crab/thumb/software_interrupt.cr b/src/crab/thumb/software_interrupt.cr deleted file mode 100644 index bc21d1b..0000000 --- a/src/crab/thumb/software_interrupt.cr +++ /dev/null @@ -1,10 +0,0 @@ -module THUMB - def thumb_software_interrupt(instr : Word) : Nil - lr = @r[15] - 2 - switch_mode CPU::Mode::SVC - set_reg(14, lr) - @cpsr.irq_disable = true - @cpsr.thumb = false - set_reg(15, 0x08) - end -end diff --git a/src/crab/thumb/sp_relative_load_store.cr b/src/crab/thumb/sp_relative_load_store.cr deleted file mode 100644 index c8ff151..0000000 --- a/src/crab/thumb/sp_relative_load_store.cr +++ /dev/null @@ -1,15 +0,0 @@ -module THUMB - def thumb_sp_relative_load_store(instr : Word) : Nil - load = bit?(instr, 11) - rd = bits(instr, 8..10) - word = bits(instr, 0..7) - address = @r[13] &+ (word << 2) - if load - set_reg(rd, @gba.bus.read_word_rotate(address)) - else - @gba.bus[address] = @r[rd] - end - - step_thumb - end -end diff --git a/src/crab/thumb/thumb.cr b/src/crab/thumb/thumb.cr deleted file mode 100644 index b812016..0000000 --- a/src/crab/thumb/thumb.cr +++ /dev/null @@ -1,56 +0,0 @@ -module THUMB - def thumb_execute(instr : Word) : Nil - thumb_lut[instr >> 8].call instr - end - - def fill_thumb_lut - lut = Slice(Proc(Word, Nil)).new 256, ->thumb_unimplemented(Word) - 256.times do |idx| - if idx & 0b11110000 == 0b11110000 - lut[idx] = ->thumb_long_branch_link(Word) - elsif idx & 0b11111000 == 0b11100000 - lut[idx] = ->thumb_unconditional_branch(Word) - elsif idx & 0b11111111 == 0b11011111 - lut[idx] = ->thumb_software_interrupt(Word) - elsif idx & 0b11110000 == 0b11010000 - lut[idx] = ->thumb_conditional_branch(Word) - elsif idx & 0b11110000 == 0b11000000 - lut[idx] = ->thumb_multiple_load_store(Word) - elsif idx & 0b11110110 == 0b10110100 - lut[idx] = ->thumb_push_pop_registers(Word) - elsif idx & 0b11111111 == 0b10110000 - lut[idx] = ->thumb_add_offset_to_stack_pointer(Word) - elsif idx & 0b11110000 == 0b10100000 - lut[idx] = ->thumb_load_address(Word) - elsif idx & 0b11110000 == 0b10010000 - lut[idx] = ->thumb_sp_relative_load_store(Word) - elsif idx & 0b11110000 == 0b10000000 - lut[idx] = ->thumb_load_store_halfword(Word) - elsif idx & 0b11100000 == 0b01100000 - lut[idx] = ->thumb_load_store_immediate_offset(Word) - elsif idx & 0b11110010 == 0b01010010 - lut[idx] = ->thumb_load_store_sign_extended(Word) - elsif idx & 0b11110010 == 0b01010000 - lut[idx] = ->thumb_load_store_register_offset(Word) - elsif idx & 0b11111000 == 0b01001000 - lut[idx] = ->thumb_pc_relative_load(Word) - elsif idx & 0b11111100 == 0b01000100 - lut[idx] = ->thumb_high_reg_branch_exchange(Word) - elsif idx & 0b11111100 == 0b01000000 - lut[idx] = ->thumb_alu_operations(Word) - elsif idx & 0b11100000 == 0b00100000 - lut[idx] = ->thumb_move_compare_add_subtract(Word) - elsif idx & 0b11111000 == 0b00011000 - lut[idx] = ->thumb_add_subtract(Word) - elsif idx & 0b11100000 == 0b00000000 - lut[idx] = ->thumb_move_shifted_register(Word) - end - end - lut - end - - def thumb_unimplemented(instr : Word) : Nil - puts "Unimplemented instruction: #{hex_str instr.to_u16}" - exit 1 - end -end diff --git a/src/crab/thumb/unconditional_branch.cr b/src/crab/thumb/unconditional_branch.cr deleted file mode 100644 index 7cbde8c..0000000 --- a/src/crab/thumb/unconditional_branch.cr +++ /dev/null @@ -1,7 +0,0 @@ -module THUMB - def thumb_unconditional_branch(instr : Word) : Nil - offset = bits(instr, 0..10) - offset = (offset << 5).to_i16! >> 4 - set_reg(15, @r[15] &+ offset) - end -end diff --git a/src/crab/timer.cr b/src/crab/timer.cr deleted file mode 100644 index 80b96f9..0000000 --- a/src/crab/timer.cr +++ /dev/null @@ -1,96 +0,0 @@ -class Timer - PERIODS = [1, 64, 256, 1024] - - @interrupt_flags : Array(Proc(Nil)) - - def initialize(@gba : GBA) - @tmcnt = Array(Reg::TMCNT).new 4 { Reg::TMCNT.new 0 } # control registers - @tmd = Array(UInt16).new 4, 0 # reload values - @tm = Array(UInt16).new 4, 0 # counted values - @cycle_enabled = Array(UInt64).new 4, 0 # cycle that the timer was enabled - @events = Array(Proc(Nil)).new 4 { |i| overflow i } # overflow closures for each timer - @event_types = [Scheduler::EventType::Timer0, Scheduler::EventType::Timer1, - Scheduler::EventType::Timer2, Scheduler::EventType::Timer3] - @interrupt_flags = [->{ @gba.interrupts.reg_if.timer0 = true }, ->{ @gba.interrupts.reg_if.timer1 = true }, - ->{ @gba.interrupts.reg_if.timer2 = true }, ->{ @gba.interrupts.reg_if.timer3 = true }] - end - - def overflow(num : Int) : Proc(Nil) - ->{ - @tm[num] = @tmd[num] - @cycle_enabled[num] = @gba.scheduler.cycles - if num < 3 && @tmcnt[num + 1].cascade && @tmcnt[num + 1].enable - @tm[num + 1] &+= 1 - @events[num + 1].call if @tm[num + 1] == 0 # call overflow handler if cascaded timer overflowed - end - @gba.apu.timer_overflow num if num <= 1 # alert apu of timer 0-1 overflow - if @tmcnt[num].irq_enable # set interupt flag for this timer - @interrupt_flags[num].call - @gba.interrupts.schedule_interrupt_check - end - @gba.scheduler.schedule cycles_until_overflow(num), @events[num], @event_types[num] unless @tmcnt[num].cascade - } - end - - def cycles_until_overflow(num : Int) : Int32 - PERIODS[@tmcnt[num].frequency] * (0x10000 - @tm[num]) - end - - def get_current_tm(num : Int) : UInt16 - if @tmcnt[num].enable && !@tmcnt[num].cascade - elapsed = @gba.scheduler.cycles - @cycle_enabled[num] - @tm[num] &+ elapsed // PERIODS[@tmcnt[num].frequency] - else - @tm[num] - end - end - - def update_tm(num : Int) : Nil - @tm[num] = get_current_tm(num) - @cycle_enabled[num] = @gba.scheduler.cycles - end - - def read_io(io_addr : Int) : UInt8 - num = (io_addr & 0xF) // 4 - tmcnt = @tmcnt[num] - value = if bit?(io_addr, 1) - tmcnt.value - else - get_current_tm(num) - end - value >>= 8 if bit?(io_addr, 0) - value.to_u8! - end - - def write_io(io_addr : Int, value : UInt8) : Nil - num = (io_addr & 0xF) // 4 - high = bit?(io_addr, 0) - mask = 0xFF00_u16 - if high - mask >>= 8 - value = value.to_u16 << 8 - end - if bit?(io_addr, 1) - unless high - update_tm(num) - tmcnt = @tmcnt[num] - was_enabled = tmcnt.enable - was_cascade = tmcnt.cascade - tmcnt.value = (tmcnt.value & mask) | value - if tmcnt.enable - if tmcnt.cascade - @gba.scheduler.clear @event_types[num] - elsif !was_enabled || was_cascade # enabled or no longer cascade - @cycle_enabled[num] = @gba.scheduler.cycles - @tm[num] = @tmd[num] if !was_enabled - @gba.scheduler.schedule cycles_until_overflow(num), @events[num], @event_types[num] - end - elsif was_enabled # disabled - @gba.scheduler.clear(@event_types[num]) - end - end - else - @tmd[num] = (@tmd[num] & mask) | value - end - end -end diff --git a/src/crab/types.cr b/src/crab/types.cr deleted file mode 100644 index e86cc82..0000000 --- a/src/crab/types.cr +++ /dev/null @@ -1,36 +0,0 @@ -alias Byte = UInt8 -alias HalfWord = UInt16 -alias Word = UInt32 -alias Words = Slice(UInt32) -record BGR16, value : UInt16 do # xBBBBBGGGGGRRRRR -# Create a new BGR16 struct with the given values. Trucates at 5 bits. - def initialize(blue : Number, green : Number, red : Number) - @value = (blue <= 0x1F ? blue.to_u16 : 0x1F_u16) << 10 | - (green <= 0x1F ? green.to_u16 : 0x1F_u16) << 5 | - (red <= 0x1F ? red.to_u16 : 0x1F_u16) - end - - def blue : UInt16 - bits(value, 0xA..0xE) - end - - def green : UInt16 - bits(value, 0x5..0x9) - end - - def red : UInt16 - bits(value, 0x0..0x4) - end - - def +(other : BGR16) : BGR16 - BGR16.new(blue + other.blue, green + other.green, red + other.red) - end - - def -(other : BGR16) : BGR16 - BGR16.new(blue.to_i - other.blue, green.to_i - other.green, red.to_i - other.red) - end - - def *(operand : Number) : BGR16 - BGR16.new(blue * operand, green * operand, red * operand) - end -end