diff --git a/src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs b/src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs
index f631b615..818def01 100644
--- a/src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs
+++ b/src/AcDream.App/Rendering/Gpu/GpuPipelineDescription.cs
@@ -126,6 +126,29 @@ internal sealed record GpuPipelineDescription
/// Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.
public bool ColorWrite { get; init; } = true;
+ ///
+ /// Format of the colour attachment this pipeline renders into.
+ ///
+ /// Vulkan's dynamic rendering bakes the attachment format into the pipeline:
+ /// VkPipelineRenderingCreateInfo has to name it at creation, and a
+ /// pipeline whose declared format differs from the attachment it is used
+ /// with is undefined. GL has no equivalent — a framebuffer carries its own
+ /// attachment formats and a program is bound to whatever is attached — so
+ /// the GL backend ignores this field entirely.
+ ///
+ /// The default is the offscreen render-target format, which the Vulkan
+ /// backend maps to the swapchain's B8G8R8A8_UNORM
+ /// (VulkanTextureFormatMapping.CanonicalColorAttachmentFormat) so
+ /// that backbuffer and offscreen pipelines really do agree. That mapping is
+ /// what made this field necessary and is why the default preserves it: slice
+ /// V6c had to hard-code one format for every pipeline because the contract
+ /// could not express the question, and recorded the gap rather than hiding
+ /// it. Naming the format here is the reviewed fix, in the same shape as
+ /// (V4c) and
+ /// (V4d).
+ ///
+ public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
+
/// Sample count of the passes this pipeline is used in. Must match the pass.
public int SampleCount { get; init; } = 1;
}
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs
index 18874f4e..57ffd275 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs
@@ -278,11 +278,13 @@ internal sealed unsafe partial class VulkanGpuDevice
ArgumentNullException.ThrowIfNull(description);
(ShaderModule vertex, ShaderModule fragment) = LoadShaderModules(description.Shaders.Name);
- // One colour format for every pipeline; see
+ // Slice V6d: the pipeline names the format it renders into, rather than
+ // every pipeline being hard-coded to one. Rgba8UnormRenderTarget — the
+ // default — still maps to the swapchain's format; see
// VulkanTextureFormatMapping.CanonicalColorAttachmentFormat for why the
- // offscreen targets adopt the swapchain`s format rather than the other
+ // offscreen targets adopt the swapchain's format rather than the other
// way round.
- Format colorFormat = VulkanTextureFormatMapping.CanonicalColorAttachmentFormat;
+ Format colorFormat = VulkanTextureFormatMapping.FormatOf(description.ColorFormat);
return new VulkanGpuPipeline(
_vk,
_device,
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs
index 095ef290..0b31dca9 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs
@@ -18,32 +18,28 @@ namespace AcDream.App.Rendering.Gpu.Vk;
internal static class VulkanTextureFormatMapping
{
///
- /// The one colour-attachment format every acdream pipeline renders into.
+ /// The colour-attachment format
+ /// resolves to, and therefore the format every acdream pipeline renders into
+ /// until one names a different .
///
/// Vulkan bakes attachment formats into a pipeline (dynamic rendering
/// declares them in VkPipelineRenderingCreateInfo), and a pipeline
- /// whose format disagrees with the pass it is used in is invalid usage. But
- /// GpuPipelineDescription — pinned at V0 — has no field for the
- /// attachment format: it names SampleCount and nothing else about the
- /// target. Without one, an offscreen pipeline built for
- /// Rgba8UnormRenderTarget and a backbuffer pipeline built for the
- /// B8G8R8A8_UNORM swapchain (plan §4.9) could not share a description,
- /// and the backend would have no way to tell them apart.
+ /// whose format disagrees with the pass it is used in is invalid usage.
+ /// Slice V6c had no field to ask the question with — the pinned
+ /// GpuPipelineDescription named SampleCount and nothing else
+ /// about the target — so it hard-coded this constant for every pipeline and
+ /// recorded the gap. Slice V6d closed it: pipelines now declare their colour
+ /// format and the backend maps it through .
///
///
- /// So offscreen colour attachments use the swapchain's format too, and
- /// the substitution is invisible above the API: an image is sampled through
- /// its format's component mapping, so texture() on a BGRA image
- /// returns (R,G,B,A) exactly as it does on an RGBA one. The only place the
- /// byte order is observable is a CPU readback, and the one readback that
- /// exists — — swizzles explicitly.
+ /// The substitution this constant performs remains, because it is what
+ /// lets an offscreen target and the backbuffer share one description. It is
+ /// invisible above the API: an image is sampled through its format's
+ /// component mapping, so texture() on a BGRA image returns (R,G,B,A)
+ /// exactly as it does on an RGBA one. The only place the byte order is
+ /// observable is a CPU readback, and the one readback that exists —
+ /// — swizzles explicitly.
///
- ///
- /// Recorded rather than hidden: this is a real expressiveness gap in
- /// the pinned contract, and the honest fix is a colour-format field on
- /// GpuPipelineDescription in a reviewed contract commit, exactly as
- /// GpuBlendMode.InverseAlpha and GpuVertexFormat.UByte4UInt
- /// were added when V4c and V4d met the same wall.
///
internal const Format CanonicalColorAttachmentFormat = Format.B8G8R8A8Unorm;
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs
index b4286ca5..d27ada39 100644
--- a/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs
@@ -100,6 +100,32 @@ public sealed class GpuContractTests
Assert.Contains(GpuVertexFormat.UByte4UInt, Enum.GetValues());
}
+ [Fact]
+ public void APipelineNamesTheColorFormatItRendersInto()
+ {
+ // Vulkan's dynamic rendering bakes the colour-attachment format into the
+ // pipeline, so a pipeline that cannot name it either forces one format on
+ // every pass or is undefined against the ones it does not match. Slice V6c
+ // hit that wall and hard-coded the swapchain format for every pipeline,
+ // recording the gap in VulkanTextureFormatMapping rather than hiding it.
+ var description = new GpuPipelineDescription
+ {
+ Name = "contract-default",
+ Shaders = new GpuShaderSet("ui_text"),
+ VertexLayout = GpuVertexLayout.None,
+ };
+
+ // The default has to be the render-target format, because that is what
+ // the Vulkan backend already maps to the B8G8R8A8_UNORM swapchain — so
+ // every pipeline written before this field existed keeps its behaviour.
+ Assert.Equal(GpuTextureFormat.Rgba8UnormRenderTarget, description.ColorFormat);
+
+ // And it has to be settable, or naming it would be decoration.
+ GpuPipelineDescription single = description with { ColorFormat = GpuTextureFormat.R8Unorm };
+ Assert.Equal(GpuTextureFormat.R8Unorm, single.ColorFormat);
+ Assert.Equal(GpuTextureFormat.Rgba8UnormRenderTarget, description.ColorFormat);
+ }
+
[Fact]
public void UniformBindingsDoNotCollide()
{