Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions docs/render-pass-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ public void Render(BasicRenderContext renderContext)
renderContext.CommandBuffer.PushFragmentUniformData(0, color);

// 2. CREATE RenderPass
using IRenderPass renderPass = new RenderPassBuilder(renderContext.CommandBuffer)
.AddColorTarget(renderContext.SwapchainTexture)
.SetSharedColorTargetSettings(ColorTargetSettings.Clear)
.Build();
using IRenderPass renderPass = renderContext.CommandBuffer.CreateRenderPass(
renderContext.SwapchainTexture, ColorTargetSettings.Clear);

// 3. INSIDE RenderPass: Bind and draw
renderPass.BindGraphicsPipeline(_graphicsPipeline);
Expand Down Expand Up @@ -94,21 +92,56 @@ Typical order inside a RenderPass:

For multiple objects, rebind vertex buffers and push new uniforms between draws.

## RenderPassBuilder
## Creating a RenderPass

`CommandBuffer.CreateRenderPass` takes the pass description directly. It allocates nothing, so it is
what a renderer should call every frame:

```csharp
new RenderPassBuilder(commandBuffer)
.AddColorTarget(texture) // Output texture
.SetSharedColorTargetSettings(ColorTargetSettings.Clear) // Clear on start
.Build()
// One color target
using IRenderPass pass = commandBuffer.CreateRenderPass(texture, ColorTargetSettings.Clear);

// One color target and a depth buffer
using IRenderPass pass = commandBuffer.CreateRenderPass(
texture, ColorTargetSettings.Clear, depthBuffer, DepthBufferSettings.Default);

// Depth only, no color target
using IRenderPass pass = commandBuffer.CreateDepthOnlyRenderPass(depthBuffer, DepthBufferSettings.Default);

// Several color targets for deferred rendering (G-buffer), from storage the caller owns
using IRenderPass pass = commandBuffer.CreateRenderPass(
_gBufferTextures, _gBufferSettings, _depthBuffer, DepthBufferSettings.Default);
```

The span overload takes one settings entry per color target, and at most `CommandBuffer.MaxColorTargets`
(8, the point at which SDL itself rejects the pass) targets.

**ColorTargetSettings options:**
- `Clear` - Clear the target before rendering
- `Load` - Keep existing contents
- Others may exist for different load/store operations

Add multiple color targets for deferred rendering (G-buffer).
## RenderPassBuilder

`RenderPassBuilder` collects the same description across several statements, for a pass composed
conditionally or from a varying number of targets:

```csharp
RenderPassBuilder builder = new RenderPassBuilder(commandBuffer)
.AddColorTarget(_albedo)
.SetSharedColorTargetSettings(ColorTargetSettings.Clear);

if (_depthEnabled)
{
builder.SetDepthBuffer(_depthBuffer, DepthBufferSettings.Default);
}

using IRenderPass pass = builder.Build();
```

It is a class and allocates, so prefer `CreateRenderPass` in a per-frame render path. `Build()` resets
the builder, which can then describe the next pass. Either give every color target its own settings,
or set shared settings for all of them - mixing the two throws.

## Common Patterns

Expand Down
17 changes: 11 additions & 6 deletions docs/subrenderers.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ public class GeometryPhase : IRenderer<GameRenderContext>
private readonly IReadOnlyList<IGeometrySubrenderer> _subrenderers;
private readonly GameRenderContextBuffers _buffers;

// Owned by the renderer so describing the pass every frame allocates nothing
private readonly Texture[] _gBufferTextures = new Texture[3];
private readonly ColorTargetSettings[] _gBufferSettings =
[ColorTargetSettings.Clear, ColorTargetSettings.Clear, ColorTargetSettings.Clear];

public GeometryPhase(
IEnumerable<IGeometrySubrenderer> subrenderers,
GameRenderContextBuffers buffers)
Expand All @@ -112,12 +117,12 @@ public class GeometryPhase : IRenderer<GameRenderContext>

