mirror of
https://github.com/mattrberry/crab.git
synced 2025-01-29 20:35:13 +01:00
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:
parent
b8a7fc1428
commit
26d747801d
151 changed files with 12181 additions and 4239 deletions
|
@ -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
|
||||
|
|
45
src/crab.cr
45
src/crab.cr
|
@ -1,8 +1,11 @@
|
|||
require "colorize"
|
||||
require "option_parser"
|
||||
|
||||
require "bitfield"
|
||||
require "sdl"
|
||||
|
||||
require "./crab/common/*"
|
||||
require "./crab/gb"
|
||||
require "./crab/gba"
|
||||
|
||||
Colorize.on_tty_only!
|
||||
|
@ -13,9 +16,45 @@ module Crab
|
|||
extend self
|
||||
|
||||
def run
|
||||
gba = GBA.new ARGV[0], ARGV[1]
|
||||
gba.post_init
|
||||
gba.run
|
||||
rom = nil
|
||||
bios = nil
|
||||
fifo = false
|
||||
pink = false
|
||||
sync = true
|
||||
headless = false
|
||||
|
||||
OptionParser.parse do |parser|
|
||||
parser.banner = "#{"crab".colorize.bold} - An accurate and readable Game Boy (Color) (Advance) emulator"
|
||||
parser.separator
|
||||
parser.separator("Usage: bin/crab [BIOS] ROM")
|
||||
parser.separator
|
||||
parser.on("-h", "--help", "Show the help message") do
|
||||
puts parser
|
||||
exit
|
||||
end
|
||||
parser.on("--fifo", "Enable FIFO rendering") { fifo = true }
|
||||
parser.on("--pink", "Set the 2-bit DMG color theme to pink") { pink = true }
|
||||
parser.on("--no-sync", "Disable audio syncing") { sync = false }
|
||||
parser.on("--headless", "Don't open window or play audio") { headless = true }
|
||||
parser.unknown_args do |args|
|
||||
case args.size
|
||||
when 1 then rom = args[0]
|
||||
when 2 then bios, rom = args[0], args[1]
|
||||
end
|
||||
abort parser if rom.nil?
|
||||
abort "GBA ROMs need a bios provided" if rom.not_nil!.ends_with?(".gba") && bios.nil?
|
||||
end
|
||||
end
|
||||
|
||||
if rom.not_nil!.ends_with?(".gba")
|
||||
gba = GBA::GBA.new bios.not_nil!, rom.not_nil!
|
||||
gba.post_init
|
||||
gba.run
|
||||
else
|
||||
gb = GB::GB.new bios, rom.not_nil!, fifo, sync, headless
|
||||
gb.post_init
|
||||
gb.run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
202
src/crab/apu.cr
202
src/crab/apu.cr
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
175
src/crab/bus.cr
175
src/crab/bus.cr
|
@ -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
|
|
@ -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
|
6
src/crab/common/bindings.cr
Normal file
6
src/crab/common/bindings.cr
Normal 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
|
|
@ -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 %}
|
298
src/crab/cpu.cr
298
src/crab/cpu.cr
|
@ -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
|
|
@ -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
|
|
@ -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
|
154
src/crab/dma.cr
154
src/crab/dma.cr
|
@ -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
179
src/crab/gb/apu.cr
Normal 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
|
100
src/crab/gb/audio/abstract_channels.cr
Normal file
100
src/crab/gb/audio/abstract_channels.cr
Normal 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
|
150
src/crab/gb/audio/channel1.cr
Normal file
150
src/crab/gb/audio/channel1.cr
Normal 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
|
100
src/crab/gb/audio/channel2.cr
Normal file
100
src/crab/gb/audio/channel2.cr
Normal 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
|
130
src/crab/gb/audio/channel3.cr
Normal file
130
src/crab/gb/audio/channel3.cr
Normal 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
|
103
src/crab/gb/audio/channel4.cr
Normal file
103
src/crab/gb/audio/channel4.cr
Normal 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
110
src/crab/gb/cartridge.cr
Normal 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
173
src/crab/gb/cpu.cr
Normal 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
85
src/crab/gb/display.cr
Normal 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
276
src/crab/gb/fifo_ppu.cr
Normal 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
84
src/crab/gb/gb.cr
Normal 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
98
src/crab/gb/interrupts.cr
Normal 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
81
src/crab/gb/joypad.cr
Normal 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
68
src/crab/gb/mbc/mbc1.cr
Normal 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
45
src/crab/gb/mbc/mbc2.cr
Normal 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
54
src/crab/gb/mbc/mbc3.cr
Normal 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
42
src/crab/gb/mbc/mbc5.cr
Normal 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
33
src/crab/gb/mbc/rom.cr
Normal 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
300
src/crab/gb/memory.cr
Normal 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
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
469
src/crab/gb/ppu.cr
Normal 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
177
src/crab/gb/scanline_ppu.cr
Normal 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
90
src/crab/gb/scheduler.cr
Normal 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
94
src/crab/gb/timer.cr
Normal 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
|
505
src/crab/gb/update_opcodes.cr
Normal file
505
src/crab/gb/update_opcodes.cr
Normal 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
43
src/crab/gb/util.cr
Normal 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
|
|
@ -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
199
src/crab/gba/apu.cr
Normal 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
|
100
src/crab/gba/apu/abstract_channels.cr
Normal file
100
src/crab/gba/apu/abstract_channels.cr
Normal 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
|
149
src/crab/gba/apu/channel1.cr
Normal file
149
src/crab/gba/apu/channel1.cr
Normal 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
|
101
src/crab/gba/apu/channel2.cr
Normal file
101
src/crab/gba/apu/channel2.cr
Normal 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
|
129
src/crab/gba/apu/channel3.cr
Normal file
129
src/crab/gba/apu/channel3.cr
Normal 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
|
103
src/crab/gba/apu/channel4.cr
Normal file
103
src/crab/gba/apu/channel4.cr
Normal 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
|
58
src/crab/gba/apu/dma_channels.cr
Normal file
58
src/crab/gba/apu/dma_channels.cr
Normal 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
100
src/crab/gba/arm/arm.cr
Normal 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
|
53
src/crab/gba/arm/block_data_transfer.cr
Normal file
53
src/crab/gba/arm/block_data_transfer.cr
Normal 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
|
10
src/crab/gba/arm/branch.cr
Normal file
10
src/crab/gba/arm/branch.cr
Normal 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
|
13
src/crab/gba/arm/branch_exchange.cr
Normal file
13
src/crab/gba/arm/branch_exchange.cr
Normal 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
|
108
src/crab/gba/arm/data_processing.cr
Normal file
108
src/crab/gba/arm/data_processing.cr
Normal 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
|
59
src/crab/gba/arm/halfword_data_transfer_imm.cr
Normal file
59
src/crab/gba/arm/halfword_data_transfer_imm.cr
Normal 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
|
58
src/crab/gba/arm/halfword_data_transfer_reg.cr
Normal file
58
src/crab/gba/arm/halfword_data_transfer_reg.cr
Normal 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
|
17
src/crab/gba/arm/multiply.cr
Normal file
17
src/crab/gba/arm/multiply.cr
Normal 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
|
29
src/crab/gba/arm/multiply_long.cr
Normal file
29
src/crab/gba/arm/multiply_long.cr
Normal 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
|
45
src/crab/gba/arm/psr_transfer.cr
Normal file
45
src/crab/gba/arm/psr_transfer.cr
Normal 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
|
21
src/crab/gba/arm/single_data_swap.cr
Normal file
21
src/crab/gba/arm/single_data_swap.cr
Normal 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
|
62
src/crab/gba/arm/single_data_transfer.cr
Normal file
62
src/crab/gba/arm/single_data_transfer.cr
Normal 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
|
11
src/crab/gba/arm/software_interrupt.cr
Normal file
11
src/crab/gba/arm/software_interrupt.cr
Normal 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
177
src/crab/gba/bus.cr
Normal 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
34
src/crab/gba/cartridge.cr
Normal 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
300
src/crab/gba/cpu.cr
Normal 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
105
src/crab/gba/debugger.cr
Normal 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
122
src/crab/gba/display.cr
Normal 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
156
src/crab/gba/dma.cr
Normal 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
87
src/crab/gba/gba.cr
Normal 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
|
65
src/crab/gba/interrupts.cr
Normal file
65
src/crab/gba/interrupts.cr
Normal 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
105
src/crab/gba/keypad.cr
Normal 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
91
src/crab/gba/mmio.cr
Normal 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
35
src/crab/gba/pipeline.cr
Normal 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
564
src/crab/gba/ppu.cr
Normal 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
258
src/crab/gba/reg.cr
Normal 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
73
src/crab/gba/scheduler.cr
Normal 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
68
src/crab/gba/storage.cr
Normal 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
|
90
src/crab/gba/storage/flash.cr
Normal file
90
src/crab/gba/storage/flash.cr
Normal 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
|
14
src/crab/gba/storage/sram.cr
Normal file
14
src/crab/gba/storage/sram.cr
Normal 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
|
15
src/crab/gba/thumb/add_offset_to_stack_pointer.cr
Normal file
15
src/crab/gba/thumb/add_offset_to_stack_pointer.cr
Normal 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
|
23
src/crab/gba/thumb/add_subtract.cr
Normal file
23
src/crab/gba/thumb/add_subtract.cr
Normal 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
|
40
src/crab/gba/thumb/alu_operations.cr
Normal file
40
src/crab/gba/thumb/alu_operations.cr
Normal 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
|
13
src/crab/gba/thumb/conditional_branch.cr
Normal file
13
src/crab/gba/thumb/conditional_branch.cr
Normal 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
Loading…
Add table
Reference in a new issue