feat(render): let a pipeline name the colour format it renders into
Campaign V slice V6d, commit 1 of 3. The third contract amendment of the campaign, in the same shape as GpuBlendMode.InverseAlpha (V4c) and GpuVertexFormat.UByte4UInt (V4d): a slice met a wall the pinned contract could not express, and the fix is a reviewed field rather than a backend working around it.
Vulkan's dynamic rendering bakes the colour-attachment format into the pipeline. VkPipelineRenderingCreateInfo has to name it at creation, and a pipeline whose declared format disagrees with the attachment it is used with is undefined. GpuPipelineDescription named SampleCount and nothing else about the target, so slice V6c had no way to ask the question and hard-coded VulkanTextureFormatMapping.CanonicalColorAttachmentFormat for every pipeline it built. It recorded that as a real expressiveness gap rather than hiding it, and named this commit as the honest fix.
GpuPipelineDescription.ColorFormat defaults to Rgba8UnormRenderTarget, which the Vulkan backend already maps to the swapchain's B8G8R8A8_UNORM, so every pipeline written before the field existed keeps exactly the format it was getting. GL ignores the field entirely: a GL framebuffer carries its own attachment formats and a program binds to whatever is attached, so there is nothing for the GL backend to declare. The substitution that makes an offscreen Rgba8UnormRenderTarget resolve to the swapchain's byte order stays — it is what lets a backbuffer pipeline and an offscreen pipeline share one description, and it is invisible above the API because an image is sampled through its format's component mapping.
The contract test asserts both halves that matter: the default is the render-target format (so nothing moves), and the field is really settable (so naming it is not decoration).
App tests 4,057 passed / 3 skipped, up one from the 4,056 baseline. Offline pixel gate against 234fe91d: differing fraction 2.84e-05, 16 pixels of 563,200 compared, inside the documented 15-23 pixel same-commit noise band and about 35x under the 0.001 threshold.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
234fe91d3b
commit
871c406b99
4 changed files with 70 additions and 23 deletions
|
|
@ -126,6 +126,29 @@ internal sealed record GpuPipelineDescription
|
|||
/// <summary>Whether the pipeline writes colour at all. False for depth/stencil-only prepasses.</summary>
|
||||
public bool ColorWrite { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Format of the colour attachment this pipeline renders into.
|
||||
///
|
||||
/// Vulkan's dynamic rendering bakes the attachment format into the pipeline:
|
||||
/// <c>VkPipelineRenderingCreateInfo</c> 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 <c>B8G8R8A8_UNORM</c>
|
||||
/// (<c>VulkanTextureFormatMapping.CanonicalColorAttachmentFormat</c>) 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
|
||||
/// <see cref="GpuBlendMode.InverseAlpha"/> (V4c) and
|
||||
/// <see cref="GpuVertexFormat.UByte4UInt"/> (V4d).
|
||||
/// </summary>
|
||||
public GpuTextureFormat ColorFormat { get; init; } = GpuTextureFormat.Rgba8UnormRenderTarget;
|
||||
|
||||
/// <summary>Sample count of the passes this pipeline is used in. Must match the pass.</summary>
|
||||
public int SampleCount { get; init; } = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -18,32 +18,28 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
internal static class VulkanTextureFormatMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// The one colour-attachment format every acdream pipeline renders into.
|
||||
/// The colour-attachment format <see cref="GpuTextureFormat.Rgba8UnormRenderTarget"/>
|
||||
/// resolves to, and therefore the format every acdream pipeline renders into
|
||||
/// until one names a different <see cref="GpuPipelineDescription.ColorFormat"/>.
|
||||
///
|
||||
/// <para>Vulkan bakes attachment formats into a pipeline (dynamic rendering
|
||||
/// declares them in <c>VkPipelineRenderingCreateInfo</c>), and a pipeline
|
||||
/// whose format disagrees with the pass it is used in is invalid usage. But
|
||||
/// <c>GpuPipelineDescription</c> — pinned at V0 — has no field for the
|
||||
/// attachment format: it names <c>SampleCount</c> and nothing else about the
|
||||
/// target. Without one, an offscreen pipeline built for
|
||||
/// <c>Rgba8UnormRenderTarget</c> and a backbuffer pipeline built for the
|
||||
/// <c>B8G8R8A8_UNORM</c> 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
|
||||
/// <c>GpuPipelineDescription</c> named <c>SampleCount</c> 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 <see cref="FormatOf"/>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>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 <c>texture()</c> 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 — <see cref="IGpuDevice.CaptureBackbuffer"/> — swizzles explicitly.
|
||||
/// <para>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 <c>texture()</c> 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 —
|
||||
/// <see cref="IGpuDevice.CaptureBackbuffer"/> — swizzles explicitly.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>Recorded rather than hidden: this is a real expressiveness gap in
|
||||
/// the pinned contract, and the honest fix is a colour-format field on
|
||||
/// <c>GpuPipelineDescription</c> in a reviewed contract commit, exactly as
|
||||
/// <c>GpuBlendMode.InverseAlpha</c> and <c>GpuVertexFormat.UByte4UInt</c>
|
||||
/// were added when V4c and V4d met the same wall.</para>
|
||||
/// </summary>
|
||||
internal const Format CanonicalColorAttachmentFormat = Format.B8G8R8A8Unorm;
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,32 @@ public sealed class GpuContractTests
|
|||
Assert.Contains(GpuVertexFormat.UByte4UInt, Enum.GetValues<GpuVertexFormat>());
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue