Moved CryBoy into the crab codebase, along with all associated changes

The next step will be abstracting features between the two, like the
scheduler, apu, display, and anything else than can be abstracted. Once
this is abstracted, I can go back to trying to fix the CryBoy APU that
started having trouble once it was on the scheduler. This will ensure
consistency between the two projects.
This commit is contained in:
Matthew Berry 2021-05-06 00:29:04 -07:00
parent b8a7fc1428
commit 26d747801d
151 changed files with 12181 additions and 4239 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 %}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

179
src/crab/gb/apu.cr Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

110
src/crab/gb/cartridge.cr Normal file
View file

@ -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

173
src/crab/gb/cpu.cr Normal file
View file

@ -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

85
src/crab/gb/display.cr Normal file
View file

@ -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

276
src/crab/gb/fifo_ppu.cr Normal file
View file

@ -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

84
src/crab/gb/gb.cr Normal file
View file

@ -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

98
src/crab/gb/interrupts.cr Normal file
View file

@ -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

81
src/crab/gb/joypad.cr Normal file
View file

@ -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

68
src/crab/gb/mbc/mbc1.cr Normal file
View file

@ -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

45
src/crab/gb/mbc/mbc2.cr Normal file
View file

@ -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

54
src/crab/gb/mbc/mbc3.cr Normal file
View file

@ -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

42
src/crab/gb/mbc/mbc5.cr Normal file
View file

@ -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

33
src/crab/gb/mbc/rom.cr Normal file
View file

@ -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

300
src/crab/gb/memory.cr Normal file
View file

@ -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

4100
src/crab/gb/opcodes.cr Normal file

File diff suppressed because it is too large Load diff

469
src/crab/gb/ppu.cr Normal file
View file

@ -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

177
src/crab/gb/scanline_ppu.cr Normal file
View file

@ -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

90
src/crab/gb/scheduler.cr Normal file
View file

@ -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

94
src/crab/gb/timer.cr Normal file
View file

@ -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

View file

@ -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

43
src/crab/gb/util.cr Normal file
View file

@ -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

View file

@ -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

199
src/crab/gba/apu.cr Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

100
src/crab/gba/arm/arm.cr Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

177
src/crab/gba/bus.cr Normal file
View file

@ -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

34
src/crab/gba/cartridge.cr Normal file
View file

@ -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

300
src/crab/gba/cpu.cr Normal file
View file

@ -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

105
src/crab/gba/debugger.cr Normal file
View file

@ -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

122
src/crab/gba/display.cr Normal file
View file

@ -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

156
src/crab/gba/dma.cr Normal file
View file

@ -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

87
src/crab/gba/gba.cr Normal file
View file

@ -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

View file

@ -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

105
src/crab/gba/keypad.cr Normal file
View file

@ -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

91
src/crab/gba/mmio.cr Normal file
View file

@ -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

35
src/crab/gba/pipeline.cr Normal file
View file

@ -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

564
src/crab/gba/ppu.cr Normal file
View file

@ -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

258
src/crab/gba/reg.cr Normal file
View file

@ -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

73
src/crab/gba/scheduler.cr Normal file
View file

@ -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

68
src/crab/gba/storage.cr Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more