public void Render(GameRenderContext renderContext)
{
using IRenderPass renderPass = new RenderPassBuilder(renderContext.CommandBuffer)
.AddColorTarget(_buffers.AlbedoBuffer.Texture)
.AddColorTarget(_buffers.NormalBuffer.Texture)
.AddColorTarget(_buffers.PositionBuffer.Texture)
.SetSharedColorTargetSettings(ColorTargetSettings.Clear)
.Build();
_gBufferTextures[0] = _buffers.AlbedoBuffer.Texture;
_gBufferTextures[1] = _buffers.NormalBuffer.Texture;
_gBufferTextures[2] = _buffers.PositionBuffer.Texture;

using IRenderPass renderPass = renderContext.CommandBuffer.CreateRenderPass(
_gBufferTextures, _gBufferSettings, null, DepthBufferSettings.Default);

foreach (IGeometrySubrenderer subrenderer in _subrenderers)
{
Expand Down
13 changes: 3 additions & 10 deletions src/Pixely.Pencuil/PencuilRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,7 @@ private void RenderPencil(CommandBuffer commandBuffer)

_maxDepthValue = coloredRectangleInstructions.Count + textureRegionInstructions.Count;

using IRenderPass renderPass = new RenderPassBuilder(commandBuffer)
.AddColorTarget(_retainedTexture, _guiColorTargetSettings)
.SetDepthBuffer(_depthBuffer, DepthBufferSettings.Default)
.Build();
using IRenderPass renderPass = commandBuffer.CreateRenderPass(_retainedTexture, _guiColorTargetSettings, _depthBuffer, DepthBufferSettings.Default);

commandBuffer.PushVertexUniformData(0, _viewProjection);

Expand Down Expand Up @@ -224,9 +221,7 @@ private void RenderPencil(CommandBuffer commandBuffer)

private void Clear(CommandBuffer commandBuffer)
{
using IRenderPass clearPass = new RenderPassBuilder(commandBuffer)
.AddColorTarget(_retainedTexture, _guiColorTargetSettings)
.Build();
using IRenderPass clearPass = commandBuffer.CreateRenderPass(_retainedTexture, _guiColorTargetSettings);
}

private void Present(CommandBuffer commandBuffer, Texture target)
Expand All @@ -235,9 +230,7 @@ private void Present(CommandBuffer commandBuffer, Texture target)
? ColorTargetSettings.Clear
: new ColorTargetSettings { LoadOperation = LoadOperation.Load };

using IRenderPass presentPass = new RenderPassBuilder(commandBuffer)
.AddColorTarget(target, settings)
.Build();
using IRenderPass presentPass = commandBuffer.CreateRenderPass(target, settings);

commandBuffer.PushVertexUniformData(0, _presentViewProjection);
commandBuffer.PushVertexUniformData(1, Matrix4x4.Identity);
Expand Down
44 changes: 39 additions & 5 deletions src/Pixely/Gpu/CommandBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,47 @@ public void PushVertexUniformData<TType>(uint slot, TType variable) where TType
}
}

public IRenderPass CreateRenderPass(List<Texture> colorTargets, List<ColorTargetSettings> colorTargetSettings, Texture? depthBuffer, DepthBufferSettings depthBufferSettings)
// What SDL_BeginGPURenderPass rejects beyond: MAX_COLOR_TARGET_BINDINGS in SDL_sysgpu.h.
// SDL exposes no constant for it, and the prose in SDL_gpu.h still claims four.
public const int MaxColorTargets = 8;

public IRenderPass CreateRenderPass(Texture colorTarget, ColorTargetSettings colorTargetSettings)
{
return CreateRenderPass(new ReadOnlySpan<Texture>(in colorTarget), new ReadOnlySpan<ColorTargetSettings>(in colorTargetSettings), null, DepthBufferSettings.Default);
}

public IRenderPass CreateRenderPass(Texture colorTarget, ColorTargetSettings colorTargetSettings, Texture depthBuffer, DepthBufferSettings depthBufferSettings)
{
return CreateRenderPass(new ReadOnlySpan<Texture>(in colorTarget), new ReadOnlySpan<ColorTargetSettings>(in colorTargetSettings), depthBuffer, depthBufferSettings);
}

