<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: not a pro</title>
    <description>The latest articles on DEV Community by not a pro (@whiplash).</description>
    <link>https://dev.to/whiplash</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3808137%2Fee0c1dac-30f3-477e-bed6-9d38201ab143.png</url>
      <title>DEV Community: not a pro</title>
      <link>https://dev.to/whiplash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/whiplash"/>
    <language>en</language>
    <item>
      <title>d3d9-webgl — Run Legacy D3D9 Code in the Browser Without Rewriting It</title>
      <dc:creator>not a pro</dc:creator>
      <pubDate>Thu, 05 Mar 2026 14:50:12 +0000</pubDate>
      <link>https://dev.to/whiplash/d3d9-webgl-run-legacy-d3d9-code-in-the-browser-without-rewriting-it-1108</link>
      <guid>https://dev.to/whiplash/d3d9-webgl-run-legacy-d3d9-code-in-the-browser-without-rewriting-it-1108</guid>
      <description>&lt;p&gt;I ported a 2003 online game to the browser. The game was written in C++ with Direct3D 9. Emscripten handles the C++-to-Wasm part fine, but the moment you &lt;code&gt;#include &amp;lt;d3d9.h&amp;gt;&lt;/code&gt;, the build dies — that header only ships with the Windows DirectX SDK.&lt;/p&gt;

&lt;p&gt;The obvious options all sucked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rewrite the renderer in WebGL / Three.js&lt;/td&gt;
&lt;td&gt;You're basically rewriting the game&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Convert D3D9 calls to OpenGL one by one&lt;/td&gt;
&lt;td&gt;Still a massive rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Re-implement the D3D9 API itself, backed by WebGL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The existing code doesn't need to change&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So I went with option 3 and built &lt;strong&gt;&lt;a href="https://github.com/LostMyCode/d3d9-webgl" rel="noopener noreferrer"&gt;d3d9-webgl&lt;/a&gt;&lt;/strong&gt;: a header + source file set that implements D3D9 interfaces on top of WebGL 2.0. You drop it into an Emscripten project, and your D3D9 code compiles and runs in the browser as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it actually work?
&lt;/h2&gt;

&lt;p&gt;The game I was porting is &lt;strong&gt;GunZ: The Duel&lt;/strong&gt; (2003). The rendering code required almost no changes. You can play it at &lt;a href="https://gunz.sigr.io/" rel="noopener noreferrer"&gt;gunz.sigr.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60viifpimvkbobj3f2dd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60viifpimvkbobj3f2dd.png" alt=" " width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://gunz.sigr.io/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;gunz.sigr.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;IDirect3D9&lt;/code&gt;, &lt;code&gt;IDirect3DDevice9&lt;/code&gt;, &lt;code&gt;IDirect3DTexture9&lt;/code&gt; — all the COM interfaces are re-implemented. When your app calls &lt;code&gt;IDirect3DDevice9::DrawPrimitive()&lt;/code&gt;, the wrapper translates it to &lt;code&gt;glDrawArrays()&lt;/code&gt; internally. The application can't tell the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rebuilding the Fixed Function Pipeline in GLSL
&lt;/h3&gt;

&lt;p&gt;D3D9's FFP — &lt;code&gt;SetLight&lt;/code&gt;, &lt;code&gt;SetMaterial&lt;/code&gt;, &lt;code&gt;SetTransform&lt;/code&gt; — doesn't exist in WebGL 2.0. I had to write GLSL shaders that replicate the entire lighting model: World/View/Projection transforms, normal transforms, per-vertex diffuse + specular for up to 3 point lights in the vertex shader, and texture blending + color composition in the fragment shader.&lt;/p&gt;

&lt;p&gt;This was the most time-consuming part of the whole project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parsing FVF at runtime
&lt;/h3&gt;

