Implement event-based scheduler instead of lock-step components

This commit is contained in:
Connor McLaughlin 2020-01-24 14:53:40 +10:00
parent 624888e131
commit 1b9609ef61
26 changed files with 1089 additions and 520 deletions

View File

@ -59,34 +59,34 @@ void AudioStream::Shutdown()
m_output_paused = true;
}
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_samples)
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames)
{
m_buffer_mutex.lock();
EnsureBuffer();
Buffer& buffer = m_buffers[m_first_free_buffer];
*buffer_ptr = buffer.data.data() + buffer.write_position;
*num_samples = m_buffer_size - buffer.write_position;
*buffer_ptr = buffer.data.data() + (buffer.write_position * m_channels);
*num_frames = m_buffer_size - buffer.write_position;
}
void AudioStream::WriteSamples(const SampleType* samples, u32 num_samples)
void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
{
u32 remaining_samples = num_samples;
u32 remaining_frames = num_frames;
std::unique_lock<std::mutex> lock(m_buffer_mutex);
while (remaining_samples > 0)
while (remaining_frames > 0)
{
EnsureBuffer();
Buffer& buffer = m_buffers[m_first_free_buffer];
const u32 to_this_buffer = std::min(m_buffer_size - buffer.write_position, remaining_samples);
const u32 to_this_buffer = std::min(m_buffer_size - buffer.write_position, remaining_frames);
const u32 copy_count = to_this_buffer * m_channels;
std::memcpy(&buffer.data[buffer.write_position * m_channels], samples, copy_count * sizeof(SampleType));
samples += copy_count;
std::memcpy(&buffer.data[buffer.write_position * m_channels], frames, copy_count * sizeof(SampleType));
frames += copy_count;
remaining_samples -= to_this_buffer;
remaining_frames -= to_this_buffer;
buffer.write_position += to_this_buffer;
// End of the buffer?
@ -102,11 +102,11 @@ void AudioStream::WriteSamples(const SampleType* samples, u32 num_samples)
}
}
void AudioStream::EndWrite(u32 num_samples)
void AudioStream::EndWrite(u32 num_frames)
{
Buffer& buffer = m_buffers[m_first_free_buffer];
DebugAssert((buffer.write_position + num_samples) <= m_buffer_size);
buffer.write_position += num_samples;
DebugAssert((buffer.write_position + num_frames) <= m_buffer_size);
buffer.write_position += num_frames;
// End of the buffer?
if (buffer.write_position == m_buffer_size)

View File

@ -37,9 +37,9 @@ public:
void Shutdown();
void BeginWrite(SampleType** buffer_ptr, u32* num_samples);
void WriteSamples(const SampleType* samples, u32 num_samples);
void EndWrite(u32 num_samples);
void BeginWrite(SampleType** buffer_ptr, u32* num_frames);
void WriteFrames(const SampleType* frames, u32 num_frames);
void EndWrite(u32 num_frames);
static std::unique_ptr<AudioStream> CreateNullAudioStream();

View File

@ -65,6 +65,8 @@ add_library(core
system.h
timers.cpp
timers.h
timing_event.cpp
timing_event.h
types.h
)

View File

@ -1,6 +1,6 @@
#include "cdrom.h"
#include "common/log.h"
#include "common/cd_image.h"
#include "common/log.h"
#include "common/state_wrapper.h"
#include "dma.h"
#include "imgui.h"
@ -22,6 +22,10 @@ void CDROM::Initialize(System* system, DMA* dma, InterruptController* interrupt_
m_dma = dma;
m_interrupt_controller = interrupt_controller;
m_spu = spu;
m_command_event =
m_system->CreateTimingEvent("CDROM Command Event", 1, 1, std::bind(&CDROM::ExecuteCommand, this), false);
m_drive_event = m_system->CreateTimingEvent("CDROM Drive Event", 1, 1,
std::bind(&CDROM::ExecuteDrive, this, std::placeholders::_2), false);
}
void CDROM::Reset()
@ -35,9 +39,9 @@ void CDROM::Reset()
void CDROM::SoftReset()
{
m_command = Command::None;
m_command_event->Deactivate();
m_drive_state = DriveState::Idle;
m_command_remaining_ticks = 0;
m_drive_remaining_ticks = 0;
m_drive_event->Deactivate();
m_status.bits = 0;
m_secondary_status.bits = 0;
m_mode.bits = 0;
@ -85,8 +89,6 @@ bool CDROM::DoState(StateWrapper& sw)
{
sw.Do(&m_command);
sw.Do(&m_drive_state);
sw.Do(&m_command_remaining_ticks);
sw.Do(&m_drive_remaining_ticks);
sw.Do(&m_status.bits);
sw.Do(&m_secondary_status.bits);
sw.Do(&m_mode.bits);
@ -124,10 +126,8 @@ bool CDROM::DoState(StateWrapper& sw)
if (sw.IsReading())
{
if (HasPendingCommand())
m_system->SetDowncount(m_command_remaining_ticks);
if (!IsDriveIdle())
m_system->SetDowncount(m_drive_remaining_ticks);
UpdateCommandEvent();
m_drive_event->SetState(!IsDriveIdle());
// load up media if we had something in there before
if (m_media && !m_media->Seek(media_lba))
@ -361,8 +361,8 @@ void CDROM::WriteRegister(u32 offset, u8 value)
{
if (HasPendingAsyncInterrupt())
DeliverAsyncInterrupt();
else if (HasPendingCommand())
m_system->SetDowncount(m_command_remaining_ticks);
else
UpdateCommandEvent();
}
// Bit 6 clears the parameter FIFO.
@ -443,6 +443,7 @@ void CDROM::DeliverAsyncInterrupt()
m_pending_async_interrupt = 0;
UpdateInterruptRequest();
UpdateStatusRegister();
UpdateCommandEvent();
}
void CDROM::SendACKAndStat()
@ -520,73 +521,11 @@ TickCount CDROM::GetTicksForSeek() const
return ticks;
}
void CDROM::Execute(TickCount ticks)
{
if (HasPendingCommand() && !HasPendingInterrupt())
{
m_command_remaining_ticks -= ticks;
if (m_command_remaining_ticks <= 0)
ExecuteCommand();
else
m_system->SetDowncount(m_command_remaining_ticks);
}
if (m_drive_state != DriveState::Idle)
{
m_drive_remaining_ticks -= ticks;
if (m_drive_remaining_ticks <= 0)
{
switch (m_drive_state)
{
case DriveState::SpinningUp:
DoSpinUpComplete();
break;
case DriveState::SeekingPhysical:
case DriveState::SeekingLogical:
DoSeekComplete();
break;
case DriveState::Pausing:
DoPauseComplete();
break;
case DriveState::Stopping:
DoStopComplete();
break;
case DriveState::ReadingID:
DoIDRead();
break;
case DriveState::ReadingTOC:
DoTOCRead();
break;
case DriveState::Reading:
case DriveState::Playing:
DoSectorRead();
break;
case DriveState::Idle:
default:
break;
}
}
else
{
m_system->SetDowncount(m_drive_remaining_ticks);
}
}
}
void CDROM::BeginCommand(Command command)
{
m_system->Synchronize();
m_command = command;
m_command_remaining_ticks = GetAckDelayForCommand();
m_system->SetDowncount(m_command_remaining_ticks);
m_command_event->SetDowncount(GetAckDelayForCommand());
UpdateCommandEvent();
UpdateStatusRegister();
}
@ -595,7 +534,7 @@ void CDROM::EndCommand()
m_param_fifo.Clear();
m_command = Command::None;
m_command_remaining_ticks = 0;
m_command_event->Deactivate();
UpdateStatusRegister();
}
@ -645,7 +584,7 @@ void CDROM::ExecuteCommand()
SendACKAndStat();
m_drive_state = DriveState::ReadingID;
m_drive_remaining_ticks = 18000;
m_drive_event->Schedule(18000);
}
EndCommand();
@ -664,7 +603,7 @@ void CDROM::ExecuteCommand()
SendACKAndStat();
m_drive_state = DriveState::ReadingTOC;
m_drive_remaining_ticks = MASTER_CLOCK / 2; // half a second
m_drive_event->Schedule(MASTER_CLOCK / 2); // half a second
}
EndCommand();
@ -767,12 +706,12 @@ void CDROM::ExecuteCommand()
case Command::Pause:
{
const bool was_reading = (m_drive_state == DriveState::Reading || m_drive_state == DriveState::Playing);
const TickCount pause_time = was_reading ? (m_mode.double_speed ? 2000000 : 1000000) : 7000;
Log_DebugPrintf("CDROM pause command");
SendACKAndStat();
m_drive_state = DriveState::Pausing;
m_drive_remaining_ticks = was_reading ? (m_mode.double_speed ? 2000000 : 1000000) : 7000;
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->Schedule(pause_time);
EndCommand();
return;
@ -781,12 +720,12 @@ void CDROM::ExecuteCommand()
case Command::Stop:
{
const bool was_motor_on = m_secondary_status.motor_on;
const TickCount stop_time = was_motor_on ? (m_mode.double_speed ? 25000000 : 13000000) : 7000;
Log_DebugPrintf("CDROM stop command");
SendACKAndStat();
m_drive_state = DriveState::Stopping;
m_drive_remaining_ticks = was_motor_on ? (m_mode.double_speed ? 25000000 : 13000000) : 7000;
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->Schedule(stop_time);
EndCommand();
return;
@ -801,8 +740,7 @@ void CDROM::ExecuteCommand()
m_mode.bits = 0;
m_drive_state = DriveState::SpinningUp;
m_drive_remaining_ticks = 80000;
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->Schedule(80000);
EndCommand();
return;
@ -821,8 +759,7 @@ void CDROM::ExecuteCommand()
SendACKAndStat();
m_drive_state = DriveState::SpinningUp;
m_drive_remaining_ticks = 80000;
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->Schedule(80000);
}
EndCommand();
@ -1011,7 +948,62 @@ void CDROM::ExecuteTestCommand(u8 subcommand)
}
}
void CDROM::BeginReading()
void CDROM::UpdateCommandEvent()
{
// if there's a pending interrupt, we can't execute the command yet
// so deactivate it until the interrupt is acknowledged
if (!HasPendingCommand() || HasPendingInterrupt())
{
m_command_event->Deactivate();
return;
}
else if (HasPendingCommand())
{
m_command_event->Activate();
}
}
void CDROM::ExecuteDrive(TickCount ticks_late)
{
switch (m_drive_state)
{
case DriveState::SpinningUp:
DoSpinUpComplete();
break;
case DriveState::SeekingPhysical:
case DriveState::SeekingLogical:
DoSeekComplete(ticks_late);
break;
case DriveState::Pausing:
DoPauseComplete();
break;
case DriveState::Stopping:
DoStopComplete();
break;
case DriveState::ReadingID:
DoIDRead();
break;
case DriveState::ReadingTOC:
DoTOCRead();
break;
case DriveState::Reading:
case DriveState::Playing:
DoSectorRead();
break;
case DriveState::Idle:
default:
break;
}
}
void CDROM::BeginReading(TickCount ticks_late)
{
Log_DebugPrintf("Starting reading");
if (m_setloc_pending)
@ -1026,12 +1018,13 @@ void CDROM::BeginReading()
// TODO: Should the sector buffer be cleared here?
m_sector_buffer.clear();
const TickCount ticks = GetTicksForRead();
m_drive_state = DriveState::Reading;
m_drive_remaining_ticks = GetTicksForRead();
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->SetInterval(ticks);
m_drive_event->Schedule(ticks - ticks_late);
}
void CDROM::BeginPlaying(u8 track_bcd)
void CDROM::BeginPlaying(u8 track_bcd, TickCount ticks_late)
{
Log_DebugPrintf("Starting playing CDDA track %x", track_bcd);
m_last_cdda_report_frame_nibble = 0xFF;
@ -1063,9 +1056,10 @@ void CDROM::BeginPlaying(u8 track_bcd)
// TODO: Should the sector buffer be cleared here?
m_sector_buffer.clear();
const TickCount ticks = GetTicksForRead();
m_drive_state = DriveState::Playing;
m_drive_remaining_ticks = GetTicksForRead();
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->SetInterval(ticks);
m_drive_event->Schedule(ticks - ticks_late);
}
void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_seek)
@ -1088,8 +1082,7 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see
m_sector_buffer.clear();
m_drive_state = logical ? DriveState::SeekingLogical : DriveState::SeekingPhysical;
m_drive_remaining_ticks = seek_time;
m_system->SetDowncount(m_drive_remaining_ticks);
m_drive_event->SetIntervalAndSchedule(seek_time);
// Read sub-q early.. this is because we're not reading sectors while seeking.
// Fixes music looping in Spyro.
@ -1101,6 +1094,7 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see
void CDROM::DoSpinUpComplete()
{
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
m_secondary_status.motor_on = true;
@ -1109,10 +1103,11 @@ void CDROM::DoSpinUpComplete()
SetAsyncInterrupt(Interrupt::INT2);
}
void CDROM::DoSeekComplete()
void CDROM::DoSeekComplete(TickCount ticks_late)
{
const bool logical = (m_drive_state == DriveState::SeekingLogical);
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
m_secondary_status.ClearActiveBits();
m_sector_buffer.clear();
@ -1145,11 +1140,11 @@ void CDROM::DoSeekComplete()
// INT2 is not sent on play/read
if (m_read_after_seek)
{
BeginReading();
BeginReading(ticks_late);
}
else if (m_play_after_seek)
{
BeginPlaying(m_play_track_number_bcd);
BeginPlaying(m_play_track_number_bcd, ticks_late);
}
else
{
@ -1175,6 +1170,7 @@ void CDROM::DoPauseComplete()
{
Log_DebugPrintf("Pause complete");
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
m_secondary_status.ClearActiveBits();
m_sector_buffer.clear();
@ -1187,6 +1183,7 @@ void CDROM::DoStopComplete()
{
Log_DebugPrintf("Stop complete");
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
m_secondary_status.ClearActiveBits();
m_secondary_status.motor_on = false;
m_sector_buffer.clear();
@ -1204,6 +1201,7 @@ void CDROM::DoIDRead()
Log_DebugPrintf("ID read complete");
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
m_secondary_status.ClearActiveBits();
m_secondary_status.motor_on = true;
m_sector_buffer.clear();
@ -1219,6 +1217,7 @@ void CDROM::DoTOCRead()
{
Log_DebugPrintf("TOC read complete");
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
m_async_response_fifo.Clear();
m_async_response_fifo.Push(m_secondary_status.bits);
SetAsyncInterrupt(Interrupt::INT2);
@ -1254,6 +1253,7 @@ void CDROM::DoSectorRead()
m_secondary_status.ClearActiveBits();
m_drive_state = DriveState::Idle;
m_drive_event->Deactivate();
return;
}
}
@ -1290,9 +1290,6 @@ void CDROM::DoSectorRead()
Log_DevPrintf("Skipping sector %u [%02u:%02u:%02u] due to invalid subchannel Q", m_media->GetPositionOnDisc() - 1,
pos.minute, pos.second, pos.frame);
}
m_drive_remaining_ticks += GetTicksForRead();
m_system->SetDowncount(m_drive_remaining_ticks);
}
void CDROM::ProcessDataSectorHeader(const u8* raw_sector, bool set_valid)
@ -1711,7 +1708,7 @@ void CDROM::DrawDebugWindow()
if (HasPendingCommand())
{
ImGui::TextColored(active_color, "Command: 0x%02X (%d ticks remaining)", static_cast<u8>(m_command),
m_command_remaining_ticks);
m_command_event->IsActive() ? m_command_event->GetTicksUntilNextExecution() : 0);
}
else
{
@ -1725,7 +1722,8 @@ void CDROM::DrawDebugWindow()
else
{
ImGui::TextColored(active_color, "Drive: %s (%d ticks remaining)",
drive_state_names[static_cast<u8>(m_drive_state)], m_drive_remaining_ticks);
drive_state_names[static_cast<u8>(m_drive_state)],
m_drive_event->IsActive() ? m_drive_event->GetTicksUntilNextExecution() : 0);
}
ImGui::Text("Interrupt Enable Register: 0x%02X", m_interrupt_enable_register);

