BizHawk/BizHawk.Emulation.Cores/Consoles/PC Engine/ScsiCDBus.cs

702 lines
18 KiB
C#

using System;
using System.IO;
using System.Globalization;
using BizHawk.Common;
using BizHawk.Common.NumberExtensions;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Common.Components;
using BizHawk.Emulation.DiscSystem;
namespace BizHawk.Emulation.Cores.PCEngine
{
// TODO we can adjust this to have Think take the number of cycles and not require
// a reference to Cpu.TotalExecutedCycles
// which incidentally would allow us to put it back to an int from a long if we wanted to
public sealed class ScsiCDBus
{
const int STATUS_GOOD = 0;
const int STATUS_CHECK_CONDITION = 1;
const int STATUS_CONDITION_MET = 2;
const int STATUS_BUSY = 4;
const int STATUS_INTERMEDIATE = 8;
const int SCSI_TEST_UNIT_READY = 0x00;
const int SCSI_REQUEST_SENSE = 0x03;
const int SCSI_READ = 0x08;
const int SCSI_AUDIO_START_POS = 0xD8;
const int SCSI_AUDIO_END_POS = 0xD9;
const int SCSI_PAUSE = 0xDA;
const int SCSI_READ_SUBCODE_Q = 0xDD;
const int SCSI_READ_TOC = 0xDE;
bool bsy, sel, cd, io, msg, req, ack, atn, rst;
bool signalsChanged;
public bool BSY
{
get { return bsy; }
set
{
if (value != BSY) signalsChanged = true;
bsy = value;
}
}
public bool SEL
{
get { return sel; }
set
{
if (value != SEL) signalsChanged = true;
sel = value;
}
}
public bool CD // CONTROL = true, DATA = false
{
get { return cd; }
set
{
if (value != CD) signalsChanged = true;
cd = value;
}
}
public bool IO // INPUT = true, OUTPUT = false
{
get { return io; }
set
{
if (value != IO) signalsChanged = true;
io = value;
}
}
public bool MSG
{
get { return msg; }
set
{
if (value != MSG) signalsChanged = true;
msg = value;
}
}
public bool REQ
{
get { return req; }
set
{
if (value != REQ) signalsChanged = true;
req = value;
}
}
public bool ACK
{
get { return ack; }
set
{
if (value != ACK) signalsChanged = true;
ack = value;
}
}
public bool ATN
{
get { return atn; }
set
{
if (value != ATN) signalsChanged = true;
atn = value;
}
}
public bool RST
{
get { return rst; }
set
{
if (value != RST) signalsChanged = true;
rst = value;
}
}
public byte DataBits;
const byte BusPhase_BusFree = 0;
const byte BusPhase_Command = 1;
const byte BusPhase_DataIn = 2;
const byte BusPhase_DataOut = 3;
const byte BusPhase_MessageIn = 4;
const byte BusPhase_MessageOut = 5;
const byte BusPhase_Status = 6;
bool busPhaseChanged;
byte Phase = BusPhase_BusFree;
bool MessageCompleted;
bool StatusCompleted;
byte MessageValue;
QuickList<byte> CommandBuffer = new QuickList<byte>(10); // 10 = biggest command
public QuickQueue<byte> DataIn = new QuickQueue<byte>(2048); // one data sector
// ******** Data Transfer / READ command support ********
public long DataReadWaitTimer;
public bool DataReadInProgress;
public bool DataTransferWasDone;
public bool DataTransferInProgress;
public int CurrentReadingSector;
public int SectorsLeftToRead;
// ******** Resources ********
PCEngine pce;
public Disc disc;
DiscSectorReader DiscSectorReader;
SubchannelQ subchannelQ;
int audioStartLBA;
int audioEndLBA;
public ScsiCDBus(PCEngine pce, Disc disc)
{
this.pce = pce;
this.disc = disc;
DiscSectorReader = new DiscSectorReader(disc);
}
public void Think()
{
if (RST)
{
ResetDevice();
return;
}
if (DataReadInProgress && pce.Cpu.TotalExecutedCycles > DataReadWaitTimer)
{
if (SectorsLeftToRead > 0)
pce.DriveLightOn = true;
if (DataIn.Count == 0)
{
// read in a sector and shove it in the queue
DiscSystem.DiscSectorReader dsr = new DiscSectorReader(disc); //TODO - cache reader
dsr.ReadLBA_2048(CurrentReadingSector, DataIn.GetBuffer(), 0);
DataIn.SignalBufferFilled(2048);
CurrentReadingSector++;
SectorsLeftToRead--;
pce.IntDataTransferReady = true;
// If more sectors, should set the next think-clock to however long it takes to read 1 sector
// but I dont. I dont think transfers actually happen sector by sector
// like this, they probably become available as the bits come off the disc.
// but lets get some basic functionality before we go crazy.
// Idunno, maybe they do come in a sector at a time.
//note to vecna: maybe not at the sector level, but at a level > 1 sample and <= 1 sector, samples come out in blocks
//due to the way they are jumbled up (seriously, like put into a blender) for error correction purposes.
//we may as well assume that the cd audio decoding magic works at the level of one sector, but it isnt one sample.
if (SectorsLeftToRead == 0)
{
DataReadInProgress = false;
DataTransferWasDone = true;
}
SetPhase(BusPhase_DataIn);
}
}
do
{
signalsChanged = false;
busPhaseChanged = false;
if (SEL && !BSY)
{
SetPhase(BusPhase_Command);
}
else if (ATN && !REQ && !ACK)
{
SetPhase(BusPhase_MessageOut);
}
else switch (Phase)
{
case BusPhase_Command: ThinkCommandPhase(); break;
case BusPhase_DataIn: ThinkDataInPhase(); break;
case BusPhase_DataOut: ThinkDataOutPhase(); break;
case BusPhase_MessageIn: ThinkMessageInPhase(); break;
case BusPhase_MessageOut: ThinkMessageOutPhase(); break;
case BusPhase_Status: ThinkStatusPhase(); break;
default: break;
}
} while (signalsChanged || busPhaseChanged);
}
void ResetDevice()
{
CD = false;
IO = false;
MSG = false;
REQ = false;
ACK = false;
ATN = false;
DataBits = 0;
Phase = BusPhase_BusFree;
CommandBuffer.Clear();
DataIn.Clear();
DataReadInProgress = false;
pce.CDAudio.Stop();
}
void ThinkCommandPhase()
{
if (REQ && ACK)
{
CommandBuffer.Add(DataBits);
REQ = false;
}
if (!REQ && !ACK && CommandBuffer.Count > 0)
{
bool complete = CheckCommandBuffer();
if (complete)
{
CommandBuffer.Clear();
}
else
{
REQ = true; // needs more data!
}
}
}
void ThinkDataInPhase()
{
if (REQ && ACK)
{
REQ = false;
}
else if (!REQ && !ACK)
{
if (DataIn.Count > 0)
{
DataBits = DataIn.Dequeue();
REQ = true;
}
else
{
// data transfer is finished
pce.IntDataTransferReady = false;
if (DataTransferWasDone)
{
DataTransferInProgress = false;
DataTransferWasDone = false;
pce.IntDataTransferComplete = true;
}
SetStatusMessage(STATUS_GOOD, 0);
}
}
}
void ThinkDataOutPhase()
{
Console.WriteLine("*********** DATA OUT PHASE, DOES THIS HAPPEN? ****************");
SetPhase(BusPhase_BusFree);
}
void ThinkMessageInPhase()
{
if (REQ && ACK)
{
REQ = false;
MessageCompleted = true;
}
if (!REQ && !ACK && MessageCompleted)
{
MessageCompleted = false;
SetPhase(BusPhase_BusFree);
}
}
void ThinkMessageOutPhase()
{
Console.WriteLine("******* IN MESSAGE OUT PHASE. DOES THIS EVER HAPPEN? ********");
SetPhase(BusPhase_BusFree);
}
void ThinkStatusPhase()
{
if (REQ && ACK)
{
REQ = false;
StatusCompleted = true;
}
if (!REQ && !ACK && StatusCompleted)
{
StatusCompleted = false;
DataBits = MessageValue;
SetPhase(BusPhase_MessageIn);
}
}
// returns true if command completed, false if more data bytes needed
bool CheckCommandBuffer()
{
switch (CommandBuffer[0])
{
case SCSI_TEST_UNIT_READY:
if (CommandBuffer.Count < 6) return false;
SetStatusMessage(STATUS_GOOD, 0);
return true;
case SCSI_READ:
if (CommandBuffer.Count < 6) return false;
CommandRead();
return true;
case SCSI_AUDIO_START_POS:
if (CommandBuffer.Count < 10) return false;
CommandAudioStartPos();
return true;
case SCSI_AUDIO_END_POS:
if (CommandBuffer.Count < 10) return false;
CommandAudioEndPos();
return true;
case SCSI_PAUSE:
if (CommandBuffer.Count < 10) return false;
CommandPause();
return true;
case SCSI_READ_SUBCODE_Q:
if (CommandBuffer.Count < 10) return false;
CommandReadSubcodeQ();
return true;
case SCSI_READ_TOC:
if (CommandBuffer.Count < 10) return false;
CommandReadTOC();
return true;
default:
Console.WriteLine("UNRECOGNIZED SCSI COMMAND! {0:X2}", CommandBuffer[0]);
SetStatusMessage(STATUS_GOOD, 0);
break;
}
return false;
}
void CommandRead()
{
int sector = (CommandBuffer[1] & 0x1f) << 16;
sector |= CommandBuffer[2] << 8;
sector |= CommandBuffer[3];
DataReadInProgress = true;
DataTransferInProgress = true;
CurrentReadingSector = sector;
SectorsLeftToRead = CommandBuffer[4];
if (CommandBuffer[4] == 0)
SectorsLeftToRead = 256;
DataReadWaitTimer = pce.Cpu.TotalExecutedCycles + 5000; // figure out proper read delay later
pce.CDAudio.Stop();
}
void CommandAudioStartPos()
{
switch (CommandBuffer[9] & 0xC0)
{
case 0x00: // Set start offset in LBA units
audioStartLBA = (CommandBuffer[3] << 16) | (CommandBuffer[4] << 8) | CommandBuffer[5];
break;
case 0x40: // Set start offset in absolute MSF units
byte m = CommandBuffer[2].BCDtoBin();
byte s = CommandBuffer[3].BCDtoBin();
byte f = CommandBuffer[4].BCDtoBin();
audioStartLBA = DiscUtils.Convert_AMSF_To_LBA(m, s, f);
break;
case 0x80: // Set start offset in track units
byte trackNo = CommandBuffer[2].BCDtoBin();
audioStartLBA = disc.Session1.Tracks[trackNo].LBA;
break;
}
if (CommandBuffer[1] == 0)
{
pce.CDAudio.PlayStartingAtLba(audioStartLBA);
pce.CDAudio.Pause();
}
else
{
pce.CDAudio.PlayStartingAtLba(audioStartLBA);
}
SetStatusMessage(STATUS_GOOD, 0);
pce.IntDataTransferComplete = true;
}
void CommandAudioEndPos()
{
switch (CommandBuffer[9] & 0xC0)
{
case 0x00: // Set end offset in LBA units
audioEndLBA = (CommandBuffer[3] << 16) | (CommandBuffer[4] << 8) | CommandBuffer[5];
break;
case 0x40: // Set end offset in absolute MSF units
byte m = CommandBuffer[2].BCDtoBin();
byte s = CommandBuffer[3].BCDtoBin();
byte f = CommandBuffer[4].BCDtoBin();
audioEndLBA = DiscUtils.Convert_AMSF_To_LBA(m, s, f);
break;
case 0x80: // Set end offset in track units
byte trackNo = CommandBuffer[2].BCDtoBin();
if (trackNo - 1 >= disc.Session1.Tracks.Count)
audioEndLBA = disc.Session1.LeadoutLBA;
else
audioEndLBA = disc.Session1.Tracks[trackNo].LBA;
break;
}
switch (CommandBuffer[1])
{
case 0: // end immediately
pce.CDAudio.Stop();
break;
case 1: // play in loop mode. I guess this constitues A-B looping
pce.CDAudio.PlayStartingAtLba(audioStartLBA);
pce.CDAudio.EndLBA = audioEndLBA;
pce.CDAudio.PlayMode = CDAudio.PlaybackMode_LoopOnCompletion;
break;
case 2: // Play audio, fire IRQ2 when end position reached, maybe
pce.CDAudio.PlayStartingAtLba(audioStartLBA);
pce.CDAudio.EndLBA = audioEndLBA;
pce.CDAudio.PlayMode = CDAudio.PlaybackMode_CallbackOnCompletion;
break;
case 3: // Play normal
pce.CDAudio.PlayStartingAtLba(audioStartLBA);
pce.CDAudio.EndLBA = audioEndLBA;
pce.CDAudio.PlayMode = CDAudio.PlaybackMode_StopOnCompletion;
break;
}
SetStatusMessage(STATUS_GOOD, 0);
}
void CommandPause()
{
pce.CDAudio.Stop();
SetStatusMessage(STATUS_GOOD, 0);
}
void CommandReadSubcodeQ()
{
bool playing = pce.CDAudio.Mode != CDAudio.CDAudioMode_Stopped;
int sectorNum = playing ? pce.CDAudio.CurrentSector : CurrentReadingSector;
DataIn.Clear();
switch (pce.CDAudio.Mode)
{
case CDAudio.CDAudioMode_Playing: DataIn.Enqueue(0); break;
case CDAudio.CDAudioMode_Paused: DataIn.Enqueue(2); break;
case CDAudio.CDAudioMode_Stopped: DataIn.Enqueue(3); break;
}
DiscSectorReader.ReadLBA_SubQ(sectorNum, out subchannelQ);
DataIn.Enqueue(subchannelQ.q_status); //status (control and q-mode; control is useful to know if it's a data or audio track)
DataIn.Enqueue(subchannelQ.q_tno.BCDValue); // track //zero 03-jul-2015 - did I adapt this right>
DataIn.Enqueue(subchannelQ.q_index.BCDValue); // index //zero 03-jul-2015 - did I adapt this right>
DataIn.Enqueue(subchannelQ.min.BCDValue); // M(rel)
DataIn.Enqueue(subchannelQ.sec.BCDValue); // S(rel)
DataIn.Enqueue(subchannelQ.frame.BCDValue); // F(rel)
DataIn.Enqueue(subchannelQ.ap_min.BCDValue); // M(abs)
DataIn.Enqueue(subchannelQ.ap_sec.BCDValue); // S(abs)
DataIn.Enqueue(subchannelQ.ap_frame.BCDValue); // F(abs)
SetPhase(BusPhase_DataIn);
}
void CommandReadTOC()
{
switch (CommandBuffer[1])
{
case 0: // return number of tracks
{
DataIn.Clear();
DataIn.Enqueue(0x01);
DataIn.Enqueue(((byte)disc.Session1.Tracks.Count).BinToBCD());
SetPhase(BusPhase_DataIn);
break;
}
case 1: // return total disc length in minutes/seconds/frames
{
//zero 07-jul-2015 - I may have broken this
int totalLbaLength = disc.Session1.LeadoutLBA;
byte m, s, f;
DiscUtils.Convert_LBA_To_AMSF(totalLbaLength + 150, out m, out s, out f);
DataIn.Clear();
DataIn.Enqueue(m.BinToBCD());
DataIn.Enqueue(s.BinToBCD());
DataIn.Enqueue(f.BinToBCD());
SetPhase(BusPhase_DataIn);
break;
}
case 2: // Return starting position of specified track in MSF format. TODO - did zero adapt this right? track indexing might be off
{
int track = CommandBuffer[2].BCDtoBin();
var tracks = disc.Session1.Tracks;
if (CommandBuffer[2] > 0x99)
throw new Exception("invalid track number BCD request... is something I need to handle?");
if (track == 0) track = 1;
int lbaPos;
if (track > disc.Session1.InformationTrackCount)
lbaPos = disc.Session1.LeadoutLBA; //zero 03-jul-2015 - did I adapt this right?
else
lbaPos = tracks[track].LBA;
byte m, s, f;
DiscUtils.Convert_LBA_To_AMSF(lbaPos, out m, out s, out f);
DataIn.Clear();
DataIn.Enqueue(m.BinToBCD());
DataIn.Enqueue(s.BinToBCD());
DataIn.Enqueue(f.BinToBCD());
if (track > tracks.Count || disc.Session1.Tracks[track].IsAudio)
DataIn.Enqueue(0);
else
DataIn.Enqueue(4);
SetPhase(BusPhase_DataIn);
break;
}
default:
Console.WriteLine("unimplemented READ TOC command argument!");
break;
}
}
void SetStatusMessage(byte status, byte message)
{
MessageValue = message;
StatusCompleted = false;
MessageCompleted = false;
DataBits = status == STATUS_GOOD ? (byte)0x00 : (byte)0x01;
SetPhase(BusPhase_Status);
}
void SetPhase(byte phase)
{
if (Phase == phase)
return;
Phase = phase;
busPhaseChanged = true;
switch (phase)
{
case BusPhase_BusFree:
BSY = false;
MSG = false;
CD = false;
IO = false;
REQ = false;
pce.IntDataTransferComplete = false;
break;
case BusPhase_Command:
BSY = true;
MSG = false;
CD = true;
IO = false;
REQ = true;
break;
case BusPhase_DataIn:
BSY = true;
MSG = false;
CD = false;
IO = true;
REQ = false;
break;
case BusPhase_DataOut:
BSY = true;
MSG = false;
CD = false;
IO = false;
REQ = true;
break;
case BusPhase_MessageIn:
BSY = true;
MSG = true;
CD = true;
IO = true;
REQ = true;
break;
case BusPhase_MessageOut:
BSY = true;
MSG = true;
CD = true;
IO = false;
REQ = true;
break;
case BusPhase_Status:
BSY = true;
MSG = false;
CD = true;
IO = true;
REQ = true;
break;
}
}
// ***************************************************************************
public void SyncState(Serializer ser)
{
ser.BeginSection("SCSI");
ser.Sync("BSY", ref bsy);
ser.Sync("SEL", ref sel);
ser.Sync("CD", ref cd);
ser.Sync("IO", ref io);
ser.Sync("MSG", ref msg);
ser.Sync("REQ", ref req);
ser.Sync("ACK", ref ack);
ser.Sync("ATN", ref atn);
ser.Sync("RST", ref rst);
ser.Sync("DataBits", ref DataBits);
ser.Sync("Phase", ref Phase);
ser.Sync("MessageCompleted", ref MessageCompleted);
ser.Sync("StatusCompleted", ref StatusCompleted);
ser.Sync("MessageValue", ref MessageValue);
ser.Sync("DataReadWaitTimer", ref DataReadWaitTimer);
ser.Sync("DataReadInProgress", ref DataReadInProgress);
ser.Sync("DataTransferWasDone", ref DataTransferWasDone);
ser.Sync("DataTransferInProgress", ref DataTransferInProgress);
ser.Sync("CurrentReadingSector", ref CurrentReadingSector);
ser.Sync("SectorsLeftToRead", ref SectorsLeftToRead);
ser.Sync("CommandBuffer", ref CommandBuffer.buffer, false);
ser.Sync("CommandBufferPosition", ref CommandBuffer.Position);
ser.Sync("DataInBuffer", ref DataIn.buffer, false);
ser.Sync("DataInHead", ref DataIn.head);
ser.Sync("DataInTail", ref DataIn.tail);
ser.Sync("DataInSize", ref DataIn.size);
ser.Sync("AudioStartLBA", ref audioStartLBA);
ser.Sync("AudioEndLBA", ref audioEndLBA);
ser.EndSection();
}
}
}