basic apu support (copy from CryBoy w/ slight modification), scheduler short-circuit, scheduler clear

This commit is contained in:
Matthew Berry 2020-12-05 12:25:55 -08:00
parent 70913a4ee8
commit ac8319b19c
10 changed files with 776 additions and 11 deletions

182
src/crab/apu.cr Normal file
View file

@ -0,0 +1,182 @@
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
@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
@left_enable = true
@left_volume = 7_u8
@right_enable = true
@right_volume = 7_u8
# @nr51 : UInt8 = 0x00
@nr51 : UInt8 = 0xFF
@audiospec : LibSDL::AudioSpec
@obtained_spec : LibSDL::AudioSpec
def initialize(@gba : GBA)
@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 @gba
@channel2 = Channel2.new @gba
@channel3 = Channel3.new @gba
@channel4 = Channel4.new @gba
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 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
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
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
@gba.scheduler.schedule SAMPLE_PERIOD, ->get_sample
end
def read_io(index : Int) : UInt8
case index
when @channel1 then @channel1.read_io index
when @channel2 then @channel2.read_io index
when @channel3 then @channel3.read_io index
when @channel4 then @channel4.read_io 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 write_io(index : Int, value : UInt8) : Nil
return unless @sound_enabled || index == 0x84 || Channel3::WAVE_RAM_RANGE.includes?(index)
case index
when @channel1 then @channel1.write_io index, value
when @channel2 then @channel2.write_io index, value
when @channel3 then @channel3.write_io index, value
when @channel4 then @channel4.write_io 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 0x84
if value & 0x80 == 0 && @sound_enabled
(0xFF10..0xFF25).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
end
end
end

View file

@ -0,0 +1,98 @@
# 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) : Bool
# 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 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

144
src/crab/apu/channel1.cr Normal file
View file

@ -0,0 +1,144 @@
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 = 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
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 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 raise "Reading from invalid Channel1 register: #{hex_str index.to_u16}"
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
else raise "Writing to invalid Channel1 register: #{hex_str index.to_u16}"
end
end
end

94
src/crab/apu/channel2.cr Normal file
View file

@ -0,0 +1,94 @@
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 = 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
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 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 raise "Reading from invalid Channel2 register: #{hex_str index.to_u16}"
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 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
else raise "Writing to invalid Channel2 register: #{hex_str index.to_u16}"
end
end
end

123
src/crab/apu/channel3.cr Normal file
View file

@ -0,0 +1,123 @@
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 = 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 * 4
end
def schedule_reload(frequency_timer : UInt32) : Nil
@gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel3
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 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_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 write_io(index : Int, value : UInt8) : Nil
case index
when 0x70
@dac_enabled = value & 0x80 > 0
@enabled = false if !@dac_enabled
when 0x72
@length_load = value
# Internal values
@length_counter = 0x100 - @length_load
when 0x73
@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 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 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

96
src/crab/apu/channel4.cr Normal file
View file

@ -0,0 +1,96 @@
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
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 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 raise "Reading from invalid Channel4 register: #{hex_str index.to_u16}"
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 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
else raise "Writing to invalid Channel4 register: #{hex_str index.to_u16}"
end
end
end

View file

@ -6,6 +6,8 @@ class CPU
include ARM
include THUMB
CLOCK_SPEED = 2**24
enum Mode : UInt32
USR = 0b10000
FIQ = 0b10001

View file

@ -9,6 +9,7 @@ require "./interrupts"
require "./cpu"
require "./display"
require "./ppu"
require "./apu"
require "./debugger"
class GBA
@ -21,6 +22,7 @@ class GBA
getter! cpu : CPU
getter! display : Display
getter! ppu : PPU
getter! apu : APU
getter! debugger : Debugger
def initialize(@bios_path : String, rom_path : String)
@ -41,6 +43,7 @@ class GBA
@cpu = CPU.new self
@display = Display.new
@ppu = PPU.new self
@apu = APU.new self
@debugger = Debugger.new self
end

View file

@ -22,6 +22,8 @@ class MMIO
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 >= 0x200 && io_addr <= 0x203) || (io_addr >= 0x208 && io_addr <= 0x209)
@gba.interrupts.read_io io_addr
elsif io_addr >= 0x130 && io_addr <= 0x133
@ -40,6 +42,8 @@ class MMIO
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 >= 0x200 && io_addr <= 0x203) || (io_addr >= 0x208 && io_addr <= 0x209)
@gba.interrupts.write_io io_addr, value
elsif io_addr >= 0x130 && io_addr <= 0x133

View file

@ -1,15 +1,20 @@
class Scheduler
private record Event, cycles : UInt64, proc : Proc(Void)
enum EventType
DEFAULT
APUChannel1
APUChannel2
APUChannel3
APUChannel4
end
private record Event, cycles : UInt64, proc : Proc(Void), type : EventType
@events : Deque(Event) = Deque(Event).new 10
@cycles : UInt64 = 0
@next_event : UInt64 = UInt64::MAX
def schedule(cycles : Int, proc : Proc(Void)) : Nil
self << Event.new @cycles + cycles, proc
end
def schedule(cycles : Int, &block : ->)
self << Event.new @cycles + cycles, block
def schedule(cycles : Int, proc : Proc(Void), type = EventType::DEFAULT) : Nil
self << Event.new @cycles + cycles, proc, type
end
@[AlwaysInline]
@ -20,12 +25,21 @@ class Scheduler
else
@events << event
end
@next_event = @events[0].cycles
end
def clear(type : EventType) : Nil
@events.delete_if { |event| event.type == type }
end
def tick(cycles : Int) : Nil
cycles.times do
@cycles += 1
call_current
if @cycles + cycles < @next_event
@cycles += cycles
else
cycles.times do
@cycles += 1
call_current
end
end
end
@ -36,7 +50,12 @@ class Scheduler
event.proc.call
@events.shift
else
break
if event
@next_event = event.cycles
else
@next_event = UInt64::MAX
end
return
end
end
end