View File

@ -12,6 +12,7 @@
class StateWrapper;
class System;
class TimingEvent;
class DMA;
class InterruptController;
class SPU;
@ -36,8 +37,6 @@ public:
void WriteRegister(u32 offset, u8 value);
void DMARead(u32* words, u32 word_count);
void Execute(TickCount ticks);
// Render statistics debug window.
void DrawDebugWindow();
@ -198,10 +197,12 @@ private:
void EndCommand(); // also updates status register
void ExecuteCommand();
void ExecuteTestCommand(u8 subcommand);
void BeginReading();
void BeginPlaying(u8 track_bcd);
void UpdateCommandEvent();
void ExecuteDrive(TickCount ticks_late);
void BeginReading(TickCount ticks_late = 0);
void BeginPlaying(u8 track_bcd, TickCount ticks_late = 0);
void DoSpinUpComplete();
void DoSeekComplete();
void DoSeekComplete(TickCount ticks_late);
void DoPauseComplete();
void DoStopComplete();
void DoIDRead();
@ -219,11 +220,11 @@ private:
InterruptController* m_interrupt_controller = nullptr;
SPU* m_spu = nullptr;
std::unique_ptr<CDImage> m_media;
std::unique_ptr<TimingEvent> m_command_event;
std::unique_ptr<TimingEvent> m_drive_event;
Command m_command = Command::None;
DriveState m_drive_state = DriveState::Idle;
TickCount m_command_remaining_ticks = 0;
TickCount m_drive_remaining_ticks = 0;
StatusRegister m_status = {};
SecondaryStatusRegister m_secondary_status = {};

View File

@ -82,6 +82,7 @@
<ClCompile Include="spu.cpp" />
<ClCompile Include="system.cpp" />
<ClCompile Include="timers.cpp" />
<ClCompile Include="timing_event.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="analog_controller.h" />
@ -121,6 +122,7 @@
<ClInclude Include="spu.h" />
<ClInclude Include="system.h" />
<ClInclude Include="timers.h" />
<ClInclude Include="timing_event.h" />
<ClInclude Include="types.h" />
</ItemGroup>
<ItemGroup>

View File

@ -39,6 +39,7 @@
<ClCompile Include="controller.cpp" />
<ClCompile Include="analog_controller.cpp" />
<ClCompile Include="host_display.cpp" />
<ClCompile Include="timing_event.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -79,6 +80,7 @@
<ClInclude Include="sio.h" />
<ClInclude Include="controller.h" />
<ClInclude Include="analog_controller.h" />
<ClInclude Include="timing_event.h" />
</ItemGroup>
<ItemGroup>
<None Include="cpu_core.inl" />

View File

@ -51,11 +51,8 @@ public:
ALWAYS_INLINE void ResetPendingTicks() { m_pending_ticks = 0; }
ALWAYS_INLINE void AddPendingTicks(TickCount ticks) { m_pending_ticks += ticks; }
ALWAYS_INLINE void SetDowncount(TickCount downcount)
{
m_downcount = (downcount < m_downcount) ? downcount : m_downcount;
}
ALWAYS_INLINE void ResetDowncount() { m_downcount = MAX_SLICE_SIZE; }
ALWAYS_INLINE TickCount GetDowncount() const { return m_downcount; }
ALWAYS_INLINE void SetDowncount(TickCount downcount) { m_downcount = downcount; }
// Sets the PC and flushes the pipeline.
void SetPC(u32 new_pc);

View File

