summary refs log tree commit diff
path: root/src/Graphics_D3D9.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Graphics_D3D9.c
initial commit
Diffstat (limited to 'src/Graphics_D3D9.c')
-rw-r--r--src/Graphics_D3D9.c885
1 files changed, 885 insertions, 0 deletions
diff --git a/src/Graphics_D3D9.c b/src/Graphics_D3D9.c
new file mode 100644
index 0000000..fbec5b7
--- /dev/null
+++ b/src/Graphics_D3D9.c
@@ -0,0 +1,885 @@
+#include "Core.h"
+#if CC_GFX_BACKEND == CC_GFX_BACKEND_D3D9
+#include "_GraphicsBase.h"
+#include "Errors.h"
+#include "Window.h"
+
+/* Avoid pointless includes */
+#define WIN32_LEAN_AND_MEAN
+#define NOSERVICE
+#define NOMCX
+#define NOIME
+/* NOTE: Direct3D9Ex backend was dropped on 2022-02-25 in favour of Direct3D11 */
+#include <d3d9.h>
+#include <d3d9caps.h>
+#include <d3d9types.h>
+
+/* https://docs.microsoft.com/en-us/windows/win32/dxtecharts/resource-management-best-practices */
+/* https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline */
+
+/* https://docs.microsoft.com/en-us/windows/win32/direct3d9/d3dfvf-texcoordsizen */
+static DWORD d3d9_formatMappings[] = { D3DFVF_XYZ | D3DFVF_DIFFUSE, D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 };
+
+static IDirect3D9* d3d;
+static IDirect3DDevice9* device;
+static DWORD createFlags;
+static D3DFORMAT viewFormat, depthFormat;
+static int cachedWidth, cachedHeight;
+static int depthBits;
+static float totalMem;
+static cc_bool fallbackRendering;
+
+static void D3D9_RestoreRenderStates(void);
+static void D3D9_FreeResource(GfxResourceID resource) {
+	cc_uintptr addr;
+	ULONG refCount;
+	IUnknown* unk;
+	
+	unk = (IUnknown*)resource;
+	if (!unk) return;
+
+#ifdef __cplusplus
+	refCount = unk->Release();
+#else
+	refCount = unk->lpVtbl->Release(unk);
+#endif
+
+	if (refCount <= 0) return;
+	addr = (cc_uintptr)unk;
+	Platform_Log2("D3D9 resource has %i outstanding references! ID 0x%x", &refCount, &addr);
+}
+
+static IDirect3D9* (WINAPI *_Direct3DCreate9)(UINT SDKVersion);
+
+static void LoadD3D9Library(void) {
+	static const struct DynamicLibSym funcs[] = {
+		DynamicLib_Sym(Direct3DCreate9)
+	};
+	static const cc_string path = String_FromConst("d3d9.dll");
+	void* lib;
+	DynamicLib_LoadAll(&path, funcs, Array_Elems(funcs), &lib);
+
+	if (lib) return;
+	Logger_FailToStart("Failed to load d3d9.dll. You may need to install Direct3D9.");
+}
+
+static void CreateD3D9Instance(void) {
+	d3d = _Direct3DCreate9(D3D_SDK_VERSION);
+
+	/* Normal Direct3D9 supports POOL_MANAGED textures */
+	/*  (Direct3D9Ex does not support them however) */
+	Gfx.ManagedTextures = true;
+	if (!d3d) Logger_Abort("Direct3DCreate9 returned NULL");
+
+	fallbackRendering = Options_GetBool("fallback-rendering", false);
+	if (!fallbackRendering) return;
+	Platform_LogConst("WARNING: Using fallback rendering mode, which will reduce performance");
+}
+
+static void FindCompatibleViewFormat(void) {
+	static const D3DFORMAT formats[] = { D3DFMT_X8R8G8B8, D3DFMT_R8G8B8, D3DFMT_R5G6B5, D3DFMT_X1R5G5B5 };
+	cc_result res;
+	int i;
+
+	for (i = 0; i < Array_Elems(formats); i++) {
+		viewFormat = formats[i];
+		res = IDirect3D9_CheckDeviceType(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, viewFormat, viewFormat, true);
+		if (!res) return;
+	}
+	Logger_FailToStart("Failed to create back buffer. Graphics drivers may not be installed.\n\nIf that still does not work, try the OpenGL build instead");
+}
+
+static void FindCompatibleDepthFormat(void) {
+	static const D3DFORMAT formats[] = { D3DFMT_D32, D3DFMT_D24X8, D3DFMT_D24S8, D3DFMT_D24X4S4, D3DFMT_D16, D3DFMT_D15S1 };
+	cc_result res;
+	int i;
+
+	for (i = 0; i < Array_Elems(formats); i++) {
+		depthFormat = formats[i];
+		res = IDirect3D9_CheckDepthStencilMatch(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, viewFormat, viewFormat, depthFormat);
+		if (!res) return;
+	}
+	Logger_FailToStart("Failed to create depth buffer. Graphics drivers may not be installed.\n\nIf that still does not work, try the OpenGL build instead");
+}
+
+static void D3D9_FillPresentArgs(D3DPRESENT_PARAMETERS* args) {
+	args->AutoDepthStencilFormat = depthFormat;
+	args->BackBufferWidth  = Game.Width;
+	args->BackBufferHeight = Game.Height;
+	args->BackBufferFormat = viewFormat;
+	args->BackBufferCount  = 1;
+	args->EnableAutoDepthStencil = true;
+	args->PresentationInterval   = gfx_vsync ? D3DPRESENT_INTERVAL_ONE : D3DPRESENT_INTERVAL_IMMEDIATE;
+	args->SwapEffect = D3DSWAPEFFECT_DISCARD;
+	args->Windowed   = true;
+	//args->MultiSampleType = D3DMULTISAMPLE_8_SAMPLES;
+}
+
+static const int D3D9_DepthBufferBits(void) {
+	switch (depthFormat) {
+	case D3DFMT_D32:     return 32;
+	case D3DFMT_D24X8:   return 24;
+	case D3DFMT_D24S8:   return 24;
+	case D3DFMT_D24X4S4: return 24;
+	case D3DFMT_D16:     return 16;
+	case D3DFMT_D15S1:   return 15;
+	}
+	return 0;
+}
+
+static void D3D9_UpdateCachedDimensions(void) {
+	cachedWidth  = Game.Width;
+	cachedHeight = Game.Height;
+}
+
+static cc_bool deviceCreated;
+static void TryCreateDevice(void) {
+	cc_result res;
+	D3DCAPS9 caps;
+	HWND winHandle = (HWND)Window_Main.Handle;
+	D3DPRESENT_PARAMETERS args = { 0 };
+	D3D9_FillPresentArgs(&args);
+
+	/* Try to create a device with as much hardware usage as possible. */
+	createFlags = D3DCREATE_HARDWARE_VERTEXPROCESSING;
+	res = IDirect3D9_CreateDevice(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, winHandle, createFlags, &args, &device);
+	/* Another running fullscreen application might prevent creating device */
+	if (res == D3DERR_DEVICELOST) { Gfx.LostContext = true; return; }
+
+	/* Fallback with using CPU for some parts of rendering */
+	if (res) {
+		createFlags = D3DCREATE_MIXED_VERTEXPROCESSING;
+		res = IDirect3D9_CreateDevice(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, winHandle, createFlags, &args, &device);
+	}
+	if (res) {
+		createFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
+		res = IDirect3D9_CreateDevice(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, winHandle, createFlags, &args, &device);
+	}
+
+	/* Not enough memory? Try again later in a bit */
+	if (res == D3DERR_OUTOFVIDEOMEMORY) { Gfx.LostContext = true; return; }
+
+	if (res) Logger_Abort2(res, "Creating Direct3D9 device");
+	res = IDirect3DDevice9_GetDeviceCaps(device, &caps);
+	if (res) Logger_Abort2(res, "Getting Direct3D9 capabilities");
+
+	D3D9_UpdateCachedDimensions();
+	deviceCreated    = true;
+	Gfx.MaxTexWidth  = caps.MaxTextureWidth;
+	Gfx.MaxTexHeight = caps.MaxTextureHeight;
+	totalMem = IDirect3DDevice9_GetAvailableTextureMem(device) / (1024.0f * 1024.0f);
+}
+
+void Gfx_Create(void) {
+	LoadD3D9Library();
+	CreateD3D9Instance();
+	FindCompatibleViewFormat();
+	FindCompatibleDepthFormat();
+	depthBits = D3D9_DepthBufferBits();
+
+	customMipmapsLevels = true;
+	Gfx.Created         = true;
+	TryCreateDevice();
+}
+
+cc_bool Gfx_TryRestoreContext(void) {
+	static int availFails;
+	D3DPRESENT_PARAMETERS args = { 0 };
+	cc_result res;
+
+	/* Rarely can't even create device to begin with */
+	if (!deviceCreated) {
+		TryCreateDevice();
+		return deviceCreated;
+	}
+
+	res = IDirect3DDevice9_TestCooperativeLevel(device);
+	if (res && res != D3DERR_DEVICENOTRESET) return false;
+
+	D3D9_FillPresentArgs(&args);
+	res = IDirect3DDevice9_Reset(device, &args);
+	if (res == D3DERR_DEVICELOST) return false;
+
+	/* A user reported an issue where after changing some settings in */
+	/*  nvidia control panel, IDirect3DDevice9_Reset would return */
+	/*  D3DERR_NOTAVAILABLE and hence crash the game */
+	/* So try to workaround this by only crashing after 50 failures */
+	if (res == D3DERR_NOTAVAILABLE && availFails++ < 50) return false;
+
+	if (res) Logger_Abort2(res, "Error recreating D3D9 context");
+	D3D9_UpdateCachedDimensions();
+	return true;
+}
+
+void Gfx_Free(void) {
+	Gfx_FreeState();
+	D3D9_FreeResource(device); device = NULL;
+	D3D9_FreeResource(d3d);    d3d    = NULL;
+}
+
+static void Gfx_FreeState(void) { 
+	FreeDefaultResources();
+	cachedWidth  = 0;
+	cachedHeight = 0;
+}
+
+static void Gfx_RestoreState(void) {
+	Gfx_SetFaceCulling(false);
+	InitDefaultResources();
+	gfx_format = -1;
+
+	IDirect3DDevice9_SetRenderState(device, D3DRS_COLORVERTEX,       false);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_LIGHTING,          false);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_SPECULARENABLE,    false);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_LOCALVIEWER,       false);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_DEBUGMONITORTOKEN, false);
+
+	/* States relevant to the game */
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHAFUNC, D3DCMP_GREATER);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHAREF,  127);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ZFUNC,     D3DCMP_GREATEREQUAL);
+	D3D9_RestoreRenderStates();
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------Textures--------------------------------------------------------*
+*#########################################################################################################################*/
+static void D3D9_SetTextureData(IDirect3DTexture9* texture, struct Bitmap* bmp, int rowWidth, int lvl) {
+	D3DLOCKED_RECT rect;
+	cc_result res = IDirect3DTexture9_LockRect(texture, lvl, &rect, NULL, 0);
+	if (res) Logger_Abort2(res, "D3D9_LockTextureData");
+
+	CopyTextureData(rect.pBits, rect.Pitch, bmp, rowWidth << 2);
+
+	res = IDirect3DTexture9_UnlockRect(texture, lvl);
+	if (res) Logger_Abort2(res, "D3D9_UnlockTextureData");
+}
+
+static void D3D9_SetTexturePartData(IDirect3DTexture9* texture, int x, int y, const struct Bitmap* bmp, int rowWidth, int lvl) {
+	D3DLOCKED_RECT rect;
+	cc_result res;
+	RECT part;
+	part.left = x; part.right  = x + bmp->width;
+	part.top  = y; part.bottom = y + bmp->height;
+
+	res = IDirect3DTexture9_LockRect(texture, lvl, &rect, &part, 0);
+	if (res) Logger_Abort2(res, "D3D9_LockTexturePartData");
+
+	CopyTextureData(rect.pBits, rect.Pitch, bmp, rowWidth << 2);
+
+	res = IDirect3DTexture9_UnlockRect(texture, lvl);
+	if (res) Logger_Abort2(res, "D3D9_UnlockTexturePartData");
+}
+
+static void D3D9_DoMipmaps(IDirect3DTexture9* texture, int x, int y, struct Bitmap* bmp, int rowWidth, cc_bool partial) {
+	BitmapCol* prev = bmp->scan0;
+	BitmapCol* cur;
+	struct Bitmap mipmap;
+
+	int lvls = CalcMipmapsLevels(bmp->width, bmp->height);
+	int lvl, width = bmp->width, height = bmp->height;
+
+	for (lvl = 1; lvl <= lvls; lvl++) {
+		x /= 2; y /= 2;
+		if (width > 1)  width /= 2;
+		if (height > 1) height /= 2;
+
+		cur = (BitmapCol*)Mem_Alloc(width * height, 4, "mipmaps");
+		GenMipmaps(width, height, cur, prev, rowWidth);
+
+		Bitmap_Init(mipmap, width, height, cur);
+		if (partial) {
+			D3D9_SetTexturePartData(texture, x, y, &mipmap, width, lvl);
+		} else {
+			D3D9_SetTextureData(texture, &mipmap, width, lvl);
+		}
+
+		if (prev != bmp->scan0) Mem_Free(prev);
+		prev     = cur;
+		rowWidth = width;
+	}
+	if (prev != bmp->scan0) Mem_Free(prev);
+}
+
+static cc_bool D3D9_CheckResult(cc_result res, const char* func) {
+	if (!res) return true;
+
+	if (res == D3DERR_OUTOFVIDEOMEMORY || res == E_OUTOFMEMORY) {
+		if (!Game_ReduceVRAM()) Logger_Abort("Out of video memory!");
+	} else {
+		Logger_Abort2(res, func);
+	}
+	return false;
+}
+
+static IDirect3DTexture9* DoCreateTexture(struct Bitmap* bmp, int levels, int pool) {
+	IDirect3DTexture9* tex;
+	cc_result res;
+	
+	for (;;) {
+		res = IDirect3DDevice9_CreateTexture(device, bmp->width, bmp->height, levels,
+			0, D3DFMT_A8R8G8B8, pool, &tex, NULL);
+		if (D3D9_CheckResult(res, "D3D9_CreateTexture failed")) break;
+	}
+	return tex;
+}
+
+static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) {
+	IDirect3DTexture9* tex;
+	IDirect3DTexture9* sys;
+	cc_result res;
+
+	int mipmapsLevels = CalcMipmapsLevels(bmp->width, bmp->height);
+	int levels = 1 + (mipmaps ? mipmapsLevels : 0);
+
+	if (flags & TEXTURE_FLAG_MANAGED) {
+		while ((res = IDirect3DDevice9_CreateTexture(device, bmp->width, bmp->height, levels,
+				0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL))) 
+		{
+			if (res == D3DERR_OUTOFVIDEOMEMORY || res == E_OUTOFMEMORY) {
+				/* insufficient VRAM or RAM left to allocate texture, try to reduce the memory in use first */
+				/*  if can't reduce, return 'empty' texture so that at least the game will continue running */
+				if (!Game_ReduceVRAM()) return 0;
+			} else {
+				/* unknown issue, so don't even try to handle the error */
+				Logger_Abort2(res, "D3D9_CreateManagedTexture failed");
+			}
+		}
+
+		D3D9_SetTextureData(tex, bmp, rowWidth, 0);
+		if (mipmaps) D3D9_DoMipmaps(tex, 0, 0, bmp, rowWidth, false);
+		return tex;
+	}
+
+	sys = DoCreateTexture(bmp, levels, D3DPOOL_SYSTEMMEM);
+	D3D9_SetTextureData(sys, bmp, rowWidth, 0);
+	if (mipmaps) D3D9_DoMipmaps(sys, 0, 0, bmp, rowWidth, false);
+		
+	tex = DoCreateTexture(bmp, levels, D3DPOOL_DEFAULT);
+	res = IDirect3DDevice9_UpdateTexture(device, (IDirect3DBaseTexture9*)sys, (IDirect3DBaseTexture9*)tex);
+	if (res) Logger_Abort2(res, "D3D9_CreateTexture - Update");
+
+	D3D9_FreeResource(sys);
+	return tex;
+}
+
+void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) {
+	IDirect3DTexture9* texture = (IDirect3DTexture9*)texId;
+	D3D9_SetTexturePartData(texture, x, y, part, rowWidth, 0);
+	if (mipmaps) D3D9_DoMipmaps(texture, x, y, part, rowWidth, true);
+}
+
+void Gfx_BindTexture(GfxResourceID texId) {
+	cc_result res = IDirect3DDevice9_SetTexture(device, 0, (IDirect3DBaseTexture9*)texId);
+	if (res) Logger_Abort2(res, "D3D9_BindTexture");
+}
+
+void Gfx_DeleteTexture(GfxResourceID* texId) { D3D9_FreeResource(*texId); *texId = NULL; }
+
+void Gfx_EnableMipmaps(void) {
+	if (!Gfx.Mipmaps) return;
+	IDirect3DDevice9_SetSamplerState(device, 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
+}
+
+void Gfx_DisableMipmaps(void) {
+	if (!Gfx.Mipmaps) return;
+	IDirect3DDevice9_SetSamplerState(device, 0, D3DSAMP_MIPFILTER, D3DTEXF_NONE);
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------State management----------------------------------------------------*
+*#########################################################################################################################*/
+static D3DFOGMODE gfx_fogMode = D3DFOG_NONE;
+static cc_bool gfx_alphaBlending;
+static cc_bool gfx_depthTesting, gfx_depthWriting;
+static PackedCol gfx_clearColor, gfx_fogColor;
+static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f;
+
+/* NOTE: Although SetRenderState is okay to call on a lost device, it's also possible */
+/*   the context is lost because the device was never created to begin with!          */
+/* In that case, device will be NULL, so calling SetRenderState will crash the game.  */
+/*  (see Gfx_Create, TryCreateDevice, Gfx_TryRestoreContext)                          */
+
+void Gfx_SetFaceCulling(cc_bool enabled) {
+	D3DCULL mode = enabled ? D3DCULL_CW : D3DCULL_NONE;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_CULLMODE, mode);
+}
+
+void Gfx_SetFog(cc_bool enabled) {
+	if (gfx_fogEnabled == enabled) return;
+	gfx_fogEnabled = enabled;
+
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGENABLE, enabled);
+}
+
+void Gfx_SetFogCol(PackedCol color) {
+	if (color == gfx_fogColor) return;
+	gfx_fogColor = color;
+
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGCOLOR, gfx_fogColor);
+}
+
+void Gfx_SetFogDensity(float value) {
+	union IntAndFloat raw;
+	if (value == gfx_fogDensity) return;
+	gfx_fogDensity = value;
+
+	raw.f = value;
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGDENSITY, raw.u);
+}
+
+void Gfx_SetFogEnd(float value) {
+	union IntAndFloat raw;
+	if (value == gfx_fogEnd) return;
+	gfx_fogEnd = value;
+
+	raw.f = value;
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGEND, raw.u);
+}
+
+void Gfx_SetFogMode(FogFunc func) {
+	static D3DFOGMODE modes[3] = { D3DFOG_LINEAR, D3DFOG_EXP, D3DFOG_EXP2 };
+	D3DFOGMODE mode = modes[func];
+	if (mode == gfx_fogMode) return;
+
+	gfx_fogMode = mode;
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGTABLEMODE, mode);
+}
+
+static void SetAlphaTest(cc_bool enabled) {
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHATESTENABLE, enabled);
+}
+
+static void SetAlphaBlend(cc_bool enabled) {
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHABLENDENABLE, enabled);
+}
+
+void Gfx_SetAlphaArgBlend(cc_bool enabled) {
+	D3DTEXTUREOP op = enabled ? D3DTOP_MODULATE : D3DTOP_SELECTARG1;
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_ALPHAOP, op);
+}
+
+void Gfx_ClearColor(PackedCol color) { gfx_clearColor = color; }
+
+static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) {
+	DWORD channels = (r ? 1u : 0u) | (g ? 2u : 0u) | (b ? 4u : 0u) | (a ? 8u : 0u);
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_COLORWRITEENABLE, channels);
+}
+
+void Gfx_SetDepthTest(cc_bool enabled) {
+	gfx_depthTesting = enabled;
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ZENABLE, enabled);
+}
+
+void Gfx_SetDepthWrite(cc_bool enabled) {
+	gfx_depthWriting = enabled;
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ZWRITEENABLE, enabled);
+}
+
+void Gfx_DepthOnlyRendering(cc_bool depthOnly) {
+	cc_bool enabled = !depthOnly;
+	SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], 
+				  enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]);
+	if (depthOnly) IDirect3DDevice9_SetTexture(device, 0, NULL);
+
+	/* For when Direct3D9 device doesn't support D3DRS_COLORWRITEENABLE */
+	/*  Technically, the correct way to check for whether it works or not */
+	/*  is by checking whether the PrimitiveMiscCaps field in D3DCAPS9 */
+	/*  has the D3DPMISCCAPS_COLORWRITEENABLE flag set */
+	/* But since I'm unsure if there might be some GPU drivers out there */
+	/*  that do support D3DRS_COLORWRITEENABLE but forget to set the flag, */
+	/*  I've decided to require the user to manually enable this fallback */
+	if (!fallbackRendering) return;
+
+	/* https://gamedev.net/forums/topic/375017-c-enabling-disabling-writing-to-buffers/3473819/ */
+	if (depthOnly) {
+		/* finalX = srcX*0 + dstX*1 */
+		/*  So in other words, final pixel = existing pixel */
+		/*  Pretty costly performance wise though */
+		IDirect3DDevice9_SetRenderState(device, D3DRS_SRCBLEND,  D3DBLEND_ZERO);
+		IDirect3DDevice9_SetRenderState(device, D3DRS_DESTBLEND, D3DBLEND_ONE);
+		Gfx_SetAlphaBlending(true);
+	} else {
+		/* finalX = srcX*srcA + dstX*(1-srcA) */
+		IDirect3DDevice9_SetRenderState(device, D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA);
+		IDirect3DDevice9_SetRenderState(device, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
+	}
+}
+
+static void D3D9_RestoreRenderStates(void) {
+	union IntAndFloat raw;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHATESTENABLE,   gfx_alphaTest);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHABLENDENABLE, gfx_alphaBlending);
+
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGENABLE, gfx_fogEnabled);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGCOLOR,  gfx_fogColor);
+	raw.f = gfx_fogDensity;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGDENSITY, raw.u);
+	raw.f = gfx_fogEnd;
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGEND, raw.u);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_FOGTABLEMODE, gfx_fogMode);
+
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ZENABLE,      gfx_depthTesting);
+	IDirect3DDevice9_SetRenderState(device, D3DRS_ZWRITEENABLE, gfx_depthWriting);
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------Index buffers-----------------------------------------------------*
+*#########################################################################################################################*/
+static void D3D9_SetIbData(IDirect3DIndexBuffer9* buffer, int count, Gfx_FillIBFunc fillFunc, void* obj) {
+	void* dst = NULL;
+	cc_result res = IDirect3DIndexBuffer9_Lock(buffer, 0, count * 2, &dst, 0);
+	if (res) Logger_Abort2(res, "D3D9_LockIb");
+
+	fillFunc((cc_uint16*)dst, count, obj);
+	res = IDirect3DIndexBuffer9_Unlock(buffer);
+	if (res) Logger_Abort2(res, "D3D9_UnlockIb");
+}
+
+GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) {
+	int size = count * 2;
+	IDirect3DIndexBuffer9* ibuffer;
+	cc_result res = IDirect3DDevice9_CreateIndexBuffer(device, size, D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &ibuffer, NULL);
+	if (res) Logger_Abort2(res, "D3D9_CreateIb");
+
+	D3D9_SetIbData(ibuffer, count, fillFunc, obj);
+	return ibuffer;
+}
+
+void Gfx_BindIb(GfxResourceID ib) {
+	IDirect3DIndexBuffer9* ibuffer = (IDirect3DIndexBuffer9*)ib;
+	cc_result res = IDirect3DDevice9_SetIndices(device, ibuffer);
+	if (res) Logger_Abort2(res, "D3D9_BindIb");
+}
+
+void Gfx_DeleteIb(GfxResourceID* ib) { D3D9_FreeResource(*ib); *ib = NULL; }
+
+
+/*########################################################################################################################*
+*------------------------------------------------------Vertex buffers-----------------------------------------------------*
+*#########################################################################################################################*/
+static IDirect3DVertexBuffer9* D3D9_AllocVertexBuffer(VertexFormat fmt, int count, DWORD usage) {
+	IDirect3DVertexBuffer9* vbuffer;
+	int size = count * strideSizes[fmt];
+	cc_result res;
+
+	res = IDirect3DDevice9_CreateVertexBuffer(device, size, usage,
+				d3d9_formatMappings[fmt], D3DPOOL_DEFAULT, &vbuffer, NULL);
+
+	if (res == D3DERR_OUTOFVIDEOMEMORY || res == E_OUTOFMEMORY)
+		return NULL;
+
+	if (res) Logger_Abort2(res, "D3D9_AllocVertexBuffer failed");
+	return vbuffer;
+}
+
+static void D3D9_SetVbData(IDirect3DVertexBuffer9* buffer, void* data, int size, int lockFlags) {
+	void* dst = NULL;
+	cc_result res = IDirect3DVertexBuffer9_Lock(buffer, 0, size, &dst, lockFlags);
+	if (res) Logger_Abort2(res, "D3D9_LockVb");
+
+	Mem_Copy(dst, data, size);
+	res = IDirect3DVertexBuffer9_Unlock(buffer);
+	if (res) Logger_Abort2(res, "D3D9_UnlockVb");
+}
+
+static void* D3D9_LockVb(GfxResourceID vb, VertexFormat fmt, int count, int lockFlags) {
+	IDirect3DVertexBuffer9* buffer = (IDirect3DVertexBuffer9*)vb;
+	void* dst = NULL;
+	int size  = count * strideSizes[fmt];
+
+	cc_result res = IDirect3DVertexBuffer9_Lock(buffer, 0, size, &dst, lockFlags);
+	if (res) Logger_Abort2(res, "D3D9_LockVb");
+	return dst;
+}
+
+static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) {
+	return D3D9_AllocVertexBuffer(fmt, count, D3DUSAGE_WRITEONLY);
+}
+
+void Gfx_DeleteVb(GfxResourceID* vb) { D3D9_FreeResource(*vb); *vb = NULL; }
+
+void Gfx_BindVb(GfxResourceID vb) {
+	IDirect3DVertexBuffer9* vbuffer = (IDirect3DVertexBuffer9*)vb;
+	cc_result res = IDirect3DDevice9_SetStreamSource(device, 0, vbuffer, 0, gfx_stride);
+	if (res) Logger_Abort2(res, "D3D9_BindVb");
+}
+
+void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) {
+	return D3D9_LockVb(vb, fmt, count, 0);
+}
+
+void Gfx_UnlockVb(GfxResourceID vb) {
+	IDirect3DVertexBuffer9* buffer = (IDirect3DVertexBuffer9*)vb;
+	cc_result res = IDirect3DVertexBuffer9_Unlock(buffer);
+	if (res) Logger_Abort2(res, "Gfx_UnlockVb");
+}
+
+
+/*########################################################################################################################*
+*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------*
+*#########################################################################################################################*/
+static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) {
+	return D3D9_AllocVertexBuffer(fmt, maxVertices, D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY);
+}
+
+void Gfx_DeleteDynamicVb(GfxResourceID* vb) { D3D9_FreeResource(*vb); *vb = NULL; }
+
+void Gfx_BindDynamicVb(GfxResourceID vb) {
+	IDirect3DVertexBuffer9* vbuffer = (IDirect3DVertexBuffer9*)vb;
+	cc_result res = IDirect3DDevice9_SetStreamSource(device, 0, vbuffer, 0, gfx_stride);
+	if (res) Logger_Abort2(res, "D3D9_BindDynamicVb");
+}
+
+void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) {
+	return D3D9_LockVb(vb, fmt, count, D3DLOCK_DISCARD);
+}
+
+void Gfx_UnlockDynamicVb(GfxResourceID vb) {
+	Gfx_UnlockVb(vb);
+	Gfx_BindDynamicVb(vb); /* TODO: Inline this? */
+}
+
+void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount) {
+	int size = vCount * gfx_stride;
+	IDirect3DVertexBuffer9* buffer = (IDirect3DVertexBuffer9*)vb;
+	D3D9_SetVbData(buffer, vertices, size, D3DLOCK_DISCARD);
+
+	cc_result res = IDirect3DDevice9_SetStreamSource(device, 0, buffer, 0, gfx_stride);
+	if (res) Logger_Abort2(res, "D3D9_SetDynamicVbData - Bind");
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------Vertex rendering----------------------------------------------------*
+*#########################################################################################################################*/
+void Gfx_SetVertexFormat(VertexFormat fmt) {
+	cc_result res;
+	if (fmt == gfx_format) return;
+	gfx_format = fmt;
+
+	if (fmt == VERTEX_FORMAT_COLOURED) {
+		/* it's necessary to unbind the texture, otherwise the alpha from the last bound texture */
+		/*  gets used - because D3DTSS_ALPHAOP texture stage state is still set to D3DTOP_SELECTARG1 */
+		IDirect3DDevice9_SetTexture(device, 0, NULL);
+		/*  IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_COLOROP, fmt == VERTEX_FORMAT_COLOURED ? D3DTOP_DISABLE : D3DTOP_MODULATE); */
+		/*  IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_ALPHAOP, fmt == VERTEX_FORMAT_COLOURED ? D3DTOP_DISABLE : D3DTOP_SELECTARG1); */
+		/* SetTexture(NULL) seems to be enough, not really required to call SetTextureStageState */
+	}
+
+	res = IDirect3DDevice9_SetFVF(device, d3d9_formatMappings[fmt]);
+	if (res) Logger_Abort2(res, "D3D9_SetVertexFormat");
+	gfx_stride = strideSizes[fmt];
+}
+
+void Gfx_DrawVb_Lines(int verticesCount) {
+	/* NOTE: Skip checking return result for Gfx_DrawXYZ for performance */
+	IDirect3DDevice9_DrawPrimitive(device, D3DPT_LINELIST, 0, verticesCount >> 1);
+}
+
+void Gfx_DrawVb_IndexedTris(int verticesCount) {
+	IDirect3DDevice9_DrawIndexedPrimitive(device, D3DPT_TRIANGLELIST,
+		0, 0, verticesCount, 0, verticesCount >> 1);
+}
+
+void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) {
+	IDirect3DDevice9_DrawIndexedPrimitive(device, D3DPT_TRIANGLELIST,
+		startVertex, 0, verticesCount, 0, verticesCount >> 1);
+}
+
+void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) {
+	IDirect3DDevice9_DrawIndexedPrimitive(device, D3DPT_TRIANGLELIST,
+		startVertex, 0, verticesCount, 0, verticesCount >> 1);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------Matrices--------------------------------------------------------*
+*#########################################################################################################################*/
+static D3DTRANSFORMSTATETYPE matrix_modes[2] = { D3DTS_PROJECTION, D3DTS_VIEW };
+
+void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) {
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetTransform(device, matrix_modes[type], (const D3DMATRIX*)matrix);
+}
+
+void Gfx_LoadIdentityMatrix(MatrixType type) {
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetTransform(device, matrix_modes[type], (const D3DMATRIX*)&Matrix_Identity);
+}
+
+static struct Matrix texMatrix = Matrix_IdentityValue;
+void Gfx_EnableTextureOffset(float x, float y) {
+	texMatrix.row3.x = x; texMatrix.row3.y = y;
+	if (Gfx.LostContext) return;
+
+	IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
+	IDirect3DDevice9_SetTransform(device, D3DTS_TEXTURE0, (const D3DMATRIX*)&texMatrix);
+}
+
+void Gfx_DisableTextureOffset(void) {
+	if (Gfx.LostContext) return;
+	IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
+	IDirect3DDevice9_SetTransform(device, D3DTS_TEXTURE0, (const D3DMATRIX*)&Matrix_Identity);
+}
+
+void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) {
+	/* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh */
+	/*   The simplified calculation below uses: L = 0, R = width, T = 0, B = height */
+	/* NOTE: This calculation is shared with Direct3D 11 backend */
+	*matrix = Matrix_Identity;
+
+	matrix->row1.x =  2.0f / width;
+	matrix->row2.y = -2.0f / height;
+	matrix->row3.z =  1.0f / (zNear - zFar);
+
+	matrix->row4.x = -1.0f;
+	matrix->row4.y =  1.0f;
+	matrix->row4.z = zNear / (zNear - zFar);
+}
+
+static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); }
+void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) {
+	/* Deliberately swap zNear/zFar in projection matrix calculation to produce */
+	/*  a projection matrix that results in a reversed depth buffer */
+	/* https://developer.nvidia.com/content/depth-precision-visualized */
+	float zNear_ = zFar;
+	float zFar_  = Reversed_CalcZNear(fov, depthBits);
+
+	/* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh */
+	/* NOTE: This calculation is shared with Direct3D 11 backend */
+	float c = Cotangent(0.5f * fov);
+	*matrix = Matrix_Identity;
+
+	matrix->row1.x =  c / aspect;
+	matrix->row2.y =  c;
+	matrix->row3.z = zFar_ / (zNear_ - zFar_);
+	matrix->row3.w = -1.0f;
+	matrix->row4.z = (zNear_ * zFar_) / (zNear_ - zFar_);
+	matrix->row4.w =  0.0f;
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------------Misc----------------------------------------------------------*
+*#########################################################################################################################*/
+cc_result Gfx_TakeScreenshot(struct Stream* output) {
+	IDirect3DSurface9* backbuffer = NULL;
+	IDirect3DSurface9* temp = NULL;
+	D3DSURFACE_DESC desc;
+	D3DLOCKED_RECT rect;
+	struct Bitmap bmp;
+	cc_result res;
+
+	res = IDirect3DDevice9_GetBackBuffer(device, 0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer);
+	if (res) goto finished;
+	res = IDirect3DSurface9_GetDesc(backbuffer, &desc);
+	if (res) goto finished;
+
+	res = IDirect3DDevice9_CreateOffscreenPlainSurface(device, desc.Width, desc.Height, D3DFMT_X8R8G8B8, D3DPOOL_SYSTEMMEM, &temp, NULL);
+	if (res) goto finished; /* TODO: For DX 8 use IDirect3DDevice8::CreateImageSurface */
+	res = IDirect3DDevice9_GetRenderTargetData(device, backbuffer, temp);
+	if (res) goto finished;
+	
+	res = IDirect3DSurface9_LockRect(temp, &rect, NULL, D3DLOCK_READONLY | D3DLOCK_NO_DIRTY_UPDATE);
+	if (res) goto finished;
+	{
+		Bitmap_Init(bmp, desc.Width, desc.Height, (BitmapCol*)rect.pBits);
+		res = Png_Encode(&bmp, output, NULL, false, NULL);
+		if (res) { IDirect3DSurface9_UnlockRect(temp); goto finished; }
+	}
+	res = IDirect3DSurface9_UnlockRect(temp);
+	if (res) goto finished;
+
+finished:
+	D3D9_FreeResource(backbuffer);
+	D3D9_FreeResource(temp);
+	return res;
+}
+
+static void UpdateSwapchain(const char* reason) {
+	/* TODO: Can Direct3D9Ex fast path still be used here? */
+	Gfx_LoseContext(reason);
+}
+
+void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) {
+	gfx_minFrameMs = minFrameMs;
+	if (gfx_vsync == vsync) return;
+
+	gfx_vsync = vsync;
+	if (device) UpdateSwapchain(" (toggling VSync)");
+}
+
+void Gfx_BeginFrame(void) { 
+	IDirect3DDevice9_BeginScene(device);
+}
+
+void Gfx_ClearBuffers(GfxBuffers buffers) {
+	DWORD targets = 0;
+	if (buffers & GFX_BUFFER_COLOR) targets |= D3DCLEAR_TARGET;
+	if (buffers & GFX_BUFFER_DEPTH) targets |= D3DCLEAR_ZBUFFER;
+	
+	cc_result res = IDirect3DDevice9_Clear(device, 0, NULL, targets, gfx_clearColor, 0.0f, 0);
+	if (res) Logger_Abort2(res, "D3D9_Clear");
+}
+
+void Gfx_EndFrame(void) {
+	IDirect3DDevice9_EndScene(device);
+	cc_result res = IDirect3DDevice9_Present(device, NULL, NULL, NULL, NULL);
+
+	if (res) {
+		if (res != D3DERR_DEVICELOST) Logger_Abort2(res, "D3D9_EndFrame");
+		/* TODO: Make sure this actually works on all graphics cards. */
+		Gfx_LoseContext(" (Direct3D9 device lost)");
+	}
+	if (gfx_minFrameMs) LimitFPS();
+}
+
+cc_bool Gfx_WarnIfNecessary(void) { return false; }
+static const char* D3D9_StrFlags(void) {
+	if (createFlags & D3DCREATE_HARDWARE_VERTEXPROCESSING) return "Hardware";
+	if (createFlags & D3DCREATE_MIXED_VERTEXPROCESSING)    return "Mixed";
+	if (createFlags & D3DCREATE_SOFTWARE_VERTEXPROCESSING) return "Software";
+	return "(none)";
+}
+
+void Gfx_GetApiInfo(cc_string* info) {
+	D3DADAPTER_IDENTIFIER9 adapter = { 0 };
+	int pointerSize = sizeof(void*) * 8;
+	float curMem;
+
+	IDirect3D9_GetAdapterIdentifier(d3d, D3DADAPTER_DEFAULT, 0, &adapter);
+	curMem = IDirect3DDevice9_GetAvailableTextureMem(device) / (1024.0f * 1024.0f);
+	String_Format1(info, "-- Using Direct3D9 (%i bit) --\n", &pointerSize);
+
+	String_Format1(info, "Adapter: %c\n",         adapter.Description);
+	String_Format1(info, "Processing mode: %c\n", D3D9_StrFlags());
+	String_Format2(info, "Video memory: %f2 MB total, %f2 free\n", &totalMem, &curMem);
+	PrintMaxTextureInfo(info);
+	String_Format1(info, "Depth buffer bits: %i", &depthBits);
+}
+
+void Gfx_OnWindowResize(void) {
+	if (Game.Width == cachedWidth && Game.Height == cachedHeight) return;
+	/* Only resize when necessary */
+	UpdateSwapchain(" (resizing window)");
+}
+
+void Gfx_SetViewport(int x, int y, int w, int h) { }
+#endif