GPU Programming
This page explains how to use the Tinman.Gpu
component to write GPU programs in C# and how to feed that C# code into the workflow of the Code-X Framework, in order to generate fully-functional HLSL / GLSL code in an automated process.
Having functional HLSL / GLSL code ready, this page continues to explain how to use the GPU Rendering abstraction layer in order to write Code-X compliant interface code that will enable an application to use the GPU shaders, while abstracting away API-specific details.
Writing Gpu-X Code
To write a Gpu-X compliant GPU program, start with setting up a regular C# project in a development environment of your choice and then add a dependency to the Tinman.Gpu
component.
The source code of a Gpu-X project will never be executed.
It will only be processed by the Gpu-X workflow, which generates HLSL / GLSL source code and .vcxproj
project files.
Compilation and syntax analysis are still useful tools to spot errors up-front.
.csproj
file for a Gpu-X project<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="Current" DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Framework -->
<Import Project="Properties_NetStandard20.csproj"/> (1)
<Import Project="Properties_CodeX.csproj"/> (1)
<!-- Global properties -->
<PropertyGroup>
<AssemblyName>Example.Shaders</AssemblyName>
<OutputType>Library</OutputType>
<ProjectGuid>...</ProjectGuid>
<RootNamespace>Example.Shaders</RootNamespace>
</PropertyGroup>
<!-- Project references -->
<ItemGroup>
<Reference Include="CodeX.System">
<HintPath>CodeX.System.dll</HintPath> (2)
</Reference>
<Reference Include="Tinman.Core">
<HintPath>Tinman.Core.dll</HintPath> (2)
</Reference>
<Reference Include="Tinman.Gpu">
<HintPath>Tinman.Gpu.dll</HintPath> (2)
</Reference>
<ProjectReference Include="Example.CodeX.csproj"/> (3)
</ItemGroup>
</Project>
1 | Can be found here: ide.cs/vs/ |
2 | Link to the CodeX.System , Tinman.Core and Tinman.Gpu , see dependencies of Tinman.Shaders . |
3 | Reference your Code-X interface project, if you want to use XmlDoc links. |
Writing Gpu-X compliant code simply means to write standard C# code which sticks to the API and rules of the Tinman.Gpu
library.
To get started, please refer to the documentation of the GpuCode class, which represents the main entry point of the Gpu-X API documentation.
The ConstantBufferAttribute classes define the values that must be specified by the application, for example vectors and matrices.
The ShaderResourcesAttribute classes depict the GPU resources which must be provided by the application, such as textures or buffers.
The StageInOutAttribute structs that are used as input of a vertex shader define the logical vertex buffer format, which must be mapped to the application’s binary vertex buffer format, for example via semantics.
The SDK directory src.fx/ contains the HLSL and GLSL code that has been generated with the Gpu-X workflow, based on the Tinman.Shaders component.
The associated project files are here: src.hlsl/vs/
|
On top of the syntactic and semantic requirements that are defined by the Tinman.Gpu
component, the following conventions should be followed:
- §1 - Directory Structure
-
Use sub-namespaces to group related code (usually by render effect) and put shared code into the root namespace. Code in one sub-namespace should not use code in another sub-namespace. For example:
-
Demo/
⇒ Example_DemoFactory -
Graphics/
⇒ IGraphicsEffect -
Model/
⇒ IModelRendererEffect -
Process/
⇒ IProcessEffect -
Renderer/
⇒ IRendererEffect -
Sky/
⇒ ISkyEffect -
Terrain/
⇒ ITerrainEffect
-
- §2 - Naming Convention
-
Use name prefixes:
-
CB_
for ConstantBufferAttribute classes -
SR_
for ShaderResourcesAttribute classes -
CS_
for ComputeShaderAttribute methods -
GS_
for GeometryShaderAttribute methods -
PS_
for PixelShaderAttribute methods -
TP_
for TessellatePatchShaderAttribute methods -
TV_
for TessellateVertexShaderAttribute methods -
VS_
for VertexShaderAttribute methods -
CS_
,GS_
,PS_
,TS_
,VS_
for StageInOutAttribute structs, according to the shader stage it is associated with.
-
- §3 - Configuration
-
Use ConfigurationAttribute on a single class named
Configuration
in the root namespace, to keep all compile-time settings in one place.
Demo Render Effect
The following sections show the Gpu-X code of the render effect for Example_DemoParameters, which is included in the Demo Application.
The demo render effect implements the Game Of Life on the GPU.
It uses a GPU program that uses a Vertex Shader (see VS
function) and a Pixel Shader function (see PS
function).
Demo.cs
The Demo
class derives from GpuCode, which turns it into a container of GPU functions, including shader main functions and other general-purpose utility functions.
The intrinsic functions like gxTexture2D and sign are defined by the GpuCode base class. When generating HLSL and GLSL code, their usages are automatically refactored into applicable code constructs.
src.cs/Tinman.Shaders.GpuX/Demo/Demo.cs
using ...;
namespace Tinman.Shaders.Demo
{
public sealed class Demo : GpuCode
{
#region Public / Methods
[PixelShader]
public static PS_Out_Target PS(PS_In data)
{
PS_Out_Target result;
result.Color = gxToFloat4(CellCompute(data.Coords.x, data.Coords.y));
return result;
}
[VertexShader]
public static PS_In VS(VS_In data)
{
PS_In result;
result.Position = new float4(data.Position,
Configuration.NEAR_AT_ZERO ? 0 : -1, 1);
result.Coords = new float2(
(data.Position.x + 1f) / 2f * CB_Demo_Static.BufferSize
+ Configuration.PIXEL_OFFSET,
(1f - data.Position.y) / 2f * CB_Demo_Static.BufferSize
+ Configuration.PIXEL_OFFSET);
return result;
}
#endregion
#region Private / Methods
static float CellAt(float x, float y)
{
float2 coords;
float4 temp;
coords = new float2(
x / CB_Demo_Static.BufferSize, y / CB_Demo_Static.BufferSize);
temp = gxTexture2D(SR_Demo.Buffer, coords);
// Return 0 for dead cells and 1 for alive cells.
return abs(sign(temp.r));
}
static float CellCompute(float x, float y)
{
float alive;
// Conway's Game of Life
// https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
// Every cell interacts with its eight neighbours, which are the cells that
// are horizontally, vertically, or diagonally adjacent.
alive = 0f;
alive += CellAt(x - 1f, y);
alive += CellAt(x + 1f, y);
alive += CellAt(x, y - 1f);
alive += CellAt(x, y + 1f);
alive += CellAt(x - 1f, y - 1f);
alive += CellAt(x + 1f, y - 1f);
alive += CellAt(x - 1f, y + 1f);
alive += CellAt(x + 1f, y + 1f);
if (CellAt(x, y) != 0f)
{
// "Any live cell with fewer than two live neighbours dies, as if by
// underpopulation."
if (alive < 2f)
return 0f;
// "Any live cell with more than three live neighbours dies, as if by
// overpopulation."
if (alive > 3f)
return 0f;
// "Any live cell with two or three live neighbours lives on to the next
// generation."
return 1f;
}
// "Any dead cell with exactly three live neighbours becomes a live cell, as
// if by reproduction."
if (alive == 3f)
return 1f;
return 0f;
}
#endregion
}
}
VS_In.cs
This structure has the StageInOutAttribute, which means that it may transport data between GPU stages. In this case, the structure defines the input vertex format of the render effect.
All inter-stage structure elements must have an associated semantic (for example PositionAttribute), which is used to establish a mapping to vertex buffer elements (see VertexElement). Apart from that mapping, semantics have no further meaning. For example, it would be fine to use TangentAttribute to transport an sRGB color value.
src.cs/Tinman.Shaders.GpuX/Demo/VS_In.cs
using ...;
namespace Tinman.Shaders.Demo
{
[StageInOut]
public struct VS_In
{
#region Public / Attributes
[Position]
public float2 Position;
#endregion
}
}
PS_In.cs
This structure represents the input to the final pixel shader stage. The system semantic Value.Position identifies the homogeneous coordinates, which will be converted to normalized device coordinates by dividing by the w coordinate.
src.cs/Tinman.Shaders.GpuX/Demo/PS_In.cs
using ...;
namespace Tinman.Shaders.Demo
{
[StageInOut]
public struct PS_In
{
#region Public / Attributes
[TexCoord]
public float2 Coords;
[System(Value.Position)]
public float4 Position;
#endregion
}
}
SR_Demo.cs
Render effects usually consume GPU resources, such as textures. These resources are declared in a static class, annotated with ShaderResourcesAttribute.
Here, a single two-dimensional texture (see TEXTURE_2D) is used, which buffers the Game-of-Life state.
src.cs/Tinman.Shaders.GpuX/Demo/SR_Demo.cs
using ...;
namespace Tinman.Shaders.Demo
{
/// <summary>
/// See <see cref="Example_DemoParameters"/>.
/// </summary>
[ShaderResources]
public static class SR_Demo
{
#region Public / Attributes
public static TEXTURE_2D<float4> Buffer;
#endregion
}
}
CB_Demo_Static.cs
The constant input data for GPU programs is provided via constant buffers (see ConstantBufferAttribute), which are static classes.
For the demo render effect, only the pixel size of the Game-of-Life buffer texture is provided as a constant input value.
src.cs/Tinman.Shaders.GpuX/Demo/CB_Demo_Static.cs
using ...;
namespace Tinman.Shaders.Demo
{
/// <summary>
/// See <see cref="Example_DemoParameters"/>.
/// </summary>
[ConstantBuffer]
public static class CB_Demo_Static
{
#region Public / Attributes
/// <summary>
/// The pixel size of the square Game-of-Life buffer texture.
/// </summary>
public static float BufferSize;
#endregion
}
}
Configuration.cs
src.cs/Tinman.Shaders.GpuX/Configuration.cs
using ...;
namespace Tinman.Shaders
{
[Configuration]
public static class Configuration
{
#region Public / Constants
...
/// <summary>
/// Depicts the value range of the Z-component of normalized device
/// coordinates (NDC): <br/>
/// <c>true</c> : [0 .. 1] <br/>
/// <c>false</c> : [-1 .. +1]
/// </summary>
public static readonly bool NEAR_AT_ZERO = true;
/// <summary>
/// The offset from viewport coordinates (0,0) to the center of the
/// corresponding pixel.
/// </summary>
public static readonly float PIXEL_OFFSET = 0f;
...
#endregion
}
}
Generating HLSL / GLSL Code
Based on Gpu-X compliant C# code as input, the Code-X Processor will generate fully functional HLSL and GLSL source code automatically. Alternatively, shader source code may also be authored manually.
To generate HLSL / GLSL code from C# code, the project file must define at least one target for the Gpu-X Workflow. Please refer to the Demo Application for a workable example.
During processing, the Gpu-X compliant C# source code is analysed and the HLSL and GLSL source code is generated.
Optionally, Visual Studio / MSBuild project files (*.vcxproj
) are generated, which can be used to edit and/or compile the shader code.
The OpenGLContext and OpenGLESContext classes will compile the generated GLSL code at runtime.
For Direct3D 9 and 11, the Effect-Compiler Tool / fxc
of the Windows SDK is required.
For Direct3D 12, the DirectX Shader Compiler / dxc
is used, via its NuGet redistributable package.
Writing the Code-X Interface
When an application interfaces GPU programs, it performs the following steps with a render API:
-
Create a GPU program, usually by compiling source code, loading compiled code and/or linking code.
-
Reflect the GPU program, in order to discover input parameters, resources and vertex formats.
-
Set input parameters, specify resources and map vertex formats to the layouts of the used vertex buffers.
-
Perform rendering, usually grouped by render pass, updating render state along the way, as necessary.
The GPU Rendering abstraction layer allows an application to perform all steps in a way that is independent of the underlying render API, by using a Code-X interface that consists of several interfaces and classes, as explained by the subsequent sections.
In most cases, a separate implementation for the IRenderEffect<TParameters> interface is required for each combination of RenderEffectParameters implementation and IGraphicsContext implementation.
For example, there is a render effect implementation for Example_DemoParameters and each of the following:
For writing a render effect implementation, a specific base class (derived from IRenderEffect) must be used. The base class provides low-level access to shader management functions of the underlying graphics API:
The render effect implementation is responsible for managing render state, mapping input parameters, shader resources and vertex formats. To do this, it uses the specific render API.
Using only the low-level management functions allows access to all features and offers the greatest flexibility. However, it requires considerable amount of code, which in addition is then specific to the graphics API and cannot be re-used.
In many cases, only a small fraction of features and flexibility is actually required. Then, one of the simplification and abstraction layers can be used, in order to produce re-usable compact code. The following sections show the separate convenience layers, starting with the most convenient one and moving towards extensive low-level management.
Render Effect / Common
Many render effects perform the same kind of steps, in similar order:
-
Load a number shader files.
-
Setup a number of render passes.
-
Map input vertex data to vertex buffer elements.
-
Associate shaders with render passes.
-
Associate render state with render passes.
-
Associate sampler state with textures.
The IRenderEffectComponents interface provides methods for performing these steps in a way that is independent of the underlying graphics API, which makes the building code fully re-usable. The downside is that the building code is limited to the methods that are exposed by the builder interface.
To implement such building code (herein referred to as variant A), override the RenderEffectParameters.EffectComponents method in your Render Effect Parameters implementation class. In the minimal case, the render effect implementation consists of only a single base constructor call.
All render effects for the variant A have similar implementations, only the IRenderEffect base class is different. |
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for DirectX 9.
/// </summary>
/// <remarks>
/// The common render effect component builder pattern is used, see
/// <see cref="RenderEffectParameters.EffectComponents"/>.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_DX9_A : DirectX9Effect<Example_DemoParameters>
{
internal Example_Demo_DX9_A(
Example_DemoParameters parameters, DirectX9Context owner, Path repository)
: base(parameters, owner, repository, 0) (1)
{
}
}
}
1 | Passing 0 here triggers the builder pattern. |
Render Effect / Presets
The variant B does not use EffectComponents, it only uses common state presets via EffectRenderState and EffectSamplerState.
The render effect source code for Direct3D 11 / Direct3D 12 and OpenGL / OpenGLES is similar, only the IRenderEffect base class is different. This is because of their common base classes DirectXEffect and GLEffect. |
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for DirectX 9.
/// </summary>
/// <remarks>
/// The render state preset and sample state preset patterns is used, see
/// <see cref="RenderEffectParameters.EffectRenderState"/> and
/// <see cref="RenderEffectParameters.EffectSamplerState"/>.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_DX9_B : DirectX9Effect<Example_DemoParameters>
{
public override void RenderStateSet(RenderStatePreset renderState)
{
base.RenderStateSet(renderState); (1)
SamplerStateApply("g_buffer"); (2)
}
internal Example_Demo_DX9_B(
Example_DemoParameters parameters, DirectX9Context owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
PassCreate(Example_DemoParameters.PassDefault); (1)
VS_Demo = ShaderCreate(VS, "Demo.VS.cso", "Tinman");
PS_Demo = ShaderCreate(PS, "Demo.PS.cso", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
1 | Triggers use of EffectRenderState. |
2 | Triggers user of EffectSamplerState. |
using System;
using Tinman.AddOns.DirectX11;
using Tinman.AddOns.DirectX11.Effects;
using Tinman.Engine.Rendering;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for DirectX 11.
/// </summary>
/// <remarks>
/// The render state preset and sample state preset patterns is used, see
/// <see cref="RenderEffectParameters.EffectRenderState"/> and
/// <see cref="RenderEffectParameters.EffectSamplerState"/>.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_DX11_B : DirectX11Effect<Example_DemoParameters>
{
internal Example_Demo_DX11_B(
Example_DemoParameters parameters, DirectX11Context owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int VS_Demo;
int PS_Demo;
PassCreateWithState(Example_DemoParameters.PassDefault); (1)
VS_Demo = ShaderCreate(VS, "Demo.VS.cso", "Tinman");
PS_Demo = ShaderCreate(PS, "Demo.PS.cso", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
ConstantBufferDeclare("CB_Demo_Static");
ShaderResourceDeclareWithState("g_buffer"); (2)
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
1 | Triggers use of EffectRenderState. |
2 | Triggers user of EffectSamplerState. |
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for
/// OpenGL 4.1 (or newer).
/// </summary>
/// <remarks>
/// The render state preset and sample state preset patterns is used, see
/// <see cref="RenderEffectParameters.EffectRenderState"/> and
/// <see cref="RenderEffectParameters.EffectSamplerState"/>.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_GL_B : OpenGLEffect<Example_DemoParameters>
{
internal Example_Demo_GL_B(
Example_DemoParameters parameters, OpenGLContext owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
PassCreate(Example_DemoParameters.PassDefault); (1)
VS_Demo = ShaderCreate(VS, "Demo.VS.glsl", "Tinman");
PS_Demo = ShaderCreate(FS, "Demo.PS.glsl", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
VertexAttributeDeclare("data_Position", 0, VertexElementUsage.Position);
SamplerStateAssign("g_buffer"); (2)
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
1 | Triggers use of EffectRenderState. |
2 | Triggers user of EffectSamplerState. |
Render Effect / Low-Level
At the lowest level, a render effect needs to use the following classes and structures, which are inherently dependent on the underlying graphics API:
- Direct3D 9
- Direct3D 11
- Direct3D 12
- OpenGL
- OpenGLES
In a real-world scenario, a render effect will likely use a mixture of all three API levels:
-
Use the Render Effect / Common pattern, wherever possible.
-
Otherwise, use the Render Effect Parameters pattern, wherever possible.
-
Otherwise, use the Render Effect / Low-Level pattern.
The render effect source code for Direct3D 11 / Direct3D 12 and OpenGL / OpenGLES looks similar, but is different, because it is based on different graphics APIs. |
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for DirectX 9.
/// </summary>
/// <remarks>
/// Only the low-level management function are used.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_DX9_C : DirectX9Effect<Example_DemoParameters>
{
public override void RenderStateSet(RenderStatePreset renderState)
{
int n;
RenderStateDefault();
RenderStateInt(DirectX9State.RS_CULLMODE, DirectX9State.CULL_NONE);
n = ParameterIndex("g_buffer");
n = TextureStage(n);
SamplerStateInt(n, DirectX9State.SAMP_MINFILTER, DirectX9State.TEXF_POINT);
SamplerStateInt(n, DirectX9State.SAMP_MIPFILTER, DirectX9State.TEXF_POINT);
SamplerStateInt(n, DirectX9State.SAMP_MAGFILTER, DirectX9State.TEXF_POINT);
}
internal Example_Demo_DX9_C(
Example_DemoParameters parameters, DirectX9Context owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
PassCreate(Example_DemoParameters.PassDefault);
VS_Demo = ShaderCreate(VS, "Demo.VS.cso", "Tinman");
PS_Demo = ShaderCreate(PS, "Demo.PS.cso", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for DirectX 11.
/// </summary>
/// <remarks>
/// Only the low-level management function are used.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_DX11_C : DirectX11Effect<Example_DemoParameters>
{
internal Example_Demo_DX11_C(
Example_DemoParameters parameters, DirectX11Context owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
DirectX11RasterizerState rasterizerState;
DirectX11SamplerState samplerState;
PassCreate(Example_DemoParameters.PassDefault);
rasterizerState = new DirectX11RasterizerState();
rasterizerState.CullMode = DirectX11State.CULL_NONE;
RasterizerStateAssign(rasterizerState, Example_DemoParameters.PassDefault);
VS_Demo = ShaderCreate(VS, "Demo.VS.cso", "Tinman");
PS_Demo = ShaderCreate(PS, "Demo.PS.cso", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
ConstantBufferDeclare("CB_Demo_Static");
ShaderResourceDeclare("g_buffer");
samplerState = new DirectX11SamplerState();
samplerState.Filter = DirectX11State.FILTER_MIN_MAG_MIP_POINT;
samplerState.AddressU = DirectX11State.TEXTURE_ADDRESS_WRAP;
samplerState.AddressV = DirectX11State.TEXTURE_ADDRESS_WRAP;
SamplerStateAssign(samplerState, "g_bufferSampler");
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for DirectX 12.
/// </summary>
/// <remarks>
/// Only the low-level management function are used.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_DX12_C : DirectX12Effect<Example_DemoParameters>
{
internal Example_Demo_DX12_C(
Example_DemoParameters parameters, DirectX12Context owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
DirectX12RasterizerState rasterizerState;
DirectX12SamplerState samplerState;
PassCreate(Example_DemoParameters.PassDefault);
rasterizerState = new DirectX12RasterizerState();
rasterizerState.CullMode = DirectX12State.CULL_MODE_NONE;
RasterizerStateAssign(rasterizerState, Example_DemoParameters.PassDefault);
VS_Demo = ShaderCreate(VS, "Demo.VS.cso", "Tinman");
PS_Demo = ShaderCreate(PS, "Demo.PS.cso", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
ConstantBufferDeclare("CB_Demo_Static");
ShaderResourceDeclare("g_buffer");
samplerState = new DirectX12SamplerState();
samplerState.Filter = DirectX12State.FILTER_MIN_MAG_MIP_POINT;
samplerState.AddressU = DirectX12State.TEXTURE_ADDRESS_MODE_WRAP;
samplerState.AddressV = DirectX12State.TEXTURE_ADDRESS_MODE_WRAP;
SamplerStateAssign(samplerState, "g_bufferSampler");
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for
/// OpenGL 4.1 (or newer).
/// </summary>
/// <remarks>
/// Only the low-level management function are used.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_GL_C : OpenGLEffect<Example_DemoParameters>
{
public override void RenderStateSet(int context)
{
if (context < 0)
{
// When the render effect begins, reset to default state.
RenderStateDefault();
}
else if (context == 0)
{
// When the render pass begins, set the corresponding render state.
gl.Disable(GL.CULL_FACE);
}
else
{
// When the render effect ends, restore original render state.
gl.Enable(GL.CULL_FACE);
}
}
internal Example_Demo_GL_C(
Example_DemoParameters parameters, OpenGLContext owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
int sampler;
PassCreate(Example_DemoParameters.PassDefault);
VS_Demo = ShaderCreate(VS, "Demo.VS.glsl", "Tinman");
PS_Demo = ShaderCreate(FS, "Demo.PS.glsl", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
VertexAttributeDeclare("data_Position", 0, VertexElementUsage.Position);
sampler = SamplerStateCreate();
gl.SamplerParameteri(sampler, GL.TEXTURE_MIN_FILTER, GL.NEAREST_MIPMAP_NEAREST);
gl.SamplerParameteri(sampler, GL.TEXTURE_MAG_FILTER, GL.NEAREST);
gl.SamplerParameteri(sampler, GL.TEXTURE_WRAP_S, GL.REPEAT);
gl.SamplerParameteri(sampler, GL.TEXTURE_WRAP_T, GL.REPEAT);
SamplerStateAssign("g_buffer", SamplerStateCreate(sampler));
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom render effect for
/// OpenGLES 3.0 (or newer).
/// </summary>
/// <remarks>
/// Only the low-level management function are used.
/// </remarks>
/// <seealso cref="Example_DemoFactory"/>
public sealed class Example_Demo_GLES_C : OpenGLESEffect<Example_DemoParameters>
{
public override void RenderStateSet(int context)
{
if (context < 0)
{
// When the render effect begins, reset to default state.
RenderStateDefault();
}
else if (context == 0)
{
// When the render pass begins, set the corresponding render state.
gl.Disable(GLES.CULL_FACE);
}
else
{
// When the render effect ends, restore original render state.
gl.Enable(GLES.CULL_FACE);
}
}
internal Example_Demo_GLES_C(
Example_DemoParameters parameters, OpenGLESContext owner, Path repository)
: base(parameters, owner, repository)
{
try
{
int PS_Demo;
int VS_Demo;
int sampler;
PassCreate(Example_DemoParameters.PassDefault);
VS_Demo = ShaderCreate(VS, "Demo.VS.glsl", "Tinman");
PS_Demo = ShaderCreate(FS, "Demo.PS.glsl", "Tinman");
ShaderAssign(VS_Demo, Example_DemoParameters.PassDefault);
ShaderAssign(PS_Demo, Example_DemoParameters.PassDefault);
VertexAttributeDeclare("data_Position", 0, VertexElementUsage.Position);
sampler = SamplerStateCreate();
gl.SamplerParameteri(sampler, GLES.TEXTURE_MIN_FILTER,
GLES.NEAREST_MIPMAP_NEAREST);
gl.SamplerParameteri(sampler, GLES.TEXTURE_MAG_FILTER, GLES.NEAREST);
gl.SamplerParameteri(sampler, GLES.TEXTURE_WRAP_S, GLES.REPEAT);
gl.SamplerParameteri(sampler, GLES.TEXTURE_WRAP_T, GLES.REPEAT);
SamplerStateAssign("g_buffer", SamplerStateCreate(sampler));
parameters.Declare(this);
}
catch (Exception)
{
Dispose();
throw;
}
}
}
}
Render Effect Factory
Render effects are created by a IRenderEffectFactory object, based on the type of the given IGraphicsContext and RenderEffectParameters objects. A factory must analyse the given graphics context and render effect parameters objects, in order to choose an implementation class for the render effect.
Before a render effect can be used by an application, its factory must be registered with IGraphicsContext.RegisterRenderEffectFactory. The render effect can then be created by passing an instance of its render effect parameters class to the IGraphicsContext.CreateRenderEffect method.
The Example_DemoFactory class demonstrates how to implement a render effect factory class and Tutorial_01_Conway demonstrates how to use that render effect:
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom of
/// <see cref="IRenderEffectFactory"/>.
/// </summary>
/// <seealso cref="Example_DemoParameters"/>
public sealed class Example_DemoFactory : IRenderEffectFactory
{
/// <summary>
/// Creates a new instance of <see cref="Example_DemoFactory"/>.
/// </summary>
/// <param name="repositoryPrivate">
/// [not-null] The private GPU shader repository path or
/// <see cref="Path.Unknown"/> if none.
/// </param>
public Example_DemoFactory(Path repositoryPrivate)
{
#if DEBUG
...
#endif
this.repositoryPrivate = repositoryPrivate;
}
public bool CanCreateRenderEffect(IGraphicsContext context,
RenderEffectParameters parameters)
{
#if DEBUG
...
#endif
if (context.ShaderRepository.IsUnknown && repositoryPrivate.IsUnknown) (1)
return false;
if (parameters is Example_DemoParameters) (2)
return true;
return false;
}
public IRenderEffectBase CreateRenderEffect(IGraphicsContext context,
RenderEffectParameters parameters)
{
#if DEBUG
...
#endif
Example_DemoParameters temp;
if (!CanCreateRenderEffect(context, parameters)) (3)
return null;
// The type of the given parameters object depicts which render effect
// to create.
if ((temp = parameters as Example_DemoParameters) != null) (2)
{
OpenGLContext openGL;
OpenGLESContext openGLES;
// The type of the given graphics context depicts which render effect base
// class to use. Typically, there is one dedicated base class for each
// graphics API, which provides low-level but API-specific functionality
// (for example Compute Shaders for DirectX 11+).
if (context.IsCompatibleWith(new DirectX9ContextFactory())) (4)
return CreateDirectX9(temp, context);
if (context.IsCompatibleWith(new DirectX11ContextFactory())) (4)
return CreateDirectX11(temp, context);
if (context.IsCompatibleWith(new DirectX12ContextFactory())) (4)
return CreateDirectX12(temp, context);
if ((openGL = context as OpenGLContext) != null) (4)
{
RenderContext renderContext;
renderContext = openGL.RenderContext;
renderContext.BeginThrow(
"Example_RenderEffectFactory.CreateRenderEffect"); (5)
try
{
return CreateOpenGL(temp, openGL);
}
finally
{
renderContext.End();
}
}
if ((openGLES = context as OpenGLESContext) != null) (4)
{
RenderContext renderContext;
renderContext = openGLES.RenderContext;
renderContext.BeginThrow(
"Example_RenderEffectFactory.CreateRenderEffect"); (5)
try
{
return CreateOpenGLES(temp, openGLES);
}
finally
{
renderContext.End();
}
}
}
return null;
}
...
/// <summary>
/// The private GPU shader repository or <see cref="Path.Unknown"/>.
/// </summary>
readonly Path repositoryPrivate;
...
[OwnerReturn]
IRenderEffectBase CreateDirectX9(Example_DemoParameters temp,
IGraphicsContext context)
{
DirectX9Context directX9;
Path repository;
directX9 = (DirectX9Context) context;
repository = repositoryPrivate.AppendName("hlsl.dx9");
switch (temp.Level)
{
case 'A':
return new Example_Demo_DX9_A(temp, directX9, repository); (6)
case 'B':
return new Example_Demo_DX9_B(temp, directX9, repository); (6)
case 'C':
return new Example_Demo_DX9_C(temp, directX9, repository); (6)
}
return null;
}
}
}
1 | Usually, a render effect needs to load shaders, which are provided as files in the shader repository. |
2 | The render effect factory examines the RenderEffectParameters object type, to see if it is supported. |
3 | As best-practice, the support check logic should be reused. |
4 | The render effect factory looks at the concrete type of the IGraphicsContext object, in order to choose an implementation class for the render effect. |
5 | OpenGL has the concept of a rendering context, which must be active when a render effect is created. The try-finally statement activates the rendering context, if necessary. |
6 | This chooses the render effect for the API level, looks similar for all others. |
Render Effect Parameters
Implementations of the abstract class RenderEffectParameters are used by an application to manage input parameter values and shader resources, in a way that does not depend on a specific render API.
There is a dedicated render effect parameter class for each render effect. Render effect parameter classes may aggregate others, for example to share common parameters, such as lighting or camera settings.
The Example_DemoParameters class demonstrates how to implement a render effect parameter class:
using ...;
namespace Tinman.Demo.Examples.Impl
{
/// <summary>
/// This example shows how to implement a custom set of
/// <see cref="RenderEffectParameters"/>.
/// </summary>
/// <remarks>
/// The following low-level render effect parameter slots are defined:
/// <list type="bullet">
/// <item>
/// 'g_buffer', <see cref="RenderEffectParameterType.Texture2D"/>,
/// mapped to <see cref="Buffer"/>. (1)
/// </item>
/// <item>
/// 'g_bufferSize', <see cref="RenderEffectParameterType.Int"/>,
/// mapped to <see cref="Buffer"/>. (1)
/// </item>
/// </list>
/// </remarks>
public sealed class Example_DemoParameters : RenderEffectParameters
{
/// <summary>
/// Changed flag for <see cref="Buffer"/>.
/// </summary>
public const int ChangedBuffer = 1; (2)
/// <summary>
/// The default pass of the render effect.
/// </summary>
public const int PassDefault = 0; (3)
/// <summary>
/// The Game-of-Life buffer.
/// </summary>
/// <value>The buffer texture.</value>
/// <seealso cref="ChangedBuffer"/>
public ITexture2D Buffer (4)
{
get { return buffer; }
set
{
if (value == buffer)
return;
buffer = value;
Set(ChangedBuffer); (5)
}
}
/// <summary>
/// Creates a new instance of <see cref="Example_DemoParameters"/>.
/// </summary>
public Example_DemoParameters()
: base("Example")
{
slotBuffer = -1; (6)
slotBufferSize = -1;
}
public override void Apply(IRenderEffectParameters effect,
bool forceTextures = false)
{
// The base call can be skipped, because there are no aggregated children.
if (Has(ChangedBuffer) || forceTextures) (7)
effect.ParameterTexture(slotBuffer, 0, buffer);
if (Has(ChangedBuffer)) (7)
effect.ParameterFloat(slotBufferSize, 0, buffer == null ? 0 : buffer.Width);
}
public override void ClearResources() (8)
{
// The base call can be skipped, because there are no aggregated children.
Buffer = null;
}
public override void Declare(IRenderEffectParameters effect, int flags = 0) (9)
{
slotBuffer = effect.ParameterDeclare(
RenderEffectParameterType.Texture2D, "g_buffer");
slotBufferSize = effect.ParameterDeclare(
RenderEffectParameterType.Float, "g_bufferSize");
}
public override void EffectComponents(IRenderEffectComponents effect,
int flags = 0) (10)
{
if (Level == 'A')
{
// This will load the two shaders, create a single pass and associate the
// shaders with it.
effect.Pass(PassDefault, "VS | Demo.VS | Tinman", "PS | Demo.PS | Tinman");
// This associated the vertex input data with semantic for mapping with
// vertex buffers.
effect.Vertex("data_Position", 0, VertexElementUsage.Position);
effect.Buffer("CB_Demo_Static");
effect.Texture("g_buffer");
}
}
public override RenderStatePreset EffectRenderState(int pass) (11)
{
if (Level <= 'B')
{
// Then the render pass is activated, this render state preset is applied
// automatically.
return RenderStatePreset.NoCulling;
}
return RenderStatePreset.None;
}
public override SamplerStatePreset EffectSamplerState(string name) (11)
{
if (Level <= 'B')
{
// The following sampler state preset will be applied to the texture
// automatically.
if (name == "g_buffer")
return SamplerStatePreset.Point | SamplerStatePreset.Wrap;
}
return SamplerStatePreset.None;
}
ITexture2D buffer; (12)
int slotBuffer;
int slotBufferSize;
}
}
1 | Low-level parameters are defined by HLSL / GLSL source code, usually be declaring uniform variables. The documentation should include the names of all used low-level parameters and how they are mapped to render effect parameters. |
2 | Each render effect parameter has a flag that indicates whether its value has changed or not. |
3 | Each render effect pass should have its own constant and the documentation should explain which render effect parameters are required for the pass. |
4 | Render effect parameters are simple properties. The documentation should specify the default value and contain a link to the changed flag. |
5 | When a render effect parameter value changes, the changed flag must be set by the property setter. |
6 | Low-level parameters are bound to slot indices. Negative slot indices mean that the binding is not yet known. |
7 | During rendering, updated render effect parameters are uploaded to the GPU. Resources may need to be specified again, even if the corresponding render effect parameter has not been modified. |
8 | Resources may need to be cleared, for example when an application has finished rendering with a render effect. |
9 | During initialization, the slot binding is queried: for a given low-level parameter name and type, the corresponding slot index is returned. |
10 | Common render effect components pattern, see Render Effect / Common |
11 | Render / sampler state presets, see Render Effect / Presets |
12 | Render effect parameter values and slot indices are stored in simple backing fields. |