@ -1,8 +1,9 @@
#include "dma.h"
#include "common/log.h"
#include "bus.h"
#include "cdrom.h"
#include "common/log.h"
#include "common/state_wrapper.h"
#include "common/string_util.h"
#include "gpu.h"
#include "interrupt_controller.h"
#include "mdec.h"
@ -25,14 +26,28 @@ void DMA::Initialize(System* system, Bus* bus, InterruptController* interrupt_co
m_spu = spu;
m_mdec = mdec;
m_transfer_buffer.resize(32);
for (u32 i = 0; i < NUM_CHANNELS; i++)
{
m_state[i].transfer_event = system->CreateTimingEvent(
StringUtil::StdStringFromFormat("DMA%u Transfer", i), 1, 1,
std::bind(&DMA::TransferChannel, this, static_cast<Channel>(i), std::placeholders::_2), false);
}
}
void DMA::Reset()
{
m_transfer_in_progress = false;
std::memset(&m_state, 0, sizeof(m_state));
m_DPCR.bits = 0x07654321;
m_DICR.bits = 0;
for (u32 i = 0; i < NUM_CHANNELS; i++)
{
ChannelState& cs = m_state[i];
cs.base_address = 0;
cs.block_control.bits = 0;
cs.channel_control.bits = 0;
cs.request = false;
cs.transfer_event->Deactivate();
}
}
bool DMA::DoState(StateWrapper& sw)
@ -43,7 +58,6 @@ bool DMA::DoState(StateWrapper& sw)
sw.Do(&cs.base_address);
sw.Do(&cs.block_control.bits);
sw.Do(&cs.channel_control.bits);
sw.Do(&cs.transfer_ticks);
sw.Do(&cs.request);
}
@ -52,13 +66,11 @@ bool DMA::DoState(StateWrapper& sw)
if (sw.IsReading())
{
m_transfer_min_ticks = std::numeric_limits<TickCount>::max();
for (const ChannelState& cs : m_state)
for (u32 i = 0; i < NUM_CHANNELS; i++)
{
if (cs.transfer_ticks > 0)
m_transfer_min_ticks = std::min(m_transfer_min_ticks, cs.transfer_ticks);
m_state[i].transfer_event->Deactivate();
UpdateChannelTransferEvent(static_cast<Channel>(i));
}
m_system->SetDowncount(m_transfer_min_ticks);
}
return !sw.HasError();
@ -126,7 +138,7 @@ void DMA::WriteRegister(u32 offset, u32 value)
{
Log_TracePrintf("DMA channel %u block control <- 0x%08X", channel_index, value);
state.block_control.bits = value;
QueueTransferChannel(static_cast<Channel>(channel_index));
UpdateChannelTransferEvent(static_cast<Channel>(channel_index));
return;
}
@ -135,7 +147,7 @@ void DMA::WriteRegister(u32 offset, u32 value)
state.channel_control.bits = (state.channel_control.bits & ~ChannelState::ChannelControl::WRITE_MASK) |
(value & ChannelState::ChannelControl::WRITE_MASK);
Log_TracePrintf("DMA channel %u channel control <- 0x%08X", channel_index, state.channel_control.bits);
QueueTransferChannel(static_cast<Channel>(channel_index));
UpdateChannelTransferEvent(static_cast<Channel>(channel_index));
return;
}
@ -151,7 +163,8 @@ void DMA::WriteRegister(u32 offset, u32 value)
{
Log_TracePrintf("DPCR <- 0x%08X", value);
m_DPCR.bits = value;
QueueTransfer();
for (u32 i = 0; i < NUM_CHANNELS; i++)
UpdateChannelTransferEvent(static_cast<Channel>(i));
return;
}
@ -180,7 +193,7 @@ void DMA::SetRequest(Channel channel, bool request)
cs.request = request;
if (request)
QueueTransfer();
UpdateChannelTransferEvent(channel);
}
TickCount DMA::GetTransferDelay(Channel channel) const
@ -188,6 +201,10 @@ TickCount DMA::GetTransferDelay(Channel channel) const
const ChannelState& cs = m_state[static_cast<u32>(channel)];
switch (channel)
{
case Channel::MDECin:
case Channel::MDECout:
return 1;
case Channel::SPU:
{
if (cs.channel_control.sync_mode == SyncMode::Request)
@ -198,7 +215,7 @@ TickCount DMA::GetTransferDelay(Channel channel) const
break;
default:
return 1;
return 0;
}
}
@ -217,9 +234,6 @@ bool DMA::CanTransferChannel(Channel channel) const
if (cs.channel_control.sync_mode == SyncMode::Manual && !cs.channel_control.start_trigger)
return false;
if (cs.transfer_ticks > 0)
return false;
return true;
}
@ -244,72 +258,34 @@ void DMA::UpdateIRQ()
}
}
void DMA::QueueTransferChannel(Channel channel)
void DMA::UpdateChannelTransferEvent(Channel channel)
{
ChannelState& cs = m_state[static_cast<u32>(channel)];
if (cs.transfer_ticks > 0 || !CanTransferChannel(channel))
if (!CanTransferChannel(channel))
{
cs.transfer_event->Deactivate();
return;
}
if (cs.transfer_event->IsActive())
return;
const TickCount ticks = GetTransferDelay(channel);
if (ticks == 0)
{
// immediate transfer
TransferChannel(channel);
TransferChannel(channel, 0);
return;
}
if (!m_transfer_in_progress)
m_system->Synchronize();
cs.transfer_ticks = ticks;
m_transfer_min_ticks = std::min(m_transfer_min_ticks, ticks);
m_system->SetDowncount(ticks);
cs.transfer_event->SetPeriodAndSchedule(ticks);
}
void DMA::QueueTransfer()
{
for (u32 i = 0; i < NUM_CHANNELS; i++)
QueueTransferChannel(static_cast<Channel>(i));
}
void DMA::Execute(TickCount ticks)
{
m_transfer_min_ticks -= ticks;
if (m_transfer_min_ticks > 0)
{
m_system->SetDowncount(m_transfer_min_ticks);
return;
}
DebugAssert(!m_transfer_in_progress);
m_transfer_in_progress = true;
// keep going until all transfers are done. one channel can start others (e.g. MDEC)
m_transfer_min_ticks = std::numeric_limits<TickCount>::max();
for (u32 i = 0; i < NUM_CHANNELS; i++)
{
const Channel channel = static_cast<Channel>(i);
if (m_state[i].transfer_ticks <= 0)
continue;
m_state[i].transfer_ticks -= ticks;
if (CanTransferChannel(channel))
{
TransferChannel(channel);
}
else
{
m_transfer_min_ticks = std::min(m_transfer_min_ticks, ticks);
}
}
m_system->SetDowncount(m_transfer_min_ticks);
m_transfer_in_progress = false;
}
void DMA::TransferChannel(Channel channel)
void DMA::TransferChannel(Channel channel, TickCount ticks_late)
{
ChannelState& cs = m_state[static_cast<u32>(channel)];
cs.transfer_event->Deactivate();
const bool copy_to_device = cs.channel_control.copy_to_device;
// start/trigger bit is cleared on beginning of transfer
@ -416,7 +392,6 @@ void DMA::TransferChannel(Channel channel)
}
// start/busy bit is cleared on end of transfer
cs.transfer_ticks = 0;
cs.channel_control.enable_busy = false;
if (m_DICR.IsIRQEnabled(channel))
{

View File

@ -2,11 +2,13 @@
#include "common/bitfield.h"
#include "types.h"
#include <array>
#include <memory>
#include <vector>
class StateWrapper;
class System;
class TimingEvent;
class Bus;
class InterruptController;
class GPU;
@ -49,8 +51,6 @@ public:
// changing interfaces
void SetGPU(GPU* gpu) { m_gpu = gpu; }
void Execute(TickCount ticks);
private:
static constexpr PhysicalMemoryAddress BASE_ADDRESS_MASK = UINT32_C(0x00FFFFFF);
static constexpr PhysicalMemoryAddress ADDRESS_MASK = UINT32_C(0x001FFFFC);
@ -72,10 +72,8 @@ private:
bool CanRunAnyChannels() const;
void UpdateIRQ();
void QueueTransferChannel(Channel channel);
void QueueTransfer();
void TransferChannel(Channel channel);
void UpdateChannelTransferEvent(Channel channel);
void TransferChannel(Channel channel, TickCount ticks_late);
// from device -> memory
void TransferDeviceToMemory(Channel channel, u32 address, u32 increment, u32 word_count);
@ -95,6 +93,7 @@ private:
struct ChannelState
{
std::unique_ptr<TimingEvent> transfer_event;
u32 base_address;
union BlockControl
@ -131,7 +130,6 @@ private:
static constexpr u32 WRITE_MASK = 0b01110001'01110111'00000111'00000011;
} channel_control;
TickCount transfer_ticks = 0;
bool request = false;
};
@ -207,7 +205,4 @@ private:
master_flag = master_enable && ((((bits >> 16) & u32(0b1111111)) & ((bits >> 24) & u32(0b1111111))) != 0);
}
} m_DICR = {};
TickCount m_transfer_min_ticks = 0;
bool m_transfer_in_progress = false;
};

View File

@ -1,6 +1,6 @@
#include "gpu.h"
#include "common/log.h"
#include "common/heap_array.h"
#include "common/log.h"
#include "common/state_wrapper.h"
#include "dma.h"
#include "host_interface.h"
@ -27,6 +27,8 @@ bool GPU::Initialize(HostDisplay* host_display, System* system, DMA* dma, Interr
m_interrupt_controller = interrupt_controller;
m_timers = timers;
m_force_progressive_scan = m_system->GetSettings().gpu_force_progressive_scan;
m_tick_event =
m_system->CreateTimingEvent("GPU Tick", 1, 1, std::bind(&GPU::Execute, this, std::placeholders::_1), true);
return true;
}
@ -63,6 +65,9 @@ void GPU::SoftReset()
m_draw_mode.SetTextureWindow(0);
UpdateGPUSTAT();
UpdateCRTCConfig();
m_tick_event->Deactivate();
UpdateSliceTicks();
}
bool GPU::DoState(StateWrapper& sw)
@ -273,6 +278,11 @@ void GPU::DMAWrite(const u32* words, u32 word_count)
}
}
void GPU::Synchronize()
{
m_tick_event->InvokeEarly();
}
void GPU::UpdateCRTCConfig()
{
static constexpr std::array<TickCount, 8> dot_clock_dividers = {{10, 8, 5, 4, 7, 7, 7, 7}};
@ -357,31 +367,31 @@ void GPU::UpdateCRTCConfig()
// Ensure the numbers are sane, and not due to a misconfigured active display range.
cs.display_aspect_ratio = (std::isnormal(display_ratio) && display_ratio != 0.0f) ? display_ratio : (4.0f / 3.0f);
m_tick_event->SetInterval(cs.horizontal_total);
}
UpdateSliceTicks();
static TickCount GPUTicksToSystemTicks(u32 gpu_ticks)
{
// convert to master clock, rounding up as we want to overshoot not undershoot
return (gpu_ticks * 7 + 10) / 11;
}
void GPU::UpdateSliceTicks()
{
// the next event is at the end of the next scanline
#if 1
TickCount ticks_until_next_event;
if (m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start)
ticks_until_next_event = m_crtc_state.horizontal_display_start - m_crtc_state.current_tick_in_scanline;
else if (m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_end)
ticks_until_next_event = m_crtc_state.horizontal_display_end - m_crtc_state.current_tick_in_scanline;
else
ticks_until_next_event = m_crtc_state.horizontal_total - m_crtc_state.current_tick_in_scanline;
#else
// or at vblank. this will depend on the timer config..
const TickCount ticks_until_next_event =
((m_crtc_state.vertical_total - m_crtc_state.current_scanline) * m_crtc_state.horizontal_total) -
m_crtc_state.current_tick_in_scanline;
#endif
// figure out how many GPU ticks until the next vblank
const u32 lines_until_vblank =
(m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end ?
(m_crtc_state.vertical_total - m_crtc_state.current_scanline + m_crtc_state.vertical_display_end) :
(m_crtc_state.vertical_display_end - m_crtc_state.current_scanline));
const u32 ticks_until_vblank =
lines_until_vblank * m_crtc_state.horizontal_total - m_crtc_state.current_tick_in_scanline;
const u32 ticks_until_hblank =
(m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end) ?
(m_crtc_state.horizontal_total - m_crtc_state.current_tick_in_scanline + m_crtc_state.horizontal_display_end) :
(m_crtc_state.horizontal_display_end - m_crtc_state.current_tick_in_scanline);
// convert to master clock, rounding up as we want to overshoot not undershoot
const TickCount system_ticks = (ticks_until_next_event * 7 + 10) / 11;
m_system->SetDowncount(system_ticks);
m_tick_event->Schedule(GPUTicksToSystemTicks(ticks_until_vblank));
m_tick_event->SetPeriod(GPUTicksToSystemTicks(ticks_until_hblank));
}
void GPU::Execute(TickCount ticks)
@ -393,13 +403,76 @@ void GPU::Execute(TickCount ticks)
m_crtc_state.fractional_ticks = temp % 7;
}
while (m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_total)
if (m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_total)
{
m_crtc_state.current_tick_in_scanline -= m_crtc_state.horizontal_total;
m_crtc_state.current_scanline++;
// short path when we execute <1 line.. this shouldn't occur often.
const bool old_hblank = m_crtc_state.in_hblank;
const bool new_hblank = m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start ||
m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end;
if (!old_hblank && new_hblank && m_timers->IsUsingExternalClock(HBLANK_TIMER_INDEX))
m_timers->AddTicks(HBLANK_TIMER_INDEX, 1);
UpdateSliceTicks();
return;
}
u32 lines_to_draw = m_crtc_state.current_tick_in_scanline / m_crtc_state.horizontal_total;
m_crtc_state.current_tick_in_scanline %= m_crtc_state.horizontal_total;
#if 0
Log_WarningPrintf("Old line: %u, new line: %u, drawing %u", m_crtc_state.current_scanline,
m_crtc_state.current_scanline + lines_to_draw, lines_to_draw);
#endif
const bool old_hblank = m_crtc_state.in_hblank;
const bool new_hblank = m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start ||
m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end;
m_crtc_state.in_hblank = new_hblank;
if (m_timers->IsUsingExternalClock(HBLANK_TIMER_INDEX))
{
const u32 hblank_timer_ticks = BoolToUInt32(!old_hblank) + BoolToUInt32(new_hblank) + (lines_to_draw - 1);
m_timers->AddTicks(HBLANK_TIMER_INDEX, static_cast<TickCount>(hblank_timer_ticks));
}
while (lines_to_draw > 0)
{
const u32 lines_to_draw_this_loop =
std::min(lines_to_draw, m_crtc_state.vertical_total - m_crtc_state.current_scanline);
const u32 prev_scanline = m_crtc_state.current_scanline;
m_crtc_state.current_scanline += lines_to_draw_this_loop;
DebugAssert(m_crtc_state.current_scanline <= m_crtc_state.vertical_total);
lines_to_draw -= lines_to_draw_this_loop;
// clear the vblank flag if the beam would pass through the display area
if (prev_scanline < m_crtc_state.vertical_display_start &&
m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end)
{
m_crtc_state.in_vblank = false;
}
const bool new_vblank = m_crtc_state.current_scanline < m_crtc_state.vertical_display_start ||
m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end;
if (m_crtc_state.in_vblank != new_vblank)
{
m_crtc_state.in_vblank = new_vblank;
if (new_vblank)
{
static u32 x = 0;
Log_DebugPrintf("Now in v-blank %u ticks %u hblanks", m_system->GetGlobalTickCounter() - x);
x = m_system->GetGlobalTickCounter();
m_interrupt_controller->InterruptRequest(InterruptController::IRQ::VBLANK);
// flush any pending draws and "scan out" the image
FlushRender();
UpdateDisplay();
m_system->IncrementFrameNumber();
}
m_timers->SetGate(HBLANK_TIMER_INDEX, new_vblank);
}
// past the end of vblank?
if (m_crtc_state.current_scanline >= m_crtc_state.vertical_total)
if (m_crtc_state.current_scanline == m_crtc_state.vertical_total)
{
// start the new frame
m_crtc_state.current_scanline = 0;
@ -415,25 +488,6 @@ void GPU::Execute(TickCount ticks)
m_GPUSTAT.interlaced_field = false;
}
}
const bool new_vblank = m_crtc_state.current_scanline < m_crtc_state.vertical_display_start ||
m_crtc_state.current_scanline >= m_crtc_state.vertical_display_end;
if (m_crtc_state.in_vblank != new_vblank)
{
m_crtc_state.in_vblank = new_vblank;
if (new_vblank)
{
Log_DebugPrintf("Now in v-blank");
m_interrupt_controller->InterruptRequest(InterruptController::IRQ::VBLANK);
// flush any pending draws and "scan out" the image
FlushRender();
UpdateDisplay();
m_system->IncrementFrameNumber();
}
m_timers->SetGate(HBLANK_TIMER_INDEX, new_vblank);
}
// alternating even line bit in 240-line mode
@ -447,16 +501,6 @@ void GPU::Execute(TickCount ticks)
m_GPUSTAT.drawing_even_line =
ConvertToBoolUnchecked((m_crtc_state.regs.Y + m_crtc_state.current_scanline) & u32(1));
}
}
const bool new_hblank = m_crtc_state.current_tick_in_scanline < m_crtc_state.horizontal_display_start ||
m_crtc_state.current_tick_in_scanline >= m_crtc_state.horizontal_display_end;
if (m_crtc_state.in_hblank != new_hblank)
{
m_crtc_state.in_hblank = new_hblank;
if (new_hblank && m_timers->IsUsingExternalClock(HBLANK_TIMER_INDEX))
m_timers->AddTicks(HBLANK_TIMER_INDEX, 1);
}
UpdateSliceTicks();
}
@ -560,18 +604,30 @@ void GPU::WriteGP1(u32 value)
case 0x06: // Set horizontal display range
{
m_crtc_state.regs.horizontal_display_range = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK;
Log_DebugPrintf("Horizontal display range <- 0x%08X", m_crtc_state.regs.horizontal_display_range);
const u32 new_value = param & CRTCState::Regs::HORIZONTAL_DISPLAY_RANGE_MASK;
Log_DebugPrintf("Horizontal display range <- 0x%08X", new_value);
if (m_crtc_state.regs.horizontal_display_range != new_value)
{
m_tick_event->InvokeEarly(true);
m_crtc_state.regs.horizontal_display_range = new_value;
UpdateCRTCConfig();
}
}
break;
case 0x07: // Set display start address
{
m_crtc_state.regs.vertical_display_range = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK;
Log_DebugPrintf("Vertical display range <- 0x%08X", m_crtc_state.regs.vertical_display_range);
const u32 new_value = param & CRTCState::Regs::VERTICAL_DISPLAY_RANGE_MASK;
Log_DebugPrintf("Vertical display range <- 0x%08X", new_value);
if (m_crtc_state.regs.vertical_display_range != new_value)
{
m_tick_event->InvokeEarly(true);
m_crtc_state.regs.vertical_display_range = new_value;
UpdateCRTCConfig();
}
}
break;
case 0x08: // Set display mode
@ -590,17 +646,23 @@ void GPU::WriteGP1(u32 value)
};
const GP1_08h dm{param};
m_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1;
m_GPUSTAT.vertical_resolution = dm.vertical_resolution;
m_GPUSTAT.pal_mode = dm.pal_mode;
m_GPUSTAT.display_area_color_depth_24 = dm.display_area_color_depth;
m_GPUSTAT.vertical_interlace = dm.vertical_interlace;
m_GPUSTAT.horizontal_resolution_2 = dm.horizontal_resolution_2;
m_GPUSTAT.reverse_flag = dm.reverse_flag;
GPUSTAT new_GPUSTAT{m_GPUSTAT.bits};
new_GPUSTAT.horizontal_resolution_1 = dm.horizontal_resolution_1;
new_GPUSTAT.vertical_resolution = dm.vertical_resolution;
new_GPUSTAT.pal_mode = dm.pal_mode;
new_GPUSTAT.display_area_color_depth_24 = dm.display_area_color_depth;
new_GPUSTAT.vertical_interlace = dm.vertical_interlace;
new_GPUSTAT.horizontal_resolution_2 = dm.horizontal_resolution_2;
new_GPUSTAT.reverse_flag = dm.reverse_flag;
Log_DebugPrintf("Set display mode <- 0x%08X", dm.bits);
if (m_GPUSTAT.bits != new_GPUSTAT.bits)
{
m_tick_event->InvokeEarly(true);
m_GPUSTAT.bits = new_GPUSTAT.bits;
UpdateCRTCConfig();
}
}
break;
case 0x09: // Allow texture disable

