6.6动态着色器链接
自从可编程图形硬件成为主流以来,采用三维图形的应用程序已经开发了越来越大和复杂的着色器代码集来实现其图形功能。然而,在Direct3D11之前,HLSL没有提供在着色器程序中启用动态调度的内置方法。着色器模型3.0引入了着色器在值上动态分支的功能。然而,这样做会带来沉重的性能损失,因此作为实现动态调度的一种手段,它的成本高得令人望而却步。自Shader Model 2.0以来,静态分支也得到了支持,但其对分支数量的限制以及对性能的影响使其同样不足。因此,许多应用程序采用静态编译着色器程序的所有所需排列,要么使用宏和条件编译,要么将较小片段的代码拼接在一起。虽然这种方法具有不需要动态分支的优点,并为优化器提供了对完整程序的完全访问,但随着新选项的添加,所需排列的数量呈指数级增长。这通常被称为着色器组合爆炸。因此,程序员经常面临一个艰难的选择,要么降低着色器性能,要么增加着色器编译时间,要么使其着色器构建管道更加复杂.
为了纠正这种情况,Direct3D11引入了一种称为动态着色器链接的功能。它本质上允许应用程序在将着色器程序绑定到管道时从HLSL代码路径的多个实现中进行动态选择,从而有效地允许在Draw或dispatch调用级别进行动态调度。我们将在以下部分中进一步探讨此功能,以了解如何在实时渲染应用程序中使用它。
6.6.1为动态链接编写着色器
将使用动态链接的着色器程序必须使用HLSL接口和类编写。从本质上讲,该过程类似于C++中的虚拟调度:接口是用一组方法声明的,着色器代码调用这些接口上的方法。然后在运行时,主机应用程序分配一个类的实例,该实例实现Draw或Dispatch调用期间要使用的接口。使用接口的着色器程序可以声明接口的全局实例,就像任何其他变量类型一样。然后,该实例的功能类似于C++中的多态指针,并将对其调用的任何方法转发到主机应用程序指定的类实例。
清单6.18演示了声明接口实例并调用其中一个方法的语法.
interface Light
{
float3 GetLighting( fioat3 Position, float3 Normal );
} ;
Light LightInstance;
float4 PSMain( in float3 Position : POSITION,
in float3 Normal : NORMAL ) : SV_Target
{
float3 lightColor = LightInstance.GetLighting( Position, Normal );
return float4( lightColor, 1.0f );
}
任何可能用于实现接口的类也必须在HLSL着色器程序中声明,或者通过使用#include杂注来包含。如果类具有成员变量,则必须在常量缓冲区中声明该类的实例。这允许主机应用程序在运行时为这些成员指定值。
清单6.19演示了用于声明实现接口的类和在常量缓冲区中声明实例的语法。
class DirectionalLight
{
float3 Color;
float3 Direction;
float3 GetLighting( float3 Position, float3 Normal )
{
return saturate( dot( Normal, Direction) ) * Color;
}
};
cbuffer Classlnstances : register( cb0 )
{
DirectionalLight DLightlnstance;
}
6.6.2链接类到接口
在运行时,使用动态着色器链接的应用程序必须指定将使用哪个类来实现着色器程序使用的接口。第一步是创建一个ID3D11ClassLinkage接口,这是通过调用ID3DllDevice::CreateClassLinkage来完成的。一旦创建了类链接接口,就可以将其绑定到着色器程序的实例。这是通过将ID3D11Class Linkage接口作为CreateVertexShader、CreateHullShader、Create DomainShader、CreateGeometryShader、reatePixelShader或CreateComputeShader的pClassLinkage参数来实现的。
清单6.20演示了像素着色器的此过程
ID3D10Blob* compiledShader;
ID3D10Blob* errorMessages;
HRESULT hr = D3DXllCompileFromFile( filePath, NULL, NULL, "PSMain","ps_5_0", 0, 0, NULL, &compiledShader,&errorMessages, NULL );
ID3DllClassLinkage* classLinkage = NULL;
if ( SUCCEEDED( hr ) )
{
device->CreateClassLinkage(SclassLinkage );
device->CreatePixelShader(compiledShader->GetBufferPointer(),
compiledShader->GetBufferSize(),classLinkage,&pixelShader );
}
一旦类链接绑定到着色器,就可以检索ID3DllClassInstance接口。类实例表示着色器中声明的可用于实现接口的类之一。获取类实例的最简单方法是在ID3DllClassLinkage接口上调用GetClassInstance,并传递着色器程序中声明的类实例的名称。CreateClassInstance也可用于未在常量缓冲区中声明类实例的着色器程序,这对于不包含成员变量的类很有用。
清单6.21演示了获取类实例接口的过程。
ID3DllClassInstance* dLightlnstance = NULL;
classLinkage->GetClassLinkage( L"DLightInstance", 0, &dLightlnstance );
使用动态链接的最后一步是在将着色器程序绑定到管道时指定类实例的数组。ID3DllDeviceContext::SSetShader方法都有一个ppClassInstances参数,该参数接受ID3DllClassInstance接口的数组。此数组必须为着色器程序中使用的每个接口包含一个类实例。
清单6.22演示了为像素着色器初始化和传递这样一个数组
ID3DllClassInstance* classlnstances[1];
classlnstances[0] = dLightlnstance;
deviceContext->PSSetShader( pixelShader, classlnstances, 1 );
着色器程序中使用的每个接口都有一个唯一的索引,该索引对应于传递给包含类实例的SSetShader的数组的索引。可以使用ID3D11ShaderReflectionVariable接口检索此索引,该接口是着色器反射API的一部分。有关详细信息,请参见“着色器反射”部分