public IRenderPass CreateDepthOnlyRenderPass(Texture depthBuffer, DepthBufferSettings depthBufferSettings)
{
return CreateRenderPass(ReadOnlySpan<Texture>.Empty, ReadOnlySpan<ColorTargetSettings>.Empty, depthBuffer, depthBufferSettings);
}

public IRenderPass CreateRenderPass(ReadOnlySpan<Texture> colorTargets, ReadOnlySpan<ColorTargetSettings> colorTargetSettings, Texture? depthBuffer, DepthBufferSettings depthBufferSettings)
{
ThrowIfDisposed();

Span<SDL_GPUColorTargetInfo> colorTargetInfos = stackalloc SDL_GPUColorTargetInfo[colorTargets.Count];

for (int i = 0; i < colorTargets.Count; i++)

if (colorTargets.Length == 0 && depthBuffer == null)
{
throw new ArgumentException("At least one color target or a depth buffer is required.", nameof(colorTargets));
}

if (colorTargets.Length > MaxColorTargets)
{
throw new ArgumentException($"A render pass cannot have more than {MaxColorTargets} color targets.", nameof(colorTargets));
}

if (colorTargetSettings.Length != colorTargets.Length)
{
throw new ArgumentException($"Expected settings for {colorTargets.Length} color targets, got {colorTargetSettings.Length}.", nameof(colorTargetSettings));
}

Span<SDL_GPUColorTargetInfo> colorTargetInfos = stackalloc SDL_GPUColorTargetInfo[colorTargets.Length];

for (int i = 0; i < colorTargets.Length; i++)
{
Texture colorTarget = colorTargets[i];
ColorTargetSettings colorTargetSetting = colorTargetSettings[i];
Expand Down
120 changes: 57 additions & 63 deletions src/Pixely/Gpu/RenderPassBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,90 +1,68 @@
namespace Pixely.Gpu;

internal struct RenderPassBuilderState
{
public RenderPassBuilderState()
{
ResetState();
}
using System.Runtime.InteropServices;

public List<Texture> ColorTargets { get; } = new();
public List<ColorTargetSettings> ColorTargetSettings { get; } = new();
public Texture? DepthBuffer { get; set; }
public DepthBufferSettings DepthBufferSettings { get; set; } = DepthBufferSettings.Default;
public ColorTargetSettings? SharedColorTargetSettings { get; set; }

public void ResetState()
{
ColorTargets.Clear();
ColorTargetSettings.Clear();
DepthBuffer = null;
DepthBufferSettings = DepthBufferSettings.Default;
SharedColorTargetSettings = null;
}
}

public interface IRenderPassBuilder
{
IRenderPassBuilder AddColorTarget(Texture texture);
IRenderPassBuilder AddColorTarget(Texture texture, ColorTargetSettings settings);
IRenderPassBuilder AddColorTargets(ReadOnlySpan<Texture> textures);
IRenderPassBuilder SetSharedColorTargetSettings(ColorTargetSettings settings);
IRenderPassBuilder SetDepthBuffer(Texture depthBuffer, DepthBufferSettings settings);

IRenderPass Build();
}
namespace Pixely.Gpu;

public class RenderPassBuilder : IRenderPassBuilder
/// <summary>
/// Collects the description of a render pass across several statements, for callers that compose one
/// conditionally or from a varying number of targets. It allocates, so a renderer that describes the same
/// pass every frame should call <see cref="CommandBuffer.CreateRenderPass(Texture, ColorTargetSettings)"/>
/// or one of its overloads instead.
/// </summary>
public class RenderPassBuilder
{
private RenderPassBuilderState _state = new();
private readonly CommandBuffer _commandBuffer;
private readonly List<Texture> _colorTargets = new();
private readonly List<ColorTargetSettings> _colorTargetSettings = new();
private Texture? _depthBuffer;
private DepthBufferSettings _depthBufferSettings = DepthBufferSettings.Default;
private ColorTargetSettings? _sharedColorTargetSettings;

public RenderPassBuilder(CommandBuffer commandBuffer)
{
_commandBuffer = commandBuffer;
}
public IRenderPassBuilder AddColorTarget(Texture texture)

public RenderPassBuilder AddColorTarget(Texture texture)
{
_state.ColorTargets.Add(texture);
_colorTargets.Add(texture);
return this;
}
public IRenderPassBuilder AddColorTargets(ReadOnlySpan<Texture> textures)

public RenderPassBuilder AddColorTarget(Texture texture, ColorTargetSettings settings)
{
foreach (var texture in textures)
{
AddColorTarget(texture);
}
_colorTargets.Add(texture);
_colorTargetSettings.Add(settings);
return this;
}

public IRenderPassBuilder AddColorTarget(Texture texture, ColorTargetSettings settings)
public RenderPassBuilder AddColorTargets(ReadOnlySpan<Texture> textures)
{
_state.ColorTargets.Add(texture);
_state.ColorTargetSettings.Add(settings);
foreach (Texture texture in textures)
{
_colorTargets.Add(texture);
}
return this;
}

public IRenderPassBuilder SetSharedColorTargetSettings(ColorTargetSettings settings)
public RenderPassBuilder SetSharedColorTargetSettings(ColorTargetSettings settings)
{
_state.SharedColorTargetSettings = settings;
_sharedColorTargetSettings = settings;
return this;
}

public IRenderPassBuilder SetDepthBuffer(Texture depthBuffer, DepthBufferSettings settings)
public RenderPassBuilder SetDepthBuffer(Texture depthBuffer, DepthBufferSettings settings)
{
_state.DepthBuffer = depthBuffer;
_state.DepthBufferSettings = settings;
_depthBuffer = depthBuffer;
_depthBufferSettings = settings;
return this;
}

public IRenderPass Build()
{
bool hasShared = _state.SharedColorTargetSettings != null;
bool hasPerTarget = _state.ColorTargetSettings.Count > 0;
bool hasColorTargets = _state.ColorTargets.Count > 0;
bool hasDepthBuffer = _state.DepthBuffer != null;
bool hasShared = _sharedColorTargetSettings != null;
bool hasPerTarget = _colorTargetSettings.Count > 0;
bool hasColorTargets = _colorTargets.Count > 0;
bool hasDepthBuffer = _depthBuffer != null;

if (hasShared && hasPerTarget)
{
Expand All @@ -96,25 +74,41 @@ public IRenderPass Build()
throw new InvalidOperationException("Must have either shared or per-target settings set when using color targets.");
}

if (hasPerTarget && _colorTargetSettings.Count != _colorTargets.Count)
{
throw new InvalidOperationException("Every color target needs its own settings when per-target settings are used.");
}

if (!hasColorTargets && !hasDepthBuffer)
{
throw new InvalidOperationException("At least one color target or a depth buffer is required.");
}

if (hasShared)
{
for (int i = 0; i < _state.ColorTargets.Count; i++)
for (int i = 0; i < _colorTargets.Count; i++)
{
_state.ColorTargetSettings.Add(_state.SharedColorTargetSettings!);
_colorTargetSettings.Add(_sharedColorTargetSettings!);
}
}

IRenderPass renderPass = _commandBuffer.CreateRenderPass(_state.ColorTargets, _state.ColorTargetSettings, _state.DepthBuffer,
_state.DepthBufferSettings);
IRenderPass renderPass = _commandBuffer.CreateRenderPass(
CollectionsMarshal.AsSpan(_colorTargets),
CollectionsMarshal.AsSpan(_colorTargetSettings),
_depthBuffer,
_depthBufferSettings);

_state.ResetState();
ResetState();

return renderPass;
}
}

private void ResetState()
{
_colorTargets.Clear();
_colorTargetSettings.Clear();
_depthBuffer = null;
_depthBufferSettings = DepthBufferSettings.Default;
_sharedColorTargetSettings = null;
}
}
Loading