&lt;p&gt;D3D9 vertex buffers use Flexible Vertex Format (FVF) — bit flags that describe the vertex layout. The wrapper parses these at runtime to set up &lt;code&gt;glVertexAttribPointer&lt;/code&gt; with the right stride and offset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Position + Normal + Vertex Color + 1 UV set&lt;/span&gt;
&lt;span class="n"&gt;DWORD&lt;/span&gt; &lt;span class="n"&gt;fvf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;D3DFVF_XYZ&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;D3DFVF_NORMAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;D3DFVF_DIFFUSE&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;D3DFVF_TEX1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Texture format mismatches
&lt;/h3&gt;

&lt;p&gt;D3D9 uses BGRA; WebGL wants RGBA. &lt;code&gt;A8R8G8B8&lt;/code&gt; gets swizzled on upload. 16-bit formats like &lt;code&gt;R5G6B5&lt;/code&gt; and &lt;code&gt;A4R4G4B4&lt;/code&gt; are expanded to RGBA8.&lt;/p&gt;

&lt;p&gt;DXT1/DXT3/DXT5 compressed textures are passed straight through — the &lt;code&gt;WEBGL_compressed_texture_s3tc&lt;/code&gt; extension covers this on pretty much every desktop browser.&lt;/p&gt;
&lt;h3&gt;
  
  
  Y-axis flip
&lt;/h3&gt;

&lt;p&gt;D3D9: top-left origin, Y goes down. OpenGL: bottom-left origin, Y goes up. When rendering to an FBO via &lt;code&gt;SetRenderTarget&lt;/code&gt;, the image ends up flipped. The wrapper compensates during &lt;code&gt;StretchRect&lt;/code&gt; blit to screen. Direct screen rendering doesn't need the fix.&lt;/p&gt;
&lt;h3&gt;
  
  
  Clip planes via &lt;code&gt;discard&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;WebGL has no hardware clip planes. The fragment shader calculates clip-plane distance and discards fragments on the wrong side. Simple, but one of those things you don't think about until everything clips wrong.&lt;/p&gt;
&lt;h3&gt;
  
  
  State caching
&lt;/h3&gt;

&lt;p&gt;D3D9 apps call &lt;code&gt;SetRenderState&lt;/code&gt; / &lt;code&gt;SetTexture&lt;/code&gt; / &lt;code&gt;SetSamplerState&lt;/code&gt; hundreds of times per frame, and most of those calls set the same value that's already set. The wrapper caches everything — texture bindings, shader programs, sampler states, viewport, scissor — and only issues a GL call when something actually changes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdd6km9u1lzeh4gjl9tc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdd6km9u1lzeh4gjl9tc.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy five files into your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;d3d9.h&lt;/code&gt; — D3D9 type definitions &amp;amp; interfaces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d3d9.cpp&lt;/code&gt; — WebGL 2.0 implementation (~3,400 lines)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d3dx9math.h&lt;/code&gt; — D3DX math library&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d3dx9.h&lt;/code&gt; — D3DX stubs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;windows_compat.h&lt;/code&gt; — Windows API stubs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CMake:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;add_executable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;my_app main.cpp d3d9.cpp&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;target_link_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;my_app PRIVATE
    -sUSE_WEBGL2=1
    -sFULL_ES3=1
    -sWASM=1
    -sALLOW_MEMORY_GROWTH=1
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;emcmake cmake &lt;span class="nb"&gt;.&lt;/span&gt;
emmake make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FFP only.&lt;/strong&gt; HLSL vertex/pixel shaders are not supported. The wrapper reports &lt;code&gt;VertexShaderVersion = 0&lt;/code&gt;, so applications with shader code paths need to fall back to FFP.&lt;/p&gt;