View File

@ -15,6 +15,7 @@ class StateWrapper;
class HostDisplay;
class System;
class TimingEvent;
class DMA;
class InterruptController;
class Timers;
@ -127,12 +128,12 @@ public:
void DMARead(u32* words, u32 word_count);
void DMAWrite(const u32* words, u32 word_count);
// Synchronizes the CRTC, updating the hblank timer.
void Synchronize();
// Recompile shaders/recreate framebuffers when needed.
virtual void UpdateSettings();
// Ticks for hblank/vblank.
void Execute(TickCount ticks);
// gpu_hw_d3d11.cpp
static std::unique_ptr<GPU> CreateHardwareD3D11Renderer();
@ -299,6 +300,9 @@ protected:
// Updates dynamic bits in GPUSTAT (ready to send VRAM/ready to receive DMA)
void UpdateGPUSTAT();
// Ticks for hblank/vblank.
void Execute(TickCount ticks);
/// Returns true if scanout should be interlaced.
bool IsDisplayInterlaced() const { return !m_force_progressive_scan && m_GPUSTAT.In480iMode(); }
@ -331,6 +335,8 @@ protected:
InterruptController* m_interrupt_controller = nullptr;
Timers* m_timers = nullptr;
std::unique_ptr<TimingEvent> m_tick_event;
// Pointer to VRAM, used for reads/writes. In the hardware backends, this is the shadow buffer.
u16* m_vram_ptr = nullptr;

View File

@ -360,7 +360,7 @@ void HostInterface::Throttle()
const s64 sleep_time = static_cast<s64>(m_last_throttle_time - time);
if (std::abs(sleep_time) >= MAX_VARIANCE_TIME)
{
#ifdef Y_BUILD_CONFIG_RELEASE
#ifndef _DEBUG
// Don't display the slow messages in debug, it'll always be slow...
// Limit how often the messages are displayed.
if (m_speed_lost_time_timestamp.GetTimeSeconds() >= 1.0f)
@ -437,6 +437,9 @@ void HostInterface::UpdateSpeedLimiterState()
(audio_sync_enabled && video_sync_enabled) ? " and video" : (video_sync_enabled ? "video" : ""));
m_audio_stream->SetSync(audio_sync_enabled);
if (audio_sync_enabled)
m_audio_stream->EmptyBuffers();
m_display->SetVSync(video_sync_enabled);
m_throttle_timer.Reset();
m_last_throttle_time = 0;

View File

@ -68,6 +68,9 @@ public:
/// Returns a path relative to the user directory.
std::string GetUserDirectoryRelativePath(const char* format, ...) const;
/// Throttles the system, i.e. sleeps until it's time to execute the next frame.
void Throttle();
protected:
using ThrottleClock = std::chrono::steady_clock;
@ -131,9 +134,6 @@ protected:
void RunFrame();
/// Throttles the system, i.e. sleeps until it's time to execute the next frame.
void Throttle();
void UpdateSpeedLimiterState();
void DrawFPSWindow();

View File

