jak-project/game/overlord/iso.cpp
2022-11-03 22:28:12 +00:00

1485 lines
46 KiB
C++

/*!
* @file iso.cpp
* CD/DVD Reading.
* This is a huge mess
*/
#include "iso.h"
#include <cstdio>
#include <cstring>
#include "dma.h"
#include "fake_iso.h"
#include "iso_api.h"
#include "iso_cd.h"
#include "iso_queue.h"
#include "stream.h"
#include "common/log/log.h"
#include "common/util/Assert.h"
#include "game/common/dgo_rpc_types.h"
#include "game/overlord/srpc.h"
#include "game/runtime.h"
#include "game/sce/iop.h"
#include "game/sound/sdshim.h"
#include "game/sound/sndshim.h"
using namespace iop;
u32 ISOThread();
u32 DGOThread();
u32 RunDGOStateMachine(IsoMessage* _cmd, IsoBufferHeader* buffer_header);
u32 CopyDataToEE(IsoMessage* _cmd, IsoBufferHeader* buffer_header);
u32 CopyDataToIOP(IsoMessage* _cmd, IsoBufferHeader* buffer_header);
u32 NullCallback(IsoMessage* _cmd, IsoBufferHeader* buffer_header);
static void InitVAGCmd(VagCommand* cmd, u32 x);
static u32 ProcessVAGData(IsoMessage* _cmd, IsoBufferHeader* buffer_header);
static s32 CheckVAGStreamProgress(VagCommand* vag);
static void StopVAG(VagCommand* vag);
static void PauseVAG(VagCommand* vag);
static void UnpauseVAG(VagCommand* vag);
static s32 GetPlayPos();
static void UpdatePlayPos();
static void VAG_MarkLoopEnd(void* data, u32 size);
constexpr int LOADING_SCREEN_SIZE = 0x800000;
constexpr u32 LOADING_SCREEN_DEST_ADDR = 0x1000000;
static constexpr s32 LOOP_END = 1;
static constexpr s32 LOOP_REPEAT = 2;
static constexpr s32 LOOP_START = 4;
// Empty ADPCM block with loop flags
// clang-format off
static u8 VAG_SilentLoop[0x60] = {
0x0, LOOP_START | LOOP_REPEAT, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, LOOP_REPEAT, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, LOOP_END | LOOP_REPEAT, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
};
// clang-format on
IsoFs* isofs;
u32 iso_init_flag;
s32 sync_mbx;
s32 iso_mbx;
s32 dgo_mbx;
s32 iso_thread;
s32 dgo_thread;
s32 str_thread;
s32 play_thread;
VagDir gVagDir;
u32 gPlayPos;
static RPC_Dgo_Cmd sRPCBuff[1]; // todo move...
DgoCommand scmd;
static VagCommand vag_cmd;
VagCommand* gVAGCMD = nullptr;
s32 gDialogVolume = 0;
s32 gFakeVAGClockPaused = 0;
s32 gFakeVAGClockRunning = 0;
s32 gFakeVAGClock = 0;
s32 gRealVAGClockRunning = 0;
s32 gRealVAGClock = 0;
s32 gRealVAGClockS = 0;
s32 gPlaying = 0;
s32 gSampleRate = 0;
bool gLastVagHalf = false;
s32 gVoice;
void iso_init_globals() {
isofs = nullptr;
iso_init_flag = 0;
sync_mbx = 0;
iso_mbx = 0;
dgo_mbx = 0;
iso_thread = 0;
dgo_thread = 0;
str_thread = 0;
play_thread = 0;
memset(&gVagDir, 0, sizeof(gVagDir));
gPlayPos = 0;
memset(sRPCBuff, 0, sizeof(sRPCBuff));
memset(&scmd, 0, sizeof(DgoCommand));
}
/*!
* Initialize the ISO Driver.
* Requires a buffer large enough to hold 3 sector (or 4 if you have DUP files)
*/
void InitDriver(u8* buffer) {
MsgPacket msg_packet;
if (!isofs->init(buffer)) {
// succesful init!
iso_init_flag = 0;
}
// you idiots, you're giving the kernel a pointer to a stack variable!
// (this is fixed in Jak 1 Japan and NTSC Greatest Hits)
SendMbx(sync_mbx, &msg_packet);
}
/*!
* Does the messagebox have a message in it?
*/
u32 LookMbx(s32 mbx) {
MsgPacket* msg_packet;
return PollMbx((&msg_packet), mbx) != KE_MBOX_NOMSG;
}
/*!
* Wait for a messagebox to have a message. This is inefficient and polls with a 100 us wait.
* This is stupid because the IOP does have much better syncronization primitives so you don't have
* to do this.
*/
void WaitMbx(s32 mbx) {
while (!LookMbx(mbx)) {
DelayThread(100);
}
}
/*!
* Initialize the ISO FileSystem system.
* Returns 0 on success.
*/
u32 InitISOFS(const char* fs_mode, const char* loading_screen) {
// in retail:
// isofs = &iso_cd;
// ADDED
if (!strcmp(fs_mode, "iso_cd")) {
isofs = &iso_cd_;
} else if (!strcmp(fs_mode, "fakeiso")) {
isofs = &fake_iso;
} else {
printf("[OVERLORD ISO] ISOFS has unknown fs_mode %s\n", fs_mode);
}
// END ADDED
// mark us as NOT initialized.
iso_init_flag = 1;
while (!DMA_SendToSPUAndSync(&VAG_SilentLoop, 0x30, gTrapSRAM)) {
DelayThread(1000);
}
// INITIALIZE MESSAGE BOXES
MbxParam mbx_param;
mbx_param.attr = 0;
mbx_param.option = 0;
iso_mbx = CreateMbx(&mbx_param);
if (iso_mbx <= 0) {
return 1;
}
mbx_param.attr = 0;
mbx_param.option = 0;
dgo_mbx = CreateMbx(&mbx_param);
if (dgo_mbx <= 0) {
return 1;
}
mbx_param.attr = 0;
mbx_param.option = 0;
sync_mbx = CreateMbx(&mbx_param);
if (sync_mbx <= 0) {
return 1;
}
// INITIALIZE THREADS
ThreadParam thread_param;
thread_param.attr = TH_C;
thread_param.initPriority = 100;
thread_param.stackSize = 0x1000;
thread_param.option = 0;
thread_param.entry = (void*)ISOThread;
strcpy(thread_param.name, "ISOThread");
iso_thread = CreateThread(&thread_param);
if (iso_thread <= 0) {
return 1;
}
thread_param.attr = TH_C;
thread_param.initPriority = 98;
thread_param.stackSize = 0x800;
thread_param.option = 0;
thread_param.entry = (void*)DGOThread;
strcpy(thread_param.name, "DGOThread");
dgo_thread = CreateThread(&thread_param);
if (dgo_thread <= 0) {
return 1;
}
thread_param.attr = TH_C;
thread_param.initPriority = 97;
thread_param.stackSize = 0x800;
thread_param.option = 0;
thread_param.entry = (void*)STRThread;
strcpy(thread_param.name, "STRThread");
str_thread = CreateThread(&thread_param);
if (str_thread <= 0) {
return 1;
}
thread_param.attr = TH_C;
thread_param.initPriority = 97;
thread_param.stackSize = 0x800;
thread_param.option = 0;
thread_param.entry = (void*)PLAYThread;
strcpy(thread_param.name, "PLAYThread");
play_thread = CreateThread(&thread_param);
if (play_thread <= 0) {
return 1;
}
// Start the threads!
StartThread(iso_thread, 0);
StartThread(dgo_thread, 0);
StartThread(str_thread, 0);
StartThread(play_thread, 0);
// wait for ISO Thread to initialize
WaitMbx(sync_mbx);
// LOAD VAGDIR file
FileRecord* vagdir_file = FindISOFile("VAGDIR.AYB");
if (vagdir_file) {
LoadISOFileToIOP(vagdir_file, &gVagDir, sizeof(gVagDir));
}
FileRecord* loading_screen_file = FindISOFile(loading_screen);
if (loading_screen_file) {
LoadISOFileToEE(loading_screen_file, LOADING_SCREEN_DEST_ADDR, LOADING_SCREEN_SIZE);
}
// should be set by ISOThread to 0 before the WaitMbx(sync_mbx);
return iso_init_flag;
}
/*!
* Find a file by name. Return nullptr if it fails.
*/
FileRecord* FindISOFile(const char* name) {
return isofs->find(name);
}
/*!
* Get the length of an ISO File by FileRecord
*/
u32 GetISOFileLength(FileRecord* f) {
return isofs->get_length(f);
}
/*!
* Find VAG file by "name", where name is 8 bytes (chars with spaces at the end, treated as two
* s32's). Returns pointer to name in the VAGDIR file data.
*/
VagDirEntry* FindVAGFile(const char* name) {
VagDirEntry* entry = gVagDir.vag;
for (u32 idx = 0; idx < gVagDir.count; idx++) {
// check if matching name
if (memcmp(entry->name, name, 8) == 0) {
return entry;
}
entry++;
}
return nullptr;
}
/*!
* The CD/DVD Reading Thread. This is a mess.
*/
u32 ISOThread() {
// Initialize!
InitBuffers();
auto temp_buffer = AllocateBuffer(BUFFER_PAGE_SIZE);
InitDriver(temp_buffer->get_data()); // unblocks InitISOFS's WaitMbx
FreeBuffer(temp_buffer);
VagCommand* in_progress_vag_command = nullptr;
s32 vag_paused = 0;
s32 unk = 0;
// main CD/DVD read loop
for (;;) {
/////////////////////////////////////
// Receive Messages and Add to Queue
/////////////////////////////////////
// receive a message
IsoMessage* msg_from_mbx;
s32 mbx_status = PollMbx((MsgPacket**)(&msg_from_mbx), iso_mbx);
if (mbx_status == KE_OK) {
// we got a new message!
// initialize fields of the message
msg_from_mbx->callback_buffer = nullptr;
msg_from_mbx->ready_for_data = 1;
msg_from_mbx->callback_function = NullCallback;
msg_from_mbx->fd = nullptr;
switch (msg_from_mbx->cmd_id) {
case LOAD_TO_EE_CMD_ID:
case LOAD_TO_IOP_CMD_ID:
case LOAD_TO_EE_OFFSET_CMD_ID: {
// A Simple File Load, add it to the queue
if (!QueueMessage(msg_from_mbx, 2, "LoadSingle")) {
break;
}
auto* load_single_cmd = (IsoCommandLoadSingle*)msg_from_mbx;
// if queued successfully, start by opening the file:
if (load_single_cmd->cmd_id == LOAD_TO_EE_OFFSET_CMD_ID) {
load_single_cmd->fd =
isofs->open(load_single_cmd->file_record, load_single_cmd->offset);
} else {
// open takes -1 as "no offset", same as 0.
load_single_cmd->fd = isofs->open(load_single_cmd->file_record, -1);
}
// Check to see if it opened correctly:
if (!load_single_cmd->fd) {
// nope, set the status to indicate we failed
load_single_cmd->status = CMD_STATUS_FAILED_TO_OPEN;
// remove us from the queue...
UnqueueMessage(load_single_cmd);
// and wake up whoever requested this.
ReturnMessage(load_single_cmd);
break;
}
// yep, opened correctly. Set up the pointers/sizes
load_single_cmd->dst_ptr = load_single_cmd->dest_addr;
load_single_cmd->bytes_done = 0;
// by default, copy size is the full file.
load_single_cmd->length_to_copy = isofs->get_length(load_single_cmd->file_record);
if (load_single_cmd->length_to_copy == 0) {
// if we get zero for some reason, use the commanded length.
ASSERT(false);
load_single_cmd->length_to_copy = load_single_cmd->length;
} else if (load_single_cmd->length < load_single_cmd->length_to_copy) {
// if we ask for less than the full length, use the smaller value.
load_single_cmd->length_to_copy = load_single_cmd->length;
}
// set status and callback function.
load_single_cmd->status = CMD_STATUS_IN_PROGRESS;
u32 cmd_id = msg_from_mbx->cmd_id;
if (cmd_id == LOAD_TO_EE_CMD_ID || cmd_id == LOAD_TO_EE_OFFSET_CMD_ID) {
msg_from_mbx->callback_function = CopyDataToEE;
} else if (cmd_id == LOAD_TO_IOP_CMD_ID) {
msg_from_mbx->callback_function = CopyDataToIOP;
}
} break;
case LOAD_DGO_CMD_ID: {
if (!QueueMessage(msg_from_mbx, 0, "LoadDGO")) {
break;
}
// Got a DGO command. There is one LoadDGO command for the entire DGO.
// queued successfully, open the file.
auto* load_single_cmd = (IsoCommandLoadSingle*)msg_from_mbx;
load_single_cmd->fd = isofs->open(load_single_cmd->file_record, -1);
if (!load_single_cmd->fd) {
// failed to open, return error
load_single_cmd->status = CMD_STATUS_FAILED_TO_OPEN;
UnqueueMessage(load_single_cmd);
ReturnMessage(load_single_cmd);
} else {
// init DGO state machine and register as the callback.
load_single_cmd->status = CMD_STATUS_IN_PROGRESS;
((DgoCommand*)load_single_cmd)->dgo_state = DgoState::Init;
load_single_cmd->callback_function = RunDGOStateMachine;
}
} break;
case LOAD_SOUND_BANK: {
// NOTE: this check has been removed. there doesn't seem to be any issues with this, and
// it fixes some other issues. there doesn't appear to be any extra safety from it either
// if there's an in progress vag command, try again.
// if (in_progress_vag_command && !in_progress_vag_command->paused) {
// SendMbx(iso_mbx, msg_from_mbx);
// break;
// }
auto buff = TryAllocateBuffer(BUFFER_PAGE_SIZE);
if (!buff) {
// no buffers, try again.
SendMbx(iso_mbx, msg_from_mbx);
break;
}
auto* cmd = (SoundBankLoadCommand*)msg_from_mbx;
isofs->load_sound_bank(cmd->bank_name, cmd->bank);
FreeBuffer(buff);
ReturnMessage(msg_from_mbx);
} break;
case LOAD_MUSIC: {
// NOTE: this check has been removed. there doesn't seem to be any issues with this, and
// it fixes some other issues. there doesn't appear to be any extra safety from it either
// if there's an in progress vag command, try again.
// if (in_progress_vag_command && !in_progress_vag_command->paused) {
// SendMbx(iso_mbx, msg_from_mbx);
// break;
// }
auto buff = TryAllocateBuffer(BUFFER_PAGE_SIZE);
if (!buff) {
// no buffers, try again.
SendMbx(iso_mbx, msg_from_mbx);
break;
}
auto* cmd = (MusicLoadCommand*)msg_from_mbx;
isofs->load_music(cmd->music_name, cmd->music_handle);
FreeBuffer(buff);
ReturnMessage(msg_from_mbx);
} break;
case QUEUE_VAG_STREAM: {
auto* cmd = (VagCommand*)msg_from_mbx;
if (cmd->vag &&
(!in_progress_vag_command || in_progress_vag_command->vag != cmd->vag ||
in_progress_vag_command->file != cmd->file) &&
(!in_progress_vag_command || (!vag_paused && in_progress_vag_command->paused))) {
if (in_progress_vag_command) {
gVAGCMD = nullptr;
StopVAG(in_progress_vag_command);
ReleaseMessage(in_progress_vag_command);
}
in_progress_vag_command = &vag_cmd;
memcpy(&vag_cmd, cmd, sizeof(vag_cmd));
InitVAGCmd(&vag_cmd, 1);
LoadStackEntry* file = nullptr;
if (QueueMessage(&vag_cmd, 3, "QueueVAG")) {
if (vag_cmd.vag) {
file = isofs->open_wad(vag_cmd.file, vag_cmd.vag->offset);
}
vag_cmd.fd = file;
vag_cmd.status = -1;
vag_cmd.callback_function = ProcessVAGData;
gVAGCMD = &vag_cmd;
} else {
in_progress_vag_command = nullptr;
}
}
ReturnMessage(cmd);
} break;
case PLAY_VAG_STREAM: {
auto* cmd = (VagCommand*)msg_from_mbx;
bool thing = true;
if (in_progress_vag_command && in_progress_vag_command->vag == cmd->vag &&
in_progress_vag_command->file == cmd->file) {
in_progress_vag_command->volume = cmd->volume;
in_progress_vag_command->sound_id = cmd->sound_id;
if (in_progress_vag_command->paused) {
if (vag_paused) {
unk = 1;
} else {
UnpauseVAG(in_progress_vag_command);
}
}
} else {
if (in_progress_vag_command && !in_progress_vag_command->paused &&
cmd->priority < in_progress_vag_command->priority) {
thing = false;
}
if (thing) {
if (in_progress_vag_command) {
gVAGCMD = nullptr;
StopVAG(in_progress_vag_command);
ReleaseMessage(in_progress_vag_command);
}
in_progress_vag_command = &vag_cmd;
memcpy(&vag_cmd, cmd, sizeof(vag_cmd));
if (vag_paused) {
InitVAGCmd(&vag_cmd, 1);
unk = 1;
} else {
InitVAGCmd(&vag_cmd, 0);
}
vag_cmd.messagebox_to_reply = 0;
vag_cmd.thread_id = 0;
if (QueueMessage(&vag_cmd, 3, "PlayVag")) {
if (vag_cmd.vag) {
vag_cmd.fd = isofs->open_wad(vag_cmd.file, vag_cmd.vag->offset);
} else {
vag_cmd.fd = nullptr;
}
vag_cmd.status = -1;
vag_cmd.callback_function = ProcessVAGData;
gVAGCMD = &vag_cmd;
} else {
in_progress_vag_command = nullptr;
}
}
}
if (thing) {
if (!in_progress_vag_command || in_progress_vag_command->fd) {
gRealVAGClock = 0;
gRealVAGClockS = 0;
gRealVAGClockRunning = true;
} else {
gFakeVAGClock = 0;
gFakeVAGClockRunning = true;
gFakeVAGClockPaused = 0;
}
gVAG_Id = in_progress_vag_command->sound_id;
}
ReturnMessage(cmd);
} break;
case STOP_VAG_STREAM: {
auto* cmd = (VagCommand*)msg_from_mbx;
if (in_progress_vag_command && (!cmd->vag || in_progress_vag_command->vag == cmd->vag) &&
(cmd->priority >= in_progress_vag_command->priority)) {
gVAGCMD = nullptr;
StopVAG(in_progress_vag_command);
ReleaseMessage(in_progress_vag_command);
in_progress_vag_command = nullptr;
}
vag_paused = 0;
unk = 0;
ReturnMessage(cmd);
} break;
case PAUSE_VAG_STREAM: {
auto* cmd = (VagCommand*)msg_from_mbx;
gFakeVAGClockPaused = 1;
if (!vag_paused) {
if (!in_progress_vag_command || in_progress_vag_command->paused) {
unk = 0;
} else {
PauseVAG(in_progress_vag_command);
unk = 1;
}
vag_paused = 1;
}
ReturnMessage(cmd);
} break;
case CONTINUE_VAG_STREAM: {
auto* cmd = (VagCommand*)msg_from_mbx;
gFakeVAGClockPaused = 0;
if (vag_paused) {
if (unk) {
UnpauseVAG(in_progress_vag_command);
}
vag_paused = 0;
unk = 0;
}
ReturnMessage(cmd);
} break;
case SET_VAG_VOLUME: {
auto* cmd = (VagCommand*)msg_from_mbx;
if (in_progress_vag_command) {
in_progress_vag_command->volume = cmd->volume;
SetVAGVol();
}
ReturnMessage(cmd);
} break;
case SET_DIALOG_VOLUME: {
auto* cmd = (VagCommand*)msg_from_mbx;
gDialogVolume = cmd->volume;
if (in_progress_vag_command)
SetVAGVol();
ReturnMessage(cmd);
} break;
default:
printf("[OVERLORD] Unknown ISOThread message id 0x%x\n", msg_from_mbx->cmd_id);
}
} else if (mbx_status == KE_WAIT_DELETE) {
return 0;
}
////////////////////////////
// Handle Sound
////////////////////////////
if (in_progress_vag_command && !CheckVAGStreamProgress(in_progress_vag_command)) {
gVAGCMD = nullptr;
StopVAG(in_progress_vag_command);
ReleaseMessage(in_progress_vag_command);
in_progress_vag_command = nullptr;
// added. this variable seems to determine whether a vag stream is actually playing, and it is
// possible to get into a scenario where (for example) you want to unpause a vag stream but a
// different sound command hasn't run yet to correct this value, which makes the game either
// play the wrong sound or crash right away if no actual sound is to be played with the vag
// stream
unk = 0;
}
////////////////////////////
// Begin a read
////////////////////////////
IsoBufferHeader* read_buffer = nullptr;
IsoMessage* cmd_to_process = GetMessage();
if (cmd_to_process) { // okay, there's a command queued that we should process
// prep for a read !! DANGER !! - this read _may_ complete after the command is done.
// At this point we don't know if the command actually needs another read or not!
if (cmd_to_process->callback_function == ProcessVAGData) {
read_buffer = AllocateBuffer(STR_BUFFER_DATA_SIZE);
} else {
read_buffer = AllocateBuffer(BUFFER_PAGE_SIZE);
}
if (!read_buffer) {
// there aren't enough buffers. give up on this command for now.
cmd_to_process = nullptr;
} else {
// kick off read
if (cmd_to_process->callback_function == ProcessVAGData) {
cmd_to_process->status =
isofs->begin_read(cmd_to_process->fd, read_buffer->get_data(), STR_BUFFER_DATA_SIZE);
} else {
cmd_to_process->status =
isofs->begin_read(cmd_to_process->fd, read_buffer->get_data(), BUFFER_PAGE_SIZE);
}
// if we have bad status, kill read buffer
if (cmd_to_process->status != CMD_STATUS_IN_PROGRESS) {
FreeBuffer(read_buffer);
read_buffer = nullptr;
cmd_to_process = nullptr;
}
}
}
if (!cmd_to_process) {
// drive is doing nothing, make sure the DVD is still in there.
isofs->poll_drive();
}
// Deal with completed reads. NOTE - this can close files and terminate return commands!
ProcessMessageData();
if (!read_buffer) {
// didn't actually start a read, just delay for a bit I guess.
DelayThread(100);
} else {
// attempt to sync read. If we closed the file mid-read in ProcessMessageData, this returns
// an error code.
u32 read_status = isofs->sync_read();
if (read_status == CMD_STATUS_READ_ERR) {
// closed file mid-read, or the read failed. Either way we can't give this read buffer to
// anybody, so we should just free it.
FreeBuffer(read_buffer);
} else {
// read is good!
cmd_to_process->status = read_status;
// setup the buffer for the callback.
if (cmd_to_process->callback_function == ProcessVAGData) {
read_buffer->data = read_buffer->get_data();
read_buffer->data_size = STR_BUFFER_DATA_SIZE;
} else {
read_buffer->data = read_buffer->get_data();
read_buffer->data_size = BUFFER_PAGE_SIZE;
}
// add buffer to linked list of buffers.
if (!cmd_to_process->callback_buffer) {
cmd_to_process->callback_buffer = read_buffer;
} else {
auto* bh = cmd_to_process->callback_buffer;
while (bh->next) {
bh = (IsoBufferHeader*)bh->next;
}
bh->next = read_buffer;
}
}
}
} // for
return 0;
}
/*!
* Handler for DGO data buffers.
*/
u32 RunDGOStateMachine(IsoMessage* _cmd, IsoBufferHeader* buffer) {
auto* cmd = (DgoCommand*)_cmd;
u32 return_value = CMD_STATUS_IN_PROGRESS;
u8* unprocessed_data = (u8*)buffer->data;
u32 bytes_left = buffer->data_size;
// loop until we've read all the data
while (bytes_left) {
// printf("run DGO in state %d (%s) with %d unprocessed buffered bytes\n", cmd->dgoState,
// names[cmd->dgoState], buffer->data_size);
switch (cmd->dgo_state) {
case DgoState::Init: // init
cmd->bytes_processed = 0;
// start by reading header.
cmd->dgo_state = DgoState::Read_Header;
cmd->finished_first_obj = 0;
cmd->want_abort = 0;
break;
case DgoState::Read_Header: // read dgo header. If we are unlucky this crosses a boundary
// and we have to do this in two chunks
{
u32 bytes_to_read = sizeof(DgoHeader) - cmd->bytes_processed;
if (bytes_to_read > bytes_left) {
bytes_to_read = bytes_left;
}
// copy to our local storage
memcpy((u8*)&cmd->dgo_header + cmd->bytes_processed, unprocessed_data, bytes_to_read);
unprocessed_data += bytes_to_read;
bytes_left -= bytes_to_read;
cmd->bytes_processed += bytes_to_read;
// if we are done with header
if (cmd->bytes_processed == sizeof(DgoHeader)) {
lg::info("[Overlord DGO] Got DGO file header for {} with {} objects",
cmd->dgo_header.name, cmd->dgo_header.object_count);
cmd->bytes_processed = 0;
cmd->objects_loaded = 0;
if (cmd->dgo_header.object_count == 1) {
// if there's only one object, load to top immediately
cmd->buffer_toggle = 0;
cmd->ee_destination_buffer = cmd->buffer_heaptop;
cmd->dgo_state = DgoState::Read_Obj_Header;
} else {
// otherwise load to buffer1 first.
cmd->buffer_toggle = 1;
cmd->ee_destination_buffer = cmd->buffer1;
cmd->dgo_state = DgoState::Read_Obj_Header;
}
}
} break;
case DgoState::Finish_Obj: // we have reached the end of an object file!
{
// EE synchronization occurs here.
// we "Return" the command to tell the EE that we've loaded stuff
// EE sends us data that we read with LookMbx so we keep loading.
// the order is that we wait for the EE to tell us the next location before we return
// the most recently loaded object.
// we skip this if we're loading the first object so we can double buffer the
// linking/loading process and have two in flight at a time (one loading, other linking)
// this fills the pipeline.
// for jak 2, double buffered is disabled for the "borrow" style loads.
// this is detected by seeing that buffer1 and buffer2 are the same address.
// this function decompiles poorly in ghidra, so this is a best guess.
// in this mode, the order is swapped - the overlord returns the message once loading
// is done, then waits for the next loaddgo.
if (cmd->finished_first_obj) {
// in all cases, need sync after the first object.
s32 isSync = LookMbx(sync_mbx); // did we get a "sync" message?
if (isSync) {
// if so, this means we got a CancelDGO or NextDGO
if (cmd->want_abort) {
// we got a CancelDGO.
cmd->dgo_state = DgoState::Finish_Dgo;
break;
}
} else {
// nope, ee isn't ready. bail and wait for next run.
goto cleanup_and_return;
}
}
if (cmd->buffer1 != cmd->buffer2) {
// normal double buffered case.
cmd->finished_first_obj = 1;
cmd->status = CMD_STATUS_IN_PROGRESS;
// select a buffer for next time.
if (cmd->buffer_toggle == 1) {
cmd->selectedBuffer = cmd->buffer1;
} else {
cmd->selectedBuffer = cmd->buffer2;
}
// we've processed the command, go wake up the DGO RPC thread.
// doesn't terminate the command (ReleaseMessage does this, ReturnMessage just
// wakes up the caller while keeping the command alive).
ReturnMessage(cmd);
} else {
// single buffer mode. before we can move on, we need to wait for the EE to send us
// an update load location. We've already informed the EE where we loaded the most
// recent object, and it is busy linking...
if (cmd->finished_first_obj == 0) {
s32 isSync = LookMbx(sync_mbx); // did we get a "sync" message?
if (!isSync) {
goto cleanup_and_return;
}
cmd->finished_first_obj = 1;
}
}
// toggle buffer
if (cmd->buffer_toggle == 1) {
cmd->ee_destination_buffer = cmd->buffer2;
cmd->buffer_toggle = 2;
} else {
cmd->ee_destination_buffer = cmd->buffer1;
cmd->buffer_toggle = 1;
}
// setup for next run
if (cmd->objects_loaded + 1 == cmd->dgo_header.object_count &&
cmd->buffer1 != cmd->buffer2) {
cmd->dgo_state = DgoState::Read_Last_Obj;
} else {
cmd->dgo_state = DgoState::Read_Obj_Header;
}
break;
}
case DgoState::Read_Last_Obj: // setup load last
{
// extra sync here
s32 sync = LookMbx(sync_mbx);
if (sync) {
if (cmd->want_abort) {
cmd->dgo_state = DgoState::Finish_Dgo;
} else {
// EE ready, no abort. Nothing in flight, so we are safe to do a top load!
cmd->ee_destination_buffer = cmd->buffer_heaptop;
cmd->buffer_toggle = 0;
cmd->dgo_state = DgoState::Read_Obj_Header;
}
} else {
goto cleanup_and_return;
}
} break;
case DgoState::Read_Obj_Header: // read object file header
{
u32 bytesToRead = sizeof(ObjectHeader) - cmd->bytes_processed;
if (bytes_left < bytesToRead) {
bytesToRead = bytes_left;
}
// for now, buffer locally
memcpy((u8*)&cmd->objHeader + cmd->bytes_processed, unprocessed_data, bytesToRead);
unprocessed_data += bytesToRead;
bytes_left -= bytesToRead;
cmd->bytes_processed += bytesToRead;
// once we're done, send the header to the EE, and start reading object data
if (cmd->bytes_processed == sizeof(ObjectHeader)) {
// printf("[Overlord DGO] Got object header for %s, object size 0x%x bytes (sent
// to 0x%p)\n",
// cmd->objHeader.name, cmd->objHeader.size, cmd->ee_destination_buffer);
DMA_SendToEE(&cmd->objHeader, sizeof(ObjectHeader), cmd->ee_destination_buffer);
DMA_Sync();
cmd->ee_destination_buffer += sizeof(ObjectHeader);
cmd->objHeader.size = (cmd->objHeader.size + 0xf) & 0xfffffff0;
cmd->dgo_state = DgoState::Read_Obj_data;
cmd->bytes_processed = 0;
}
} break;
case DgoState::Read_Obj_data: // read object file data
{
u32 bytesToRead = cmd->objHeader.size - cmd->bytes_processed;
if (bytes_left < bytesToRead) {
bytesToRead = bytes_left;
}
// send contents directly to EE
DMA_SendToEE(unprocessed_data, bytesToRead, cmd->ee_destination_buffer);
DMA_Sync();
unprocessed_data += bytesToRead;
bytes_left -= bytesToRead;
cmd->ee_destination_buffer += bytesToRead;
cmd->bytes_processed += bytesToRead;
if (cmd->bytes_processed == cmd->objHeader.size) {
cmd->objects_loaded++;
if (cmd->objects_loaded == cmd->dgo_header.object_count) {
cmd->dgo_state = DgoState::Finish_Dgo;
} else {
// this logic makes jak 2 single buffer loads go to NoDoubleBuffer to return the
// command as soon as loading is done.
cmd->dgo_state = (cmd->buffer1 == cmd->buffer2) ? DgoState::Finish_Obj_NoDoubleBuffer
: DgoState::Finish_Obj;
cmd->bytes_processed = 0;
}
}
} break;
case DgoState::Finish_Dgo: {
// done with buffer, complete. Kill the ISO thread read.
return_value = CMD_STATUS_DONE;
goto cleanup_and_return;
}
case DgoState::Finish_Obj_NoDoubleBuffer: {
// new, added for jak2 - here we return the message once loading finishes.
cmd->status = CMD_STATUS_IN_PROGRESS;
if (cmd->buffer_toggle == 1) {
cmd->selectedBuffer = cmd->buffer1;
} else {
cmd->selectedBuffer = cmd->buffer2;
}
ReturnMessage(cmd);
cmd->dgo_state = DgoState::Finish_Obj;
} break;
default:
printf("unknown dgoState!\n");
}
}
// printf("[DGO State Machine Complete] Out of things to read!\n");
cleanup_and_return:
if (return_value == 0) {
buffer->data = nullptr;
buffer->data_size = 0;
} else {
if (!bytes_left) {
buffer->data = nullptr;
buffer->data_size = 0;
} else {
buffer->data = unprocessed_data;
buffer->data_size = bytes_left;
}
}
return return_value;
}
/*!
* Callback for sending to EE.
*/
u32 CopyDataToEE(IsoMessage* _cmd, IsoBufferHeader* buffer_header) {
auto* cmd = (IsoCommandLoadSingle*)_cmd;
s32 bytes_to_send = cmd->length_to_copy - cmd->bytes_done;
// make sure we don't copy too much (if the buffer does not have enough data)
if (buffer_header->data_size < (u32)bytes_to_send) {
bytes_to_send = (s32)buffer_header->data_size;
}
DMA_SendToEE(buffer_header->get_data(), bytes_to_send, cmd->dest_addr);
DMA_Sync();
cmd->dest_addr += bytes_to_send;
cmd->bytes_done += bytes_to_send;
buffer_header->data = nullptr;
buffer_header->data_size = 0;
if (cmd->bytes_done == cmd->length_to_copy) {
return CMD_STATUS_DONE;
} else {
return CMD_STATUS_IN_PROGRESS;
}
}
/*!
* Callback for loading to IOP buffer.
*/
u32 CopyDataToIOP(IsoMessage* _cmd, IsoBufferHeader* buffer_header) {
auto* cmd = (IsoCommandLoadSingle*)_cmd;
s32 bytes_to_send = cmd->length_to_copy - cmd->bytes_done;
// make sure we don't copy too much (if the buffer does not have enough data)
if (buffer_header->data_size < (u32)bytes_to_send) {
bytes_to_send = (s32)buffer_header->data_size;
}
memcpy(cmd->dst_ptr, buffer_header->get_data(), bytes_to_send);
cmd->dst_ptr += bytes_to_send;
cmd->bytes_done += bytes_to_send;
buffer_header->data = nullptr;
buffer_header->data_size = 0;
if (cmd->bytes_done == cmd->length_to_copy) {
return CMD_STATUS_DONE;
} else {
return CMD_STATUS_IN_PROGRESS;
}
}
/*!
* Callback which does nothing.
*/
u32 NullCallback(IsoMessage* _cmd, IsoBufferHeader* buffer_header) {
(void)_cmd;
buffer_header->data_size = 0;
return CMD_STATUS_NULL_CB;
}
/*!
* Initialize a VagCommand.
*/
static void InitVAGCmd(VagCommand* cmd, u32 x) {
cmd->buffer_number = 0;
cmd->data_left = 0;
cmd->started = 0;
cmd->paused = x;
cmd->sample_rate = 0;
cmd->stop = 0;
cmd->end_point = -1;
cmd->unk2 = 0;
gPlayPos = 48;
cmd->messagebox_to_reply = 0;
cmd->thread_id = 0;
}
/*!
* Byte-swap.
*/
u32 bswap(u32 in) {
return ((in >> 0x18) & 0xff) | ((in >> 8) & 0xff00) | ((in & 0xff00) << 8) | (in << 0x18);
}
static u32 ProcessVAGData(IsoMessage* _cmd, IsoBufferHeader* buffer_header) {
auto* vag = (VagCommand*)_cmd;
if (vag->stop) {
buffer_header->data_size = 0;
return CMD_STATUS_IN_PROGRESS;
}
if (vag->buffer_number == 0) {
// first buffer, set stuff up
u32* data = (u32*)buffer_header->data;
if (data[0] != 0x70474156 /* 'pGAV' */ && data[0] != 0x56414770 /* 'VAGp' */) {
vag->stop = true;
buffer_header->data_size = 0;
return CMD_STATUS_IN_PROGRESS;
}
vag->sample_rate = data[4];
vag->data_left = data[3];
if (data[0] == 0x70474156 /* 'pGAV' */) {
vag->sample_rate = bswap(vag->sample_rate);
vag->data_left = bswap(vag->data_left);
}
gSampleRate = vag->sample_rate;
gLastVagHalf = false;
vag->data_left += 48;
if (buffer_header->data_size >= vag->data_left) {
vag->end_point = vag->data_left - 16;
}
if (!DMA_SendToSPUAndSync(buffer_header->data, buffer_header->data_size, gStreamSRAM)) {
return CMD_STATUS_IN_PROGRESS;
}
sceSdSetParam(gVoice | SD_VP_VOLL, 0);
sceSdSetParam(gVoice | SD_VP_VOLR, 0);
u32 vmix = sceSdGetSwitch((gVoice & 1) | SD_S_VMIXL);
sceSdSetSwitch((gVoice & 1) | SD_S_VMIXL, vmix | (1 << (gVoice >> 1)));
vmix = sceSdGetSwitch((gVoice & 1) | SD_S_VMIXR);
sceSdSetSwitch((gVoice & 1) | SD_S_VMIXR, vmix | (1 << (gVoice >> 1)));
sceSdSetParam(gVoice | SD_VP_PITCH, 0);
sceSdSetAddr(gVoice | SD_VA_SSA, gStreamSRAM + 0x30);
sceSdSetParam(gVoice | SD_VP_ADSR1, 0xf);
sceSdSetParam(gVoice | SD_VP_ADSR2, 0x1fc0);
if (vag->end_point == -1) {
sceSdSetAddr(gVoice | SD_VA_LSAX, gTrapSRAM);
}
snd_keyOnVoiceRaw(gVoice & 1, gVoice >> 1);
vag->started = 1;
vag->data_left -= buffer_header->data_size;
buffer_header->data_size = 0;
}
if (vag->buffer_number == 1) {
if (buffer_header->data_size < vag->data_left) {
VAG_MarkLoopEnd(buffer_header->data, buffer_header->data_size);
FlushDcache();
} else {
vag->end_point = vag->data_left + 0x5FF0;
}
if (!DMA_SendToSPUAndSync(buffer_header->data, buffer_header->data_size,
gStreamSRAM + 0x6000)) {
return CMD_STATUS_IN_PROGRESS;
}
if (!vag->paused) {
vag->paused = 1;
UnpauseVAG(vag);
}
vag->ready_for_data = 0;
vag->data_left -= buffer_header->data_size;
buffer_header->data_size = 0;
gPlayPos = 48;
gPlaying = true;
}
if (vag->buffer_number > 1) {
if ((vag->buffer_number & 1) != 0) {
if (buffer_header->data_size < vag->data_left) {
VAG_MarkLoopEnd(buffer_header->data, buffer_header->data_size);
FlushDcache();
} else {
vag->end_point = vag->data_left + 0x5FF0;
}
if (!DMA_SendToSPUAndSync(buffer_header->data, buffer_header->data_size,
gStreamSRAM + 0x6000)) {
return CMD_STATUS_IN_PROGRESS;
}
sceSdSetAddr(gVoice | SD_VA_LSAX, gStreamSRAM + 0x6000);
} else {
if (buffer_header->data_size < vag->data_left) {
VAG_MarkLoopEnd(buffer_header->data, buffer_header->data_size);
FlushDcache();
} else {
vag->end_point = vag->data_left - 16;
}
if (!DMA_SendToSPUAndSync(buffer_header->data, buffer_header->data_size, gStreamSRAM)) {
return CMD_STATUS_IN_PROGRESS;
}
sceSdSetAddr(gVoice | SD_VA_LSAX, gStreamSRAM);
}
vag->ready_for_data = 0;
vag->data_left -= buffer_header->data_size;
buffer_header->data_size = 0;
}
vag->buffer_number++;
return CMD_STATUS_IN_PROGRESS;
}
static s32 CheckVAGStreamProgress(VagCommand* vag) {
if (vag->stop) {
return 0;
}
if (!vag->started) {
return 1;
}
if (vag->end_point != -1) {
if ((s32)(gPlayPos & 0xFFFFFFF0) == vag->end_point) {
return 0;
}
if (((gPlayPos < 0x6000) && (vag->end_point < 0x6000)) ||
((0x5fff < gPlayPos && (0x5fff < vag->end_point)))) {
if ((vag->unk2 == 0) && (gPlayPos < (u32)vag->end_point)) {
sceSdSetAddr(gVoice | SD_VA_LSAX, gStreamSRAM + vag->end_point);
vag->unk2 = 1;
}
return 1;
}
}
if (gPlayPos < 0x6000) {
if ((vag->buffer_number & 1) != 0) {
vag->ready_for_data = 1;
sceSdSetAddr(gVoice | SD_VA_LSAX, gTrapSRAM);
}
} else {
if ((vag->buffer_number & 1) == 0) {
vag->ready_for_data = 1;
sceSdSetAddr(gVoice | SD_VA_LSAX, gTrapSRAM);
}
}
return 1;
}
static void StopVAG(VagCommand* vag) {
gPlaying = false;
PauseVAG(vag);
snd_keyOffVoiceRaw(gVoice & 1, gVoice >> 1);
gFakeVAGClockRunning = false;
gRealVAGClockRunning = false;
gVAG_Id = 1;
}
static void PauseVAG(VagCommand* vag) {
gFakeVAGClockPaused = true;
if (vag->paused) {
return;
}
vag->paused = true;
if (vag->started) {
sceSdSetParam(gVoice | SD_VP_VOLL, 0);
sceSdSetParam(gVoice | SD_VP_VOLR, 0);
sceSdSetParam(gVoice | SD_VP_PITCH, 0);
}
}
static void CalculateVAGVolumes(s32 volume, s32 positioned, Vec3w* trans, s32* left, s32* right) {
if (positioned) {
volume = CalculateFallofVolume(trans, (volume * gDialogVolume) >> 10, 1, 10, 50);
auto* pan = &gPanTable[(630 - CalculateAngle(trans)) % 360];
*left = (pan->left * volume) >> 10;
*right = (pan->right * volume) >> 10;
if (*left >= 0x4000) {
*left = 0x3FFF;
}
if (*right >= 0x4000) {
*right = 0x3FFF;
}
} else {
volume = (volume * gDialogVolume) >> 6;
if (volume >= 0x4000) {
volume = 0x3FFF;
}
*left = volume;
*right = volume;
}
}
static void UnpauseVAG(VagCommand* vag) {
gFakeVAGClockPaused = false;
if (vag->paused) {
if (vag->started) {
s32 left = 0, right = 0;
CalculateVAGVolumes(vag->volume, vag->positioned, &vag->trans, &left, &right);
sceSdSetParam(gVoice | SD_VP_VOLL, left);
sceSdSetParam(gVoice | SD_VP_VOLR, right);
sceSdSetParam(gVoice | SD_VP_PITCH, (vag->sample_rate << 12) / 48000);
}
vag->paused = false;
}
}
void SetVAGVol() {
if (gVAGCMD && gVAGCMD->started && !gVAGCMD->paused) {
s32 left = 0, right = 0;
CalculateVAGVolumes(gVAGCMD->volume, gVAGCMD->positioned, &gVAGCMD->trans, &left, &right);
sceSdSetParam(gVoice | SD_VP_VOLL, left);
sceSdSetParam(gVoice | SD_VP_VOLR, right);
}
}
static s32 GetPlayPos() {
// This does the safe NAX read by reading NAX in a loop until it returns
// the same value 3 times in a row. We can skip this on pc.
u32 NAX = sceSdGetAddr(gVoice | SD_VA_NAX);
// We can also say our sample buffer always starts at 0 to simplify things.
if (NAX > 0xBFFF) {
return -1;
} else {
return NAX;
}
}
static void UpdatePlayPos() {
if (!gPlaying) {
return;
}
u32 pos = GetPlayPos();
if (pos == 0xffffffff) {
if (gLastVagHalf) {
pos = 0xC000;
} else {
pos = 0x6000;
}
} else {
gLastVagHalf = pos >= 0x6000;
}
if (pos >= gPlayPos) {
gRealVAGClockS += pos - gPlayPos;
} else {
gRealVAGClockS += pos + 0xC000 - gPlayPos;
}
gRealVAGClock = 4 * (0x1C00 * (gRealVAGClockS / 16) / gSampleRate);
gPlayPos = pos;
}
void* RPC_DGO(unsigned int fno, void* _cmd, int y);
void LoadDGO(RPC_Dgo_Cmd* cmd);
void LoadNextDGO(RPC_Dgo_Cmd* cmd);
void CancelDGO(RPC_Dgo_Cmd* cmd);
/*!
* DGO RPC Thread.
*/
u32 DGOThread() {
sceSifQueueData dq;
sceSifServeData serve;
// setup RPC.
CpuDisableIntr();
sceSifInitRpc(0);
sceSifSetRpcQueue(&dq, GetThreadId());
sceSifRegisterRpc(&serve, DGO_RPC_ID[g_game_version], RPC_DGO, sRPCBuff, nullptr, nullptr, &dq);
CpuEnableIntr();
sceSifRpcLoop(&dq);
return 0;
}
/*!
* DGO RPC Handler.
*/
void* RPC_DGO(unsigned int fno, void* _cmd, int y) {
(void)y;
auto* cmd = (RPC_Dgo_Cmd*)_cmd;
// call appropriate handler.
switch (fno) {
case DGO_RPC_LOAD_FNO:
LoadDGO(cmd);
break;
case DGO_RPC_LOAD_NEXT_FNO:
LoadNextDGO(cmd);
break;
case DGO_RPC_CANCEL_FNO:
CancelDGO(cmd);
break;
default:
cmd->result = DGO_RPC_RESULT_ERROR;
}
return cmd;
}
/*!
* Begin loading a DGO. Returns when the first obj is loaded.
* Then will load the next obj into the second buffer.
* Then the DGO loader will block until LoadNextDGO is called.
* This approach keeps two loads in flight at a time to increase loading throughput.
* One load will be read from DVD / DMA'd to EE
* Another will be linked on the EE.
* The final load is done directly onto the heap, and isn't double buffered
* (otherwise the linking object could allocate on the heap where the final loading object is
* being copied). This avoids having to relocate the data from the temporary load buffer to the
* heap, and is the only way to make sure that the entire heap can be filled.
*/
void LoadDGO(RPC_Dgo_Cmd* cmd) {
// Find the file
FileRecord* fr = isofs->find(cmd->name);
if (!fr) {
cmd->result = DGO_RPC_RESULT_ERROR;
return;
}
// cancel an in progress command and wait for it to end.
// note - this doesn't handle a nullptr correctly, so if this actually ends up cancelling
// it will crash.
CancelDGO(nullptr);
// set up the ISO Command
scmd.cmd_id = LOAD_DGO_CMD_ID;
scmd.messagebox_to_reply = dgo_mbx;
scmd.thread_id = 0;
scmd.buffer1 = (u8*)(u64)(cmd->buffer1);
scmd.buffer2 = (u8*)(u64)(cmd->buffer2);
scmd.buffer_heaptop = (u8*)(u64)(cmd->buffer_heap_top);
scmd.fr = fr;
// printf("LOAD DGO -- 0x%x\n", cmd->buffer1);
// send the command to ISO Thread
SendMbx(iso_mbx, &scmd);
// wait for the ReturnMessage in the DGO callback state machine.
// this happens when the first file is loaded
WaitMbx(dgo_mbx);
if (scmd.status == CMD_STATUS_IN_PROGRESS) {
// we got one, but there's more to load.
// we don't set cmd->buffer1 as it's already the correct buffer in this case -
// when there are >1 objs, we load into buffer1 first.
cmd->result = DGO_RPC_RESULT_MORE;
} else if (scmd.status == CMD_STATUS_DONE) {
// all done! make sure our reply says we loaded to the top.
cmd->result = DGO_RPC_RESULT_DONE;
cmd->buffer1 = cmd->buffer_heap_top;
scmd.cmd_id = 0;
} else {
// error.
cmd->result = DGO_RPC_RESULT_ERROR;
scmd.cmd_id = 0;
}
}
/*!
* Signal to the IOP it can keep loading and overwrite the oldest obj buffer.
* This will return when there's another loaded obj.
*/
void LoadNextDGO(RPC_Dgo_Cmd* cmd) {
// printf("LOAD NEXT DGO -- 0x%x\n", cmd->buffer1);
if (scmd.cmd_id == 0) {
// something went wrong.
cmd->result = DGO_RPC_RESULT_ERROR;
} else {
// update heap location
scmd.buffer_heaptop = (u8*)(u64)cmd->buffer_heap_top;
if (g_game_version != GameVersion::Jak1) {
scmd.buffer1 = (u8*)(u64)cmd->buffer1;
scmd.buffer2 = (u8*)(u64)cmd->buffer2;
}
// allow DGO state machine to advance
SendMbx(sync_mbx, nullptr);
// wait for another load to finish.
WaitMbx(dgo_mbx);
// another load finished, respond with the result.
if (scmd.status == CMD_STATUS_IN_PROGRESS) {
// more, use the selected buffer.
cmd->result = DGO_RPC_RESULT_MORE;
cmd->buffer1 = (u32)(u64)scmd.selectedBuffer;
} else if (scmd.status == CMD_STATUS_DONE) {
// last obj, always loaded to top.
cmd->result = DGO_RPC_RESULT_DONE;
cmd->buffer1 = cmd->buffer_heap_top;
scmd.cmd_id = 0;
} else {
cmd->result = DGO_RPC_RESULT_ERROR;
scmd.cmd_id = 0;
}
}
}
/*!
* Abort an in progress load.
*/
void CancelDGO(RPC_Dgo_Cmd* cmd) {
if (scmd.cmd_id) {
scmd.want_abort = 1;
// wake up DGO state machine with abort
SendMbx(sync_mbx, nullptr);
// wait for it to abort.
WaitMbx(dgo_mbx);
// this will cause a crash if we cancel because we try to load 2 dgos at the same time.
// this should succeed if it's an actual cancel because we changed which level we're trying to
// load.
// I don't understand how this works in the real game.
// maybe the IOP doesn't crash on writing to 0x0?
// or, we have some other bug.
if (cmd) {
printf("null pointer case in CancelDGO hit!\n");
cmd->result = DGO_RPC_RESULT_ABORTED;
}
scmd.cmd_id = 0;
}
}
s32 GetVAGStreamPos() {
UpdatePlayPos();
if (gFakeVAGClockRunning) {
return gFakeVAGClock;
}
if (gRealVAGClockRunning) {
return gRealVAGClock;
}
return -1;
}
static void VAG_MarkLoopEnd(void* data, u32 size) {
((u8*)data)[size - 15] = 3;
}