&lt;p&gt;Other limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Max 3 point lights (no directional or spotlights)&lt;/li&gt;
&lt;li&gt;Vertex buffer stream 0 only&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;LockRect&lt;/code&gt; on render targets (no GPU readback)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;D3DXMatrixInverse&lt;/code&gt; is stubbed (returns identity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, most games and tools from the early 2000s used FFP anyway. Programmable shaders only became common in the later DX9 era, so anything before ~2005 is likely covered.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I'm releasing this
&lt;/h2&gt;

&lt;p&gt;While building the GunZ port, I kept searching for something like this. It didn't exist. I spent a lot of time writing this wrapper, and I figured someone else out there is probably stuck on the same problem right now.&lt;/p&gt;

&lt;p&gt;If you have an old D3D9 codebase and you've been curious about running it in a browser — try it out and let me know how it goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/LostMyCode/d3d9-webgl" rel="noopener noreferrer"&gt;d3d9-webgl&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/LostMyCode" rel="noopener noreferrer"&gt;
        LostMyCode
      &lt;/a&gt; / &lt;a href="https://github.com/LostMyCode/d3d9-webgl" rel="noopener noreferrer"&gt;
        d3d9-webgl
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Direct3D 9 Fixed-Function Pipeline → WebGL 2.0 wrapper for Emscripten/WASM
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;d3d9-webgl&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A Direct3D 9 Fixed-Function Pipeline implementation targeting WebGL 2.0 via Emscripten/WebAssembly.&lt;/p&gt;

&lt;p&gt;Drop-in D3D9 headers and a single &lt;code&gt;.cpp&lt;/code&gt; file that translates D3D9 API calls to WebGL — enabling legacy D3D9 applications to run in the browser without rewriting their rendering code.&lt;/p&gt;

&lt;p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;
&lt;br&gt;
&lt;strong&gt;01 — Rotating Cube&lt;/strong&gt;&lt;br&gt;Textures, VB/IB, DrawIndexedPrimitive&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;
&lt;br&gt;
&lt;strong&gt;02 — FFP Lighting&lt;/strong&gt;&lt;br&gt;3 Point Lights, Materials, DrawPrimitiveUP&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/LostMyCode/d3d9-webgl/screenshots/01-rotating-cube.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FLostMyCode%2Fd3d9-webgl%2Fscreenshots%2F01-rotating-cube.png" width="400"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/LostMyCode/d3d9-webgl/screenshots/02-lighting.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FLostMyCode%2Fd3d9-webgl%2Fscreenshots%2F02-lighting.png" width="400"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Features&lt;/h2&gt;
&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;strong&gt;Full FFP Emulation&lt;/strong&gt; — Per-vertex lighting (3 point lights), materials, texture stage states, transform matrices&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;All Draw Paths&lt;/strong&gt; — &lt;code&gt;DrawIndexedPrimitive&lt;/code&gt;, &lt;code&gt;DrawIndexedPrimitiveUP&lt;/code&gt;, &lt;code&gt;DrawPrimitiveUP&lt;/code&gt;, &lt;code&gt;DrawPrimitive&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;FVF Parsing&lt;/strong&gt; — Automatic vertex layout from &lt;code&gt;D3DFVF_XYZ&lt;/code&gt;, &lt;code&gt;D3DFVF_XYZRHW&lt;/code&gt;, &lt;code&gt;D3DFVF_NORMAL&lt;/code&gt;, &lt;code&gt;D3DFVF_DIFFUSE&lt;/code&gt;, &lt;code&gt;D3DFVF_TEX1&lt;/code&gt;–&lt;code&gt;TEX8&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Texture Formats&lt;/strong&gt; — DXT1/3/5 (via S3TC extension), A8R8G8B8, X8R8G8B8, R5G6B5, A4R4G4B4, A1R5G5B5, R8G8B8&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Render States&lt;/strong&gt; — Alpha blending, alpha test, depth test, stencil operations, culling, scissor test, color write mask&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Texture Stage States&lt;/strong&gt; — Stage 0 color/alpha ops (MODULATE, MODULATE2X, SELECTARG), Stage 1 lightmap blending (MODULATE…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/LostMyCode/d3d9-webgl" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>webdev</category>
      <category>webgl</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
