LOG IN
Docs
By Generals Remastered team

D3D11 Rendering Backend

How the DX11 port replaces the original D3D8 pipeline while keeping the WW3D2 API intact for the rest of the engine.

The original Generals shipped on Direct3D 8. A D3D8 device will still create on modern Windows, but the fixed-function pipeline, the D3DXMESH helpers, and a pile of deprecated surface types have all eroded with every driver generation. The remaster replaces the entire backend with a Direct3D 11 device while keeping the game-facing WW3D2 API unchanged. Almost nothing above the renderer boundary knows a port happened.

What replaced what

Original (D3D8) Remaster (D3D11)
IDirect3DDevice8 ID3D11Device + ID3D11DeviceContext
Fixed-function T&L Shared HLSL vertex + pixel shaders
D3DXMESH Plain vertex + index buffers
Per-call SetRenderState Immutable state objects via cached descs
Fixed-function texture stages Bound SRVs + samplers in PS

Everything above the WW3D2 boundary — renderers, draw modules, scene graph — is untouched. The WW3D2 classes (RenderObjClass, TextureClass, ShaderClass) still compile and link, but their implementations now route through the new Render::Renderer singleton in Renderer/Renderer.cpp.

The shim layer

GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/D3D11Shims.cpp is the bridge. Its header explains the role directly:

// Provides real minimal implementations for symbols that were previously
// supplied by D3D8-dependent source files which have been removed from the
// build during the D3D8 -> D3D11 migration.
//
// This is NOT a collection of empty stubs - each implementation is the
// minimal correct version needed for the game engine to link and run.

Every RenderFoo function that used to live next to its D3D8 implementation — terrain, shadows, particles, water tracks, dazzles, road rendering — now has a RenderFooDX11 counterpart in this file. They accept the same WW3D parameters (CameraClass*, RenderObjClass*, world transforms) and translate into draws on the shared Render::Renderer.

The state cache

D3D11 has no concept of "set this one render state". You build a D3D11_RASTERIZER_DESC (or blend, depth, sampler), hand it to the device, and receive back an immutable state object that binds as a unit. The renderer in Renderer/Core/Shader.cpp wraps this as RasterizerState, BlendState, and DepthStencilState classes with named factory methods:

bool BlendState::CreateAdditive(Device& device)
{
    D3D11_BLEND_DESC desc = {};
    desc.RenderTarget[0].BlendEnable = TRUE;
    desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
    desc.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;
    desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
    // ... alpha channel setup ...
    return SUCCEEDED(device.GetDevice()->CreateBlendState(&desc, m_state.GetAddressOf()));
}

State objects are built once at renderer init. Per-frame rendering binds them by reference, so a 50k-vertex frame typically cycles between ~20 state combinations — cheaper than D3D8's per-state calls and trivially cacheable because the objects are already immutable.

Shader layout

All HLSL lives in Renderer/Shaders/HLSL/. The two primary files are Shader3D.hlsl (lit world geometry) and ShaderShadowDepth.hlsl (depth-only pass). Each shader reads two constant buffers — frame constants (view, projection, lights, shadow parameters) at b0, and per-object constants (world matrix, object color) at b1. Textures bind by named semantic slot:

Texture2D<float> shadowMapTexture : register(t4);
SamplerComparisonState shadowSampler : register(s2);

The renderer writes constant buffers once per frame in Renderer::BeginFrame for frame constants and once per draw call in RenderModelInstance for object constants. All shaders share the same constant-buffer layout so the frame CB can be reused across passes without rebinding.

Winding order

The FrontCounterClockwise = TRUE flag on the rasterizer state is load-bearing. D3D8's fixed-function pipeline used clockwise front-facing by default, but the engine's meshes ship with counter-clockwise winding. Flipping this to FALSE silently culls half the world — terrain triangulates away, unit backs face out, and debugging is painful because the geometry is still there, just invisible. The flag is set once in Shader::Create and never changed; any new rasterizer state must copy it.

Quirks

  • Sampler slots are shared between shaders. The main PS in Shader3D.hlsl samples at most one texture per material. Adding a second sampler to the terrain path and forgetting it's the same PS produces garbage on units — the sampler binding persists across draws. Terrain-specific texture work belongs in a terrain-specific shader, not in the shared one.

  • Shadow caster walk diverges from the main pass. The shadow decal pass iterates TheGameClient->getDrawableList() because drawable-owned render objects are not in the classic m_3DScene scene graph. See Shadow Decals for the full pipeline.

  • D3D11Shims.cpp is where the port is not done. A function still missing from the D3D11 side will either be a deliberate no-op or a stub that logs once and returns. The rule in feedback_no_stubs.md is: either implement it or remove it from the build — empty stubs that silently return have caused more bugs than they fixed (the isCliffCell stub let tanks drive up cliffs; the dazzle early-return disabled every beacon; the W3DPoliceCarDraw::createDynamicLight nullptr kept police cars dark).

  • WW3D2 static members live here too. D3D11Shims.cpp owns the definitions for WW3D2 class statics that used to live in ww3d.cpp. Removing the file without moving the definitions will link-fail in interesting places — WW3D::Get_Frame_Time_Seconds, WW3D::Sync, etc.

  • No DX11Wrapper class. The original port prototypes used a wrapper class mirroring DX8Wrapper from the legacy engine, but the final shape is the Render::Renderer singleton plus state-object wrappers. If you're porting documentation from a pre-release note, translate "DX11Wrapper::X" to "Render::Renderer::Instance().X".