mirror of
https://github.com/emu-russia/pureikyubu.git
synced 2025-04-02 10:42:15 -04:00
1963 lines
41 KiB
C++
1963 lines
41 KiB
C++
// DVD API for emulator
|
|
#include "pch.h"
|
|
|
|
DVDControl dvd;
|
|
|
|
using namespace Debug;
|
|
|
|
namespace DVD
|
|
{
|
|
// Mount current dvd
|
|
bool MountFile(const wchar_t* file)
|
|
{
|
|
// try to open file
|
|
if (!Util::FileExists(file))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Unmount();
|
|
|
|
// select current DVD
|
|
bool res = GCMMountFile(file);
|
|
if (!res)
|
|
return res;
|
|
|
|
// init filesystem
|
|
res = dvd_fs_init();
|
|
if (!res)
|
|
{
|
|
GCMMountFile(nullptr);
|
|
return res;
|
|
}
|
|
|
|
Seek(0);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MountFile(const std::string& file)
|
|
{
|
|
wchar_t path[0x1000] = { 0, };
|
|
wchar_t* tcharPtr = path;
|
|
char* ansiPtr = (char *)file.c_str();
|
|
while (*ansiPtr)
|
|
{
|
|
*tcharPtr++ = *ansiPtr++;
|
|
}
|
|
*tcharPtr++ = 0;
|
|
return MountFile(path);
|
|
}
|
|
|
|
bool MountFile(const std::wstring& file)
|
|
{
|
|
wchar_t path[0x1000] = { 0, };
|
|
wchar_t* tcharPtr = path;
|
|
wchar_t* widePtr = (wchar_t*)file.c_str();
|
|
while (*widePtr)
|
|
{
|
|
*tcharPtr++ = *widePtr++;
|
|
}
|
|
*tcharPtr++ = 0;
|
|
return MountFile(path);
|
|
}
|
|
|
|
bool MountSdk(const wchar_t* path)
|
|
{
|
|
Unmount();
|
|
|
|
dvd.mountedSdk = new MountDolphinSdk(path);
|
|
|
|
if (!dvd.mountedSdk->Mounted())
|
|
{
|
|
delete dvd.mountedSdk;
|
|
dvd.mountedSdk = nullptr;
|
|
return false;
|
|
}
|
|
|
|
// init filesystem
|
|
if (!dvd_fs_init())
|
|
{
|
|
delete dvd.mountedSdk;
|
|
dvd.mountedSdk = nullptr;
|
|
return false;
|
|
}
|
|
|
|
dvd.mountedSdk->Seek(0);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MountSdk(std::string path)
|
|
{
|
|
wchar_t tcharStr[0x1000] = { 0, };
|
|
wchar_t* tcharPtr = tcharStr;
|
|
char* ansiPtr = (char*)path.c_str();
|
|
while (*ansiPtr)
|
|
{
|
|
*tcharPtr++ = *ansiPtr++;
|
|
}
|
|
*tcharPtr++ = 0;
|
|
return MountSdk(tcharStr);
|
|
}
|
|
|
|
// Unmount
|
|
void Unmount()
|
|
{
|
|
GCMMountFile(nullptr);
|
|
|
|
if (dvd.mountedSdk)
|
|
{
|
|
delete dvd.mountedSdk;
|
|
dvd.mountedSdk = nullptr;
|
|
}
|
|
|
|
dvd_fs_shutdown();
|
|
}
|
|
|
|
bool IsMounted()
|
|
{
|
|
return (dvd.mountedImage || dvd.mountedSdk != nullptr);
|
|
}
|
|
|
|
// dvd operations on current mounted dvd
|
|
|
|
void Seek(int position)
|
|
{
|
|
if (dvd.mountedImage)
|
|
{
|
|
GCMSeek(position);
|
|
}
|
|
else if (dvd.mountedSdk)
|
|
{
|
|
dvd.mountedSdk->Seek(position);
|
|
}
|
|
}
|
|
|
|
int GetSeek()
|
|
{
|
|
if (dvd.mountedImage)
|
|
{
|
|
return dvd.seekval;
|
|
}
|
|
else if (dvd.mountedSdk)
|
|
{
|
|
return dvd.mountedSdk->GetSeek();
|
|
}
|
|
else return 0;
|
|
}
|
|
|
|
bool Read(void *buffer, size_t length)
|
|
{
|
|
if (length == 0)
|
|
return true;
|
|
|
|
if (dvd.mountedImage)
|
|
{
|
|
return GCMRead((uint8_t*)buffer, length);
|
|
}
|
|
else if (dvd.mountedSdk)
|
|
{
|
|
return dvd.mountedSdk->Read((uint8_t*)buffer, length);
|
|
}
|
|
else
|
|
{
|
|
memset(buffer, 0, length); // fill by zeroes
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
long OpenFile(std::string& dvdfile)
|
|
{
|
|
if (dvd.mountedImage || dvd.mountedSdk)
|
|
{
|
|
// call DVD filesystem open
|
|
return dvd_open(dvdfile.data());
|
|
}
|
|
|
|
// Not mounted
|
|
return 0;
|
|
}
|
|
|
|
// Call somewhere
|
|
|
|
void InitSubsystem()
|
|
{
|
|
JDI::Hub.AddNode(DDU_JDI_JSON, DvdCommandsReflector);
|
|
|
|
DDU = new DduCore;
|
|
}
|
|
|
|
void ShutdownSubsystem()
|
|
{
|
|
Unmount();
|
|
JDI::Hub.RemoveNode(DDU_JDI_JSON);
|
|
delete DDU;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
Code for mounting the Dolphin SDK folder as a virtual disk.
|
|
|
|
All the necessary data (BI2, Appldr, some DOL executable, we take from the SDK). If they are not there, then the disk is simply not mounted.
|
|
*/
|
|
|
|
namespace DVD
|
|
{
|
|
MountDolphinSdk::MountDolphinSdk(const wchar_t* DolphinSDKPath)
|
|
{
|
|
wcscpy(directory, DolphinSDKPath);
|
|
|
|
// Load dvddata structure.
|
|
// TODO: Generate Json dynamically
|
|
auto dvdDataInfoText = Util::FileLoad(DvdDataJson);
|
|
if (dvdDataInfoText.empty())
|
|
{
|
|
Report(Channel::Norm, "Failed to load DolphinSDK dvddata json: %s\n", Util::WstringToString(DvdDataJson).c_str());
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
DvdDataInfo.Deserialize(dvdDataInfoText.data(), dvdDataInfoText.size());
|
|
}
|
|
catch (...)
|
|
{
|
|
Report(Channel::Norm, "Failed to Deserialize DolphinSDK dvddata json: %s\n", Util::WstringToString(DvdDataJson).c_str());
|
|
return;
|
|
}
|
|
|
|
// Generate data blobs
|
|
if (!GenDiskId())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenDiskId\n");
|
|
return;
|
|
}
|
|
if (!GenApploader())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenApploader\n");
|
|
return;
|
|
}
|
|
if (!GenBi2())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenBi2\n");
|
|
return;
|
|
}
|
|
if (!GenFst())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenFst\n");
|
|
return;
|
|
}
|
|
if (!GenDol())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenDol\n");
|
|
return;
|
|
}
|
|
if (!GenBb2())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenBb2\n");
|
|
return;
|
|
}
|
|
|
|
// Generate mapping
|
|
|
|
if (!GenMap())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenMap\n");
|
|
return;
|
|
}
|
|
if (!GenFileMap())
|
|
{
|
|
Report(Channel::Norm, "Failed to GenFileMap\n");
|
|
return;
|
|
}
|
|
|
|
Report(Channel::DVD, "DolphinSDK mounted!\n");
|
|
mounted = true;
|
|
}
|
|
|
|
MountDolphinSdk::~MountDolphinSdk()
|
|
{
|
|
}
|
|
|
|
void MountDolphinSdk::MapVector(std::vector<uint8_t>& v, uint32_t offset)
|
|
{
|
|
std::tuple<std::vector<uint8_t>&, uint32_t, size_t> entry(v, offset, v.size());
|
|
mapping.push_back(entry);
|
|
}
|
|
|
|
void MountDolphinSdk::MapFile(wchar_t* path, uint32_t offset)
|
|
{
|
|
size_t size = Util::FileSize(path);
|
|
std::tuple<wchar_t*, uint32_t, size_t> entry(path, offset, size);
|
|
fileMapping.push_back(entry);
|
|
}
|
|
|
|
// Check memory mapping
|
|
uint8_t* MountDolphinSdk::TranslateMemory(uint32_t offset, size_t requestedSize, size_t& maxSize)
|
|
{
|
|
for (auto it = mapping.begin(); it != mapping.end(); ++it)
|
|
{
|
|
uint8_t* ptr = std::get<0>(*it).data();
|
|
uint32_t startingOffset = std::get<1>(*it);
|
|
size_t size = std::get<2>(*it);
|
|
|
|
if (startingOffset <= offset && offset < (startingOffset + size))
|
|
{
|
|
maxSize = my_min(requestedSize, (startingOffset + size) - offset);
|
|
return ptr + (offset - startingOffset);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Check file mapping
|
|
FILE* MountDolphinSdk::TranslateFile(uint32_t offset, size_t requestedSize, size_t& maxSize)
|
|
{
|
|
for (auto it = fileMapping.begin(); it != fileMapping.end(); ++it)
|
|
{
|
|
wchar_t* file = std::get<0>(*it);
|
|
uint32_t startingOffset = std::get<1>(*it);
|
|
size_t size = std::get<2>(*it);
|
|
|
|
if (startingOffset <= offset && offset < (startingOffset + size))
|
|
{
|
|
maxSize = my_min(requestedSize, (startingOffset + size) - offset);
|
|
|
|
FILE* f;
|
|
f = fopen(Util::WstringToString(file).c_str(), "rb");
|
|
assert(f);
|
|
|
|
fseek(f, offset - startingOffset, SEEK_SET);
|
|
return f;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void MountDolphinSdk::Seek(int position)
|
|
{
|
|
if (!mounted)
|
|
return;
|
|
|
|
assert(position >= 0 && position < DVD_SIZE);
|
|
|
|
currentSeek = (uint32_t)position;
|
|
}
|
|
|
|
bool MountDolphinSdk::Read(void* buffer, size_t length)
|
|
{
|
|
bool result = true;
|
|
|
|
assert(buffer);
|
|
|
|
if (!mounted)
|
|
{
|
|
memset(buffer, 0, length);
|
|
return true;
|
|
}
|
|
|
|
if (currentSeek >= DVD_SIZE)
|
|
{
|
|
memset(buffer, 0, length);
|
|
return false;
|
|
}
|
|
|
|
size_t maxLength = 0;
|
|
|
|
// First, try to enter the mapped binary blob, if it doesn't work, try the mapped file.
|
|
|
|
uint8_t* ptr = TranslateMemory(currentSeek, length, maxLength);
|
|
if (ptr != nullptr)
|
|
{
|
|
memcpy(buffer, ptr, maxLength);
|
|
if (maxLength < length)
|
|
{
|
|
memset((uint8_t*)buffer + maxLength, 0, length - maxLength);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FILE* f = TranslateFile(currentSeek, length, maxLength);
|
|
if (f != nullptr)
|
|
{
|
|
fread(buffer, 1, maxLength, f);
|
|
if (maxLength < length)
|
|
{
|
|
memset((uint8_t*)buffer + maxLength, 0, length - maxLength);
|
|
}
|
|
fclose(f);
|
|
}
|
|
else
|
|
{
|
|
// None of the options came up - return zeros.
|
|
|
|
memset(buffer, 0, length);
|
|
result = false;
|
|
}
|
|
}
|
|
|
|
currentSeek += (uint32_t)length;
|
|
return result;
|
|
}
|
|
|
|
#pragma region "Data Generators"
|
|
|
|
// In addition to the actual files, the DVD image also contains a number of important binary data: DiskID, Apploader image, main program (DOL), BootInfo2 and BootBlock2 structures and FST.
|
|
// Generating almost all blobs is straightforward, with the exception of the FST, which will have to tinker with.
|
|
|
|
bool MountDolphinSdk::GenDiskId()
|
|
{
|
|
DiskId.resize(sizeof(DiskID));
|
|
|
|
DiskID* id = (DiskID*)DiskId.data();
|
|
|
|
id->gameName[0] = 'S';
|
|
id->gameName[1] = 'D';
|
|
id->gameName[2] = 'K';
|
|
id->gameName[3] = 'E';
|
|
|
|
id->company[0] = '0';
|
|
id->company[1] = '1';
|
|
|
|
id->magicNumber = _BYTESWAP_UINT32(DVD_DISKID_MAGIC);
|
|
|
|
GameName.resize(0x400);
|
|
memset(GameName.data(), 0, GameName.size());
|
|
strcpy((char*)GameName.data(), "GameCube SDK");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MountDolphinSdk::GenApploader()
|
|
{
|
|
AppldrData = Util::FileLoad(std::wstring(directory) + std::wstring(AppldrPath));
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unfortunately, all demos in the SDK are in ELF format. Therefore, we will use PONG.DOL as the main program, which is included in each release and is a full resident of the project :p
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
bool MountDolphinSdk::GenDol()
|
|
{
|
|
Dol = Util::FileLoad(DolPath);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MountDolphinSdk::GenBi2()
|
|
{
|
|
Bi2Data = Util::FileLoad(std::wstring(directory) + std::wstring(Bi2Path));
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a string with the name of the entry (directory or file name) to the NameTable.
|
|
/// </summary>
|
|
/// <param name="str"></param>
|
|
void MountDolphinSdk::AddString(std::string str)
|
|
{
|
|
for (auto& c : str)
|
|
{
|
|
NameTableData.push_back(c);
|
|
}
|
|
|
|
NameTableData.push_back(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process original Json with dvddata directory structure. The Json structure is designed to accommodate the weird FST feature when a directory is in the middle of files.
|
|
/// For a more detailed description of this oddity, see `dolwin-docs\RE\DVD\FSTNotes.md`.
|
|
/// The method is recursive tree descent.
|
|
/// In the process, meta information is added to the original Json structure, which is used by the `WalkAndGenerateFst` method to create the final binary FST blob.
|
|
/// </summary>
|
|
/// <param name="entry"></param>
|
|
void MountDolphinSdk::ParseDvdDataEntryForFst(Json::Value* entry)
|
|
{
|
|
Json::Value* parent = nullptr;
|
|
|
|
if (entry->type != Json::ValueType::Object)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (entry->children.size() != 0)
|
|
{
|
|
// Directory
|
|
|
|
// Save directory name offset
|
|
|
|
size_t nameOffset = NameTableData.size();
|
|
if (entry->name)
|
|
{
|
|
// Root has no name
|
|
AddString(entry->name);
|
|
}
|
|
entry->AddInt("nameOffset", (int)nameOffset);
|
|
entry->AddBool("dir", true);
|
|
|
|
// Save current FST index for directory
|
|
|
|
entry->AddInt("entryId", entryCounter);
|
|
entryCounter++;
|
|
|
|
// Reset totalChildren counter
|
|
|
|
entry->AddInt("totalChildren", 0);
|
|
}
|
|
else
|
|
{
|
|
// File.
|
|
// Differs from a directory in that it has no descendants
|
|
|
|
assert(entry->name);
|
|
std::string path = entry->name;
|
|
|
|
size_t nameOffset = NameTableData.size();
|
|
AddString(path);
|
|
|
|
parent = entry->parent;
|
|
|
|
// Save file name offset
|
|
|
|
entry->AddInt("nameOffset", (int)nameOffset);
|
|
|
|
// Generate full path to file
|
|
|
|
do
|
|
{
|
|
if (parent->ByName("dir"))
|
|
{
|
|
path = (parent->name ? parent->name + std::string("/") : "/") + path;
|
|
}
|
|
parent = parent->parent;
|
|
|
|
} while (parent != nullptr);
|
|
|
|
assert(path.size() < DVD_MAXPATH);
|
|
|
|
wchar_t filePath[0x1000] = { 0, };
|
|
|
|
wcscat(filePath, directory);
|
|
wcscat(filePath, FilesRoot);
|
|
wchar_t* filePathPtr = filePath + wcslen(filePath);
|
|
|
|
for (size_t i = 0; i < path.size(); i++)
|
|
{
|
|
*filePathPtr++ = (wchar_t)path[i];
|
|
}
|
|
*filePathPtr++ = 0;
|
|
|
|
//Report(Channel::Norm, "Processing file: %s\n", path.c_str());
|
|
|
|
// Save file offset
|
|
|
|
entry->AddInt("fileOffset", userFilesStart + userFilesOffset);
|
|
|
|
// Save file size
|
|
|
|
size_t fileSize = Util::FileSize(filePath);
|
|
entry->AddInt("fileSize", (int)fileSize);
|
|
|
|
userFilesOffset += RoundUp32((uint32_t)fileSize);
|
|
|
|
// Save file path
|
|
|
|
entry->AddString("filePath", filePath);
|
|
|
|
// Adjust entry counter
|
|
|
|
entry->AddInt("entryId", entryCounter);
|
|
entryCounter++;
|
|
}
|
|
|
|
// Update parent sibling counters
|
|
|
|
parent = entry->parent;
|
|
while (parent)
|
|
{
|
|
Json::Value* totalChildren = parent->ByName("totalChildren");
|
|
|
|
if (totalChildren) totalChildren->value.AsInt++;
|
|
|
|
parent = parent->parent; // :p
|
|
}
|
|
|
|
// Recursively process descendants
|
|
|
|
if (entry->ByName("dir") != nullptr)
|
|
{
|
|
for (auto it = entry->children.begin(); it != entry->children.end(); ++it)
|
|
{
|
|
ParseDvdDataEntryForFst(*it);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Based on the Json structure with the data of the dvddata directory tree, in which meta-information is added, the final FST binary blob is built.
|
|
/// </summary>
|
|
/// <param name="entry"></param>
|
|
void MountDolphinSdk::WalkAndGenerateFst(Json::Value* entry)
|
|
{
|
|
DVDFileEntry fstEntry = { 0 };
|
|
|
|
if (entry->type != Json::ValueType::Object)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Json::Value* isDir = entry->ByName("dir");
|
|
|
|
if (isDir)
|
|
{
|
|
// Directory
|
|
|
|
fstEntry.isDir = 1;
|
|
|
|
Json::Value* nameOffset = entry->ByName("nameOffset");
|
|
assert(nameOffset);
|
|
|
|
if (nameOffset)
|
|
{
|
|
fstEntry.nameOffsetHi = (uint8_t)(nameOffset->value.AsInt >> 16);
|
|
fstEntry.nameOffsetLo = _BYTESWAP_UINT16((uint16_t)nameOffset->value.AsInt);
|
|
}
|
|
|
|
if (entry->parent)
|
|
{
|
|
Json::Value* parentId = entry->parent->ByName("entryId");
|
|
if (parentId)
|
|
{
|
|
fstEntry.parentOffset = _BYTESWAP_UINT32((uint32_t)parentId->value.AsInt);
|
|
}
|
|
}
|
|
|
|
Json::Value* entryId = entry->ByName("entryId");
|
|
Json::Value* totalChildren = entry->ByName("totalChildren");
|
|
|
|
if (entryId && totalChildren)
|
|
{
|
|
fstEntry.nextOffset = _BYTESWAP_UINT32((uint32_t)(entryId->value.AsInt + totalChildren->value.AsInt) + 1);
|
|
}
|
|
|
|
FstData.insert(FstData.end(), (uint8_t*)&fstEntry, (uint8_t*)&fstEntry + sizeof(fstEntry));
|
|
|
|
if (logMount)
|
|
{
|
|
Report(Channel::Norm, "%d: directory: %s. nextOffset: %d\n",
|
|
entryId->value.AsInt, entry->name, _BYTESWAP_UINT32(fstEntry.nextOffset));
|
|
}
|
|
|
|
for (auto it = entry->children.begin(); it != entry->children.end(); ++it)
|
|
{
|
|
WalkAndGenerateFst(*it);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// File
|
|
|
|
Json::Value* entryId = entry->ByName("entryId");
|
|
Json::Value* nameOffsetValue = entry->ByName("nameOffset");
|
|
Json::Value* fileOffsetValue = entry->ByName("fileOffset");
|
|
Json::Value* fileSizeValue = entry->ByName("fileSize");
|
|
assert(nameOffsetValue && fileOffsetValue && fileSizeValue);
|
|
|
|
fstEntry.isDir = 0;
|
|
|
|
uint32_t nameOffset = (uint32_t)(nameOffsetValue->value.AsInt);
|
|
uint32_t fileOffset = (uint32_t)(fileOffsetValue->value.AsInt);
|
|
uint32_t fileSize = (uint32_t)(fileSizeValue->value.AsInt);
|
|
|
|
fstEntry.nameOffsetHi = (uint8_t)(nameOffset >> 16);
|
|
fstEntry.nameOffsetLo = _BYTESWAP_UINT16((uint16_t)nameOffset);
|
|
|
|
fstEntry.fileOffset = _BYTESWAP_UINT32(fileOffset);
|
|
fstEntry.fileLength = _BYTESWAP_UINT32(fileSize);
|
|
|
|
FstData.insert(FstData.end(), (uint8_t*)&fstEntry, (uint8_t*)&fstEntry + sizeof(fstEntry));
|
|
|
|
if (logMount)
|
|
{
|
|
Report(Channel::Norm, "%d: file: %s\n", entryId->value.AsInt, entry->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The basic idea behind generating FST is to walk by DvdDataJson.
|
|
// When traversing a structure a specific meta-information is attached to each node.
|
|
// After generation, this meta-information is collected in a final collection (FST).
|
|
bool MountDolphinSdk::GenFst()
|
|
{
|
|
try
|
|
{
|
|
ParseDvdDataEntryForFst(DvdDataInfo.root.children.back());
|
|
}
|
|
catch (...)
|
|
{
|
|
Report(Channel::Norm, "ParseDvdDataEntryForFst failed!\n");
|
|
return false;
|
|
}
|
|
|
|
if (logMount)
|
|
{
|
|
JDI::Hub.Dump(DvdDataInfo.root.children.back());
|
|
}
|
|
|
|
try
|
|
{
|
|
WalkAndGenerateFst(DvdDataInfo.root.children.back());
|
|
}
|
|
catch (...)
|
|
{
|
|
Report(Channel::Norm, "WalkAndGenerateFst failed!\n");
|
|
return false;
|
|
}
|
|
|
|
// Add Name Table to the end
|
|
|
|
FstData.insert(FstData.end(), NameTableData.begin(), NameTableData.end());
|
|
|
|
Util::FileSave(L"Data/DolphinSdkFST.bin", FstData);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MountDolphinSdk::GenBb2()
|
|
{
|
|
DVDBB2 bb2 = { 0 };
|
|
|
|
bb2.bootFilePosition = RoundUpSector(DVD_APPLDR_OFFSET + (uint32_t)AppldrData.size());
|
|
bb2.FSTLength = (uint32_t)FstData.size();
|
|
bb2.FSTMaxLength = bb2.FSTLength;
|
|
bb2.FSTPosition = RoundUpSector(bb2.bootFilePosition + (uint32_t)Dol.size() + DVD_SECTOR_SIZE);
|
|
bb2.userPosition = 0x80030000; // Ignored
|
|
bb2.userLength = RoundUpSector(bb2.FSTLength);
|
|
|
|
Bb2Data.resize(sizeof(DVDBB2));
|
|
memcpy(Bb2Data.data(), &bb2, sizeof(bb2));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MountDolphinSdk::GenMap()
|
|
{
|
|
MapVector(DiskId, DVD_ID_OFFSET);
|
|
MapVector(GameName, sizeof(DiskID));
|
|
|
|
MapVector(Bb2Data, DVD_BB2_OFFSET);
|
|
MapVector(Bi2Data, DVD_BI2_OFFSET);
|
|
MapVector(AppldrData, DVD_APPLDR_OFFSET);
|
|
|
|
DVDBB2* bb2 = (DVDBB2*)Bb2Data.data();
|
|
|
|
MapVector(Dol, bb2->bootFilePosition);
|
|
MapVector(FstData, bb2->FSTPosition);
|
|
|
|
SwapArea(bb2, sizeof(DVDBB2));
|
|
|
|
return true;
|
|
}
|
|
|
|
void MountDolphinSdk::WalkAndMapFiles(Json::Value* entry)
|
|
{
|
|
if (entry->type != Json::ValueType::Object)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Json::Value* isDir = entry->ByName("dir");
|
|
|
|
if (!isDir)
|
|
{
|
|
Json::Value* filePath = entry->ByName("filePath");
|
|
Json::Value* fileOffset = entry->ByName("fileOffset");
|
|
assert(filePath && fileOffset);
|
|
|
|
MapFile(filePath->value.AsString, (uint32_t)(fileOffset->value.AsInt));
|
|
}
|
|
else
|
|
{
|
|
for (auto it = entry->children.begin(); it != entry->children.end(); ++it)
|
|
{
|
|
WalkAndMapFiles(*it);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MountDolphinSdk::GenFileMap()
|
|
{
|
|
userFilesOffset = 0;
|
|
|
|
try
|
|
{
|
|
WalkAndMapFiles(DvdDataInfo.root.children.back());
|
|
}
|
|
catch (...)
|
|
{
|
|
Report(Channel::Norm, "WalkAndMapFiles failed!\n");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MountDolphinSdk::SwapArea(void* _addr, int sizeInBytes)
|
|
{
|
|
uint32_t* addr = (uint32_t*)_addr;
|
|
uint32_t* until = addr + sizeInBytes / sizeof(uint32_t);
|
|
|
|
while (addr != until)
|
|
{
|
|
*addr = _BYTESWAP_UINT32(*addr);
|
|
addr++;
|
|
}
|
|
}
|
|
|
|
#pragma endregion "Data Generators"
|
|
|
|
}
|
|
|
|
|
|
// Disk region detection
|
|
|
|
namespace DVD
|
|
{
|
|
// Get region by DiskID (not a very reliable method)
|
|
Region RegionById(const char* DiskId)
|
|
{
|
|
switch (DiskId[3])
|
|
{
|
|
case 'P':
|
|
case 'Y':
|
|
return Region::EUR;
|
|
case 'D':
|
|
return Region::NOE;
|
|
case 'F':
|
|
return Region::FRA;
|
|
case 'S':
|
|
return Region::ESP;
|
|
case 'I':
|
|
return Region::ITA;
|
|
case 'X':
|
|
case 'K':
|
|
return Region::FAH;
|
|
case 'H':
|
|
return Region::HOL;
|
|
case 'U':
|
|
return Region::AUS;
|
|
|
|
case 'J':
|
|
return Region::JPN;
|
|
|
|
case 'E':
|
|
return Region::USA;
|
|
case 'W':
|
|
return Region::KOR;
|
|
}
|
|
|
|
return Region::Unknown;
|
|
}
|
|
|
|
// Check that the region is NTSC-like
|
|
bool IsNtsc(Region region)
|
|
{
|
|
switch (region)
|
|
{
|
|
case Region::JPN:
|
|
case Region::USA:
|
|
case Region::KOR:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// DVD filesystem access.
|
|
|
|
// local data
|
|
static DVDBB2 bb2;
|
|
static DVDFileEntry* FstStart; // Loaded FST (byte-swapped as little-endian)
|
|
static uint32_t fstSize; // Size of loaded FST in bytes (not greater DVD_FST_MAX_SIZE)
|
|
static char* FstStringStart; // Strings(name) table
|
|
|
|
#define FSTOFS(lo, hi) (((uint32_t)hi << 16) | lo)
|
|
|
|
// Swap longs (no need in assembly, used by tools)
|
|
static void SwapArea(uint32_t* addr, int count)
|
|
{
|
|
uint32_t* until = addr + count / sizeof(uint32_t);
|
|
|
|
while (addr != until)
|
|
{
|
|
*addr = _BYTESWAP_UINT32(*addr);
|
|
addr++;
|
|
}
|
|
}
|
|
|
|
// swap bytes in FST (little-endian)
|
|
// return beginning of strings table (or NULL, if bad FST)
|
|
static char *fst_prepare(DVDFileEntry *root)
|
|
{
|
|
char* nameTablePtr = nullptr;
|
|
|
|
root->nameOffsetLo = _BYTESWAP_UINT16(root->nameOffsetLo);
|
|
root->fileOffset = _BYTESWAP_UINT32(root->fileOffset);
|
|
root->fileLength = _BYTESWAP_UINT32(root->fileLength);
|
|
|
|
// Check root: must have no parent, has zero nameOfset and non-zero nextOffset.
|
|
if (! ( root->isDir &&
|
|
root->parentOffset == 0 &&
|
|
FSTOFS(root->nameOffsetLo, root->nameOffsetHi) == 0 &&
|
|
root->nextOffset != 0 ) )
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
nameTablePtr = (char*)&root[root->nextOffset];
|
|
|
|
// Starting from next after root
|
|
for (uint32_t i = 1; i < root->nextOffset; i++)
|
|
{
|
|
DVDFileEntry* entry = &root[i];
|
|
|
|
entry->nameOffsetLo = _BYTESWAP_UINT16(entry->nameOffsetLo);
|
|
entry->fileOffset = _BYTESWAP_UINT32(entry->fileOffset);
|
|
entry->fileLength = _BYTESWAP_UINT32(entry->fileLength);
|
|
}
|
|
|
|
return nameTablePtr;
|
|
}
|
|
|
|
// initialize filesystem
|
|
bool dvd_fs_init()
|
|
{
|
|
// Check DiskID
|
|
|
|
char diskId[5] = { 0 };
|
|
|
|
DVD::Seek(DVD_ID_OFFSET);
|
|
DVD::Read(diskId, 4);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (!isalnum(diskId[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check Apploader
|
|
|
|
char apploaderBytes[5] = { 0 };
|
|
|
|
DVD::Seek(DVD_APPLDR_OFFSET);
|
|
DVD::Read(apploaderBytes, 4);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (!isdigit(apploaderBytes[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// load tables
|
|
DVD::Seek(DVD_BB2_OFFSET);
|
|
DVD::Read(&bb2, sizeof(DVDBB2));
|
|
SwapArea((uint32_t *)&bb2, sizeof(DVDBB2));
|
|
|
|
// delete previous FST
|
|
if(FstStart)
|
|
{
|
|
free(FstStart);
|
|
FstStart = NULL;
|
|
fstSize = 0;
|
|
}
|
|
|
|
// create new FST
|
|
fstSize = bb2.FSTLength;
|
|
if(fstSize > DVD_FST_MAX_SIZE)
|
|
{
|
|
return false;
|
|
}
|
|
FstStart = (DVDFileEntry *)malloc(fstSize);
|
|
if(FstStart == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
DVD::Seek(bb2.FSTPosition);
|
|
DVD::Read(FstStart, fstSize);
|
|
|
|
// swap bytes in FST and find offset of string table
|
|
FstStringStart = fst_prepare(FstStart);
|
|
if(!FstStringStart)
|
|
{
|
|
free(FstStart);
|
|
FstStart = NULL;
|
|
return false;
|
|
}
|
|
|
|
// FST loaded ok
|
|
return true;
|
|
}
|
|
|
|
void dvd_fs_shutdown()
|
|
{
|
|
if (FstStart)
|
|
{
|
|
free(FstStart);
|
|
FstStart = NULL;
|
|
fstSize = 0;
|
|
}
|
|
}
|
|
|
|
// Based on reversing of original method.
|
|
// <0: Bad path
|
|
static int DVDConvertPathToEntrynum(const char* _path)
|
|
{
|
|
char* path = (char *)_path;
|
|
|
|
// currentDirectory assigned by DVDChangeDir
|
|
int entry = 0; // running entry
|
|
|
|
// Loop1
|
|
while (true)
|
|
{
|
|
if (path[0] == 0)
|
|
return entry;
|
|
|
|
// Current/parent directory walk
|
|
|
|
if (path[0] == '/')
|
|
{
|
|
entry = 0; // root
|
|
path++;
|
|
continue; // Loop1
|
|
}
|
|
|
|
if (path[0] == '.')
|
|
{
|
|
if (path[1] == '.')
|
|
{
|
|
if (path[2] == '/')
|
|
{
|
|
entry = FstStart[entry].parentOffset;
|
|
path += 3;
|
|
continue; // Loop1
|
|
}
|
|
if (path[2] == 0)
|
|
{
|
|
return FstStart[entry].parentOffset;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (path[1] == '/')
|
|
{
|
|
path += 2;
|
|
continue; // Loop1
|
|
}
|
|
if (path[1] == 0)
|
|
{
|
|
return entry;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get a pointer to the end of a file or directory name (the end is 0 or /)
|
|
char* endPathPtr;
|
|
|
|
if (true)
|
|
{
|
|
endPathPtr = path;
|
|
while (!(endPathPtr[0] == 0 || endPathPtr[0] == '/'))
|
|
{
|
|
endPathPtr++;
|
|
}
|
|
}
|
|
|
|
// if-else Block 2
|
|
|
|
bool afterNameCharNZ = endPathPtr[0] != 0; // after-name character != 0
|
|
int prevEntry = entry; // Save previous entry
|
|
size_t nameSize = endPathPtr - path; // path element nameSize
|
|
entry++; // Increment entry
|
|
|
|
// Loop2
|
|
while (true)
|
|
{
|
|
if ((int)FstStart[prevEntry].nextOffset <= entry) // Walk forward only
|
|
return -1; // Bad FST
|
|
|
|
// Loop2 - Group 1 -- Compare names
|
|
if (FstStart[entry].isDir || afterNameCharNZ == false /* after-name is 0 */)
|
|
{
|
|
char* r21 = path; // r21 -- current pathPtr to inner loop
|
|
int nameOffset = (FstStart[entry].nameOffsetHi << 16) | FstStart[entry].nameOffsetLo;
|
|
char* r20 = &FstStringStart[nameOffset & 0xFFFFFF]; // r20 -- ptr to current entry name
|
|
|
|
bool same;
|
|
while (true)
|
|
{
|
|
if (*r20 == 0)
|
|
{
|
|
same = (*r21 == '/' || *r21 == 0);
|
|
break;
|
|
}
|
|
|
|
if (_tolower(*r20++) != _tolower(*r21++))
|
|
{
|
|
same = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (same)
|
|
{
|
|
if (afterNameCharNZ == false)
|
|
return entry;
|
|
path += nameSize + 1;
|
|
break; // break Loop2
|
|
}
|
|
}
|
|
|
|
// Walk next directory/file at same level
|
|
entry = FstStart[entry].isDir ? FstStart[entry].nextOffset : (entry + 1);
|
|
|
|
} // Loop2
|
|
|
|
} // Loop1
|
|
}
|
|
|
|
// convert DVD file name into file position on the disk
|
|
// 0, if file not found
|
|
int dvd_open(const char *path)
|
|
{
|
|
int entry = DVDConvertPathToEntrynum(path);
|
|
if (entry < 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return (int)FstStart[entry].fileOffset;
|
|
}
|
|
|
|
|
|
// simple GCM image reading.
|
|
|
|
bool GCMMountFile(const wchar_t*file)
|
|
{
|
|
FILE* gcm_file;
|
|
|
|
dvd.gcm_filename[0] = 0;
|
|
dvd.mountedImage = false;
|
|
|
|
if (file == nullptr)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// open GCM file
|
|
gcm_file = fopen(Util::WstringToString(file).c_str(), "rb");
|
|
if(!gcm_file) return false;
|
|
|
|
// get file size
|
|
fseek(gcm_file, 0, SEEK_END);
|
|
dvd.gcm_size = ftell(gcm_file);
|
|
fseek(gcm_file, 0, SEEK_SET);
|
|
|
|
fclose(gcm_file);
|
|
|
|
// protect from damaged GCMs
|
|
if(dvd.gcm_size < DVD_APPLDR_OFFSET)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// reset position
|
|
dvd.seekval = 0;
|
|
|
|
wcscpy(dvd.gcm_filename, file);
|
|
dvd.mountedImage = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void GCMSeek(int position)
|
|
{
|
|
dvd.seekval = position;
|
|
}
|
|
|
|
bool GCMRead(uint8_t*buf, size_t length)
|
|
{
|
|
FILE* gcm_file;
|
|
|
|
if (dvd.gcm_filename[0] == 0)
|
|
{
|
|
memset(buf, 0, length); // fill by zeroes
|
|
return true;
|
|
}
|
|
|
|
gcm_file = fopen ( Util::WstringToString(dvd.gcm_filename).c_str(), "rb");
|
|
|
|
if(gcm_file)
|
|
{
|
|
// out of DVD
|
|
if(dvd.seekval >= DVD_SIZE)
|
|
{
|
|
memset(buf, 0, length); // fill by zeroes
|
|
dvd.seekval += (int)length;
|
|
fclose(gcm_file);
|
|
return false;
|
|
}
|
|
|
|
// GCM files can be less than 1.4 GB,
|
|
// so just return zeroes, when seek is out of file
|
|
if(dvd.seekval >= dvd.gcm_size)
|
|
{
|
|
memset(buf, 0, length); // fill by zeroes
|
|
dvd.seekval += (int)length;
|
|
fclose(gcm_file);
|
|
return true;
|
|
}
|
|
|
|
// wrap, if seek is near to out of DVD
|
|
if( (dvd.seekval + length) >= DVD_SIZE)
|
|
{
|
|
length = DVD_SIZE - dvd.seekval;
|
|
}
|
|
|
|
// wrap, if seek is near to out of file
|
|
if( (dvd.seekval + length) >= dvd.gcm_size)
|
|
{
|
|
length = dvd.gcm_size - dvd.seekval;
|
|
}
|
|
|
|
// read data
|
|
if(length)
|
|
{
|
|
fseek(gcm_file, dvd.seekval, SEEK_SET);
|
|
// https://stackoverflow.com/questions/295994/what-is-the-rationale-for-fread-fwrite-taking-size-and-count-as-arguments
|
|
size_t bytesRead = fread(buf, 1, length, gcm_file);
|
|
fclose(gcm_file);
|
|
dvd.seekval += (int)length;
|
|
return (bytesRead == length);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
memset(buf, 0, length); // fill by zeroes
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
int32_t YnLeft[2], YnRight[2];
|
|
|
|
typedef int Nibble;
|
|
|
|
void DvdAudioInitDecoder()
|
|
{
|
|
YnLeft[0] = YnLeft[1] = 0;
|
|
YnRight[0] = YnRight[1] = 0;
|
|
}
|
|
|
|
int32_t MulSomething(Nibble arg_0, int32_t yn1, int32_t yn2)
|
|
{
|
|
int16_t a1 = 0;
|
|
int16_t a2 = 0;
|
|
|
|
switch (arg_0)
|
|
{
|
|
case 0:
|
|
a1 = 0;
|
|
a2 = 0;
|
|
break;
|
|
case 1:
|
|
a1 = 60;
|
|
a2 = 0;
|
|
break;
|
|
case 2:
|
|
a1 = 115;
|
|
a2 = -52;
|
|
break;
|
|
case 3:
|
|
a1 = 98;
|
|
a2 = -55;
|
|
break;
|
|
}
|
|
|
|
int32_t ps1 = (int32_t)a1 * yn1;
|
|
int32_t ps2 = (int32_t)a2 * yn2;
|
|
|
|
int32_t var_C = (ps1 + ps2 + 32) >> 6;
|
|
return my_max(-0x200000, my_min(var_C, 0x1FFFFF) );
|
|
}
|
|
|
|
int16_t Shifts1(Nibble arg_0, Nibble arg_4)
|
|
{
|
|
int16_t var_4 = (int16_t)arg_0 << 12;
|
|
return var_4 >> arg_4;
|
|
}
|
|
|
|
int32_t Shifts2(int16_t arg_0, int32_t arg_4)
|
|
{
|
|
return ((int32_t)arg_0 << 6) + arg_4;
|
|
}
|
|
|
|
// Clamp
|
|
uint16_t Clamp(int32_t arg_0)
|
|
{
|
|
int32_t var_8 = arg_0 >> 6;
|
|
return (uint16_t)my_max(-0x8000, my_min(var_8, 0x7FFF));
|
|
}
|
|
|
|
uint16_t DecodeSample(Nibble arg_0, uint8_t arg_4, int Yn[2])
|
|
{
|
|
uint16_t res;
|
|
|
|
Nibble var_4 = arg_4 >> 4;
|
|
Nibble var_8 = arg_4 & 0xF;
|
|
|
|
int32_t var_18 = MulSomething(var_4, Yn[0], Yn[1]);
|
|
int16_t var_C = Shifts1(arg_0, var_8);
|
|
int32_t var_14 = Shifts2(var_C, var_18);
|
|
res = Clamp(var_14);
|
|
|
|
Yn[1] = Yn[0];
|
|
Yn[0] = var_14;
|
|
|
|
return res;
|
|
}
|
|
|
|
void DvdAudioDecode(uint8_t adpcmBuffer[32], uint16_t pcmBuffer[2 * 28])
|
|
{
|
|
uint8_t* adpcmData = &adpcmBuffer[4];
|
|
|
|
if (!(adpcmBuffer[0] == adpcmBuffer[2] && adpcmBuffer[1] == adpcmBuffer[3]))
|
|
{
|
|
memset(pcmBuffer, 0, 2 * 28 * sizeof(uint16_t));
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < 28; i++)
|
|
{
|
|
pcmBuffer[2 * i] = DecodeSample(adpcmData[i] & 0xF, adpcmBuffer[0], YnLeft);
|
|
pcmBuffer[2 * i + 1] = DecodeSample(adpcmData[i] >> 4, adpcmBuffer[1], YnRight);
|
|
}
|
|
}
|
|
|
|
|
|
namespace DVD
|
|
{
|
|
DduCore * DDU;
|
|
|
|
DduCore::DduCore()
|
|
{
|
|
dduThread = EMUCreateThread(DduThreadProc, true, this, "DvdData");
|
|
dvdAudioThread = EMUCreateThread(DvdAudioThreadProc, true, this, "DvdAudio");
|
|
|
|
dataCache = new uint8_t[dataCacheSize];
|
|
memset(dataCache, 0, dataCacheSize);
|
|
streamingCache = new uint8_t[streamCacheSize];
|
|
memset(streamingCache, 0, streamCacheSize);
|
|
|
|
Reset();
|
|
|
|
if (adpcmStreamDump)
|
|
{
|
|
adpcmStreamFile = fopen("Data\\DvdAdpcm.bin", "wb");
|
|
}
|
|
if (decodedStreamDump)
|
|
{
|
|
decodedStreamFile = fopen("Data\\DvdDecodedPcm.bin", "wb");
|
|
}
|
|
}
|
|
|
|
DduCore::~DduCore()
|
|
{
|
|
if (adpcmStreamFile)
|
|
{
|
|
fclose(adpcmStreamFile);
|
|
}
|
|
if (decodedStreamFile)
|
|
{
|
|
fclose(decodedStreamFile);
|
|
}
|
|
|
|
TransferComplete();
|
|
EMUJoinThread(dduThread);
|
|
EMUJoinThread(dvdAudioThread);
|
|
delete[] dataCache;
|
|
delete[] streamingCache;
|
|
}
|
|
|
|
void DduCore::ExecuteCommand()
|
|
{
|
|
// Execute command
|
|
|
|
errorState = false;
|
|
errorCode = 0;
|
|
|
|
if (logCommands)
|
|
{
|
|
Report(Channel::DVD, "Command: %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X\n",
|
|
commandBuffer[0], commandBuffer[1], commandBuffer[2], commandBuffer[3],
|
|
commandBuffer[4], commandBuffer[5], commandBuffer[6], commandBuffer[7],
|
|
commandBuffer[8], commandBuffer[9], commandBuffer[10], commandBuffer[11]);
|
|
}
|
|
|
|
switch (commandBuffer[0])
|
|
{
|
|
// Inquiry (DVDLowInquiry), read manufacture info (DMA)
|
|
case 0x12:
|
|
state = DduThreadState::ReadBogusData;
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "DVD Inquiry.\n");
|
|
}
|
|
break;
|
|
|
|
// read sector / disk id
|
|
case 0xA8:
|
|
state = DduThreadState::ReadDvdData;
|
|
{
|
|
uint32_t seekTemp = (commandBuffer[4] << 24) |
|
|
(commandBuffer[5] << 16) |
|
|
(commandBuffer[6] << 8) |
|
|
(commandBuffer[7]);
|
|
seekVal = seekTemp << 2;
|
|
|
|
// Use transaction size as hint for pre-caching
|
|
if (commandBuffer[3] == 0x40)
|
|
{
|
|
transactionSize = sizeof(DiskID);
|
|
}
|
|
else
|
|
{
|
|
transactionSize = (commandBuffer[8] << 24) |
|
|
(commandBuffer[9] << 16) |
|
|
(commandBuffer[10] << 8) |
|
|
(commandBuffer[11]);
|
|
}
|
|
}
|
|
// Invalidate reading cache
|
|
dataCachePtr = dataCacheSize;
|
|
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "DVD Read: 0x%08X, %i bytes\n", seekVal, transactionSize);
|
|
}
|
|
break;
|
|
|
|
// seek (DVDLowSeek) (IMM)
|
|
case 0xAB:
|
|
state = DduThreadState::ReadBogusData;
|
|
|
|
{
|
|
uint32_t seekTemp = (commandBuffer[4] << 24) |
|
|
(commandBuffer[5] << 16) |
|
|
(commandBuffer[6] << 8) |
|
|
(commandBuffer[7]);
|
|
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "Seek: 0x%08X (ignored)\n", seekTemp << 2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Request Error (DVDLowRequestError) (IMM)
|
|
case 0xE0:
|
|
state = DduThreadState::ReadBogusData;
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "Request Error\n");
|
|
}
|
|
break;
|
|
|
|
// set stream (DVDLowAudioStream) (IMM)
|
|
case 0xE1:
|
|
state = DduThreadState::ReadBogusData;
|
|
|
|
{
|
|
uint32_t seekTemp = (commandBuffer[4] << 24) |
|
|
(commandBuffer[5] << 16) |
|
|
(commandBuffer[6] << 8) |
|
|
(commandBuffer[7]);
|
|
|
|
// The SDK first sets the address of the stream, and then for some reason resets it to 0.
|
|
// We make the assumption that the value 0 should be ignored.
|
|
if (seekTemp != 0)
|
|
{
|
|
streamSeekVal = seekTemp << 2;
|
|
streamCount = (commandBuffer[8] << 24) |
|
|
(commandBuffer[9] << 16) |
|
|
(commandBuffer[10] << 8) |
|
|
(commandBuffer[11]);
|
|
|
|
SetDvdAudioSampleRate(sampleRate);
|
|
DvdAudioInitDecoder();
|
|
streamEnabledByDduCommand = true;
|
|
|
|
// Invalidate streaming cache
|
|
streamingCachePtr = streamCacheSize;
|
|
|
|
// Invalidate PCM buffer
|
|
pcmPlaybackCounter = sizeof(pcmPlaybackBuffer);
|
|
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "DVD Streaming setup: stream start 0x%08X, counter: %i\n",
|
|
streamSeekVal, streamCount);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DvdAudioInitDecoder();
|
|
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "DVD Bogus Streaming setup (ignored)\n");
|
|
}
|
|
}
|
|
|
|
}
|
|
break;
|
|
|
|
// get stream status (DVDLowRequestAudioStatus) (IMM)
|
|
case 0xE2:
|
|
switch (commandBuffer[1])
|
|
{
|
|
case 0: // Get stream enable
|
|
state = DduThreadState::GetStreamEnable;
|
|
immediateBuffer[0] = 0;
|
|
immediateBuffer[1] = 0;
|
|
immediateBuffer[2] = 0;
|
|
immediateBuffer[3] = streamEnabledByDduCommand ? 1 : 0;
|
|
immediateBufferPtr = 0;
|
|
break;
|
|
case 1: // Get stream address
|
|
state = DduThreadState::GetStreamOffset;
|
|
{
|
|
uint32_t seekTemp = streamSeekVal >> 2;
|
|
immediateBuffer[0] = (seekTemp >> 24) & 0xff;
|
|
immediateBuffer[1] = (seekTemp >> 16) & 0xff;
|
|
immediateBuffer[2] = (seekTemp >> 8) & 0xff;
|
|
immediateBuffer[3] = (seekTemp) & 0xff;
|
|
}
|
|
immediateBufferPtr = 0;
|
|
break;
|
|
default:
|
|
state = DduThreadState::GetStreamBogus;
|
|
Report(Channel::DVD, "Unknown GetStreamStatus: %i\n", commandBuffer[1]);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// stop motor (DVDLowStopMotor) (IMM)
|
|
case 0xE3:
|
|
state = DduThreadState::ReadBogusData;
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "Stop motor.\n");
|
|
}
|
|
break;
|
|
|
|
// Set Audio Buffer (DVDLowAudioBufferConfig) (IMM)
|
|
// It looks like it's setting up a FIFO buffer that stores decoded PCM samples of the next 32 Bytes ADPCM chunk.
|
|
case 0xE4:
|
|
state = DduThreadState::ReadBogusData;
|
|
|
|
if (log)
|
|
{
|
|
Report(Channel::DVD, "SetAudioBuffer: Trig: %i, Enable: %i, Size: %i\n",
|
|
commandBuffer[0] & 3, commandBuffer[2] & 0x80 ? 1 : 0, commandBuffer[3] & 0xf);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
state = DduThreadState::ReadBogusData;
|
|
|
|
Report(Channel::DVD, "Unknown DDU command: %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X\n",
|
|
commandBuffer[0], commandBuffer[1], commandBuffer[2], commandBuffer[3],
|
|
commandBuffer[4], commandBuffer[5], commandBuffer[6], commandBuffer[7],
|
|
commandBuffer[8], commandBuffer[9], commandBuffer[10], commandBuffer[11]);
|
|
break;
|
|
}
|
|
|
|
commandPtr = 0;
|
|
}
|
|
|
|
void DduCore::DduThreadProc(void* Parameter)
|
|
{
|
|
DduCore* core = (DduCore*)Parameter;
|
|
|
|
// Wait Gekko ticks
|
|
if (!core->transferRateNoLimit)
|
|
{
|
|
int64_t ticks = Core->GetTicks();
|
|
if (ticks >= core->savedGekkoTicks)
|
|
{
|
|
core->savedGekkoTicks = ticks + core->dduTicksPerByte;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Until break or transfer completed
|
|
while (core->ddBusBusy)
|
|
{
|
|
if (core->busDir == DduBusDirection::HostToDdu)
|
|
{
|
|
switch (core->state)
|
|
{
|
|
case DduThreadState::WriteCommand:
|
|
if (core->commandPtr < sizeof(core->commandBuffer))
|
|
{
|
|
core->commandBuffer[core->commandPtr] = core->hostToDduCallback();
|
|
core->stats.bytesWrite++;
|
|
core->commandPtr++;
|
|
}
|
|
|
|
if (core->commandPtr >= sizeof(core->commandBuffer))
|
|
{
|
|
core->ExecuteCommand();
|
|
}
|
|
break;
|
|
|
|
// Hidden debug commands are not supported yet
|
|
|
|
default:
|
|
core->DeviceError(0);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (core->state)
|
|
{
|
|
case DduThreadState::ReadDvdData:
|
|
// Read-ahead new DVD data
|
|
if (core->dataCachePtr >= dataCacheSize)
|
|
{
|
|
Seek(core->seekVal);
|
|
size_t bytes = my_min(dataCacheSize, core->transactionSize);
|
|
bool readResult = Read(core->dataCache, bytes);
|
|
core->seekVal += (uint32_t)bytes;
|
|
core->transactionSize -= bytes;
|
|
|
|
if (core->seekVal >= DVD_SIZE || !readResult)
|
|
{
|
|
core->DeviceError(0);
|
|
}
|
|
|
|
core->dataCachePtr = 0;
|
|
}
|
|
|
|
core->dduToHostCallback(core->dataCache[core->dataCachePtr]);
|
|
core->stats.bytesRead++;
|
|
core->dataCachePtr++;
|
|
break;
|
|
|
|
case DduThreadState::ReadBogusData:
|
|
core->dduToHostCallback(0);
|
|
core->stats.bytesRead++;
|
|
break;
|
|
|
|
case DduThreadState::GetStreamEnable:
|
|
case DduThreadState::GetStreamOffset:
|
|
case DduThreadState::GetStreamBogus:
|
|
if (core->immediateBufferPtr < sizeof(core->immediateBuffer))
|
|
{
|
|
core->dduToHostCallback(core->immediateBuffer[core->immediateBufferPtr]);
|
|
core->stats.bytesRead++;
|
|
core->immediateBufferPtr++;
|
|
}
|
|
else
|
|
{
|
|
core->DeviceError(0);
|
|
}
|
|
break;
|
|
|
|
case DduThreadState::Idle:
|
|
break;
|
|
|
|
default:
|
|
core->DeviceError(0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sleep until next transfer
|
|
core->dduThread->Suspend();
|
|
}
|
|
|
|
// Enabling AISCLK forces the DDU to issue samples out even if there are none (zeros goes to output).
|
|
void DduCore::EnableAudioStreamClock(bool enable)
|
|
{
|
|
if (enable)
|
|
{
|
|
dvdAudioThread->Resume();
|
|
}
|
|
else
|
|
{
|
|
dvdAudioThread->Suspend();
|
|
}
|
|
}
|
|
|
|
void DduCore::DvdAudioThreadProc(void* Parameter)
|
|
{
|
|
uint16_t sample[2] = { 0, 0 };
|
|
DduCore* core = (DduCore*)Parameter;
|
|
|
|
while (true)
|
|
{
|
|
// If AISCLK is enabled but streaming is not enabled by the DDU command, DVD Audio will output only zeros.
|
|
|
|
// If its time to send sample
|
|
int64_t ticks = Core->GetTicks();
|
|
if (ticks < core->nextGekkoTicksToSample)
|
|
{
|
|
continue;
|
|
}
|
|
core->nextGekkoTicksToSample = ticks + core->TicksPerSample();
|
|
|
|
// Invalidate cache
|
|
if (core->streamEnabledByDduCommand)
|
|
{
|
|
if (core->streamingCachePtr >= streamCacheSize)
|
|
{
|
|
core->streamingCachePtr = 0;
|
|
Seek(core->streamSeekVal);
|
|
bool readResult = Read(core->streamingCache, streamCacheSize);
|
|
|
|
if (core->log)
|
|
{
|
|
//DBReport2(DbgChannel::DVD, "Streaming Seek: 0x%08X, Byte[0]: 0x%02X\n", core->streamSeekVal, core->streamingCache[0]);
|
|
}
|
|
|
|
//if (!readResult)
|
|
//{
|
|
// core->DeviceError(0);
|
|
//}
|
|
|
|
if (core->adpcmStreamDump && core->adpcmStreamFile)
|
|
{
|
|
fwrite(core->streamingCache, 1, streamCacheSize, core->adpcmStreamFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
// From changing the playback frequency, the size of the ADPCM data does not change. The frequency of samples output to the outside changes.
|
|
|
|
if (core->streamEnabledByDduCommand)
|
|
{
|
|
if (core->pcmPlaybackCounter >= sizeof(core->pcmPlaybackBuffer))
|
|
{
|
|
// Decode next ADPCM chunk
|
|
DvdAudioDecode(&core->streamingCache[core->streamingCachePtr], core->pcmPlaybackBuffer);
|
|
|
|
if (core->decodedStreamDump && core->decodedStreamFile)
|
|
{
|
|
fwrite(core->pcmPlaybackBuffer, 1, sizeof(core->pcmPlaybackBuffer), core->decodedStreamFile);
|
|
}
|
|
|
|
core->streamingCachePtr += 32;
|
|
core->streamSeekVal += 32;
|
|
core->streamCount -= 32;
|
|
core->pcmPlaybackCounter = 0;
|
|
}
|
|
|
|
uint8_t* rawPtr = (uint8_t *)core->pcmPlaybackBuffer + core->pcmPlaybackCounter;
|
|
sample[0] = *(uint16_t *)rawPtr;
|
|
sample[1] = *(uint16_t *)(rawPtr + 2);
|
|
core->pcmPlaybackCounter += 4;
|
|
}
|
|
else
|
|
{
|
|
sample[0] = 0;
|
|
sample[1] = 0;
|
|
}
|
|
|
|
// Send sample
|
|
|
|
if (core->streamCallback)
|
|
{
|
|
core->streamCallback(sample[0], sample[1]);
|
|
}
|
|
|
|
core->stats.sampleCounter++;
|
|
|
|
if (core->streamEnabledByDduCommand)
|
|
{
|
|
if (core->streamCount <= 0)
|
|
{
|
|
core->streamEnabledByDduCommand = false;
|
|
|
|
if (core->log)
|
|
{
|
|
Report(Channel::DVD, "DVD streaming stopped by counter value reach zero\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculates how many Gekko ticks takes 1 sample, at the selected sample rate.
|
|
int64_t DduCore::TicksPerSample()
|
|
{
|
|
if (sampleRate == DvdAudioSampleRate::Rate_32000)
|
|
{
|
|
// 32 kHz means 32,000 LR samples per second come from DVD Audio.
|
|
// To find out how many ticks a sample takes, you need to divide the number of Gekko ticks per second by 32000.
|
|
return gekkoOneSecond / 32000;
|
|
}
|
|
else
|
|
{
|
|
return gekkoOneSecond / 48000;
|
|
}
|
|
}
|
|
|
|
void DduCore::SetDvdAudioSampleRate(DvdAudioSampleRate rate)
|
|
{
|
|
sampleRate = rate;
|
|
nextGekkoTicksToSample = Core->GetTicks() + TicksPerSample();
|
|
}
|
|
|
|
// Reset internal state. If you forget something, then it will come out later..
|
|
void DduCore::Reset()
|
|
{
|
|
dduThread->Suspend();
|
|
dvdAudioThread->Suspend();
|
|
ddBusBusy = false;
|
|
errorState = false;
|
|
commandPtr = 0;
|
|
dataCachePtr = dataCacheSize;
|
|
streamingCachePtr = streamCacheSize;
|
|
state = DduThreadState::WriteCommand;
|
|
ResetStats();
|
|
gekkoOneSecond = Core->OneSecond();
|
|
dduTicksPerByte = gekkoOneSecond / transferRate;
|
|
SetDvdAudioSampleRate(DvdAudioSampleRate::Rate_48000);
|
|
streamEnabledByDduCommand = false;
|
|
}
|
|
|
|
void DduCore::Break()
|
|
{
|
|
// Abort data transfer
|
|
Report(Channel::DVD, "DDU Break\n");
|
|
ddBusBusy = false;
|
|
}
|
|
|
|
void DduCore::OpenCover()
|
|
{
|
|
if (coverStatus == CoverStatus::Open)
|
|
return;
|
|
|
|
coverStatus = CoverStatus::Open;
|
|
Report(Channel::DVD, "Cover opened\n");
|
|
|
|
// Notify host hardware
|
|
if (openCoverCallback)
|
|
{
|
|
openCoverCallback();
|
|
}
|
|
}
|
|
|
|
void DduCore::CloseCover()
|
|
{
|
|
if (coverStatus == CoverStatus::Close)
|
|
return;
|
|
|
|
coverStatus = CoverStatus::Close;
|
|
Report(Channel::DVD, "Cover closed\n");
|
|
|
|
// Notify host hardware
|
|
if (closeCoverCallback)
|
|
{
|
|
closeCoverCallback();
|
|
}
|
|
}
|
|
|
|
void DduCore::DeviceError(uint32_t reason)
|
|
{
|
|
Report(Channel::DVD, "DDU DeviceError: %08X\n", reason);
|
|
// Will deassert DIERR after the next command is received from the host
|
|
errorState = true;
|
|
errorCode = reason;
|
|
ddBusBusy = false;
|
|
if (errorCallback)
|
|
{
|
|
errorCallback();
|
|
}
|
|
}
|
|
|
|
void DduCore::StartTransfer(DduBusDirection direction)
|
|
{
|
|
if (logTransfers)
|
|
{
|
|
Report(Channel::DVD, "StartTransfer: %s\n", direction == DduBusDirection::DduToHost ? "Ddu->Host" : "Host->Ddu");
|
|
}
|
|
|
|
ddBusBusy = true;
|
|
busDir = direction;
|
|
|
|
savedGekkoTicks = Core->GetTicks() + dduTicksPerByte;
|
|
|
|
dduThread->Resume();
|
|
}
|
|
|
|
void DduCore::TransferComplete()
|
|
{
|
|
if (logTransfers)
|
|
{
|
|
Report(Channel::DVD, "TransferComplete\n");
|
|
}
|
|
|
|
ddBusBusy = false;
|
|
|
|
switch (state)
|
|
{
|
|
case DduThreadState::WriteCommand:
|
|
// Invalidate reading cache
|
|
dataCachePtr = dataCacheSize;
|
|
break;
|
|
|
|
case DduThreadState::Idle:
|
|
case DduThreadState::ReadDvdData:
|
|
case DduThreadState::ReadBogusData:
|
|
case DduThreadState::GetStreamEnable:
|
|
case DduThreadState::GetStreamOffset:
|
|
case DduThreadState::GetStreamBogus:
|
|
state = DduThreadState::WriteCommand;
|
|
break;
|
|
}
|
|
|
|
if (busDir == DduBusDirection::DduToHost)
|
|
{
|
|
stats.dduToHostTransferCount++;
|
|
}
|
|
else
|
|
{
|
|
stats.hostToDduTransferCount++;
|
|
}
|
|
}
|
|
|
|
}
|