Porting a D3D8 Game to D3D11 Without Rewriting It
The remaster's approach — keep the WW3D2 API untouched, drop in a shim layer, build a standalone D3D11 renderer beside the old engine.
Video length target: ~7-9 minutes. Recording setup: Editor with split view. In-game capture for the cold open. Debug overlay visible. Prereqs for viewer: C++, real-time rendering fundamentals, passing familiarity with D3D8 or D3D11.
Cold open (0:00 – 0:45)
[VISUAL: Live gameplay. USA base, tanks rolling. Everything lit and moving. Pan the camera up to the HUD. Zoom the debug overlay showing D3D11 feature level 11_0.]
Say:
"This game shipped in 2003 on Direct3D 8."
[VISUAL: Cut to a Google search for "Direct3D 8 tutorial" — mostly dead links, gamedev.net threads from 2004, archived MSDN pages.]
Say:
"Every D3D8 tutorial is gone from the internet. Every deprecated function is a landmine. The SDK was pulled from the Windows platform years ago."
[VISUAL: Cut back to the game. Fire a shot. Particles, glow, water reflection.]
Say:
"But the exe still links. It still ships. And the engine on top of it? Almost nothing changed. Let's see why."
Act 1 — The strategy (0:45 – 2:00)
[VISUAL: Editor. Open the Core/Libraries/Source/WWVegas/WW3D2/ folder in the file tree. Scroll. Hundreds of files.]
Say:
"This is WW3D2. Westwood's scene graph from 2002. It's the layer the game talks to —
RenderObjClass,TextureClass,ShaderClass. Thousands of call sites across the codebase reach into this folder."
[VISUAL: Grep RenderObjClass across the game tree. Watch the count climb.]
Say:
"Rewriting every call site to use a modern renderer is a year of work. So we didn't. The rule for this port: don't touch WW3D2's public API. Keep the shapes the game knows. Replace what's underneath."
[VISUAL: Whiteboard or slide. Three boxes stacked: game code → WW3D2 facade → D3D8. Cross out the bottom box. Draw a new bottom box: D3D11. Add a thin layer between the facade and the new bottom: "shim".]
Say:
"API stays. Implementation changes. The shim sits where the D3D8-specific files used to live."
Act 2 — The shim (2:00 – 3:30)
[VISUAL: Open GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/D3D11Shims.cpp. Scroll to the top.]
Say:
"Here's the shim. Read the header comment. It's the whole contract."
// 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.
Say:
"Real minimal. Not stubs. Every symbol the linker needs has a function here that does the actual work on D3D11. Let's scroll."
[VISUAL: Scroll to line 6784. Stop on the forward declarations.]
static void RenderDazzleDX11(RenderObjClass* robj, RenderInfoClass& rinfo); static void RenderParticleBufferDX11(RenderObjClass* robj, RenderInfoClass& rinfo);
Say:
"Every D3D8 render function that got deleted came back as a
FooDX11twin. Same parameters —RenderObjClass,RenderInfoClass. Same call site upstream. The dispatcher above doesn't care which backend it landed on."
[VISUAL: Jump to RenderDazzleDX11 at line 9951. Scroll the body — show that it's real code. Camera math, intensity falloff, billboard math.]
Say:
"This is a dazzle — a radar-beacon glow. The shim function reads the D3D8-era WW3D parameters, does the camera-space math, and pushes the quad through the new renderer. The game code that asks for the dazzle to be drawn hasn't moved a single line."
Act 3 — The renderer (3:30 – 5:00)
[VISUAL: Cut to the Renderer/ folder in the repo root. Expand: Core/, Shaders/, Math/.]
Say:
"The thing the shim calls into lives here.
Renderer/. This is new code. Not a port of WW3D2's D3D8 backend — a fresh library, standalone, that could in principle render any game."
[VISUAL: Open Renderer/Renderer.h. Show the Renderer class.]
class Renderer { public: static Renderer& Instance(); bool Init(void* nativeWindowHandle, bool debug = false); void Shutdown(); void BeginFrame(); void EndFrame();
Say:
"Singleton. Owns the frame lifecycle."
[VISUAL: Open Renderer/Core/Device.h. Scroll to the ComPtr members.]
ComPtr<ID3D11Device> m_device; ComPtr<ID3D11DeviceContext> m_context; ComPtr<IDXGISwapChain1> m_swapChain; ComPtr<ID3D11RenderTargetView> m_backBufferRTV; ComPtr<ID3D11DepthStencilView> m_depthStencilView;
Say:
"
Devicewraps the D3D11 handles. ID3D11Device, ID3D11DeviceContext, swap chain, depth view. Everywhere else in the renderer talks to this, not to Windows directly. When the Vulkan backend gets built out, this class is where the#ifdeffork already lives."
Act 4 — Immutable state objects (5:00 – 6:15)
[VISUAL: Open Renderer/Core/Shader.cpp. Scroll to BlendState::CreateOpaque at line 219.]
Say:
"Biggest mental shift going from D3D8 to D3D11. In D3D8 you'd call
SetRenderStatedozens of times per draw — alpha on, source factor this, destination factor that. Per. Draw. Call."
[VISUAL: Jump through the four factories in sequence.]
bool BlendState::CreateOpaque(Device& device) { D3D11_BLEND_DESC desc = {}; desc.RenderTarget[0].BlendEnable = FALSE; desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; return SUCCEEDED(device.GetDevice()->CreateBlendState(&desc, m_state.GetAddressOf())); }
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; ... return SUCCEEDED(device.GetDevice()->CreateBlendState(&desc, m_state.GetAddressOf())); }
Say:
"D3D11 doesn't let you wiggle individual states. You fill a description struct, hand it to the device, get back an immutable state object. We do that once at startup — one for opaque, one for alpha, one for additive, one for multiplicative."
[VISUAL: Show a draw site that just calls blendAdditive.Bind(device) followed by DrawIndexed.]
Say:
"At draw time, binding that state is a single call. No per-state chatter. Ten thousand draws a frame is ten thousand binds, not ten thousand times twelve render-state toggles. This is where a chunk of the performance headroom came from."
Act 5 — Winding order gotcha (6:15 – 7:00)
[VISUAL: Open Renderer/Core/Shader.cpp. Scroll to RasterizerState::Create at line 198.]
D3D11_RASTERIZER_DESC desc = {};
desc.FillMode = ToD3D11(fill);
desc.CullMode = ToD3D11(cull);
desc.FrontCounterClockwise = frontCCW ? TRUE : FALSE;
Say:
"See this flag?
FrontCounterClockwise = TRUE. This is the line that takes three hours to find when it's wrong."
[VISUAL: Side-by-side. Left: game rendering correctly. Right: game with flag flipped to FALSE — tanks missing their top halves, terrain with holes, buildings inside out.]
Say:
"D3D8 conventionally treated clockwise-wound triangles as front-facing. The shipped W3D meshes were authored for that world. D3D11's default is the opposite. If you leave the default on, exactly half the triangles in every mesh get culled as backfaces — silently. Nothing errors. Nothing logs. Geometry just vanishes."
Say:
"Set
FrontCounterClockwiseto TRUE, and the new rasterizer matches what the legacy data expects. Every call toRasterizerState::CreatepassesfrontCCW = true. It's not a choice — it's a compatibility constant."
Act 6 — What doesn't work yet (7:00 – 7:45)
[VISUAL: Editor. Open W3DDisplay.cpp. Jump to line 164.]
bool g_debugDisableShadowMap = true; // GPU shadow mapping — OFF by default (WIP)
Say:
"Being honest. The port isn't finished. The GPU shadow map pass is implemented, but it's disabled by default. You can flip it on in the Inspector, but enough edge cases in the caster pass mean it's not shippable yet."
[VISUAL: Grep IDirect3DTexture8 across the repo. Show the hits in the smudge system and the WW3D texture loader.]
Say:
"The smudge system — heat haze behind jet exhausts — still references
IDirect3DTexture8in its hardware-capability probe. On D3D11 that probe fails, so the effect is effectively off. Same story with a couple of shader-manager methods that are stubbed in the shim, waiting for a real implementation."
Say:
"These aren't emergencies. They're the long tail. You stop a port at the point where the game ships, and you chip at the tail."
Outro (7:45 – 8:15)
[VISUAL: Pull back to the game. Wide shot. Units, water, terrain, dust.]
Say:
"The lesson. A layered compatibility shim lets an old API and a new implementation coexist until you're ready to rewrite at the API level. The WW3D2 facade doesn't know it's running on D3D11. The game code doesn't know WW3D2's insides got gutted. The shim in the middle is ugly C++ with a thousand translation functions, and that's fine."
Say:
"Real shipping projects look like this. You don't rewrite the world. You replace the floor one plank at a time and keep the lights on."
[VISUAL: Fade.]
B-roll to shoot
- Fly-through of a populated base at night. Needed for the cold open and the outro.
- Editor screencap: scrolling
WW3D2/folder to convey scale. - Side-by-side: correctly rendered scene vs.
FrontCounterClockwise = FALSEbroken scene. Record the broken version by toggling the flag locally before recording. - Inspector panel showing the
GPU shadow maptoggle.
Takeaway (for the description box)
Port an old game by preserving its internal API and replacing the backend underneath. Build the new backend as a standalone library. Use a shim file to translate the old symbols into calls against the new one. Expect one or two compatibility flags — like winding order — to cost you an afternoon each. Ship the parts that work; leave the long tail visible behind debug toggles.