This started from trying to understand more about how rendering and game state works in games and in this case in Direct3D 9 with a game that I have played a lot.

I wanted to try and draw simple entity information (ESP) inside Call of Duty 4 without creating a separate overlay window. To start, drawing information on the screen is the easy part, the real problem is finding a good and reliable place inside the game’s frame loop where I could get enough information from the game state to know what to draw, and then converting the 3D world positions into 2D screen coordinates.

The first thing we need is a place inside the game’s render loop where our own drawing code can run. I ended up using Direct3D 9 EndScene. It seemed that it is not the final moment where the frame appears on screen, that would be closer to Present, but it was still late enough in the frame that the game had already submitted its scene and I had the D3D device available for Dear ImGui.

Simplified D3D9 frame in CoD4 and where we are trying to hook into:

BeginScene()
    draw world geometry
    draw models
    draw effects
    draw HUD/UI
    ...
EndScene() ← hook runs here
Present()

The exact middle order is engine-specific. Direct3D only really cares that rendering commands happen between BeginScene and EndScene and that Present is what submits the back buffer for display.

...
typedef HRESULT(_stdcall* EndScene)(IDirect3DDevice9* pDevice);

EndScene pEndScene;
HRESULT __stdcall hkEndScene(IDirect3DDevice9* pDevice) {
    if (!init) {
        ImGui::CreateContext();
        ImGui_ImplWin32_Init(FindWindow(0, "Call of Duty 4"));
        ImGui_ImplDX9_Init(pDevice);
        init = true;
    }

    ImGui_ImplDX9_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();
...

The actual hook also rendered the ImGui draw data and called the original EndScene later in the same hkEndScene function. The code finalizes the ImGui frame, then submits the draw data through the D3D9 backend and then returns through the original EndScene pointer:

ImGui::EndFrame();
ImGui::Render();
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

return pEndScene(pDevice);

Once this was working, I had a detoured EndScene handler that ran during rendering with the game’s D3D9 device already available. That was the important part. I no longer needed to create a separate layer or a transparent overlay window, or try to synchronize an external renderer with the game window.

The hook itself is installed by creating a temporary D3D9 device with Direct3DCreate9, which is then used to read the IDirect3DDevice9 vtable. In that vtable, slot 42 is the EndScene method, so we can use that slot to locate the EndScene function address:

void** vTable = *(void***)(pDevice);
pEndScene = (EndScene)DetourFunction((PBYTE)vTable[42], (PBYTE)hkEndScene);

This is different from a direct vtable hook. With a vtable hook, you would save the original function pointer from the slot, replace that slot with your hook function, draw inside the hook and then call the saved original function. In this code, the vtable is only used to find the EndScene function address. That address is passed to DetourFunction, from the old Microsoft Detours API, which installs the detour on the target function.

After that, the rendering side was mostly solved. Then we can move on to figuring out what to actually draw.

For this project, I treated the player/client records as a fixed-stride array in memory. Once we have the base address of a client/entity array and the size of each entry, walking the player slots becomes simple pointer arithmetic: base + index * stride. In this case, 0x831270 was the recovered base address and 0x4CC was the size of one entry for the tested build (v1.7). The loop bound of 64 came from the common MAX_CLIENTS assumption I started with.

int LocalPlayerId = *(int*)0x1280500;
int LocalTeam = *(int*)(0x831270 + LocalPlayerId * 0x4CC + 0x1C);

for (int i = 0; i < 64; i++) {
    Ent* Entity = (Ent*)(0x831270 + i * 0x4CC);
    if (!Entity->Valid)
        continue;

These addresses and offsets are specific to the tested CoD4 executable, build, and configuration, but based on the newer 1.8 version, it seems most of the same offsets are still valid.

The matching C++ structure makes the assumptions visible:

// 0x831270 increment -> 0x4CC
class Ent {
public:
    int32_t Valid;       // 0x0000
    char pad_0004[8];    // 0x0004
    char Name[16];       // 0x000C
    int32_t Team;        // 0x001C
    char pad_0020[888];  // 0x0020
    Vec3 Position;       // 0x0398
};

The scan range was a first assumption based on the common MAX_PLAYERS / MAX_CLIENTS = 64 convention. At this point we are interested only in the player slots. For each index, the code computes 0x831270 + i * 0x4CC, casts that address to Ent* and then tries to read fields such as valid, name, team, and position. If Entity->Valid is false, that slot is skipped and we can treat it as not being a valid player entity. If it is valid, then we treat that memory as an active player entity record and use its Name, Team and Position.

At this point we have entity origins, names and teams. That is enough to decide what should be drawn, but not where to draw it. For that, we need the camera state:

static LocalPlayerInfo* LocalPlayer = (LocalPlayerInfo*)0x793698;
static Refdef_t* RefDef = (Refdef_t*)0x78F608;

The entity positions exist in 3D world spaceand ImGui needs 2D screen coordinates. So we need to subtract the camera origin, transform the point into view space using the camera axes, reject points behind the cameraand then apply the FOV and screen dimensions:

AngleVectors(LocalPlayer->ViewAngles, matrix);
vLocal = SubVectorDist(dst, RefDef->ViewOrig);

vTransForm.x = vLocal.dotproduct(matrix.vRight);
vTransForm.y = vLocal.dotproduct(matrix.vUpward);
vTransForm.z = vLocal.dotproduct(matrix.vForward);

if (vTransForm.z < 1.f)
    return false;

ScreenPos.x = RefDef->ScreenX / 2 +
    (RefDef->ScreenX / 2 / vTransForm.z * (1 / RefDef->Fov.x)) * vTransForm.x;

ScreenPos.y = RefDef->ScreenY / 2 -
    (RefDef->ScreenY / 2 / vTransForm.z * (1 / RefDef->Fov.y)) * vTransForm.y;

For the tested build, RefDef->ViewOrig worked as the camera origin and RefDef->Fov.x/y worked as the projection scale values. In this version, the camera basis was built from LocalPlayer->ViewAngles using AngleVectors. If I had recovered the final view-axis vectors from refdef, those would likely have been a better source because they would represent the final camera better than reconstructing it from angles.

But once the WorldToScreen logic was pretty much down, the rest of the overlay, the drawing part, became a lot simpler. Drawing a name above a player is just projecting a point slightly above the entity origin point and then drawing the entity name there:

w2s::WorldToScreen(
    Vec3{ Entity->Position.x, Entity->Position.y, Entity->Position.z + 65 },
    HeadPos2D
);

Draw::DrawName(HeadPos2D, color, Entity->Name);

The name drawing helper function itself is very small. It just submits text to ImGui’s background draw list:

void Draw::DrawName(ImVec2 Pos, ImColor color, const char* Text) {
    ImGui::GetBackgroundDrawList()->AddText(Pos, color, Text);
}

The 3D box uses the same idea, but instead of one point it builds a small box around the entity origin, so a rough box around the player. The helper creates top and bottom corner points, projects them with WorldToScreenand then connects the projected points with AddLine:

void Draw::Draw3DBox(Vec3 Pos, ImColor color, int Thickness) {
    Vec3 tfl = Vec3{ Pos.x + 10, Pos.y - 10, Pos.z + 55 };
    Vec3 tfr = Vec3{ Pos.x + 10, Pos.y + 10, Pos.z + 55 };
    Vec3 tbl = Vec3{ Pos.x - 10, Pos.y + 10, Pos.z + 55 };
    Vec3 tbr = Vec3{ Pos.x - 10, Pos.y - 10, Pos.z + 55 };

    ImVec2 tfl2D, tfr2D, tbl2D, tbr2D;
    w2s::WorldToScreen(tfl, tfl2D);
    w2s::WorldToScreen(tfr, tfr2D);
    w2s::WorldToScreen(tbl, tbl2D);
    w2s::WorldToScreen(tbr, tbr2D);

    ImGui::GetBackgroundDrawList()->AddLine(tfl2D, tfr2D, color, Thickness);
    ImGui::GetBackgroundDrawList()->AddLine(tfl2D, tbr2D, color, Thickness);
    ImGui::GetBackgroundDrawList()->AddLine(tbl2D, tbr2D, color, Thickness);
}

The real helper does the same for the bottom corners and the vertical edges. It is an approximate box around the origin and not a true model bounds box. One issue with this version is that it does not check whether every WorldToScreen call succeeded before drawing the lines.

Another way to make the ESP more model aware is to use tag or skeletal/bone based drawing. Instead of drawing a fixed box around the entity origin, this way uses known player model tags such as j_head, j_elbow_le, j_knee_riand j_wrist_ri. For each tag, the code asks the engine for that tag’s world position on the current player model, projects the result into screen spaceand then draws points or lines between related tags.

CHAR* Bone_Middle[5]    = { "j_head", "neck", "j_spineupper", "j_spinelower", "pelvis" };
CHAR* Bone_Right_Arm[5] = { "neck", "j_clavicle_ri", "j_shoulder_ri", "j_elbow_ri", "j_wrist_ri" };
CHAR* Bone_Left_Arm[5]  = { "neck", "j_clavicle_le", "j_shoulder_le", "j_elbow_le", "j_wrist_le" };
CHAR* Bone_Right_Leg[5] = { "pelvis", "j_hip_ri", "j_knee_ri", "j_ankle_ri", "j_ankle_ri" };
CHAR* Bone_Left_Leg[5]  = { "pelvis", "j_hip_le", "j_knee_le", "j_ankle_le", "j_ankle_le" };

Each array means one chain of the body. The drawing code walks each chain, then resolves two neighboring tags, projects both positions with WorldToScreenand draws a line between the two screen space points.

Then the draw pass can walk those tags for a valid player:

for (int j = 0; j < ARRAYSIZE(Bones); j++) {
    Tag = CG_RegisterTag(Bones[j], 1, strlen(Bones[j]) + 1);
    Math.GetPlayerTag(Tag, &pEnt, Save);

    if (Math.WorldToScreen(Save, &Screen[0], &Screen[1])) {
        Drawing.DrawString(Screen[0], Screen[1], normalFont, 1, colRed, ".");
    }
}

But that is a different quality of overlay. In this ImGui version I used origin-based primitives, so names, distances, linesand a more approximate 3D box for this first attempt.

In the end, I had a simple ESP .DLL working in the v1.7 version of CoD4, injected with my own DLL injector. ImGui and D3D9 were just the visible layer. The difficult parts were the assumptions underneath it, like the hook target, the entity stride, the refdef layout, the FOV interpretation and the projection.

CoD4 D3D9 entity overlay showing projected entity boxes