Fetching DirectX 9 Device: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
No edit summary
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
This tutorial shows how to fetch the [https://learn.microsoft.com/en-us/windows/win32/api/d3d9helper/nn-d3d9helper-idirect3ddevice9 DirectX 9 Device] in 64-bit games on the {{tf2branch|4}}.
This tutorial shows how to fetch the [https://learn.microsoft.com/en-us/windows/win32/api/d3d9helper/nn-d3d9helper-idirect3ddevice9 DirectX 9 Device] in 64-bit games on the {{tf2branch|4}}. This method works on both Windows and Linux platforms.


Access to the low-level DirectX 9 device allows the creation of effects that [[Source]] has no support for, such as [https://learn.microsoft.com/en-us/windows/win32/direct3d9/creating-cubic-environment-map-surfaces rendering cubemaps in real-time].
Access to the low-level DirectX 9 device allows the creation of effects that [[Source]] has no support for, such as [https://learn.microsoft.com/en-us/windows/win32/direct3d9/creating-cubic-environment-map-surfaces rendering cubemaps in real-time].
Line 7: Line 7:
== Tutorial ==
== Tutorial ==


Open the client initialization code in <code>game/client/cdll_client_int.cpp</code>.
Open the render target initialization code in <code>game/client/baseclientrendertargets.cpp</code>, and it's corresponding header file <code>baseclientrendertargets.h</code>. This will be explained shortly.


Following the big wall of <code>#include</code>s, add the following:
In the header file, add the following:
<syntaxhighlight lang="cpp">
struct IDirect3DDevice9;
 
extern IDirect3DDevice9* g_pDirect3DDevice9;
</syntaxhighlight>
 
In the <code>cpp</code> file, add this <code>#include</code>:
<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
#include "shaderapi/IShaderDevice.h"
#include "shaderapi/IShaderDevice.h"
</syntaxhighlight>
</syntaxhighlight>


Scrolling further down, you will see a list of interface pointers, below the comment that says this:<syntaxhighlight lang="cpp">// IF YOU ADD AN INTERFACE, EXTERN IT IN THE HEADER FILE.</syntaxhighlight>
Following that, add this definition in:
 
Add these interfaces:
 
<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
IShaderDevice* g_pShaderDevice = NULL;
IDirect3DDevice9* g_pDirect3DDevice9 = NULL;
struct IDirect3DDevice9* g_pDirect3DDevice9 = NULL;
</syntaxhighlight>
</syntaxhighlight>


== Tutorial ==
Now a new function will be added into this file. Paste the following function in. You do not need to worry about how this works.


Now, go to the <code>CHLClient::Init</code> function and add the following after the <code>if (!g_pMatSystemSurface)</code> check:
<syntaxhighlight lang="cpp">
static bool FindDX9Device()
{
// Try both DXVK and DirectX 9 mode
CreateInterfaceFn interface = Sys_GetFactory( "shaderapivk" );
if ( !interface )
interface = Sys_GetFactory( "shaderapidx9" );


<syntaxhighlight lang="cpp">
if ( !interface )
g_pShaderDevice = (IShaderDevice*)appSystemFactory( SHADER_DEVICE_INTERFACE_VERSION, NULL );
if ( !g_pShaderDevice )
return false;
return false;
</syntaxhighlight>


Next comes the fun part. The <code>IShaderDevice</code> class has a public function that checks whether the DirectX 9 device exists, named <code>IsUsingGraphics</code>. We can abuse this to fetch the actual pointer to the device.
IShaderDevice* pShaderDevice = ( IShaderDevice* )interface( SHADER_DEVICE_INTERFACE_VERSION, NULL );
if ( !pShaderDevice )
return false;


Copy the following function into the file somewhere above. You don't need to worry about the specific details of how this works.
// Dereference the virtual table and access the IsUsingGraphics method
byte* pIShaderDevice_IsUsingGraphics = ( byte* )( *( void*** )pShaderDevice )[5];
// Resolve the RIP instruction to get the absolute address
byte* ppDirect3DDevice9 = pIShaderDevice_IsUsingGraphics + 8 + *( int32* )( pIShaderDevice_IsUsingGraphics + 3 );
g_pDirect3DDevice9 = *( IDirect3DDevice9** )ppDirect3DDevice9;


<syntaxhighlight lang="cpp">
return g_pDirect3DDevice9 != NULL;
IDirect3DDevice9* GetDX9Device()
{
    // Dereference the virtual table and access the IsUsingGraphics method
byte* IShaderDevice_IsUsingGraphics = (byte*)(*(void***)g_pShaderDevice)[5];
    // Resolve the RIP instruction to get the absolute address
byte* pDirect3DDevice9 = IShaderDevice_IsUsingGraphics + 8 + *( int32* )( IShaderDevice_IsUsingGraphics + 3 );
return *( IDirect3DDevice9** )pDirect3DDevice9;
}
}
</syntaxhighlight>
</syntaxhighlight>


Now go back to where you added the code to fetch the <code>g_pShaderDevice</code> interface, and add the following below:
Now go to the <code>CBaseClientRenderTargets::InitClientRenderTargets</code> function and add the following at the top:


<syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
IDirect3DDevice9* g_pDirect3DDevice9 = GetDX9Device();
if ( !FindDX9Device() )
if ( !g_pDirect3DDevice9 )
{
{
Error( "Failed to get DirectX9 device pointer" );
Error( "Failed to get DirectX9 device pointer" );
return false;
return;
}
}
</syntaxhighlight>
</syntaxhighlight>


And that's all! You can now call the raw DirectX9 API using the <code>g_pDirect3DDevice9</code> interface.
The reason this code is being put inside render target initialization, is that the engine calls this function before the client itself even initializes. This also allows you to create custom rendertargets that aren't normally possible with the materialsystem interface.
 
And that's all! You can now call the raw DirectX9 API using the <code>g_pDirect3DDevice9</code> interface. Include the <code>baseclientrendertargets.h</code> file in code that needs to use the device.
 
{{note|This code is independent of the client and can also be run in the shader DLLs by using a static constructor, if it's more convenient}}


== Usage ==
== Usage ==
Line 73: Line 81:


// ...
// ...
void Example()
void ExampleToFetchSwapChain()
{
{
IDirect3DSwapChain9* pSwapChain = NULL;
IDirect3DSwapChain9* pSwapChain = NULL;
Line 79: Line 87:
}
}
</syntaxhighlight>
</syntaxhighlight>
{{warning|You might get compile errors about <code>GetObjectA</code> due to a macro in the Windows headers conflicting with Source's function names. To fix this, you can add <code>#undef GetObject</code> after the include.}}


[[Category:Programming]]
[[Category:Programming]]

Latest revision as of 02:24, 4 November 2025

This tutorial shows how to fetch the DirectX 9 Device in 64-bit games on the Team Fortress 2 branch Team Fortress 2 branch. This method works on both Windows and Linux platforms.

Access to the low-level DirectX 9 device allows the creation of effects that Source has no support for, such as rendering cubemaps in real-time.

Warning.pngWarning:This is intended for advanced users. Low level access to the DirectX 9 device is very powerful, but also comes with great responsibility.

Tutorial

Open the render target initialization code in game/client/baseclientrendertargets.cpp, and it's corresponding header file baseclientrendertargets.h. This will be explained shortly.

In the header file, add the following:

struct IDirect3DDevice9;

extern IDirect3DDevice9* g_pDirect3DDevice9;

In the cpp file, add this #include:

#include "shaderapi/IShaderDevice.h"

Following that, add this definition in:

IDirect3DDevice9* g_pDirect3DDevice9 = NULL;

Now a new function will be added into this file. Paste the following function in. You do not need to worry about how this works.

static bool FindDX9Device()
{
	// Try both DXVK and DirectX 9 mode
	CreateInterfaceFn interface = Sys_GetFactory( "shaderapivk" );
	if ( !interface )
		interface = Sys_GetFactory( "shaderapidx9" );

	if ( !interface )
		return false;

	IShaderDevice* pShaderDevice = ( IShaderDevice* )interface( SHADER_DEVICE_INTERFACE_VERSION, NULL );
	if ( !pShaderDevice )
		return false;

	// Dereference the virtual table and access the IsUsingGraphics method
	byte* pIShaderDevice_IsUsingGraphics = ( byte* )( *( void*** )pShaderDevice )[5];
	// Resolve the RIP instruction to get the absolute address
	byte* ppDirect3DDevice9 = pIShaderDevice_IsUsingGraphics + 8 + *( int32* )( pIShaderDevice_IsUsingGraphics + 3 );
	g_pDirect3DDevice9 = *( IDirect3DDevice9** )ppDirect3DDevice9;

	return g_pDirect3DDevice9 != NULL;
}

Now go to the CBaseClientRenderTargets::InitClientRenderTargets function and add the following at the top:

	if ( !FindDX9Device() )
	{
		Error( "Failed to get DirectX9 device pointer" );
		return;
	}

The reason this code is being put inside render target initialization, is that the engine calls this function before the client itself even initializes. This also allows you to create custom rendertargets that aren't normally possible with the materialsystem interface.

And that's all! You can now call the raw DirectX9 API using the g_pDirect3DDevice9 interface. Include the baseclientrendertargets.h file in code that needs to use the device.

Note.pngNote:This code is independent of the client and can also be run in the shader DLLs by using a static constructor, if it's more convenient

Usage

To call functions on the DirectX 9 device, you will need to download the DirectX 9 SDK.

After you download it, extract it to a place in your mod's folder. The convention is to make a dx9sdk directory in the top-level folder of your source code.

You can then include the DirectX 9 API like-so in client code:

#include "../../dx9sdk/include/d3d9.h"

// ...
void ExampleToFetchSwapChain()
{
	IDirect3DSwapChain9* pSwapChain = NULL;
	g_pDirect3DDevice9->GetSwapChain( 0, &pSwapChain );
}
Warning.pngWarning:You might get compile errors about GetObjectA due to a macro in the Windows headers conflicting with Source's function names. To fix this, you can add #undef GetObject after the include.