-
Notifications
You must be signed in to change notification settings - Fork 59
Internals
The game is divided into separate modules called overlays. Overlays (OVL
in short) are chunk of compiled code that are loaded into a specific address based on different situations in the game. This allows to fit more content in the game.
Address | End addr. | File name | Description |
---|---|---|---|
80010000 |
8009FFFF |
main.exe |
Hardware API, bootloader, shared memory |
800A0000 |
8013BFFF |
DRA.BIN |
Game engine |
8013C000 |
8016FFFF |
BIN/ARC_F.BIN |
Alucard spritesheet |
80170000 |
80179FFF |
SERVANT/TT_*.BIN |
Familiar program |
8017A000 |
8017CFFF |
BIN/WEAPON0.BIN |
Right-hand equip program |
8017D000 |
8017FFFF |
BIN/WEAPON1.BIN |
Left-hand equip program |
80180000 |
801DFFFF |
ST/*/*.BIN |
Stages program |
801E0000 |
801E8000 |
F_MAP.BIN |
Castle map graphics |
801F0000 |
801F7800 |
Heap | |
801F???? |
801FFFF0 |
Stack | |
80280000 |
80?????? |
Used to load debug assets |
Address | End addr. | File name | Description |
---|---|---|---|
80010000 |
8009FFFF |
main.exe |
Hardware API, bootloader, shared memory |
800A0000 |
8013BFFF |
DRA.BIN |
Game engine |
8013C000 |
8017FFFF |
BIN/RIC.BIN |
Richter program |
80180000 |
801DFFFF |
ST/*/*.BIN |
Stages program |
801E0000 |
801E8000 |
F_MAP.BIN |
Castle map graphics |
801F0000 |
801F7800 |
Heap | |
801F???? |
801FFFF0 |
Stack |
Loaded by the PlayStation 1 BIOS. On the game disk the name is SLUS_000.67
and it is described by the boot configuration file SYSTEM.CNF
. In the absence of a configuration file, this executable can be named as PSX.EXE
. The name main.exe
is after its counterpart found in the PSP version.
It contains all the hardware API (eg. gamepad, CD, memory card, GPU renderer) of the PlayStation 1 console from the PSY-Q SDK. It does not contain any game logic. The entry point main
is just responsible to load DRA.BIN
and F_MAP.BIN
from the disk and execute the entry point of DRA.BIN immediately.
This executable is also responsible of statically allocate a shared portion of memory that all the overlays can access to. One typical example is the GameApi
structure. Overlays never communicate between one another with the exception of this portion of memory.
Named as DRA.BIN
. It contains all the business logic such as the game loop, the game API, logic to draw maps, load stages, render the graphics, handles the menu, defines monsters, equipment and much more. There is a state-machine that decides what the game engine is supposed to do (eg. load a new stage, spawn Alucard entity and move him across the stage, pause the game into the menu, play FMV). It is the most critical file of the game.
Contains all the unique logic to handle map specific events, cut-scenes, enemy AI, collisions and more. It also contains the rooms and entities layout. Each overlay can be considered as its own mini-game. The title screen SEL.BIN
is an example of how a stage overlay can act very differently. It exposes a small portion of endpoints into the game API including the room location list or the sprite definitions.
The following is a list of the many stage overlays found in the binary of the game:
ID | PSX | PSP | Description |
---|---|---|---|
00 | NO0 | STP00 | Marble Gallery |
01 | NO1 | STP01 | Outer Wall |
02 | LIB | STP02 | Long Library |
03 | CAT | STP03 | Catacombs |
04 | NO2 | STP04 | Olrox's Quarters |
05 | CHI | STP05 | Abandoned Mine |
06 | DAI | STP06 | Royal Chapel |
07 | NP3 | STP07 | Castle Entrance |
08 | CEN | STP08 | Castle Center |
09 | NO4 | STP09 | Underground Caverns |
0A | ARE | STP10 | Colosseum |
0B | TOP | STP11 | Castle Keep (Hallway Entrance) |
0C | NZ0 | STP12 | Alchemy Laboratory |
0D | NZ1 | STP13 | Clock Tower |
0E | WRP | STP14 | Warp Room |
0F | NO1 | STP15 | |
10 | NO0 | STP16 | |
11 | NO1 | STP17 | |
12 | DRE | STP18 | Nightmare |
13 | NZ0 | STP19 | |
14 | NZ1 | STP20 | |
15 | LIB | STP21 | |
16 | BO7 | STP22 | Cerberos (Boss) |
17 | MAR | STP23 | Maria cutscene (clock room) |
18 | BO6 | STP24 | Richter (Boss) |
19 | BO5 | STP25 | Hippogryph (Boss) |
1A | BO4 | STP26 | Doppleganger10 (Boss) |
1B | BO3 | STP27 | Scylla (Boss) |
1C | BO2 | STP28 | Minotaurus / Werewolf (Boss) |
1D | BO1 | STP29 | Granfaloon (Boss) |
1E | BO0 | STP30 | Olrox (Boss) |
1F | ST0 | STP31 | Final Stage: Bloodlines |
20 | RNO0 | STP32 | Black Marble Gallery |
21 | RNO1 | STP33 | Reverse Outer Wall |
22 | RLIB | STP34 | Forbidden Library |
23 | RCAT | STP35 | Floating Catacombs |
24 | RNO2 | STP36 | Death Wing's Lair |
25 | RCHI | STP37 | Cave (Reverse Abandoned Mine) |
26 | RDAI | STP38 | Anti-Chapel |
27 | RNO3 | STP39 | Reverse Castle Entrance |
28 | RCEN | STP40 | Reverse Castle Center (Shaft Boss Fight) |
29 | RNO4 | STP41 | Reverse Caverns |
2A | RARE | STP42 | Reverse Colosseum |
2B | RTOP | STP43 | Reverse Castle Keep (Hallway Entrance) |
2C | RNZ0 | STP44 | Necromancy Laboratory |
2D | RNZ1 | STP45 | Reverse Clock Tower |
2E | RWRP | STP46 | Reverse Warp Room |
2F | NO1 | STP47 | |
30 | NO1 | STP48 | |
31 | NO1 | STP49 | |
32 | NO1 | STP50 | |
33 | NO1 | STP51 | |
34 | NO1 | STP52 | |
35 | RNZ1 | STP53 | |
36 | RBO8 | STP54 | Galamoth (Boss) |
37 | RBO7 | STP55 | Akmodan II (Boss) |
38 | RBO6 | STP56 | Dracula (Boss) |
39 | RBO5 | STP57 | Doppleganger40 (Boss) |
3A | RBO4 | STP58 | The Creature (Boss) |
3B | RBO3 | STP59 | Medusa (Boss) |
3C | RBO2 | STP60 | Death (Boss) |
3D | RBO1 | STP61 | Beezelbub (Boss) |
3E | RBO0 | STP62 | Fake Trevor / Fake Grant / Fake Sypha (Boss) |
3F | NO1 | STP63 | |
40 | MAD | STP64 | Debug Room |
41 | NO3 | STP65 | Castle Entrance (Intro / Death Cutscene) |
42 | DAI | STP66 | |
43 | LIB | STP67 | |
44 | NO1 | STP68 | |
45 | SEL | STP69 | Title Screen / Main Menu |
46 | TE1 | STP70 | Test Room 1 |
47 | TE2 | STP71 | Test Room 2 |
48 | TE3 | STP72 | Test Room 3 |
49 | TE4 | STP73 | Test Room 4 |
4A | TE5 | STP74 | Test Room 5 |
4B | TOP | STP75 | |
4C | TE2 | STP76 | |
4D | TE2 | STP77 | |
4E | TE2 | STP78 | |
4F | TE2 | STP79 |
Please note, at the time of writing, not all stage overlays are extracted and available for decompilation. This was a deliberate decision made in an effort to keep the scope of the project manageable during the beginning phase. As the project progresses, more overlays will likely be extracted and made available for decompilation.
Exposes endpoints from different overlays in a shared memory area, so they can communicate each other indirectly.
The game has three ways to load the game files.
This is the main way the game loads files. To greatly improve loading times, files are accessed by directly specifying to the CD-ROM system the absolute position of the file to load in the disk. This saves time as the disk head would not need to fetch the file location and size from the main table searching by the name. The location is a 32-bit integer that represents the sector index of where the file is physically located on the disk. The size of a sector is physically 2352 bytes long, but only 2048 of them are used to store the actual game data (aside from the XA tracks and the STR videos).
There are mainly two tables in DRA.BIN
that stores all the game data. One is at 800A3C40
which stores every stage offsets and size. The other table is at 800ACC74
and it is used to load specific files in different part of the memory based on their type. The stage ST/SEL/SEL.BIN
contains a duplicate table of 800A3C40
, probably to have some kind of stage select built in.
As the location of the files is hard-coded, their position has to be known before building the actual game. It is believed the PSY-Q SDK has tools to offer this specific functionality. In alternative the tool sotn-disk can be used to patch those binaries after the image is being built.
The classic way of opening files with the UNIX function open
. This is only used by main.exe
to bootstrap DRA.BIN and F_MAP.BIN. It is known to be slower than using an LBA table as it has to move the head at the beginning of the disk, look for the Primary Volume Descriptor offset and walk through the directories to resolve the chosen sector offset and file length.
Used during the development. The file path prefix is sim:
(eg. sim:c:\bin\f_title1.bin
). The PlayStation 1 offers drivers for the file I/O. Most likely the developers registered the driver sim
to allow the PlayStation to load overlays on the fly from a physical hard drive to quickly iterate between new builds.
The main.exe
initially attempts to load DRA.BIN
and DRA.BIO
(formal name of F_MAP.BIN) from the simulator. If it succeeded it sets the flag at 800978AC
to a value different than 0
, effectively indicating the game needs to use the simulator instead of the LBA for all the following files.
This is a write-up to break down how the game renders graphics on screen. In short the game has a primitive list that gets consolidated into a drawing scene every frame. When graphics contains sprites or textures the resources are loaded from the VRAM.
TODO
TODO
TODO: anim set, anim frame, clut index, tpage.
This write-up focuses on how the Video RAM (VRAM) is managed by a stage. The VRAM can be divided into four quadrants, each measuring 512x256 pixels.
When working with graphics that have a 4 bits per pixel (4bpp) format, a Color Look-Up Table (CLUT) is utilized. In the following examples, a grey CLUT will be used as a reference.
To transfer graphics from the main memory to the VRAM, a DMA operation is performed using the PSY-Q SDK function LoadImage(RECT*, u_long*)
. The first parameter RECT
specifies the location in the VRAM where the graphics will be placed. The second parameter u_long*
is a pointer to the main memory location from which the graphics will be loaded. It is important to note that the coordinates and size mentioned in the RECT
parameter are based on a 16-bit image. For example, to load a 256x256 4bpp texture, the RECT
values will be set as w=64
and h=256
.
The upper-left quadrant of the VRAM serves as a double buffer and acts as the render target for creating the final picture. Utilizing a double buffer is crucial to prevent tearing artifacts in the displayed image. While one buffer is used to transfer its content to the video output, the other buffer can be populated to prepare for the next frame. The game employs two buffer descriptors, namely GpuBuffer g_GpuBuffers[2]
and the variable g_CurrentBuffer
to keep track of the currently active buffer for rendering the scene. When the frame is ready to be displayed on the screen, g_CurrentBuffer = g_CurrentBuffer->next
is called to flip the buffers, ensuring that the rendering occurs on the appropriate buffer. During the rendering of stages, each buffer is represented as two 256x240 targets one next to the other. These buffers are stored in the RGBA5551 format, where each pixel is represented by a 16-bit value.
To achieve effects like the photo burning at the end of the prologue, the game creates a duplicate copy of one of the buffers and uses it as a texture. This technique is commonly used in PlayStation 1 games and subsequent game platforms.
The second quadrant in the upper-right corner of the VRAM is dedicated to storing stage graphics. It specifically contains everything related to a given stage, including the tileset and decorative entities (e.g. the moving trees in parallax outside the castle or the clock tower in the prologue). Stage graphics are loaded from ST/{stage}/F_{stage}.BIN
which is always a 256KB file consisting of 32 images of 128x128 pixels and a 4bpp format. Typically the graphics at the far left is used for the tile map and the one at the far right is for the entities.
The first few 256x256 tpages of the stage graphics are 256x240 of the actual tiles and the bottom 16x256 is utilised as a palette collection in the RGBA5551 format. All the stage graphics exclusively use the CLUT at that location to render the tile map. As a result the last row of the tileset is reserved for the palette, often resulting to either uncomprehensive graphics or left blank.
The tiles within the tileset are referenced by an index, where each tile occupies a space of 16x16 pixels. Tile 0
is always empty and it is never rendered. Tile 1
is located at (16, 0)
while the tile 15
is is at (240, 0)
. Following this pattern, tile 16
can be located at (0, 16)
. It is worth noting that tile maps are crafted in a way where tiles from 240 to 255 are never used, as they contain the CLUT for the stage. Typically, after tile 239, the next tile would be tile 256, positioned at (256, 0).
The fourth quadrant at the bottom-right is dedicated to the game engine DRA.BIN
. It stores the graphics that are shared across stages and can be reused without the need to load them every time. This is true even if the shared entities code (e.g. the candle or the item prize) is store inside each stage overlay and not in DRA. For Alucard, these graphics are loaded from BIN/F_GAME.BIN, while for Richter, they are loaded from BIN/F_GAME2.BIN. This section of VRAM contains a series of 32 images, each with a size of 128x128 pixels and a 4bpp format.
The file from which these graphics are loaded is 264KB in size. The last 8KB of the file is allocated for the CLUT, which is loaded at the very bottom of the first quadrant, just below the two rendering targets.
This last section is still work-in-progress. It is currently unknown how the game dynamically loads this graphics and where it comes from. It seems to be dynamically loaded from memory based on the room where the player is into.
TODO
The debug room overlay ST/MAD.BIN
was compiled earlier than the first retail release of the game. All the offsets that refers to DRA.BIN points to invalid portions of data or to the wrong API calls, effectively breaking the majority of its original functionalities. That is why the debug room does not contain any object. By compiling the debug room with make mad_fix you can restore it by redirecting the old pointers to the retail version of the game.
Many entities in the game spawn other entities in a roundabout, indirect manner. For example, let's consider the function DoGravityJump, which is used when the player activates their gravity boots. It accelerates the player rapidly into the air, but it also creates a factory, using this syntax:
CreateEntFactoryFromEntity(g_CurrentEntity, FACTORY(0, 2), 0);
This will create a factory. The factory is a short-lived entity which exists solely for the purpose of spawning another entity. We pass it a basis entity (in this case g_CurrentEntity) which will act as the parent of our new entity. By default, our new entity gets spawned into the same location as the parent.
The FACTORY() macro is important. It controls the behavior of the factory to determine what child is made, and any parameters related to that.
The first parameter of FACTORY will be passed to the child. The second parameter tells the factory which blueprint to use.
When a factory is created, it has the purpose of spawning a child. It builds that child (hence being called a factory), and does so based on a set of parameters. We call these parameters the "blueprint", the design for building the child. In this case, the gravity jump is creating a factory to use blueprint 2.
To see what blueprints are doing, we can look to assets/dra/factory_blueprint.json
(note: Richter has his own set of blueprints, so any functions in RIC should look to assets/ric
for those blueprints). We can then look at blueprint 2, as the blueprint with ID of 2. That is:
{
"id": 2,
"id_hex": "2",
"ram_addr": "800AD1E0",
"childId": 3,
"unk1": 1,
"unk2": {
"bit7": true,
"bit6": true,
"bottom6": 1
},
"unk3": 0,
"unk4": {
"topHalf": 2,
"bottomHalf": 0
},
"unk5": 0
},
Everything from childId
down is controlling how the factory behaves. Some factories will spawn multiple child entities, for example. A lot of these fields are not understood, and should be researched. Look to EntityEntFactory's first block of lines to see how the data is loaded into the fields of the factory.
The most important element of the blueprint is the childId
, which tells us what child is made. In this case, the child is entity #3.
When looking at these child entities, they are numbered and are pulled from a few different tables. Factories in the main game and overlays will pull from g_DraEntityTbl
(and in Richter's code, g_RicEntityTbl
). In this case, we will look at the #3 entity (counting from 0 of course) in the DRA entity table, and we find EntityGravityBootBeam
. Lines right up! This factory will create the blue and white beam from Alucard's feet when gravity jumping.
Weapons are different. You will usually see weapon calls like
g_api.CreateEntFactoryFromEntity(self, ((g_HandId + 1) << 0xE) + FACTORY(0x100, 88), 0);
The use of g_HandId
is crucial for knowing this is a weapon call. You can look to func_8011A4D0
to understand how entity update functions are assigned to new entities. Once we identify this as a weapon call, we can look to the blueprints. We have blueprint 88, which says it has child 9 (again, in assets/dra/factory_blueprint.json
). But this is not the 9th entity in g_DraEntityTbl
! This is the 9th element in the Weapon struct in include/weapon.h
. We can count these down and see func_ptr_80170024
. This factory call will create an entity whose update function is func_ptr_80170024
.
This process of identifying what a factory does can be a little confusing. Anywhere in the code which uses a CreateEntFactoryFromEntity
call should ideally have a comment saying what entity is being made from the chosen blueprint.
It would also be nice if we had a macro for something like WPN_FACTORY()
which would bake in the g_HandId
logic, which is somewhat bulky.