diff --git a/docs/render-pass-flow.md b/docs/render-pass-flow.md index 7903137c..2499d292 100644 --- a/docs/render-pass-flow.md +++ b/docs/render-pass-flow.md @@ -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); @@ -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 diff --git a/docs/subrenderers.md b/docs/subrenderers.md index 61c59f72..11cfce5a 100644 --- a/docs/subrenderers.md +++ b/docs/subrenderers.md @@ -102,6 +102,11 @@ public class GeometryPhase : IRenderer private readonly IReadOnlyList _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 subrenderers, GameRenderContextBuffers buffers) @@ -112,12 +117,12 @@ public class GeometryPhase : IRenderer 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) { diff --git a/src/Pixely.Pencuil/PencuilRenderer.cs b/src/Pixely.Pencuil/PencuilRenderer.cs index 6da8a317..dbb680d2 100644 --- a/src/Pixely.Pencuil/PencuilRenderer.cs +++ b/src/Pixely.Pencuil/PencuilRenderer.cs @@ -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); @@ -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) @@ -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); diff --git a/src/Pixely/Gpu/CommandBuffer.cs b/src/Pixely/Gpu/CommandBuffer.cs index a435a3ab..d8aedd4d 100644 --- a/src/Pixely/Gpu/CommandBuffer.cs +++ b/src/Pixely/Gpu/CommandBuffer.cs @@ -83,13 +83,47 @@ public void PushVertexUniformData(uint slot, TType variable) where TType } } - public IRenderPass CreateRenderPass(List colorTargets, List 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(in colorTarget), new ReadOnlySpan(in colorTargetSettings), null, DepthBufferSettings.Default); + } + + public IRenderPass CreateRenderPass(Texture colorTarget, ColorTargetSettings colorTargetSettings, Texture depthBuffer, DepthBufferSettings depthBufferSettings) + { + return CreateRenderPass(new ReadOnlySpan(in colorTarget), new ReadOnlySpan(in colorTargetSettings), depthBuffer, depthBufferSettings); + } + + public IRenderPass CreateDepthOnlyRenderPass(Texture depthBuffer, DepthBufferSettings depthBufferSettings) + { + return CreateRenderPass(ReadOnlySpan.Empty, ReadOnlySpan.Empty, depthBuffer, depthBufferSettings); + } + + public IRenderPass CreateRenderPass(ReadOnlySpan colorTargets, ReadOnlySpan colorTargetSettings, Texture? depthBuffer, DepthBufferSettings depthBufferSettings) { ThrowIfDisposed(); - - Span 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 colorTargetInfos = stackalloc SDL_GPUColorTargetInfo[colorTargets.Length]; + + for (int i = 0; i < colorTargets.Length; i++) { Texture colorTarget = colorTargets[i]; ColorTargetSettings colorTargetSetting = colorTargetSettings[i]; diff --git a/src/Pixely/Gpu/RenderPassBuilder.cs b/src/Pixely/Gpu/RenderPassBuilder.cs index c678f362..257d1997 100644 --- a/src/Pixely/Gpu/RenderPassBuilder.cs +++ b/src/Pixely/Gpu/RenderPassBuilder.cs @@ -1,90 +1,68 @@ -namespace Pixely.Gpu; - -internal struct RenderPassBuilderState -{ - public RenderPassBuilderState() - { - ResetState(); - } +using System.Runtime.InteropServices; - public List ColorTargets { get; } = new(); - public List 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 textures); - IRenderPassBuilder SetSharedColorTargetSettings(ColorTargetSettings settings); - IRenderPassBuilder SetDepthBuffer(Texture depthBuffer, DepthBufferSettings settings); - - IRenderPass Build(); -} +namespace Pixely.Gpu; -public class RenderPassBuilder : IRenderPassBuilder +/// +/// 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 +/// or one of its overloads instead. +/// +public class RenderPassBuilder { - private RenderPassBuilderState _state = new(); private readonly CommandBuffer _commandBuffer; + private readonly List _colorTargets = new(); + private readonly List _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 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 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) { @@ -96,6 +74,11 @@ 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."); @@ -103,18 +86,29 @@ public IRenderPass Build() 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; + } +} diff --git a/tests/Pixely.Tests/RenderPassBuilderTests.cs b/tests/Pixely.Tests/RenderPassBuilderTests.cs new file mode 100644 index 00000000..4dd4e3b7 --- /dev/null +++ b/tests/Pixely.Tests/RenderPassBuilderTests.cs @@ -0,0 +1,59 @@ +using Pixely.Gpu; + +namespace Pixely.Tests; + +public class RenderPassBuilderTests +{ + // The builder validates before it touches the command buffer, so these cases need no GPU device. + private static RenderPassBuilder CreateBuilder() + { + return new RenderPassBuilder(null!); + } + + private sealed class FakeTexture : Texture + { + public FakeTexture() : base(default, new ShortSize(1, 1), TextureFormat.R8G8B8A8Unorm, 4) + { + } + + public override void Dispose() + { + } + } + + [Test] + public void Build_WithSharedAndPerTargetSettings_Throws() + { + RenderPassBuilder builder = CreateBuilder() + .AddColorTarget(new FakeTexture(), ColorTargetSettings.Clear) + .SetSharedColorTargetSettings(ColorTargetSettings.Clear); + + Assert.That(() => builder.Build(), Throws.InvalidOperationException); + } + + [Test] + public void Build_WithColorTargetAndNoSettings_Throws() + { + RenderPassBuilder builder = CreateBuilder().AddColorTarget(new FakeTexture()); + + Assert.That(() => builder.Build(), Throws.InvalidOperationException); + } + + [Test] + public void Build_WithoutColorTargetsOrDepthBuffer_Throws() + { + RenderPassBuilder builder = CreateBuilder(); + + Assert.That(() => builder.Build(), Throws.InvalidOperationException); + } + + [Test] + public void Build_WithFewerPerTargetSettingsThanColorTargets_Throws() + { + RenderPassBuilder builder = CreateBuilder() + .AddColorTarget(new FakeTexture(), ColorTargetSettings.Clear) + .AddColorTarget(new FakeTexture()); + + Assert.That(() => builder.Build(), Throws.InvalidOperationException); + } +} diff --git a/tutorials/Pixely.Tutorials.ClickThrough/ClickThroughRenderer.cs b/tutorials/Pixely.Tutorials.ClickThrough/ClickThroughRenderer.cs index 39ac741f..be33bc59 100644 --- a/tutorials/Pixely.Tutorials.ClickThrough/ClickThroughRenderer.cs +++ b/tutorials/Pixely.Tutorials.ClickThrough/ClickThroughRenderer.cs @@ -18,14 +18,11 @@ public ClickThroughRenderer(GraphicsPipeline graphicsPipeline, GpuVertexBuffer