jak-project/game/kernel/jak1/kdgo.cpp
water111 4f537d4a71
[jak3] Set up ckernel (#3308)
This sets up the C Kernel for Jak 3, and makes it possible to build and
load code built with `goalc --jak3`.

There's not too much interesting here, other than they switched to a
system where symbol IDs (unique numbers less than 2^14) are generated at
compile time, and those get included in the object file itself.

This is kind of annoying, since it means all tools that produce a GOAL
object file need to work together to assign unique symbol IDs. And since
the symbol IDs can't conflict, and are only a number between 0 and 2^14,
you can't just hash and hope for no collisions.

We work around this by ignoring the IDs and re-assigning our own. I
think this is very similar to what the C Kernel did on early builds of
Jak 3 which supported loading old format level files, which didn't have
the IDs included.

As far as I can tell, this shouldn't cause any problems. It defeats all
of their fancy tricks to save memory by not storing the symbol string,
but we don't care.
2024-01-16 19:24:02 -05:00

200 lines
7.3 KiB
C++

#include "kdgo.h"
#include "common/common_types.h"
#include "common/log/log.h"
#include "game/common/dgo_rpc_types.h"
#include "game/kernel/common/Ptr.h"
#include "game/kernel/common/fileio.h"
#include "game/kernel/common/kdgo.h"
#include "game/kernel/common/kmalloc.h"
#include "game/kernel/jak1/klink.h"
namespace jak1 {
RPC_Dgo_Cmd* sLastMsg; //! Last DGO command sent to IOP
RPC_Dgo_Cmd sMsg[2]; //! DGO message buffers
void kdgo_init_globals() {
sLastMsg = nullptr;
memset(sMsg, 0, sizeof(sMsg));
}
/*!
* Send message to IOP to start loading a new DGO file
* Uses a double-buffered message buffer
* @param name: the name of the DGO file
* @param buffer1 : one of the two file loading buffers
* @param buffer2 : the other of the two file loading buffers
* @param currentHeap : the current heap (for loading directly into the heap).
*
* DONE,
* MODIFIED : Added print statement to indicate when DGO load starts.
*/
void BeginLoadingDGO(const char* name, Ptr<u8> buffer1, Ptr<u8> buffer2, Ptr<u8> currentHeap) {
u8 msgID = sMsgNum;
RPC_Dgo_Cmd* mess = sMsg + sMsgNum;
sMsgNum = sMsgNum ^ 1; // toggle message buffer.
RpcSync(DGO_RPC_CHANNEL); // make sure old RPC is finished
// put a dummy value here just to make sure the IOP overwrites it.
sMsg[msgID].result = DGO_RPC_RESULT_INIT; // !! this is 666
// inform IOP of buffers
sMsg[msgID].buffer1 = buffer1.offset;
sMsg[msgID].buffer2 = buffer2.offset;
// also give a heap pointer so it can load the last object file directly into the heap to save the
// precious time.
sMsg[msgID].buffer_heap_top = currentHeap.offset;
// file name
strcpy(sMsg[msgID].name, name);
lg::debug("[Begin Loading DGO RPC] {}, 0x{:x}, 0x{:x}, 0x{:x}", name, buffer1.offset,
buffer2.offset, currentHeap.offset);
// this RPC will return once we have loaded the first object file.
// but we call async, so we don't block here.
RpcCall(DGO_RPC_CHANNEL, DGO_RPC_LOAD_FNO, true, mess, sizeof(RPC_Dgo_Cmd), mess,
sizeof(RPC_Dgo_Cmd));
sLastMsg = mess;
}
/*!
* Get the next object in the DGO. Will block until something is loaded.
* @param lastObjectFlag: will get set to 1 if this is the last object.
*
* DONE,
* MODIFIED : added exception if the sLastMessage isn't set (game just returns null as buffer)
*/
Ptr<u8> GetNextDGO(u32* lastObjectFlag) {
*lastObjectFlag = 1;
// Wait for RPC function to respond. This will happen once the first object file is loaded.
RpcSync(DGO_RPC_CHANNEL);
Ptr<u8> buffer(0);
if (sLastMsg) {
// if we got a good result, get pointer to object
if ((sLastMsg->result == DGO_RPC_RESULT_MORE) || (sLastMsg->result == DGO_RPC_RESULT_DONE)) {
buffer.offset =
sLastMsg->buffer1; // buffer 1 always contains location of most recently loaded object.
}
// not the last one, so don't set the flag.
if (sLastMsg->result == DGO_RPC_RESULT_MORE) {
*lastObjectFlag = 0;
}
// no pending message.
sLastMsg = nullptr;
} else {
// I don't see how this case can happen unless there's a bug. The game does check for this and
// nothing in this case. (maybe from GOAL this can happen?)
printf("last message not set!\n");
}
return buffer;
}
/*!
* Instruct the IOP to continue loading the next object.
* Only should be called once it is safe to overwrite the previous.
* @param heapPtr : pointer to heap so the IOP could try to load directly into a heap if it wants.
* This should be updated after each object file load to make sure the IOP knows the exact location
* of the end of the GOAL heap data.
* DONE,
* EXACT
*/
void ContinueLoadingDGO(Ptr<u8> heapPtr) {
u32 msgID = sMsgNum;
RPC_Dgo_Cmd* sendBuff = sMsg + sMsgNum;
sMsgNum = sMsgNum ^ 1;
sendBuff->result = DGO_RPC_RESULT_INIT;
sMsg[msgID].buffer1 = 0;
sMsg[msgID].buffer2 = 0;
sMsg[msgID].buffer_heap_top = heapPtr.offset;
// the IOP will wait for this RpcCall to continue the DGO state machine.
RpcCall(DGO_RPC_CHANNEL, DGO_RPC_LOAD_NEXT_FNO, true, sendBuff, sizeof(RPC_Dgo_Cmd), sendBuff,
sizeof(RPC_Dgo_Cmd));
// this async RPC call will complete when the next object is fully loaded.
sLastMsg = sendBuff;
}
/*!
* Load and link a DGO file.
* This does not use the mutli-threaded linker and will block until the entire file is done.
*/
void load_and_link_dgo(u64 name_gstr, u64 heap_info, u64 flag, u64 buffer_size) {
auto name = Ptr<char>(name_gstr + 4).c();
auto heap = Ptr<kheapinfo>(heap_info);
load_and_link_dgo_from_c(name, heap, flag, buffer_size, false);
}
/*!
* Load and link a DGO file.
* This does not use the mutli-threaded linker and will block until the entire file is done.e
*/
void load_and_link_dgo_from_c(const char* name,
Ptr<kheapinfo> heap,
u32 linkFlag,
s32 bufferSize,
bool jump_from_c_to_goal) {
lg::debug("[Load and Link DGO From C] {}", name);
u32 oldShowStall = sShowStallMsg;
// remember where the heap top point is so we can clear temporary allocations
auto oldHeapTop = heap->top;
// allocate temporary buffers from top of the given heap
// align 64 for IOP DMA
// note: both buffers named dgo-buffer-2
auto buffer2 = kmalloc(heap, bufferSize, KMALLOC_TOP | KMALLOC_ALIGN_64, "dgo-buffer-2");
auto buffer1 = kmalloc(heap, bufferSize, KMALLOC_TOP | KMALLOC_ALIGN_64, "dgo-buffer-2");
// build filename. If no extension is given, default to CGO.
char fileName[16];
kstrcpyup(fileName, name);
if (fileName[strlen(fileName) - 4] != '.') {
strcat(fileName, ".CGO");
}
// no stall messages, as this is a blocking load and when spending 100% CPU time on linking,
// the linker can beat the DVD drive.
sShowStallMsg = 0;
// start load on IOP.
BeginLoadingDGO(
fileName, buffer1, buffer2,
Ptr<u8>((heap->current + 0x3f).offset & 0xffffffc0)); // 64-byte aligned for IOP DMA
u32 lastObjectLoaded = 0;
while (!lastObjectLoaded) {
// check to see if next object is loaded (I believe it always is?)
auto dgoObj = GetNextDGO(&lastObjectLoaded);
if (!dgoObj.offset) {
continue;
}
// if we're on the last object, it is loaded at cheap->current. So we can safely reset the two
// dgo-buffer allocations. We do this _before_ we link! This way, the last file loaded has more
// heap available, which is important when we need to use the entire memory.
if (lastObjectLoaded) {
heap->top = oldHeapTop;
}
// determine the size and name of the object we got
auto obj = dgoObj + 0x40; // seek past dgo object header
u32 objSize = *(dgoObj.cast<u32>()); // size from object's link block
char objName[64];
strcpy(objName, (dgoObj + 4).cast<char>().c()); // name from dgo object header
lg::debug("[link and exec] {:18s} {} {:6d} heap-use {:8d} {:8d}: 0x{:x}", objName,
lastObjectLoaded, objSize, kheapused(kglobalheap),
kdebugheap.offset ? kheapused(kdebugheap) : 0, kglobalheap->current.offset);
link_and_exec(obj, objName, objSize, heap, linkFlag, jump_from_c_to_goal); // link now!
// inform IOP we are done
if (!lastObjectLoaded) {
ContinueLoadingDGO(Ptr<u8>((heap->current + 0x3f).offset & 0xffffffc0));
}
}
sShowStallMsg = oldShowStall;
}
} // namespace jak1