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() {