fix(render): Campaign V slice V6i-2 commit 1 — the terrain clip block reaches set 1
Plan §5.5.12 finding 2, measured on the committed SPIR-V rather than inferred:
terrain_modern.vert declared
layout(std140, binding = 2) uniform TerrainClip { ... }
with no ACDREAM_UBO_SET, so under the Vulkan dialect the block landed in set 0
binding 2 — which set 0's layout declares as a STORAGE buffer. Any terrain
pipeline built against the shared pipeline layout was therefore malformed.
Nothing had caught it: GL expands the macro to nothing and keeps its UBO and
SSBO namespaces separate, the shader compiled cleanly for both backends, and no
terrain pipeline has ever been created on Vulkan. sky.vert declares the SAME
block correctly and is the precedent, so this is a one-word omission, not a
numbering question.
spirv-dis on spv/terrain_modern.vert.spv, before and after:
before %372 = OpVariable %_ptr_Uniform__struct_370 Uniform
OpDecorate %372 DescriptorSet 0 / Binding 2
after OpDecorate %372 DescriptorSet 1 / Binding 2
with %_struct_370 = OpTypeStruct %int %_arr_v4float_uint_8 — TerrainClip's
{ int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; } — in both.
The same commit closes §5.5.8's second recorded gap. Set 1's layout declared
only bindings 1 and 3, so it was missing BOTH the terrain clip block and
UniformSkyParams at binding 4, which sky.vert and sky.frag have compiled to
SPIR-V since V6e. Both are now declared, all four dynamic, which is half
Vulkan's guaranteed maxDescriptorSetUniformBuffersDynamic of 8 and is asserted
by the capability gate as before.
Membership and ORDER now come from one predicate — IsDeclaredUniformBinding —
that the layout, the descriptor writes and vkCmdBindDescriptorSets's
dynamic-offset array are all built from, the same shape V6g gave set 0. The
three had been restated separately, which is exactly how a fifth binding would
have gone wrong the same way.
Both gaps were found by hand, months apart, and neither could fail on the
shipping backend. VulkanShaderDescriptorContractTests reads the committed .spv
and asserts the partition instead: every uniform block at a declared set-1
binding, every storage block inside set 0's declared range, every sampled
resource in the one texture table. Checked out against the pre-fix .spv, two of
its four tests fail.
Gates: Release build; App tests 4,090 / 3 skips (4,086 baseline plus four);
strict GL offline pixel gate vs 0ca802cd 3.02e-05 (17 px of 563,200, inside the
documented 9–31 px control band, 33x under threshold) — expected, since GL
executes not one changed statement; one Vulkan composition-host run with
VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors, zero
warnings and no [shutdown] diagnostic on either stream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0ca802cd7f
commit
f7344758f8
6 changed files with 335 additions and 36 deletions
|
|
@ -75,15 +75,22 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
return [.. bindings];
|
||||
}
|
||||
|
||||
/// <summary>Bindings 0..3 of set 1; only 1 (SceneLighting) and 3 (terrain tiling) are used.</summary>
|
||||
internal const int UniformBindingCount = 4;
|
||||
/// <summary>
|
||||
/// Bindings 0..4 of set 1. Slice V6i-2 raised this from 4 when the layout
|
||||
/// gained binding 4 (sky params); binding 0 remains unused and is counted
|
||||
/// only so the bookkeeping arrays stay index-aligned with the binding number.
|
||||
/// Which of them the layout DECLARES is
|
||||
/// <see cref="VulkanPipelineLayouts.IsDeclaredUniformBinding"/>.
|
||||
/// </summary>
|
||||
internal const int UniformBindingCount = 5;
|
||||
|
||||
/// <summary>
|
||||
/// How many of set 1's bindings the layout actually declares, all dynamic.
|
||||
/// Asserted against <c>maxDescriptorSetUniformBuffersDynamic</c> by the
|
||||
/// capability gate; Vulkan guarantees 8, so this is comfortable.
|
||||
/// </summary>
|
||||
internal const uint DynamicUniformBindingCount = 2;
|
||||
internal static uint DynamicUniformBindingCount { get; } =
|
||||
(uint)VulkanPipelineLayouts.DeclaredUniformBindings.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Widest range any single binding may address. Dynamic descriptors take a
|
||||
|
|
@ -179,14 +186,15 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
sets[1] = _sets[slot].Uniform;
|
||||
sets[2] = device.TextureTable.Set;
|
||||
|
||||
int dynamicCount = DynamicStorageBindings.Length + 2;
|
||||
uint[] declaredUniforms = VulkanPipelineLayouts.DeclaredUniformBindings;
|
||||
int dynamicCount = DynamicStorageBindings.Length + declaredUniforms.Length;
|
||||
uint* offsets = stackalloc uint[dynamicCount];
|
||||
// Dynamic offsets are ordered by set, then by binding number, and only
|
||||
// the DYNAMIC descriptors have a slot at all.
|
||||
for (int i = 0; i < DynamicStorageBindings.Length; i++)
|
||||
offsets[i] = _arena.StorageOffset(DynamicStorageBindings[i]);
|
||||
offsets[DynamicStorageBindings.Length + 0] = _arena.UniformOffset(GpuBindingModel.UniformSceneLighting);
|
||||
offsets[DynamicStorageBindings.Length + 1] = _arena.UniformOffset(GpuBindingModel.UniformTerrainTiling);
|
||||
for (int i = 0; i < declaredUniforms.Length; i++)
|
||||
offsets[DynamicStorageBindings.Length + i] = _arena.UniformOffset(declaredUniforms[i]);
|
||||
|
||||
_vk.CmdBindDescriptorSets(
|
||||
commands,
|
||||
|
|
@ -212,18 +220,16 @@ internal sealed unsafe class VulkanFrameBindings : IDisposable
|
|||
VulkanPipelineLayouts.IsDynamicStorageBinding(binding));
|
||||
}
|
||||
|
||||
// Only the two bindings the layout declares exist; the rest of the array
|
||||
// is bookkeeping so the offsets stay index-aligned.
|
||||
WriteUniform(
|
||||
pair.Uniform,
|
||||
GpuBindingModel.UniformSceneLighting,
|
||||
new Silk.NET.Vulkan.Buffer(_arena.UniformBuffer(GpuBindingModel.UniformSceneLighting)),
|
||||
_arena.UniformRange(GpuBindingModel.UniformSceneLighting));
|
||||
WriteUniform(
|
||||
pair.Uniform,
|
||||
GpuBindingModel.UniformTerrainTiling,
|
||||
new Silk.NET.Vulkan.Buffer(_arena.UniformBuffer(GpuBindingModel.UniformTerrainTiling)),
|
||||
_arena.UniformRange(GpuBindingModel.UniformTerrainTiling));
|
||||
// Only the bindings the layout declares exist; the rest of the array is
|
||||
// bookkeeping so the offsets stay index-aligned with the binding number.
|
||||
foreach (uint binding in VulkanPipelineLayouts.DeclaredUniformBindings)
|
||||
{
|
||||
WriteUniform(
|
||||
pair.Uniform,
|
||||
binding,
|
||||
new Silk.NET.Vulkan.Buffer(_arena.UniformBuffer(binding)),
|
||||
_arena.UniformRange(binding));
|
||||
}
|
||||
}
|
||||
|
||||
private (DescriptorSet Storage, DescriptorSet Uniform) AllocatePair()
|
||||
|
|
|
|||
|
|
@ -184,29 +184,93 @@ internal static unsafe class VulkanPipelineLayouts
|
|||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>Set 1 — the SceneLighting and terrain-tiling uniform blocks.</summary>
|
||||
/// <summary>
|
||||
/// The terrain screen-space clip block's uniform binding.
|
||||
///
|
||||
/// <para><see cref="GpuBindingModel"/> does not name this number — it only
|
||||
/// records, twice, that "binding 2 is taken by the terrain clip block" while
|
||||
/// explaining why terrain tiling is 3 and sky params are 4. The number itself
|
||||
/// has been pinned by <c>terrain_modern.vert</c> and <c>sky.vert</c> since
|
||||
/// Phase U.3. Restating it here rather than promoting it into the frozen
|
||||
/// binding model keeps slice V6i-2 out of the pinned contract; a later slice
|
||||
/// entitled to change §3.3 should move it.</para>
|
||||
/// </summary>
|
||||
internal const uint UniformTerrainClip = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Which of set 1's bindings the layout declares — the uniform-side twin of
|
||||
/// <see cref="IsDynamicStorageBinding"/>, and for the same reason: the
|
||||
/// layout, the descriptor writes and <c>vkCmdBindDescriptorSets</c>'s
|
||||
/// dynamic-offset array must agree on both membership and ORDER, and getting
|
||||
/// either wrong is a validation error. Deriving all three from one predicate
|
||||
/// is what stops them drifting.
|
||||
///
|
||||
/// <para>Binding 0 is unused — <see cref="GpuBindingModel.UniformSceneLighting"/>
|
||||
/// is 1 so that GL's separate UBO/SSBO namespaces keep their existing numbers.
|
||||
/// It is still counted by <see cref="VulkanFrameBindings.UniformBindingCount"/>
|
||||
/// so the bookkeeping arrays stay index-aligned with the binding number.</para>
|
||||
///
|
||||
/// <para>Slice V6i-2 added 2 (terrain clip) and 4 (sky params). Both blocks
|
||||
/// have compiled to SPIR-V declaring set 1 for some time — plan §5.5.8
|
||||
/// recorded the sky gap and §5.5.12 measured the terrain one — but neither
|
||||
/// binding existed in the layout, so a pipeline built for either shader was
|
||||
/// malformed. Nothing caught it because no terrain or sky pipeline has ever
|
||||
/// been created on Vulkan.</para>
|
||||
/// </summary>
|
||||
internal static bool IsDeclaredUniformBinding(uint binding) => binding switch
|
||||
{
|
||||
GpuBindingModel.UniformSceneLighting => true,
|
||||
UniformTerrainClip => true,
|
||||
GpuBindingModel.UniformTerrainTiling => true,
|
||||
GpuBindingModel.UniformSkyParams => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Set 1's declared bindings in ascending order — the order
|
||||
/// <c>vkCmdBindDescriptorSets</c> requires its dynamic offsets in.
|
||||
/// </summary>
|
||||
internal static uint[] DeclaredUniformBindings { get; } = BuildDeclaredUniformBindings();
|
||||
|
||||
private static uint[] BuildDeclaredUniformBindings()
|
||||
{
|
||||
var bindings = new List<uint>(VulkanFrameBindings.UniformBindingCount);
|
||||
for (uint binding = 0; binding < VulkanFrameBindings.UniformBindingCount; binding++)
|
||||
{
|
||||
if (IsDeclaredUniformBinding(binding))
|
||||
bindings.Add(binding);
|
||||
}
|
||||
|
||||
return [.. bindings];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set 1 — the SceneLighting, terrain-clip, terrain-tiling and sky-params
|
||||
/// uniform blocks. All four are dynamic: each is fed from the per-frame ring,
|
||||
/// so its offset moves every frame and a dynamic descriptor is exactly what
|
||||
/// spares the write. Four is half Vulkan's guaranteed
|
||||
/// <c>maxDescriptorSetUniformBuffersDynamic</c> of 8, and the capability gate
|
||||
/// asserts it.
|
||||
/// </summary>
|
||||
internal static DescriptorSetLayout CreateUniformSetLayout(Silk.NET.Vulkan.Vk vk, Device device)
|
||||
{
|
||||
DescriptorSetLayoutBinding* bindings = stackalloc DescriptorSetLayoutBinding[2];
|
||||
bindings[0] = new DescriptorSetLayoutBinding
|
||||
uint[] declared = DeclaredUniformBindings;
|
||||
DescriptorSetLayoutBinding* bindings = stackalloc DescriptorSetLayoutBinding[declared.Length];
|
||||
for (int i = 0; i < declared.Length; i++)
|
||||
{
|
||||
Binding = GpuBindingModel.UniformSceneLighting,
|
||||
DescriptorType = DescriptorType.UniformBufferDynamic,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
};
|
||||
bindings[1] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = GpuBindingModel.UniformTerrainTiling,
|
||||
DescriptorType = DescriptorType.UniformBufferDynamic,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
};
|
||||
bindings[i] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = declared[i],
|
||||
DescriptorType = DescriptorType.UniformBufferDynamic,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit,
|
||||
};
|
||||
}
|
||||
|
||||
var create = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 2,
|
||||
BindingCount = (uint)declared.Length,
|
||||
PBindings = bindings,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "336880b293e95c9ba13dce7a616a15e941191afec8bdaabe04a4993182eb6811",
|
||||
"sourceSha256": "a3f8592d482793622f7a69c08f8ba828a4e071f2fb5e1763f865be84797bceb7",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -47,7 +47,13 @@ layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
|||
// UBO, leaving binding=2 free. uTerrainClipCount == 0 (the U.3 default) ungates
|
||||
// terrain entirely (the second loop sets all 8 distances to +1.0). Uploaded by
|
||||
// ClipFrame.UploadShared each frame; TerrainModernRenderer binds it before draw.
|
||||
layout(std140, binding = 2) uniform TerrainClip {
|
||||
//
|
||||
// Campaign V slice V6i-2: ACDREAM_UBO_SET is what puts this in set 1 under the
|
||||
// Vulkan dialect and expands to nothing under GL. Omitting it left the block at
|
||||
// set 0 binding 2, which the storage layout declares as a STORAGE buffer — see
|
||||
// plan §5.5.12 finding 2, measured on the committed SPIR-V rather than inferred.
|
||||
// sky.vert declares the SAME block correctly and is the precedent.
|
||||
layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
|
||||
int uTerrainClipCount;
|
||||
vec4 uTerrainClipPlanes[8];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: every compiled SPIR-V module's descriptor decorations
|
||||
/// must match the descriptor set layouts the backend creates.
|
||||
///
|
||||
/// <para><b>Why this test exists.</b> Slice V6i measured that
|
||||
/// <c>terrain_modern.vert</c>'s <c>TerrainClip</c> block was declared
|
||||
/// <c>layout(std140, binding = 2)</c> with no <c>ACDREAM_UBO_SET</c>, so under
|
||||
/// the Vulkan dialect it landed in set 0 binding 2 — which set 0's layout
|
||||
/// declares as a STORAGE buffer. A terrain pipeline built against the shared
|
||||
/// layout was therefore malformed. Nothing caught it: GL expands the macro to
|
||||
/// nothing and keeps its UBO and SSBO namespaces separate, the shader compiled
|
||||
/// cleanly for both backends, and no terrain pipeline had ever been created on
|
||||
/// Vulkan. Plan §5.5.8 recorded a second instance of the same class — sky's
|
||||
/// <c>UniformSkyParams</c> at binding 4 was absent from the layout — found the
|
||||
/// same way, by hand, months apart.</para>
|
||||
///
|
||||
/// <para>A one-word omission in a shader that cannot fail to compile, cannot
|
||||
/// fail on the shipping backend, and is only wrong on a backend nothing has run
|
||||
/// yet, is exactly the shape of defect that needs a test rather than an audit.
|
||||
/// Reading the committed <c>.spv</c> is what makes the assertion about what the
|
||||
/// GPU will actually be handed, not about what the GLSL looks like.</para>
|
||||
/// </summary>
|
||||
public sealed class VulkanShaderDescriptorContractTests
|
||||
{
|
||||
private static string RepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
directory = directory.Parent;
|
||||
return directory?.FullName
|
||||
?? throw new InvalidOperationException("Could not locate the repository root from the test binary.");
|
||||
}
|
||||
|
||||
private static string SpirvDirectory() =>
|
||||
Path.Combine(RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", "spv");
|
||||
|
||||
/// <summary>One interface variable's set, binding and storage class.</summary>
|
||||
private readonly record struct ShaderResource(
|
||||
string Module,
|
||||
uint Id,
|
||||
SpirvStorageClass StorageClass,
|
||||
uint? Set,
|
||||
uint? Binding);
|
||||
|
||||
private enum SpirvStorageClass : uint
|
||||
{
|
||||
UniformConstant = 0,
|
||||
Uniform = 2,
|
||||
StorageBuffer = 12,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The smallest SPIR-V reader that can answer this question: walk the
|
||||
/// instruction stream collecting <c>OpDecorate</c> DescriptorSet/Binding and
|
||||
/// the storage class of every <c>OpVariable</c>. No type or name recovery is
|
||||
/// needed, so no dependency is either.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<ShaderResource> ReadResources(string path)
|
||||
{
|
||||
byte[] bytes = File.ReadAllBytes(path);
|
||||
Assert.True(bytes.Length >= 20 && bytes.Length % 4 == 0, $"{path} is not a SPIR-V module.");
|
||||
var words = new uint[bytes.Length / 4];
|
||||
Buffer.BlockCopy(bytes, 0, words, 0, bytes.Length);
|
||||
Assert.Equal(0x07230203u, words[0]);
|
||||
|
||||
const uint OpDecorate = 71;
|
||||
const uint OpVariable = 59;
|
||||
const uint DecorationBinding = 33;
|
||||
const uint DecorationDescriptorSet = 34;
|
||||
|
||||
var sets = new Dictionary<uint, uint>();
|
||||
var bindings = new Dictionary<uint, uint>();
|
||||
var variables = new List<(uint Id, SpirvStorageClass StorageClass)>();
|
||||
|
||||
int index = 5;
|
||||
while (index < words.Length)
|
||||
{
|
||||
uint header = words[index];
|
||||
int wordCount = (int)(header >> 16);
|
||||
uint opcode = header & 0xFFFF;
|
||||
Assert.True(wordCount > 0, $"{path} has a zero-length instruction at word {index}.");
|
||||
|
||||
if (opcode == OpDecorate && wordCount >= 4)
|
||||
{
|
||||
uint target = words[index + 1];
|
||||
uint decoration = words[index + 2];
|
||||
if (decoration == DecorationDescriptorSet)
|
||||
sets[target] = words[index + 3];
|
||||
else if (decoration == DecorationBinding)
|
||||
bindings[target] = words[index + 3];
|
||||
}
|
||||
else if (opcode == OpVariable && wordCount >= 4)
|
||||
{
|
||||
variables.Add((words[index + 2], (SpirvStorageClass)words[index + 3]));
|
||||
}
|
||||
|
||||
index += wordCount;
|
||||
}
|
||||
|
||||
string module = Path.GetFileName(path);
|
||||
return
|
||||
[
|
||||
.. variables.Select(v => new ShaderResource(
|
||||
module,
|
||||
v.Id,
|
||||
v.StorageClass,
|
||||
sets.TryGetValue(v.Id, out uint s) ? s : null,
|
||||
bindings.TryGetValue(v.Id, out uint b) ? b : null)),
|
||||
];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ShaderResource> AllResources() =>
|
||||
[
|
||||
.. Directory
|
||||
.EnumerateFiles(SpirvDirectory(), "*.spv")
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.SelectMany(ReadResources),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The regression itself, named. <c>TerrainClip</c> is the only uniform block
|
||||
/// <c>terrain_modern.vert</c> declares besides <c>SceneLighting</c>, so
|
||||
/// asserting the module's uniform bindings as a set pins it exactly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TerrainVertexShaderDeclaresItsClipBlockInTheUniformSet()
|
||||
{
|
||||
uint[] uniformBindings =
|
||||
[
|
||||
.. ReadResources(Path.Combine(SpirvDirectory(), "terrain_modern.vert.spv"))
|
||||
.Where(r => r.StorageClass == SpirvStorageClass.Uniform)
|
||||
.Select(r =>
|
||||
{
|
||||
Assert.Equal(GpuBindingModel.UniformSet, r.Set);
|
||||
return r.Binding!.Value;
|
||||
})
|
||||
.Order(),
|
||||
];
|
||||
|
||||
Assert.Equal(
|
||||
[GpuBindingModel.UniformSceneLighting, VulkanPipelineLayouts.UniformTerrainClip],
|
||||
uniformBindings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The general rule the specific case is an instance of: a uniform block may
|
||||
/// only live at a binding <see cref="VulkanPipelineLayouts.CreateUniformSetLayout"/>
|
||||
/// actually declares, in the set it declares them in.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EveryUniformBlockLandsAtADeclaredUniformBinding()
|
||||
{
|
||||
string[] violations =
|
||||
[
|
||||
.. AllResources()
|
||||
.Where(r => r.StorageClass == SpirvStorageClass.Uniform)
|
||||
.Where(r =>
|
||||
r.Set != GpuBindingModel.UniformSet
|
||||
|| r.Binding is null
|
||||
|| !VulkanPipelineLayouts.IsDeclaredUniformBinding(r.Binding.Value))
|
||||
.Select(r =>
|
||||
$"{r.Module}: uniform block %{r.Id} is at set {r.Set?.ToString() ?? "(none)"} "
|
||||
+ $"binding {r.Binding?.ToString() ?? "(none)"}; set 1 declares "
|
||||
+ $"[{string.Join(", ", VulkanPipelineLayouts.DeclaredUniformBindings)}]."),
|
||||
];
|
||||
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The other half of the same contract: set 0 is storage buffers only, and
|
||||
/// within the range <see cref="GpuBindingModel.StorageBindingCount"/> pins. A
|
||||
/// uniform block straying into set 0 fails this as well as the test above,
|
||||
/// which is the point — the two layouts must partition the bindings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EveryStorageBlockLandsInTheStorageSetWithinItsDeclaredRange()
|
||||
{
|
||||
string[] violations =
|
||||
[
|
||||
.. AllResources()
|
||||
.Where(r => r.StorageClass == SpirvStorageClass.StorageBuffer)
|
||||
.Where(r => r.Set != 0 || r.Binding is null || r.Binding >= GpuBindingModel.StorageBindingCount)
|
||||
.Select(r =>
|
||||
$"{r.Module}: storage block %{r.Id} is at set {r.Set?.ToString() ?? "(none)"} "
|
||||
+ $"binding {r.Binding?.ToString() ?? "(none)"}; set 0 declares bindings "
|
||||
+ $"0..{GpuBindingModel.StorageBindingCount - 1}."),
|
||||
];
|
||||
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sampled-texture table is one variable-count descriptor array at set 2
|
||||
/// binding 0. Any other combined-image-sampler would need a layout that does
|
||||
/// not exist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EverySampledTextureIsTheSharedTable()
|
||||
{
|
||||
string[] violations =
|
||||
[
|
||||
.. AllResources()
|
||||
.Where(r => r.StorageClass == SpirvStorageClass.UniformConstant && r.Set is not null)
|
||||
.Where(r => r.Set != GpuBindingModel.TextureTableSet || r.Binding != GpuBindingModel.TextureTableBinding)
|
||||
.Select(r =>
|
||||
$"{r.Module}: sampled resource %{r.Id} is at set {r.Set} binding "
|
||||
+ $"{r.Binding?.ToString() ?? "(none)"}; the only declared table is set "
|
||||
+ $"{GpuBindingModel.TextureTableSet} binding {GpuBindingModel.TextureTableBinding}."),
|
||||
];
|
||||
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue