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.hlslsamples 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 classicm_3DScenescene 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
isCliffCellstub let tanks drive up cliffs; the dazzle early-return disabled every beacon; theW3DPoliceCarDraw::createDynamicLightnullptr kept police cars dark).WW3D2 static members live here too.
D3D11Shims.cppowns the definitions for WW3D2 class statics that used to live inww3d.cpp. Removing the file without moving the definitions will link-fail in interesting places —WW3D::Get_Frame_Time_Seconds,WW3D::Sync, etc.No
DX11Wrapperclass. The original port prototypes used a wrapper class mirroringDX8Wrapperfrom the legacy engine, but the final shape is theRender::Renderersingleton plus state-object wrappers. If you're porting documentation from a pre-release note, translate "DX11Wrapper::X" to "Render::Renderer::Instance().X".