Mesen2/Core/PCE/CdRom/PceScsiBus.cpp
Sour 64126ace83 PCE: CD-ROM timing improvements
-IRQ/SCSI status should happen after seek is done when a play command is sent (fixes  audio sync in Brandish intro cutscene)
-Fixed seek delay not being applied correctly when swapping from audio playback to data loading and tweaked seek delays to make them a bit slower (fixes Brandish audio glitch during introduction)
-Fixed regression that causes the "transfer ready" irq flag to not be reset when a sector was done reading (this broke the intro sequence in "It came from the desert")
-Improved behavior when a load operation is cancelled (to match the expected results of both scsitest and verificator tests)
2024-02-03 12:00:45 +09:00

651 lines
18 KiB
C++

#include "pch.h"
#include "PCE/CdRom/PceScsiBus.h"
#include "PCE/CdRom/PceCdRom.h"
#include "PCE/CdRom/PceCdAudioPlayer.h"
#include "PCE/CdRom/PceCdSeekDelay.h"
#include "PCE/PceConsole.h"
#include "PCE/PceTypes.h"
#include "Shared/CdReader.h"
#include "Shared/Emulator.h"
#include "Shared/MessageManager.h"
#include "Utilities/HexUtilities.h"
#include "Utilities/StringUtilities.h"
#include "Utilities/Serializer.h"
using namespace ScsiSignal;
PceScsiBus::PceScsiBus(Emulator* emu, PceConsole* console, PceCdRom* cdrom, DiscInfo& disc)
{
_emu = emu;
_disc = &disc;
_console = console;
_cdrom = cdrom;
}
void PceScsiBus::SetPhase(ScsiPhase phase)
{
if(_state.Phase == phase) {
return;
}
_updateCounter = 0;
_stateChanged = true;
_needExec = true;
_state.Phase = phase;
ClearSignals(Bsy, Cd, Io, Msg, Req);
//LogDebug("[SCSI] Phase changed: " + string(magic_enum::enum_name(phase)));
switch(_state.Phase) {
case ScsiPhase::BusFree: _cdrom->ClearIrqSource(PceCdRomIrqSource::DataTransferDone); break;
case ScsiPhase::Command:
_readSectorCounter = 0; //stop any pending disc access?
SetSignals(Bsy, Cd, Req);
break;
case ScsiPhase::DataIn: SetSignals(Bsy, Io); break;
case ScsiPhase::MessageIn: SetSignals(Bsy, Cd, Io, Msg, Req); break;
case ScsiPhase::Status: SetSignals(Bsy, Cd, Io, Req); break;
}
}
void PceScsiBus::Reset()
{
ClearSignals(Ack, Atn, Cd, Io, Msg, Req);
_state.Phase = ScsiPhase::BusFree;
_state.DataPort = 0;
_state.ReadDataPort = 0;
_state.SectorsToRead = 0;
_readSectorCounter = 0;
_cmdBuffer.clear();
_dataBuffer.clear();
_cdrom->GetAudioPlayer().Stop();
//LogDebug("[SCSI] Reset");
}
void PceScsiBus::SetStatusMessage(ScsiStatus status, uint8_t data, uint32_t length)
{
_dataBuffer = deque<uint8_t>(length, data);
_state.MessageDone = false;
_state.ReadDataPort = (uint8_t)status;
SetPhase(ScsiPhase::Status);
}
void PceScsiBus::ProcessStatusPhase()
{
if(_state.Signals[Req] && _state.Signals[Ack]) {
ClearSignals(Req);
} else if(!_state.Signals[Req] && !_state.Signals[Ack]) {
if(_dataBuffer.size() > 0) {
_state.ReadDataPort = _dataBuffer.front();
_dataBuffer.pop_front();
if(_dataBuffer.empty()) {
SetPhase(ScsiPhase::MessageIn);
} else {
SetSignals(Req);
}
}
}
}
void PceScsiBus::ProcessMessageInPhase()
{
if(_state.Signals[Req] && _state.Signals[Ack]) {
ClearSignals(Req);
_state.MessageDone = true;
} else if(!_state.Signals[Req] && !_state.Signals[Ack] && _state.MessageDone) {
_state.MessageDone = false;
SetPhase(ScsiPhase::BusFree);
}
}
void PceScsiBus::QueueDriveUpdate(ScsiUpdateType action, uint32_t delay)
{
//The delays used with QueueDriveUpdate are all approximations
//and haven't been validated beyond what the validator.pce test rom checks
_updateType = action;
_updateCounter = delay;
_needExec = true;
}
void PceScsiBus::ProcessDataInPhase()
{
if(_state.Signals[Req] && _state.Signals[Ack]) {
ClearSignals(Req);
} else if(!_state.Signals[Req] && !_state.Signals[Ack]) {
if(_dataBuffer.size() > 0) {
_state.ReadDataPort = _dataBuffer.front();
_dataBuffer.pop_front();
SetSignals(Req);
} else {
//If there's no data in the buffer, clear the data ready IRQ
_cdrom->ClearIrqSource(PceCdRomIrqSource::DataTransferReady);
if(_state.SectorsToRead == 0) {
//If this is the last sector to read, set the transfer done irq (after a delay)
QueueDriveUpdate(ScsiUpdateType::SetTransferDoneIrq, 1000);
}
}
}
}
uint8_t PceScsiBus::GetCommandSize(ScsiCommand cmd)
{
switch(cmd) {
case ScsiCommand::TestUnitReady:
case ScsiCommand::Read:
return 6;
case ScsiCommand::AudioStartPos:
case ScsiCommand::AudioEndPos:
case ScsiCommand::Pause:
case ScsiCommand::ReadSubCodeQ:
case ScsiCommand::ReadToc:
return 10;
default:
return 0;
}
}
void PceScsiBus::ExecCommand(ScsiCommand cmd)
{
switch(cmd) {
case ScsiCommand::TestUnitReady: CmdTestReadyUnit(); break;
case ScsiCommand::Read: CmdRead(); break;
case ScsiCommand::AudioStartPos: CmdAudioStartPos(); break;
case ScsiCommand::AudioEndPos: CmdAudioEndPos(); break;
case ScsiCommand::Pause: CmdPause(); break;
case ScsiCommand::ReadSubCodeQ: CmdReadSubCodeQ(); break;
case ScsiCommand::ReadToc: CmdReadToc(); break;
}
}
void PceScsiBus::LogCommand(string msg)
{
if(!_emu->IsDebugging()) {
return;
}
msg = "[SCSI] CMD: " + msg + " - ";
for(size_t i = 0, len = _cmdBuffer.size(); i < len; i++) {
msg += " $" + HexUtilities::ToHex(_cmdBuffer[i]);
}
_emu->DebugLog(msg);
}
void PceScsiBus::ProcessCommandPhase()
{
if(_state.Signals[Req] && _state.Signals[Ack]) {
ClearSignals(Req);
_cmdBuffer.push_back(_state.DataPort);
} else if(!_state.Signals[Req] && !_state.Signals[Ack] && _cmdBuffer.size() > 0) {
ScsiCommand cmd = (ScsiCommand)_cmdBuffer[0];
uint8_t cmdSize = GetCommandSize(cmd);
if(cmdSize == 0) {
//Unknown/unsupported command
if(_emu->IsDebugging()) {
LogCommand("Unknown command - " + HexUtilities::ToHex(_cmdBuffer[0]));
}
SetStatusMessage(ScsiStatus::Good, 0);
_cmdBuffer.clear();
} else if(cmdSize <= _cmdBuffer.size()) {
//All bytes received - command has been processed
//LogDebug("[SCSI] Command recv: " + HexUtilities::ToHex(_cmdBuffer, ' '));
ExecCommand(cmd);
_cmdBuffer.clear();
} else {
//Command still requires more byte to be executed
QueueDriveUpdate(ScsiUpdateType::SetReqSignal, 1000);
}
}
}
int64_t PceScsiBus::GetSeekTime(uint32_t startLba, uint32_t targetLba)
{
return _console->GetMasterClockRate() * (PceCdSeekDelay::GetSeekTimeMs(startLba, targetLba) / 1000.0);
}
uint64_t PceScsiBus::GetSectorLoadTime()
{
return (int64_t)((double)_console->GetMasterClockRate() * 2048 / PceScsiBus::ReadBytesPerSecond);
}
void PceScsiBus::CmdRead()
{
uint32_t sector = _cmdBuffer[3] | (_cmdBuffer[2] << 8) | ((_cmdBuffer[1] & 0x1F) << 16);
uint8_t sectorsToRead = _cmdBuffer[4];
if(sectorsToRead == 0) {
LogCommand("Read - No sectors to read");
SetStatusMessage(ScsiStatus::Good, 0);
return;
}
uint32_t fromSector = _cdrom->GetCurrentSector();
uint32_t seekTime = GetSeekTime(fromSector, sector);
_readSectorCounter = seekTime + GetSectorLoadTime();
_needExec = true;
_state.Sector = sector;
_state.SectorsToRead = sectorsToRead;
//Set the phase to "data in" right away
//Ys IV appears to expect this to happen relatively quickly after
//sending the read command to the drive. Otherwise it keeps waiting in a loop
//until the first sector is read, which makes it display single-line graphical
//glitches during the introduction sequence, and pause the animations for a frame
SetPhase(ScsiPhase::DataIn);
_cdrom->GetAudioPlayer().SetIdle();
if(_emu->IsDebugging()) {
uint32_t seekTimeMs = (double)seekTime / _console->GetMasterClockRate() * 1000;
LogCommand("Read - Sector: " + std::to_string(_state.Sector) + " to " + std::to_string(_state.Sector + _state.SectorsToRead - 1) +
" - Seek time (" + std::to_string(fromSector) + "->" + std::to_string(sector) + "): " + std::to_string(seekTimeMs) + " ms");
}
}
uint32_t PceScsiBus::GetAudioLbaPos()
{
switch(_cmdBuffer[9] & 0xC0) {
case 0x00:
return (_cmdBuffer[3] << 16) | (_cmdBuffer[4] << 8) | _cmdBuffer[5];
case 0x40: {
DiscPosition pos;
pos.Minutes = CdReader::FromBcd(_cmdBuffer[2]);
pos.Seconds = CdReader::FromBcd(_cmdBuffer[3]);
pos.Frames = CdReader::FromBcd(_cmdBuffer[4]);
return pos.ToLba() - 150;
}
case 0x80: {
uint8_t track = CdReader::FromBcd(_cmdBuffer[2]);
int32_t sector = _disc->GetTrackFirstSector(track - 1);
return sector >= 0 ? sector : 0;
}
}
LogDebug("[SCSI] CMD: Audio pos - invalid param");
return 0;
}
void PceScsiBus::CmdAudioStartPos()
{
uint32_t startSector = GetAudioLbaPos();
if(_emu->IsDebugging()) {
uint32_t fromSector = _cdrom->GetCurrentSector();
uint32_t seekTime = PceCdSeekDelay::GetSeekTimeMs(_cdrom->GetCurrentSector(), startSector);
LogCommand(
"Audio Start Position - " + std::to_string(startSector) +
" - Seek time (" + std::to_string(fromSector) + "->" + std::to_string(startSector) + "): " + std::to_string(seekTime) + " ms"
);
}
PceCdAudioPlayer& player = _cdrom->GetAudioPlayer();
if(_cmdBuffer[1] == 0) {
player.Play(startSector, true);
} else {
player.Play(startSector, false);
}
ClearSignals(Req);
}
void PceScsiBus::CmdAudioEndPos()
{
uint32_t endSector = GetAudioLbaPos();
PceCdAudioPlayer& player = _cdrom->GetAudioPlayer();
switch(_cmdBuffer[1]) {
case 0: player.Stop(); break;
case 1: player.SetEndPosition(endSector, CdPlayEndBehavior::Loop); break;
case 2: player.SetEndPosition(endSector, CdPlayEndBehavior::Irq); break;
case 3: player.SetEndPosition(endSector, CdPlayEndBehavior::Stop); break;
}
if(_emu->IsDebugging()) {
LogCommand("Audio End Position - " + std::to_string(endSector));
}
SetStatusMessage(ScsiStatus::Good, 0);
}
void PceScsiBus::CmdPause()
{
if(_emu->IsDebugging()) {
LogCommand("Audio Pause");
}
_cdrom->GetAudioPlayer().Pause();
SetStatusMessage(ScsiStatus::Good, 0);
}
void PceScsiBus::CmdTestReadyUnit()
{
if(_emu->IsDebugging()) {
LogCommand("Test Ready Unit");
}
//Emulating this delay accurately just makes booting up games longer and is very unlikely
//to have any impact on games, so the delay is set to a much smaller value. (~100x less)
QueueDriveUpdate(ScsiUpdateType::SetDataInPhase, 150000);
}
void PceScsiBus::CmdReadSubCodeQ()
{
if(_emu->IsDebugging()) {
LogCommand("Read Sub Code Q");
}
_dataBuffer.clear();
PceCdAudioPlayer& player = _cdrom->GetAudioPlayer();
CdAudioStatus audioStatus = player.GetStatus();
uint32_t sector = _cdrom->GetCurrentSector();
int32_t track = _disc->GetTrack(sector);
bool isData = track >= 0 ? _disc->Tracks[track].Format != TrackFormat::Audio : false;
uint8_t adrControl = (
0x01 | //ADR - 1 = "sub-channel Q encodes current position data"
(isData ? 0x40 : 0x00) //Control field - Bit 2: clear = audio track, set = data track
);
uint32_t sectorGap = track >= 0 ? (_disc->Tracks[track].FirstSector - sector) : 0;
DiscPosition relPos = DiscPosition::FromLba(sectorGap);
DiscPosition absPos = DiscPosition::FromLba(sector);
_dataBuffer.push_back((uint8_t)audioStatus);
_dataBuffer.push_back(adrControl); //ADR + Control
_dataBuffer.push_back(CdReader::ToBcd(track + 1)); //track number
_dataBuffer.push_back(1); //index number
_dataBuffer.push_back(CdReader::ToBcd(relPos.Minutes));
_dataBuffer.push_back(CdReader::ToBcd(relPos.Seconds));
_dataBuffer.push_back(CdReader::ToBcd(relPos.Frames));
_dataBuffer.push_back(CdReader::ToBcd(absPos.Minutes));
_dataBuffer.push_back(CdReader::ToBcd(absPos.Seconds));
_dataBuffer.push_back(CdReader::ToBcd(absPos.Frames));
QueueDriveUpdate(ScsiUpdateType::SetDataInPhase, 1000);
}
void PceScsiBus::CmdReadToc()
{
switch(_cmdBuffer[1]) {
case 0:
//Number of tracks
if(_emu->IsDebugging()) {
LogCommand("Read ToC - Number of Tracks");
}
_dataBuffer.clear();
_dataBuffer.push_back(1);
_dataBuffer.push_back(CdReader::ToBcd((uint8_t)_disc->Tracks.size()));
_dataBuffer.push_back(0);
_dataBuffer.push_back(0);
QueueDriveUpdate(ScsiUpdateType::SetDataInPhase, 3000);
break;
case 1: {
//Total disc length
if(_emu->IsDebugging()) {
LogCommand("Read ToC - Disc Length");
}
_dataBuffer.clear();
_dataBuffer.push_back(CdReader::ToBcd(_disc->EndPosition.Minutes));
_dataBuffer.push_back(CdReader::ToBcd(_disc->EndPosition.Seconds));
_dataBuffer.push_back(CdReader::ToBcd(_disc->EndPosition.Frames));
_dataBuffer.push_back(0);
QueueDriveUpdate(ScsiUpdateType::SetDataInPhase, 3000);
break;
}
case 2: {
uint8_t track = CdReader::FromBcd(_cmdBuffer[2]);
if(_emu->IsDebugging()) {
LogCommand("Read ToC - Track #" + std::to_string(track) + " Length");
}
if(track == 0) {
track = 1;
}
DiscPosition pos;
if(track > _disc->Tracks.size()) {
pos = _disc->EndPosition;
} else {
pos = DiscPosition::FromLba(_disc->Tracks[track - 1].StartPosition.ToLba() + 150);
}
_dataBuffer.clear();
_dataBuffer.push_back(CdReader::ToBcd(pos.Minutes));
_dataBuffer.push_back(CdReader::ToBcd(pos.Seconds));
_dataBuffer.push_back(CdReader::ToBcd(pos.Frames));
if(track > _disc->Tracks.size() || _disc->Tracks[track - 1].Format == TrackFormat::Audio) {
_dataBuffer.push_back(0);
} else {
_dataBuffer.push_back(4);
}
QueueDriveUpdate(ScsiUpdateType::SetDataInPhase, 3000);
break;
}
default:
if(_emu->IsDebugging()) {
LogCommand("Read ToC - Unsupported parameters - " + HexUtilities::ToHex(_cmdBuffer[1]));
}
break;
}
}
void PceScsiBus::ProcessDiscRead()
{
if(_readSectorCounter > 0) {
_readSectorCounter -= 3;
if(_readSectorCounter <= 0) {
if(_dataBuffer.empty()) {
//read disc data
_dataBuffer.clear();
_disc->ReadDataSector(_state.Sector, _dataBuffer);
LogDebug("[SCSI] Sector #" + std::to_string(_state.Sector) + " finished reading.");
_state.Sector = (_state.Sector + 1) & 0x1FFFFF;
_state.SectorsToRead--;
//Mark state as changed because this will cause the Req signal
//to be set on the next run of ProcessDataInPhase()
//Otherwise bios code will wait on Req in an infinite loop
_stateChanged = true;
_needExec = true;
_cdrom->SetIrqSource(PceCdRomIrqSource::DataTransferReady);
if(_state.SectorsToRead == 0) {
_readSectorCounter = 0;
LogDebug("[SCSI] Read operation done");
} else {
_readSectorCounter = GetSectorLoadTime();
}
SetPhase(ScsiPhase::DataIn);
} else {
//Software did not read the previous sector's data in time
//There's a time penalty for this (drive most likely needs to re-seek to the position & re-load the sector, etc.)
//Sherlock Holmes triggers this often and seems to want something around 290ms worth of delay in this case
_readSectorCounter = _console->GetMasterClockRate() * (290.0 / 1000.0);
LogDebug("[SCSI] Read sector done but buffer not empty, delay: " + std::to_string(_readSectorCounter * 1000 / _console->GetMasterClockRate()) + " ms");
_needExec = true;
}
}
}
}
uint8_t PceScsiBus::GetStatus()
{
return (
(_state.Signals[Io] ? 0x08 : 0) |
(_state.Signals[Cd] ? 0x10 : 0) |
(_state.Signals[Msg] ? 0x20 : 0) |
(_state.Signals[Req] ? 0x40 : 0) |
(_state.Signals[Bsy] ? 0x80 : 0)
);
}
void PceScsiBus::SetDataPort(uint8_t data)
{
//LogDebug("[SCSI] CPU data port write: " + HexUtilities::ToHex(data));
_state.DataPort = data;
}
uint8_t PceScsiBus::GetDataPort()
{
//LogDebug("[SCSI] CPU data port read: " + HexUtilities::ToHex(_state.DataPort));
switch(_state.Phase) {
case ScsiPhase::Status:
case ScsiPhase::DataIn:
case ScsiPhase::MessageIn:
return _state.ReadDataPort;
default:
return _state.DataPort;
}
}
bool PceScsiBus::CheckSignal(::ScsiSignal::ScsiSignal signal)
{
return _state.Signals[signal];
}
void PceScsiBus::SetSignalValue(::ScsiSignal::ScsiSignal signal, bool val)
{
if(_state.Signals[signal] != val) {
_state.Signals[signal] = val;
_stateChanged = true;
_needExec = true;
}
}
void PceScsiBus::SetAckWithAutoClear()
{
//Set the Ack signal for 45 master clocks (and then clear it again)
//Used after manually reading a byte of data (or after ADPCM DMA reads one)
SetSignals(Ack);
_ackClearCounter = 15*3;
_needExec = true;
}
void PceScsiBus::UpdateState()
{
if(!_stateChanged) {
return;
}
if(_state.Signals[Rst]) {
Reset();
return;
}
do {
_stateChanged = false;
if(_state.Signals[Sel]) {
_updateCounter = 0;
if(_state.Phase != ScsiPhase::DataIn) {
QueueDriveUpdate(ScsiUpdateType::SetCmdPhase, 25000);
} else {
//Aborting an on-going data transfer seems to produce an 8-byte message? (scsitest.pce)
SetStatusMessage(ScsiStatus::Good, 0, 8);
//But eventually it goes back to the command phase on its own? (verificator.pce)
QueueDriveUpdate(ScsiUpdateType::SetCmdPhase, 300000);
break;
}
} else {
switch(_state.Phase) {
case ScsiPhase::Command: ProcessCommandPhase(); break;
case ScsiPhase::DataIn: ProcessDataInPhase(); break;
case ScsiPhase::MessageIn: ProcessMessageInPhase(); break;
case ScsiPhase::Status: ProcessStatusPhase(); break;
}
}
} while(_stateChanged);
}
void PceScsiBus::Exec()
{
if(_ackClearCounter > 0) {
_ackClearCounter -= 3;
if(_ackClearCounter == 0) {
SetSignalValue(Ack, false);
}
}
if(_updateCounter && --_updateCounter == 0) {
switch(_updateType) {
case ScsiUpdateType::SetCmdPhase: SetPhase(ScsiPhase::Command); break;
case ScsiUpdateType::SetReqSignal: SetSignals(Req); break;
case ScsiUpdateType::SetDataInPhase:
SetPhase(ScsiPhase::DataIn);
break;
case ScsiUpdateType::SetTransferDoneIrq:
_cdrom->SetIrqSource(PceCdRomIrqSource::DataTransferDone);
SetStatusMessage(ScsiStatus::Good, 0);
break;
case ScsiUpdateType::SetGoodStatus:
SetStatusMessage(ScsiStatus::Good, 0);
break;
}
}
if(_readSectorCounter > 0) {
ProcessDiscRead();
}
if(_stateChanged) {
UpdateState();
}
_needExec = _stateChanged || _readSectorCounter > 0 || _ackClearCounter > 0 || _updateCounter > 0;
}
void PceScsiBus::Serialize(Serializer& s)
{
for(int i = 0; i < 9; i++) {
SVI(_state.Signals[i]);
}
SV(_state.Phase);
SV(_state.MessageDone);
SV(_state.DataPort);
SV(_state.ReadDataPort);
SV(_state.Sector);
SV(_state.SectorsToRead);
SV(_stateChanged);
SV(_readSectorCounter);
SV(_ackClearCounter);
SV(_updateCounter);
SV(_updateType);
SV(_cmdBuffer);
if(s.IsSaving()) {
vector<uint8_t> dataBuffer = vector<uint8_t>(_dataBuffer.begin(), _dataBuffer.end());
SV(dataBuffer);
} else {
vector<uint8_t> dataBuffer;
SV(dataBuffer);
_dataBuffer.clear();
_dataBuffer.insert(_dataBuffer.end(), dataBuffer.begin(), dataBuffer.end());
_needExec = true;
}
}