@ -15,6 +15,8 @@ void MDEC::Initialize(System* system, DMA* dma)
{
m_system = system;
m_dma = dma;
m_block_copy_out_event = system->CreateTimingEvent("MDEC Block Copy Out", TICKS_PER_BLOCK, TICKS_PER_BLOCK,
std::bind(&MDEC::CopyOutBlock, this), false);
}
void MDEC::Reset()
@ -39,8 +41,11 @@ bool MDEC::DoState(StateWrapper& sw)
sw.Do(&m_current_coefficient);
sw.Do(&m_current_q_scale);
sw.Do(&m_block_rgb);
sw.Do(&m_block_copy_out_ticks);
sw.Do(&m_block_copy_out_pending);
bool block_copy_out_pending = HasPendingBlockCopyOut();
sw.Do(&block_copy_out_pending);
if (sw.IsReading())
m_block_copy_out_event->SetState(block_copy_out_pending);
return !sw.HasError();
}
@ -131,6 +136,11 @@ void MDEC::DMAWrite(const u32* words, u32 word_count)
} while (word_count > 0);
}
bool MDEC::HasPendingBlockCopyOut() const
{
return m_block_copy_out_event->IsActive();
}
void MDEC::SoftReset()
{
m_status.bits = 0;
@ -143,8 +153,7 @@ void MDEC::SoftReset()
m_current_block = 0;
m_current_coefficient = 64;
m_current_q_scale = 0;
m_block_copy_out_ticks = TICKS_PER_BLOCK;
m_block_copy_out_pending = false;
m_block_copy_out_event->Deactivate();
UpdateStatus();
}
@ -173,11 +182,10 @@ u32 MDEC::ReadDataRegister()
if (m_data_out_fifo.IsEmpty())
{
// Stall the CPU until we're done processing.
if (m_block_copy_out_pending)
if (HasPendingBlockCopyOut())
{
Log_DevPrint("MDEC data out FIFO empty on read - stalling CPU");
m_system->StallCPU(m_block_copy_out_ticks);
Execute(m_block_copy_out_ticks);
m_system->StallCPU(m_block_copy_out_event->GetTicksUntilNextExecution());
}
else
{
@ -205,27 +213,9 @@ void MDEC::WriteCommandRegister(u32 value)
ExecutePendingCommand();
}
void MDEC::Execute(TickCount ticks)
{
if (!m_block_copy_out_pending)
return;
m_block_copy_out_ticks -= ticks;
if (m_block_copy_out_ticks <= 0)
{
DebugAssert(m_command == Command::DecodeMacroblock);
CopyOutBlock();
if (m_remaining_halfwords == 0)
EndCommand();
else
ExecutePendingCommand();
}
}
void MDEC::ExecutePendingCommand()
{
if (m_block_copy_out_pending)
if (HasPendingBlockCopyOut())
{
// can't do anything while waiting
UpdateStatus();
@ -234,7 +224,7 @@ void MDEC::ExecutePendingCommand()
while (!m_data_in_fifo.IsEmpty())
{
DebugAssert(!m_block_copy_out_pending);
DebugAssert(!HasPendingBlockCopyOut());
if (m_command == Command::None)
{
@ -417,20 +407,16 @@ bool MDEC::DecodeColoredMacroblock()
void MDEC::ScheduleBlockCopyOut(TickCount ticks)
{
DebugAssert(!m_block_copy_out_pending);
DebugAssert(!HasPendingBlockCopyOut());
Log_DebugPrintf("Scheduling block copy out in %d ticks", ticks);
m_system->Synchronize();
m_block_copy_out_pending = true;
m_block_copy_out_ticks = ticks;
m_system->SetDowncount(ticks);
m_block_copy_out_event->Schedule(TICKS_PER_BLOCK);
}
void MDEC::CopyOutBlock()
{
DebugAssert(m_block_copy_out_pending);
m_block_copy_out_pending = false;
m_block_copy_out_ticks = 0;
DebugAssert(m_command == Command::DecodeMacroblock);
m_block_copy_out_event->Deactivate();
Log_DebugPrintf("Copying out block");
@ -535,13 +521,9 @@ void MDEC::CopyOutBlock()
// if we've copied out all blocks, command is complete
if (m_remaining_halfwords == 0)
{
DebugAssert(m_command == Command::DecodeMacroblock);
m_command = Command::None;
m_current_block = 0;
m_current_coefficient = 64;
m_current_q_scale = 0;
}
EndCommand();
else
ExecutePendingCommand();
}
static constexpr std::array<u8, 64> zigzag = {{0, 1, 5, 6, 14, 15, 27, 28, 2, 4, 7, 13, 16, 26, 29, 42,

View File

@ -3,10 +3,12 @@
#include "common/fifo_queue.h"
#include "types.h"
#include <array>
#include <memory>
class StateWrapper;
class System;
class TimingEvent;
class DMA;
class MDEC
@ -26,8 +28,6 @@ public:
void DMARead(u32* words, u32 word_count);
void DMAWrite(const u32* words, u32 word_count);
void Execute(TickCount ticks);
void DrawDebugStateWindow();
private:
@ -88,6 +88,7 @@ private:
};
bool HasPendingCommand() const { return m_command != Command::None; }
bool HasPendingBlockCopyOut() const;
void SoftReset();
void UpdateStatus();
@ -138,8 +139,7 @@ private:
u16 m_current_q_scale = 0;
std::array<u32, 256> m_block_rgb{};
TickCount m_block_copy_out_ticks = TICKS_PER_BLOCK;
bool m_block_copy_out_pending = false;
std::unique_ptr<TimingEvent> m_block_copy_out_event;
u32 m_total_blocks_decoded = 0;
};

View File

@ -16,6 +16,8 @@ void Pad::Initialize(System* system, InterruptController* interrupt_controller)
{
m_system = system;
m_interrupt_controller = interrupt_controller;
m_transfer_event = system->CreateTimingEvent("Pad Serial Transfer", 1, 1,
std::bind(&Pad::TransferEvent, this, std::placeholders::_2), false);
}
void Pad::Reset()
@ -81,7 +83,6 @@ bool Pad::DoState(StateWrapper& sw)
}
sw.Do(&m_state);
sw.Do(&m_ticks_remaining);
sw.Do(&m_JOY_CTRL.bits);
sw.Do(&m_JOY_STAT.bits);
sw.Do(&m_JOY_MODE.bits);
@ -91,6 +92,9 @@ bool Pad::DoState(StateWrapper& sw)
sw.Do(&m_receive_buffer_full);
sw.Do(&m_transmit_buffer_full);
if (sw.IsReading() && IsTransmitting())
m_transfer_event->Activate();
return !sw.HasError();
}
@ -213,24 +217,6 @@ void Pad::WriteRegister(u32 offset, u32 value)
}
}
void Pad::Execute(TickCount ticks)
{
if (m_state == State::Idle)
return;
m_ticks_remaining -= ticks;
if (m_ticks_remaining > 0)
{
m_system->SetDowncount(m_ticks_remaining);
return;
}
if (m_state == State::Transmitting)
DoTransfer();
else
DoACK();
}
void Pad::SoftReset()
{
if (IsTransmitting())
@ -254,6 +240,14 @@ void Pad::UpdateJoyStat()
m_JOY_STAT.TXRDY = !m_transmit_buffer_full;
}
void Pad::TransferEvent(TickCount ticks_late)
{
if (m_state == State::Transmitting)
DoTransfer(ticks_late);
else
DoACK();
}
void Pad::BeginTransfer()
{
DebugAssert(m_state == State::Idle && CanTransfer());
@ -278,13 +272,11 @@ void Pad::BeginTransfer()
// test in (7) will fail, and it won't send any more data. So, the transfer/interrupt must be delayed
// until after (4) and (5) have been completed.
m_system->Synchronize();
m_state = State::Transmitting;
m_ticks_remaining = GetTransferTicks();
m_system->SetDowncount(m_ticks_remaining);
m_transfer_event->SetPeriodAndSchedule(GetTransferTicks());
}
void Pad::DoTransfer()
void Pad::DoTransfer(TickCount ticks_late)
{
Log_DebugPrintf("Transferring slot %d", m_JOY_CTRL.SLOT.GetValue());
@ -361,11 +353,10 @@ void Pad::DoTransfer()
const TickCount ack_timer = GetACKTicks();
Log_DebugPrintf("Delaying ACK for %d ticks", ack_timer);
m_state = State::WaitingForACK;
m_ticks_remaining += ack_timer;
if (m_ticks_remaining <= 0)
if (ticks_late >= ack_timer)
DoACK();
else
m_system->SetDowncount(m_ticks_remaining);
m_transfer_event->SetPeriodAndSchedule(ack_timer - ticks_late);
}
UpdateJoyStat();
@ -395,7 +386,7 @@ void Pad::EndTransfer()
Log_DebugPrintf("Ending transfer");
m_state = State::Idle;
m_ticks_remaining = 0;
m_transfer_event->Deactivate();
}
void Pad::ResetDeviceTransferState()

View File

@ -8,6 +8,7 @@
class StateWrapper;
class System;
class TimingEvent;
class InterruptController;
class Controller;
class MemoryCard;
@ -31,8 +32,6 @@ public:
u32 ReadRegister(u32 offset);
void WriteRegister(u32 offset, u32 value);
void Execute(TickCount ticks);
private:
static constexpr u32 NUM_SLOTS = 2;
@ -97,8 +96,9 @@ private:
void SoftReset();
void UpdateJoyStat();
void TransferEvent(TickCount ticks_late);
void BeginTransfer();
void DoTransfer();
void DoTransfer(TickCount ticks_late);
void DoACK();
void EndTransfer();
void ResetDeviceTransferState();
@ -109,8 +109,8 @@ private:
std::array<std::unique_ptr<Controller>, NUM_SLOTS> m_controllers;
std::array<std::unique_ptr<MemoryCard>, NUM_SLOTS> m_memory_cards;
std::unique_ptr<TimingEvent> m_transfer_event;
State m_state = State::Idle;
TickCount m_ticks_remaining = 0;
JOY_CTRL m_JOY_CTRL = {};
JOY_STAT m_JOY_STAT = {};

View File

@ -1,6 +1,6 @@
#include "spu.h"
#include "common/log.h"
#include "common/audio_stream.h"
#include "common/log.h"
#include "common/state_wrapper.h"
#include "dma.h"
#include "host_interface.h"
@ -24,6 +24,8 @@ void SPU::Initialize(System* system, DMA* dma, InterruptController* interrupt_co
m_system = system;
m_dma = dma;
m_interrupt_controller = interrupt_controller;
m_sample_event = m_system->CreateTimingEvent("SPU Sample", SYSCLK_TICKS_PER_SPU_TICK, SYSCLK_TICKS_PER_SPU_TICK,
std::bind(&SPU::Execute, this, std::placeholders::_1), false);
}
void SPU::Reset()
@ -65,6 +67,7 @@ void SPU::Reset()
}
m_ram.fill(0);
UpdateEventInterval();
}
bool SPU::DoState(StateWrapper& sw)
@ -108,7 +111,10 @@ bool SPU::DoState(StateWrapper& sw)
sw.DoBytes(m_ram.data(), RAM_SIZE);
if (sw.IsReading())
{
m_system->GetHostInterface()->GetAudioStream()->EmptyBuffers();
UpdateEventInterval();
}
return !sw.HasError();
}
@ -178,6 +184,7 @@ u16 SPU::ReadRegister(u32 offset)
case 0x1F801DAE - SPU_BASE:
// Log_DebugPrintf("SPU status register -> 0x%04X", ZeroExtend32(m_SPUCNT.bits));
m_sample_event->InvokeEarly();
return m_SPUSTAT.bits;
case 0x1F801DB0 - SPU_BASE:
@ -205,7 +212,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D80 - SPU_BASE:
{
Log_DebugPrintf("SPU main volume left <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_main_volume_left.bits = value;
return;
}
@ -213,7 +220,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D82 - SPU_BASE:
{
Log_DebugPrintf("SPU main volume right <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_main_volume_right.bits = value;
return;
}
@ -221,7 +228,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D88 - SPU_BASE:
{
Log_DebugPrintf("SPU key on low <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_key_on_register = (m_key_on_register & 0xFFFF0000) | ZeroExtend32(value);
u16 bits = value;
@ -240,7 +247,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D8A - SPU_BASE:
{
Log_DebugPrintf("SPU key on high <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_key_on_register = (m_key_on_register & 0x0000FFFF) | (ZeroExtend32(value) << 16);
u16 bits = value;
@ -259,7 +266,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D8C - SPU_BASE:
{
Log_DebugPrintf("SPU key off low <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_key_on_register = (m_key_on_register & 0xFFFF0000) | ZeroExtend32(value);
u16 bits = value;
@ -278,7 +285,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D8E - SPU_BASE:
{
Log_DebugPrintf("SPU key off high <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_key_on_register = (m_key_on_register & 0x0000FFFF) | (ZeroExtend32(value) << 16);
u16 bits = value;
@ -296,7 +303,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D90 - SPU_BASE:
{
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_pitch_modulation_enable_register = (m_pitch_modulation_enable_register & 0xFFFF0000) | ZeroExtend32(value);
Log_DebugPrintf("SPU pitch modulation enable register <- 0x%08X", m_pitch_modulation_enable_register);
}
@ -304,7 +311,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D92 - SPU_BASE:
{
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_pitch_modulation_enable_register =
(m_pitch_modulation_enable_register & 0x0000FFFF) | (ZeroExtend32(value) << 16);
Log_DebugPrintf("SPU pitch modulation enable register <- 0x%08X", m_pitch_modulation_enable_register);
@ -314,7 +321,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D94 - SPU_BASE:
{
Log_DebugPrintf("SPU noise mode register <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_noise_mode_register = (m_noise_mode_register & 0xFFFF0000) | ZeroExtend32(value);
}
break;
@ -322,7 +329,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D96 - SPU_BASE:
{
Log_DebugPrintf("SPU noise mode register <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_noise_mode_register = (m_noise_mode_register & 0x0000FFFF) | (ZeroExtend32(value) << 16);
}
break;
@ -330,7 +337,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D98 - SPU_BASE:
{
Log_DebugPrintf("SPU reverb on register <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_reverb_on_register = (m_reverb_on_register & 0xFFFF0000) | ZeroExtend32(value);
}
break;
@ -338,7 +345,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801D9A - SPU_BASE:
{
Log_DebugPrintf("SPU reverb off register <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_reverb_on_register = (m_reverb_on_register & 0x0000FFFF) | (ZeroExtend32(value) << 16);
}
break;
@ -346,6 +353,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801DA4 - SPU_BASE:
{
Log_DebugPrintf("SPU IRQ address register <- 0x%04X", ZeroExtend32(value));
m_sample_event->InvokeEarly();
m_irq_address = value;
return;
}
@ -369,6 +377,8 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801DAA - SPU_BASE:
{
Log_DebugPrintf("SPU control register <- 0x%04X", ZeroExtend32(value));
m_sample_event->InvokeEarly(true);
m_SPUCNT.bits = value;
m_SPUSTAT.mode = m_SPUCNT.mode.GetValue();
m_SPUSTAT.dma_read_write_request = m_SPUCNT.ram_transfer_mode >= RAMTransferMode::DMAWrite;
@ -377,6 +387,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
m_SPUSTAT.irq9_flag = false;
UpdateDMARequest();
UpdateEventInterval();
return;
}
@ -390,7 +401,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801DB0 - SPU_BASE:
{
Log_DebugPrintf("SPU left cd audio register <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_cd_audio_volume_left = value;
}
break;
@ -398,7 +409,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
case 0x1F801DB2 - SPU_BASE:
{
Log_DebugPrintf("SPU right cd audio register <- 0x%04X", ZeroExtend32(value));
m_system->Synchronize();
m_sample_event->InvokeEarly();
m_cd_audio_volume_right = value;
}
break;
@ -424,6 +435,12 @@ u16 SPU::ReadVoiceRegister(u32 offset)
const u32 voice_index = (offset / 0x10); //((offset >> 4) & 0x1F);
Assert(voice_index < 24);
if (reg_index >= 6)
{
// adsr volume needs to be updated when reading
m_sample_event->InvokeEarly();
}
return m_voices[voice_index].regs.index[reg_index];
}
@ -436,7 +453,7 @@ void SPU::WriteVoiceRegister(u32 offset, u16 value)
Voice& voice = m_voices[voice_index];
if (voice.IsOn())
m_system->Synchronize();
m_sample_event->InvokeEarly();
switch (reg_index)
{
@ -606,13 +623,103 @@ void SPU::IncrementCaptureBufferPosition()
void SPU::Execute(TickCount ticks)
{
TickCount num_samples = (ticks + m_ticks_carry) / SYSCLK_TICKS_PER_SPU_TICK;
DebugAssert(m_SPUCNT.enable || m_SPUCNT.cd_audio_enable);
u32 remaining_frames = static_cast<u32>((ticks + m_ticks_carry) / SYSCLK_TICKS_PER_SPU_TICK);
m_ticks_carry = (ticks + m_ticks_carry) % SYSCLK_TICKS_PER_SPU_TICK;
if (num_samples == 0 || (!m_SPUCNT.enable && !m_SPUCNT.cd_audio_enable))
while (remaining_frames > 0)
{
AudioStream* const output_stream = m_system->GetHostInterface()->GetAudioStream();
s16* output_frame;
u32 output_frame_space;
output_stream->BeginWrite(&output_frame, &output_frame_space);
const u32 frames_in_this_batch = std::min(remaining_frames, output_frame_space);
for (u32 i = 0; i < frames_in_this_batch; i++)
{
s32 left_sum = 0;
s32 right_sum = 0;
if (m_SPUCNT.enable)
{
for (u32 voice = 0; voice < NUM_VOICES; voice++)
{
const auto [left, right] = SampleVoice(voice);
left_sum += left;
right_sum += right;
}
if (!m_SPUCNT.mute_n)
{
left_sum = 0;
right_sum = 0;
}
}
// Mix in CD audio.
s16 cd_audio_left;
s16 cd_audio_right;
if (!m_cd_audio_buffer.IsEmpty())
{
cd_audio_left = m_cd_audio_buffer.Pop();
cd_audio_right = m_cd_audio_buffer.Pop();
if (m_SPUCNT.cd_audio_enable)
{
left_sum += ApplyVolume(s32(cd_audio_left), m_cd_audio_volume_left);
right_sum += ApplyVolume(s32(cd_audio_right), m_cd_audio_volume_right);
}
}
else
{
cd_audio_left = 0;
cd_audio_right = 0;
}
// Apply main volume before clamping.
*(output_frame++) = Clamp16(ApplyVolume(left_sum, m_main_volume_left.GetVolume()));
*(output_frame++) = Clamp16(ApplyVolume(right_sum, m_main_volume_right.GetVolume()));
// Write to capture buffers.
WriteToCaptureBuffer(0, cd_audio_left);
WriteToCaptureBuffer(1, cd_audio_right);
WriteToCaptureBuffer(2, Clamp16(m_voices[1].last_amplitude));
WriteToCaptureBuffer(3, Clamp16(m_voices[3].last_amplitude));
IncrementCaptureBufferPosition();
}
output_stream->EndWrite(frames_in_this_batch);
remaining_frames -= frames_in_this_batch;
}
}
void SPU::UpdateEventInterval()
{
if (!m_SPUCNT.enable && !m_SPUCNT.cd_audio_enable)
{
m_sample_event->Deactivate();
return;
}
// Don't generate more than the audio buffer since in a single slice, otherwise we'll both overflow the buffers when
// we do write it, and the audio thread will underflow since it won't have enough data it the game isn't messing with
// the SPU state.
const u32 max_slice_frames = m_system->GetHostInterface()->GetAudioStream()->GetBufferSize();
// TODO: Make this predict how long until the interrupt will be hit instead...
const u32 interval = m_SPUCNT.irq9_enable ? 1 : max_slice_frames;
const TickCount interval_ticks = static_cast<TickCount>(interval) * SYSCLK_TICKS_PER_SPU_TICK;
if (m_sample_event->IsActive() && m_sample_event->GetInterval() == interval_ticks)
return;
for (TickCount i = 0; i < num_samples; i++)
GenerateSample();
// Ensure all pending ticks have been executed, since we won't get them back after rescheduling.
m_sample_event->InvokeEarly(true);
m_sample_event->SetInterval(interval_ticks);
m_sample_event->Schedule(interval_ticks - m_ticks_carry);
}
void SPU::GeneratePendingSamples()
{
m_sample_event->InvokeEarly();
}
void SPU::Voice::KeyOn()
@ -952,80 +1059,22 @@ std::tuple<s32, s32> SPU::SampleVoice(u32 voice_index)
return std::make_tuple(left, right);
}
void SPU::EnsureCDAudioSpace(u32 num_samples)
void SPU::EnsureCDAudioSpace(u32 remaining_frames)
{
if (m_cd_audio_buffer.GetSpace() < (num_samples * 2))
if (m_cd_audio_buffer.IsEmpty())
{
Log_WarningPrintf("SPU CD Audio buffer overflow - writing %u samples with %u samples space", num_samples,
// we want the audio to start playing at the right point, not a few cycles early, otherwise this'll cause sync issues.
m_sample_event->InvokeEarly();
}
if (m_cd_audio_buffer.GetSpace() < (remaining_frames * 2))
{
Log_WarningPrintf("SPU CD Audio buffer overflow - writing %u samples with %u samples space", remaining_frames,
m_cd_audio_buffer.GetSpace() / 2);
m_cd_audio_buffer.Remove((num_samples * 2) - m_cd_audio_buffer.GetSpace());
m_cd_audio_buffer.Remove((remaining_frames * 2) - m_cd_audio_buffer.GetSpace());
}
}
void SPU::GenerateSample()
{
s32 left_sum = 0;
s32 right_sum = 0;
if (m_SPUCNT.enable)
{
for (u32 i = 0; i < NUM_VOICES; i++)
{
const auto [left, right] = SampleVoice(i);
left_sum += left;
right_sum += right;
}
if (!m_SPUCNT.mute_n)
{
left_sum = 0;
right_sum = 0;
}
}
// Mix in CD audio.
s16 cd_audio_left;
s16 cd_audio_right;
if (!m_cd_audio_buffer.IsEmpty())
{
cd_audio_left = m_cd_audio_buffer.Pop();
cd_audio_right = m_cd_audio_buffer.Pop();
if (m_SPUCNT.cd_audio_enable)
{
left_sum += ApplyVolume(s32(cd_audio_left), m_cd_audio_volume_left);
right_sum += ApplyVolume(s32(cd_audio_right), m_cd_audio_volume_right);
}
}
else
{
cd_audio_left = 0;
cd_audio_right = 0;
}
// Apply main volume before clamping.
std::array<AudioStream::SampleType, 2> out_samples;
out_samples[0] = Clamp16(ApplyVolume(left_sum, m_main_volume_left.GetVolume()));
out_samples[1] = Clamp16(ApplyVolume(right_sum, m_main_volume_right.GetVolume()));
m_system->GetHostInterface()->GetAudioStream()->WriteSamples(out_samples.data(), 1);
// Write to capture buffers.
WriteToCaptureBuffer(0, cd_audio_left);
WriteToCaptureBuffer(1, cd_audio_right);
WriteToCaptureBuffer(2, Clamp16(m_voices[1].last_amplitude));
WriteToCaptureBuffer(3, Clamp16(m_voices[3].last_amplitude));
IncrementCaptureBufferPosition();
#if 0
static FILE* fp = nullptr;
if (!fp)
fp = std::fopen("D:\\spu.raw", "wb");
if (fp)
{
std::fwrite(out_samples.data(), sizeof(AudioStream::SampleType), 2, fp);
std::fflush(fp);
}
#endif
}
void SPU::DrawDebugStateWindow()
{
static const ImVec4 active_color{1.0f, 1.0f, 1.0f, 1.0f};

View File

@ -3,11 +3,12 @@
#include "common/fifo_queue.h"
#include "types.h"
#include <array>
#include <memory>
class AudioStream;
class StateWrapper;
class System;
class TimingEvent;
class DMA;
class InterruptController;
@ -27,8 +28,6 @@ public:
void DMARead(u32* words, u32 word_count);
void DMAWrite(const u32* words, u32 word_count);
void Execute(TickCount ticks);
// Render statistics debug window.
void DrawDebugStateWindow();
@ -40,6 +39,9 @@ public:
}
void EnsureCDAudioSpace(u32 num_samples);
// Executes the SPU, generating any pending samples.
void GeneratePendingSamples();
private:
static constexpr u32 RAM_SIZE = 512 * 1024;
static constexpr u32 RAM_MASK = RAM_SIZE - 1;
@ -282,11 +284,13 @@ private:
void ReadADPCMBlock(u16 address, ADPCMBlock* block);
std::tuple<s32, s32> SampleVoice(u32 voice_index);
void GenerateSample();
void Execute(TickCount ticks);
void UpdateEventInterval();
System* m_system = nullptr;
DMA* m_dma = nullptr;
InterruptController* m_interrupt_controller = nullptr;
std::unique_ptr<TimingEvent> m_sample_event = nullptr;
SPUCNT m_SPUCNT = {};
SPUSTAT m_SPUSTAT = {};

View File

@ -40,7 +40,11 @@ System::System(HostInterface* host_interface) : m_host_interface(host_interface)
m_cpu_execution_mode = host_interface->m_settings.cpu_execution_mode;
}
System::~System() = default;
System::~System()
{
// we have to explicitly destroy components because they can deregister events
DestroyComponents();
}
std::unique_ptr<System> System::Create(HostInterface* host_interface)
{
@ -56,7 +60,7 @@ bool System::RecreateGPU(GPURenderer renderer)
// save current state
std::unique_ptr<ByteStream> state_stream = ByteStream_CreateGrowableMemoryStream();
StateWrapper sw(state_stream.get(), StateWrapper::Mode::Write);
const bool state_valid = m_gpu->DoState(sw);
const bool state_valid = m_gpu->DoState(sw) && DoEventsState(sw);
if (!state_valid)
Log_ErrorPrintf("Failed to save old GPU state when switching renderers");
@ -73,6 +77,7 @@ bool System::RecreateGPU(GPURenderer renderer)
state_stream->SeekAbsolute(0);
sw.SetMode(StateWrapper::Mode::Read);
m_gpu->DoState(sw);
DoEventsState(sw);
}
return true;
@ -188,11 +193,26 @@ void System::InitializeComponents()
m_cdrom->Initialize(this, m_dma.get(), m_interrupt_controller.get(), m_spu.get());
m_pad->Initialize(this, m_interrupt_controller.get());
m_timers->Initialize(this, m_interrupt_controller.get());
m_timers->Initialize(this, m_interrupt_controller.get(), m_gpu.get());
m_spu->Initialize(this, m_dma.get(), m_interrupt_controller.get());
m_mdec->Initialize(this, m_dma.get());
}
void System::DestroyComponents()
{
m_mdec.reset();
m_spu.reset();
m_timers.reset();
m_pad.reset();
m_cdrom.reset();
m_gpu.reset();
m_interrupt_controller.reset();
m_dma.reset();
m_bus.reset();
m_cpu_code_cache.reset();
m_cpu.reset();
}
bool System::CreateGPU(GPURenderer renderer)
{
switch (renderer)
@ -230,6 +250,7 @@ bool System::CreateGPU(GPURenderer renderer)
m_bus->SetGPU(m_gpu.get());
m_dma->SetGPU(m_gpu.get());
m_timers->SetGPU(m_gpu.get());
return true;
}
@ -298,6 +319,9 @@ bool System::DoState(StateWrapper& sw)
if (!sw.DoMarker("SIO") || !m_sio->DoState(sw))
return false;
if (!sw.DoMarker("Events") || !DoEventsState(sw))
return false;
return !sw.HasError();
}
@ -318,6 +342,7 @@ void System::Reset()
m_frame_number = 1;
m_internal_frame_number = 0;
m_global_tick_counter = 0;
m_last_event_run_time = 0;
}
bool System::LoadState(ByteStream* state)
@ -335,23 +360,28 @@ bool System::SaveState(ByteStream* state)
void System::RunFrame()
{
// Duplicated to avoid branch in the while loop, as the downcount can be quite low at times.
u32 current_frame_number = m_frame_number;
m_frame_done = false;
if (m_cpu_execution_mode == CPUExecutionMode::Interpreter)
{
while (current_frame_number == m_frame_number)
do
{
UpdateCPUDowncount();
m_cpu->Execute();
Synchronize();
}
RunEvents();
} while (!m_frame_done);
}
else
{
while (current_frame_number == m_frame_number)
do
{
UpdateCPUDowncount();
m_cpu_code_cache->Execute();
Synchronize();
}
RunEvents();
} while (!m_frame_done);
}
// Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns.
m_spu->GeneratePendingSamples();
}
bool System::LoadEXE(const char* filename, std::vector<u8>& bios_image)
@ -438,34 +468,11 @@ bool System::SetExpansionROM(const char* filename)
return true;
}
void System::Synchronize()
{
const TickCount pending_ticks = m_cpu->GetPendingTicks();
if (pending_ticks == 0)
return;
m_cpu->ResetPendingTicks();
m_cpu->ResetDowncount();
m_global_tick_counter += static_cast<u32>(pending_ticks);
m_gpu->Execute(pending_ticks);
m_timers->Execute(pending_ticks);
m_cdrom->Execute(pending_ticks);
m_pad->Execute(pending_ticks);
m_spu->Execute(pending_ticks);
m_mdec->Execute(pending_ticks);
m_dma->Execute(pending_ticks);
}
void System::SetDowncount(TickCount downcount)
{
m_cpu->SetDowncount(downcount);
}
void System::StallCPU(TickCount ticks)
{
m_cpu->AddPendingTicks(ticks);
if (m_cpu->GetPendingTicks() >= m_cpu->GetDowncount() && !m_running_events)
RunEvents();
}
Controller* System::GetController(u32 slot) const
@ -530,6 +537,202 @@ void System::RemoveMedia()
m_cdrom->RemoveMedia();
}
std::unique_ptr<TimingEvent> System::CreateTimingEvent(std::string name, TickCount period, TickCount interval,
TimingEventCallback callback, bool activate)
{
std::unique_ptr<TimingEvent> event =
std::make_unique<TimingEvent>(this, std::move(name), period, interval, std::move(callback));
if (activate)
event->Activate();
return event;
}
static bool CompareEvents(const TimingEvent* lhs, const TimingEvent* rhs)
{
return lhs->GetDowncount() > rhs->GetDowncount();
}
void System::AddActiveEvent(TimingEvent* event)
{
m_events.push_back(event);
if (!m_running_events)
{
std::push_heap(m_events.begin(), m_events.end(), CompareEvents);
if (!m_frame_done)
UpdateCPUDowncount();
}
else
{
m_events_need_sorting = true;
}
}
void System::RemoveActiveEvent(TimingEvent* event)
{
auto iter = std::find_if(m_events.begin(), m_events.end(), [event](const auto& it) { return event == it; });
if (iter == m_events.end())
{
Panic("Attempt to remove inactive event");
return;
}
m_events.erase(iter);
if (!m_running_events)
{
std::make_heap(m_events.begin(), m_events.end(), CompareEvents);
if (!m_events.empty() && !m_frame_done)
UpdateCPUDowncount();
}
else
{
m_events_need_sorting = true;
}
}
void System::SortEvents()
{
if (!m_running_events)
{
std::make_heap(m_events.begin(), m_events.end(), CompareEvents);
if (!m_frame_done)
UpdateCPUDowncount();
}
else
{
m_events_need_sorting = true;
}
}
void System::RunEvents()
{
DebugAssert(!m_running_events && !m_events.empty());
const TickCount pending_ticks = m_cpu->GetPendingTicks();
m_global_tick_counter += static_cast<u32>(pending_ticks);
m_cpu->ResetPendingTicks();
TickCount time = static_cast<TickCount>(m_global_tick_counter - m_last_event_run_time);
m_running_events = true;
m_last_event_run_time = m_global_tick_counter;
// Apply downcount to all events.
// This will result in a negative downcount for those events which are late.
for (TimingEvent* evt : m_events)
{
evt->m_downcount -= time;
evt->m_time_since_last_run += time;
}
// Now we can actually run the callbacks.
while (m_events.front()->GetDowncount() <= 0)
{
TimingEvent* evt = m_events.front();
const TickCount ticks_late = -evt->m_downcount;
std::pop_heap(m_events.begin(), m_events.end(), CompareEvents);
// Factor late time into the time for the next invocation.
const TickCount ticks_to_execute = evt->m_time_since_last_run;
evt->m_downcount += evt->m_interval;
evt->m_time_since_last_run = 0;
// The cycles_late is only an indicator, it doesn't modify the cycles to execute.
evt->m_callback(ticks_to_execute, ticks_late);
// Place it in the appropriate position in the queue.
if (m_events_need_sorting)
{
// Another event may have been changed by this event, or the interval/downcount changed.
std::make_heap(m_events.begin(), m_events.end(), CompareEvents);
m_events_need_sorting = false;
}
else
{
// Keep the event list in a heap. The event we just serviced will be in the last place,
// so we can use push_here instead of make_heap, which should be faster.
std::push_heap(m_events.begin(), m_events.end(), CompareEvents);
}
}
m_running_events = false;
m_cpu->SetDowncount(m_events.front()->GetDowncount());
}
void System::UpdateCPUDowncount()
{
m_cpu->SetDowncount(m_events[0]->GetDowncount());
}
bool System::DoEventsState(StateWrapper& sw)
{
if (sw.IsReading())
{
// Load timestamps for the clock events.
// Any oneshot events should be recreated by the load state method, so we can fix up their times here.
u32 event_count = 0;
sw.Do(&event_count);
for (u32 i = 0; i < event_count; i++)
{
std::string event_name;
TickCount downcount, time_since_last_run, period, interval;
sw.Do(&event_name);
sw.Do(&downcount);
sw.Do(&time_since_last_run);
sw.Do(&period);
sw.Do(&interval);
if (sw.HasError())
return false;
TimingEvent* event = FindActiveEvent(event_name.c_str());
if (!event)
{
Log_WarningPrintf("Save state has event '%s', but couldn't find this event when loading.", event_name.c_str());
continue;
}
// Using reschedule is safe here since we call sort afterwards.
event->m_downcount = downcount;
event->m_time_since_last_run = time_since_last_run;
event->m_period = period;
event->m_interval = interval;
}
sw.Do(&m_last_event_run_time);
Log_DevPrintf("Loaded %u events from save state.", event_count);
SortEvents();
}
else
{
u32 event_count = static_cast<u32>(m_events.size());
sw.Do(&event_count);
for (TimingEvent* evt : m_events)
{
sw.Do(&evt->m_name);
sw.Do(&evt->m_downcount);
sw.Do(&evt->m_time_since_last_run);
sw.Do(&evt->m_period);
sw.Do(&evt->m_interval);
}
sw.Do(&m_last_event_run_time);
Log_DevPrintf("Wrote %u events to save state.", event_count);
}
return !sw.HasError();
}
TimingEvent* System::FindActiveEvent(const char* name)
{
auto iter =
std::find_if(m_events.begin(), m_events.end(), [&name](auto& ev) { return ev->GetName().compare(name) == 0; });
return (iter != m_events.end()) ? *iter : nullptr;
}
void System::UpdateRunningGame(const char* path, CDImage* image)
{
m_running_game_path.clear();

View File

@ -1,5 +1,6 @@
#pragma once
#include "host_interface.h"
#include "timing_event.h"
#include "types.h"
#include <memory>
#include <optional>
@ -28,6 +29,8 @@ class SIO;
class System
{
friend TimingEvent;
public:
~System();
@ -52,7 +55,11 @@ public:
u32 GetFrameNumber() const { return m_frame_number; }
u32 GetInternalFrameNumber() const { return m_internal_frame_number; }
u32 GetGlobalTickCounter() const { return m_global_tick_counter; }
void IncrementFrameNumber() { m_frame_number++; }
void IncrementFrameNumber()
{
m_frame_number++;
m_frame_done = true;
}
void IncrementInternalFrameNumber() { m_internal_frame_number++; }
const Settings& GetSettings() { return m_host_interface->GetSettings(); }
@ -78,9 +85,6 @@ public:
bool LoadEXE(const char* filename, std::vector<u8>& bios_image);
bool SetExpansionROM(const char* filename);
void SetDowncount(TickCount downcount);
void Synchronize();
// Adds ticks to the global tick counter, simulating the CPU being stalled.
void StallCPU(TickCount ticks);
@ -94,6 +98,12 @@ public:
bool InsertMedia(const char* path);
void RemoveMedia();
/// Creates a new event.
std::unique_ptr<TimingEvent> CreateTimingEvent(std::string name, TickCount period, TickCount interval,
TimingEventCallback callback, bool activate);
bool RUNNING_EVENTS() const { return m_running_events; }
private:
System(HostInterface* host_interface);
@ -101,6 +111,33 @@ private:
bool CreateGPU(GPURenderer renderer);
void InitializeComponents();
void DestroyComponents();
// Active event management
void AddActiveEvent(TimingEvent* event);
void RemoveActiveEvent(TimingEvent* event);
void SortEvents();
// Runs any pending events. Call when CPU downcount is zero.
void RunEvents();
// Updates the downcount of the CPU (event scheduling).
void UpdateCPUDowncount();
bool DoEventsState(StateWrapper& sw);
// Event lookup, use with care.
// If you modify an event, call SortEvents afterwards.
TimingEvent* FindActiveEvent(const char* name);
// Event enumeration, use with care.
// Don't remove an event while enumerating the list, as it will invalidate the iterator.
template<typename T>
void EnumerateActiveEvents(T callback) const
{
for (const TimingEvent* ev : m_events)
callback(ev);
}
void UpdateRunningGame(const char* path, CDImage* image);
@ -123,6 +160,12 @@ private:
u32 m_internal_frame_number = 1;
u32 m_global_tick_counter = 0;
std::vector<TimingEvent*> m_events;
u32 m_last_event_run_time = 0;
bool m_running_events = false;
bool m_events_need_sorting = false;
bool m_frame_done = false;
std::string m_running_game_path;
std::string m_running_game_code;
std::string m_running_game_title;

View File

@ -1,6 +1,7 @@
#include "timers.h"
#include "common/log.h"
#include "common/state_wrapper.h"
#include "gpu.h"
#include "interrupt_controller.h"
#include "system.h"
#include <imgui.h>
@ -10,10 +11,13 @@ Timers::Timers() = default;
Timers::~Timers() = default;
void Timers::Initialize(System* system, InterruptController* interrupt_controller)
void Timers::Initialize(System* system, InterruptController* interrupt_controller, GPU* gpu)
{
m_system = system;
m_interrupt_controller = interrupt_controller;
m_gpu = gpu;
m_sysclk_event = system->CreateTimingEvent("Timer SysClk Interrupt", 1, 1,
std::bind(&Timers::AddSysClkTicks, this, std::placeholders::_1), false);
}
void Timers::Reset()
@ -30,6 +34,7 @@ void Timers::Reset()
}
m_sysclk_div_8_carry = 0;
UpdateSysClkEvent();
}
bool Timers::DoState(StateWrapper& sw)
@ -47,6 +52,10 @@ bool Timers::DoState(StateWrapper& sw)
}
sw.Do(&m_sysclk_div_8_carry);
if (sw.IsReading())
UpdateSysClkEvent();
return !sw.HasError();
}
@ -88,12 +97,12 @@ void Timers::AddTicks(u32 timer, TickCount count)
bool interrupt_request = false;
if (cs.counter >= cs.target && old_counter < cs.target)
{
interrupt_request = true;
interrupt_request |= cs.mode.irq_at_target;
cs.mode.reached_target = true;
}
if (cs.counter >= 0xFFFF)
{
interrupt_request = true;
interrupt_request |= cs.mode.irq_on_overflow;
cs.mode.reached_overflow = true;
}
@ -126,7 +135,7 @@ void Timers::AddTicks(u32 timer, TickCount count)
}
}
void Timers::Execute(TickCount sysclk_ticks)
void Timers::AddSysClkTicks(TickCount sysclk_ticks)
{
if (!m_states[0].external_counting_enabled && m_states[0].counting_enabled)
AddTicks(0, sysclk_ticks);
@ -143,7 +152,7 @@ void Timers::Execute(TickCount sysclk_ticks)
AddTicks(2, sysclk_ticks);
}
UpdateDowncount();
UpdateSysClkEvent();
}
u32 Timers::ReadRegister(u32 offset)
@ -157,13 +166,28 @@ u32 Timers::ReadRegister(u32 offset)
{
case 0x00:
{
m_system->Synchronize();
if (timer_index < 2)
{
// timers 0/1 depend on the GPU
if (cs.external_counting_enabled)
m_gpu->Synchronize();
}
m_sysclk_event->InvokeEarly();
return cs.counter;
}
case 0x04:
{
m_system->Synchronize();
if (timer_index < 2)
{
// timers 0/1 depend on the GPU
if (cs.external_counting_enabled)
m_gpu->Synchronize();
}
m_sysclk_event->InvokeEarly();
const u32 bits = cs.mode.bits;
cs.mode.reached_overflow = false;
@ -192,7 +216,7 @@ void Timers::WriteRegister(u32 offset, u32 value)
case 0x00:
{
Log_DebugPrintf("Timer %u write counter %u", timer_index, value);
m_system->Synchronize();
m_sysclk_event->InvokeEarly();
cs.counter = value & u32(0xFFFF);
}
break;
@ -200,7 +224,7 @@ void Timers::WriteRegister(u32 offset, u32 value)
case 0x04:
{
Log_DebugPrintf("Timer %u write mode register 0x%04X", timer_index, value);
m_system->Synchronize();
m_sysclk_event->InvokeEarly();
cs.mode.bits = value & u32(0x1FFF);
cs.use_external_clock = (cs.mode.clock_source & (timer_index == 2 ? 2 : 1)) != 0;
cs.counter = 0;
@ -210,13 +234,14 @@ void Timers::WriteRegister(u32 offset, u32 value)
UpdateCountingEnabled(cs);
UpdateIRQ(timer_index);
UpdateSysClkEvent();
}
break;
case 0x08:
{
Log_DebugPrintf("Timer %u write target 0x%04X", timer_index, ZeroExtend32(Truncate16(value)));
m_system->Synchronize();
m_sysclk_event->InvokeEarly();
cs.target = value & u32(0xFFFF);
}
break;
@ -267,16 +292,19 @@ void Timers::UpdateIRQ(u32 index)
static_cast<InterruptController::IRQ>(static_cast<u32>(InterruptController::IRQ::TMR0) + index));
}
void Timers::UpdateDowncount()
TickCount Timers::GetTicksUntilNextInterrupt() const
{
TickCount min_ticks = std::numeric_limits<TickCount>::max();
for (u32 i = 0; i < NUM_TIMERS; i++)
{
CounterState& cs = m_states[i];
if (!cs.counting_enabled || (i < 2 && cs.external_counting_enabled))
const CounterState& cs = m_states[i];
if (!cs.counting_enabled || (i < 2 && cs.external_counting_enabled) ||
(!cs.mode.irq_at_target && !cs.mode.irq_on_overflow))
{
continue;
}
TickCount min_ticks_for_this_timer = min_ticks;
TickCount min_ticks_for_this_timer = std::numeric_limits<TickCount>::max();
if (cs.mode.irq_at_target && cs.counter < cs.target)
min_ticks_for_this_timer = static_cast<TickCount>(cs.target - cs.counter);
if (cs.mode.irq_on_overflow && cs.counter < cs.target)
@ -288,7 +316,17 @@ void Timers::UpdateDowncount()
min_ticks = std::min(min_ticks, min_ticks_for_this_timer);
}
m_system->SetDowncount(min_ticks);
return min_ticks;
}
void Timers::UpdateSysClkEvent()
{
// Still update once every 100ms. If we get polled we'll execute sooner.
const TickCount ticks = GetTicksUntilNextInterrupt();
if (ticks == std::numeric_limits<TickCount>::max())
m_sysclk_event->Schedule(MAX_SLICE_SIZE);
else
m_sysclk_event->Schedule(ticks);
}
void Timers::DrawDebugStateWindow()

View File

@ -2,11 +2,14 @@
#include "common/bitfield.h"
#include "types.h"
#include <array>
#include <memory>
class StateWrapper;
class System;
class TimingEvent;
class InterruptController;
class GPU;
class Timers
{
@ -14,7 +17,7 @@ public:
Timers();
~Timers();
void Initialize(System* system, InterruptController* interrupt_controller);
void Initialize(System* system, InterruptController* interrupt_controller, GPU* gpu);
void Reset();
bool DoState(StateWrapper& sw);
@ -25,11 +28,13 @@ public:
// dot clock/hblank/sysclk div 8
bool IsUsingExternalClock(u32 timer) const { return m_states[timer].external_counting_enabled; }
void AddTicks(u32 timer, TickCount ticks);
void Execute(TickCount sysclk_ticks);
u32 ReadRegister(u32 offset);
void WriteRegister(u32 offset, u32 value);
// changing interfaces
void SetGPU(GPU* gpu) { m_gpu = gpu; }
private:
static constexpr u32 NUM_TIMERS = 3;
@ -73,10 +78,15 @@ private:
void UpdateCountingEnabled(CounterState& cs);
void UpdateIRQ(u32 index);
void UpdateDowncount();
void AddSysClkTicks(TickCount sysclk_ticks);
TickCount GetTicksUntilNextInterrupt() const;
void UpdateSysClkEvent();
System* m_system = nullptr;
InterruptController* m_interrupt_controller = nullptr;
GPU* m_gpu = nullptr;
std::unique_ptr<TimingEvent> m_sysclk_event = nullptr;
std::array<CounterState, NUM_TIMERS> m_states{};
u32 m_sysclk_div_8_carry = 0; // partial ticks for timer 3 with sysclk/8

130
src/core/timing_event.cpp Normal file
View File

@ -0,0 +1,130 @@
#include "timing_event.h"
#include "common/assert.h"
#include "cpu_core.h"
#include "system.h"
TimingEvent::TimingEvent(System* system, std::string name, TickCount period, TickCount interval,
TimingEventCallback callback)
: m_downcount(interval), m_time_since_last_run(0), m_period(period), m_interval(interval),
m_callback(std::move(callback)), m_system(system), m_name(std::move(name)), m_active(false)
{
}
TimingEvent::~TimingEvent()
{
if (m_active)
m_system->RemoveActiveEvent(this);
}
TickCount TimingEvent::GetTicksSinceLastExecution() const
{
return m_system->m_cpu->GetPendingTicks() + m_time_since_last_run;
}
TickCount TimingEvent::GetTicksUntilNextExecution() const
{
return std::max(m_downcount - m_system->m_cpu->GetPendingTicks(), static_cast<TickCount>(0));
}
void TimingEvent::Schedule(TickCount ticks)
{
m_downcount = ticks;
m_time_since_last_run = 0;
// Factor in partial time if this was rescheduled outside of an event handler. Say, an MMIO write.
if (!m_system->m_running_events)
{
const TickCount pending_ticks = m_system->m_cpu->GetPendingTicks();
m_downcount += pending_ticks;
m_time_since_last_run -= pending_ticks;
}
if (m_active)
{
// If this is a call from an IO handler for example, re-sort the event queue.
m_system->SortEvents();
}
else
{
m_active = true;
m_system->AddActiveEvent(this);
}
}
void TimingEvent::SetIntervalAndSchedule(TickCount ticks)
{
SetInterval(ticks);
Schedule(ticks);
}
void TimingEvent::SetPeriodAndSchedule(TickCount ticks)
{
SetPeriod(ticks);
SetInterval(ticks);
Schedule(ticks);
}
void TimingEvent::Reset()
{
if (!m_active)
return;
m_downcount = m_interval;
m_time_since_last_run = 0;
m_system->SortEvents();
}
void TimingEvent::InvokeEarly(bool force /* = false */)
{
if (!m_active)
return;
const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks();
const TickCount ticks_to_execute = m_time_since_last_run + pending_ticks;
if (!force && ticks_to_execute < m_period)
return;
m_downcount = pending_ticks + m_interval;
m_time_since_last_run -= ticks_to_execute;
m_callback(ticks_to_execute, 0);
// Since we've changed the downcount, we need to re-sort the events.
m_system->SortEvents();
}
void TimingEvent::Activate()
{
if (m_active)
return;
// leave the downcount intact
const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks();
m_downcount += pending_ticks;
m_time_since_last_run -= pending_ticks;
m_active = true;
m_system->AddActiveEvent(this);
}
void TimingEvent::Deactivate()
{
if (!m_active)
return;
const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks();
m_downcount -= pending_ticks;
m_time_since_last_run += pending_ticks;
m_active = false;
m_system->RemoveActiveEvent(this);
}
void TimingEvent::SetDowncount(TickCount downcount)
{
const TickCount pending_ticks = m_system->m_running_events ? 0 : m_system->m_cpu->GetPendingTicks();
m_downcount = downcount + pending_ticks;
m_time_since_last_run = -pending_ticks;
if (m_active)
m_system->SortEvents();
}

76
src/core/timing_event.h Normal file
View File

@ -0,0 +1,76 @@
#pragma once
#include <functional>
#include <memory>
#include <vector>
#include "types.h"
class System;
class TimingEvent;
// Event callback type. Second parameter is the number of cycles the event was executed "late".
using TimingEventCallback = std::function<void(TickCount ticks, TickCount ticks_late)>;
class TimingEvent
{
friend System;
public:
TimingEvent(System* system, std::string name, TickCount period, TickCount interval, TimingEventCallback callback);
~TimingEvent();
System* GetSystem() const { return m_system; }
const std::string& GetName() const { return m_name; }
bool IsActive() const { return m_active; }
// Returns the number of ticks between each event.
TickCount GetPeriod() const { return m_period; }
TickCount GetInterval() const { return m_interval; }
TickCount GetDowncount() const { return m_downcount; }
// Includes pending time.
TickCount GetTicksSinceLastExecution() const;
TickCount GetTicksUntilNextExecution() const;
void Schedule(TickCount ticks);
void SetIntervalAndSchedule(TickCount ticks);
void SetPeriodAndSchedule(TickCount ticks);
void Reset();
// Services the event with the current accmulated time. If force is set, when not enough time is pending to
// simulate a single cycle, the callback will still be invoked, otherwise it won't be.
void InvokeEarly(bool force = false);
// Deactivates the event, preventing it from firing again.
// Do not call within a callback, return Deactivate instead.
void Activate();
void Deactivate();
ALWAYS_INLINE void SetState(bool active)
{
if (active)
Activate();
else
Deactivate();
}
// Directly alters the downcount of the event.
void SetDowncount(TickCount downcount);
// Directly alters the interval of the event.
void SetInterval(TickCount interval) { m_interval = interval; }
void SetPeriod(TickCount period) { m_period = period; }
private:
TickCount m_downcount;
TickCount m_time_since_last_run;
TickCount m_period;
TickCount m_interval;
TimingEventCallback m_callback;
System* m_system;
std::string m_name;
bool m_active;
};