Contract amendment 2 of three, and V4g's remaining half behind it. Plan section
5.5.16 defect 2: PortalDepthMaskRenderer's two-pass punch (#117) is built on
glStencilFunc/glStencilOp/glStencilMask, GpuPipelineDescription carried no
stencil state at all, and nothing else can express it - so the renderer stayed
raw GL, invisible to the Vulkan arm, and V4g's "stencil/depth-mask pipelines"
row could not be written.
The amendment splits the way core Vulkan 1.3 splits. The ENABLE and the
attachment intent are baked: GpuPipelineDescription.StencilTest, false by
default so no pipeline in the tree changed. The per-draw compare, three outcome
ops, reference and both masks are a GpuStencilState that the pipeline carries as
a DEFAULT and IGpuPassEncoder.SetStencil overrides - exactly the split cull
mode, front face and depth write already have, and exactly what
VK_DYNAMIC_STATE_STENCIL_OP/_COMPARE_MASK/_WRITE_MASK/_REFERENCE make dynamic.
The four stencil dynamic states are declared ONLY by a pipeline that tests
stencil: declaring a dynamic state obliges every draw with the pipeline to have
set it, so adding them unconditionally would make every existing pipeline depend
on a call none of them make. GpuStencilOp carries three values because the punch
uses three - Replace marks, Equal gates, Zero self-cleans - and a fourth would
be a facility with no consumer.
The arm. Three pipelines, not one, because depth COMPARE is not dynamic in the
contract and the punch's two passes differ in it: mark tests LEQUAL and writes
no depth, punch tests ALWAYS and writes, seal is ALWAYS + write with no stencil.
All three write no colour, which is what retail's "COLOR-INVISIBLE triangle fan"
means. The fan is expanded to a triangle LIST on the CPU - the contract has no
fan topology and Vulkan's is not portable - which is exact: triangle i is
(v0, v[i+1], v[i+2]), the same triangles in the same order.
portal_depth.{vert,frag} is a new committed shader pair, and this is the ONE
renderer in the campaign whose two arms do not share a source. Its clip planes
have to travel in the TerrainClip uniform block at binding 2, which is already
precisely this shape and already read by terrain_modern.vert and sky.vert - but
on GL that binding is held globally by ClipFrame for terrain, so a portal draw
that rebound it would leave every later terrain draw in the frame reading the
wrong region. The GL arm therefore keeps its inline program.
PortalDepthShaderParityTests is the tripwire: retail's far-Z constant
(0.99999988, from DrawPortalPolyInternal 0x0059bc90), #129's capped mark-bias
expression and the eight-half-plane loop are asserted to appear in both. Both
are deleted at V11. 9/10 shader pairs now compile to SPIR-V.
Two GL-side gaps closed while the state was being extended, both of section 7.1
rule 1's class rather than new work. GlAmbientCapabilityState now saves and
restores the stencil test, function, ops and both masks - the portal punch draws
mid-frame among renderers that are still raw GL and assume the test is off - and
the COLOUR MASK, which had no consumer until a colour-invisible pipeline existed
and whose absence would have blacked out every raw-GL renderer after such a
pass.
PortalTunnelPresentation was re-read and confirmed as V6k left it: it clears
depth and draws into the active viewport, binds no framebuffer of its own, and
needs no port for section 5.4's sake. It remains unported on the Vulkan arm -
the composition uses NullLocalPlayerTeleportPresentation there - which is an
absence on the V7 list, not a defect.
Gates. Release build green. App tests 4,129/3 skips; complete Release suite
9,192/5 (one solution-wide run reported a single App failure that did not
reproduce in two subsequent runs, solution-wide or alone - the documented
rerun-singly flake class). Strict GL offline pixel gate against 08ffe141:
2.31e-05, 13 differing pixels of 563,200, inside the documented 9-31 band. GL
connected -Runs 3: 3/3 RENDERED on the desktop witness and 3/3 on the client
capture. One offline Vulkan run with VK_LAYER_KHRONOS_validation proven inserted
by the loader: zero validation errors, zero warnings, a captured world frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
373 lines
18 KiB
C#
373 lines
18 KiB
C#
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using AcDream.App.Rendering.Gpu;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Gpu;
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V0 — the pinned RHI contract.
|
|
///
|
|
/// These are not behaviour tests; they are the tripwires that stop the contract
|
|
/// drifting out from under the shaders and the two backends. Every constant
|
|
/// asserted here also appears in a GLSL source file or in a backend's binding
|
|
/// setup, so a change that lands in only one place fails here rather than as a
|
|
/// corrupted frame.
|
|
/// </summary>
|
|
public sealed class GpuContractTests
|
|
{
|
|
[Fact]
|
|
public void PushConstantBlockMatchesThePinnedLayout()
|
|
{
|
|
Assert.Equal(GpuBindingModel.PushConstantBytes, Unsafe.SizeOf<GpuPushConstants>());
|
|
Assert.True(GpuBindingModel.PushConstantBytes <= GpuBindingModel.MaxPushConstantBytes);
|
|
|
|
Assert.Equal(0, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.ViewProjection)));
|
|
Assert.Equal(64, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.DrawIdOffset)));
|
|
Assert.Equal(68, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.LightingMode)));
|
|
Assert.Equal(72, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.RenderPass)));
|
|
Assert.Equal(76, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.LightDebug)));
|
|
Assert.Equal(80, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.TextureIndexA)));
|
|
Assert.Equal(84, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.TextureIndexB)));
|
|
Assert.Equal(88, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.ParamA)));
|
|
Assert.Equal(92, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.ParamB)));
|
|
}
|
|
|
|
[Fact]
|
|
public void StorageBindingsMatchTheShaderSources()
|
|
{
|
|
// mesh_modern.vert declares std430 bindings 0..8 in exactly this order;
|
|
// binding 9 is the GL-only texture handle table added by slice V2.
|
|
Assert.Equal(0u, GpuBindingModel.StorageInstances);
|
|
Assert.Equal(1u, GpuBindingModel.StorageBatches);
|
|
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
|
|
Assert.Equal(3u, GpuBindingModel.StorageClipSlots);
|
|
Assert.Equal(4u, GpuBindingModel.StorageGlobalLights);
|
|
Assert.Equal(5u, GpuBindingModel.StorageInstanceLightSets);
|
|
Assert.Equal(6u, GpuBindingModel.StorageInstanceIndoor);
|
|
Assert.Equal(7u, GpuBindingModel.StorageInstanceAlpha);
|
|
Assert.Equal(8u, GpuBindingModel.StorageInstanceSelectionLighting);
|
|
Assert.Equal(9u, GpuBindingModel.StorageTextureTable);
|
|
Assert.Equal(10u, GpuBindingModel.StorageBindingCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void UniformAndTextureTableLiveInSeparateSets()
|
|
{
|
|
// The SceneLighting UBO keeps binding=1 even though the BatchBuffer SSBO
|
|
// also uses binding=1. GL tolerates that because its SSBO and UBO binding
|
|
// tables are separate; Vulkan does not, so the set index disambiguates.
|
|
Assert.Equal(GpuBindingModel.StorageBatches, GpuBindingModel.UniformSceneLighting);
|
|
Assert.NotEqual(0u, GpuBindingModel.UniformSet);
|
|
Assert.NotEqual(GpuBindingModel.UniformSet, GpuBindingModel.TextureTableSet);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClipRegionStrideMatchesTheUploadedLayout()
|
|
{
|
|
// ClipFrame lays these bytes out on the CPU; ClipFrameLayoutTests pins the
|
|
// producer side, this pins the contract side. 16 B header + 8 x vec4.
|
|
Assert.Equal(8, GpuBindingModel.ClipPlanesPerSlot);
|
|
Assert.Equal(144, GpuBindingModel.ClipRegionStrideBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void BlendModesCoverEveryRetailTranslucencyKind()
|
|
{
|
|
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
|
|
// surface's TranslucencyKind. Retail has three, and the V0 contract shipped
|
|
// with only two — slice V4c found the gap. Mapping InvAlpha onto
|
|
// StraightAlpha would silently change how every inverse-alpha surface
|
|
// composites, so the contract has to carry all three.
|
|
Assert.Equal(4, Enum.GetValues<GpuBlendMode>().Length);
|
|
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
|
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
|
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
|
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
|
}
|
|
|
|
[Fact]
|
|
public void IntegerVertexAttributesAreRepresentableDistinctlyFromNormalizedOnes()
|
|
{
|
|
// terrain_modern.vert declares locations 2-5 as uvec4 and the CPU feeds
|
|
// them with glVertexAttribIPointer. GL leaves an integer shader input
|
|
// undefined if it arrives through the float path, and Vulkan needs the
|
|
// format named as R8G8B8A8_UINT rather than _UNORM — so the two cannot be
|
|
// the same contract value. Those packed bytes carry terrain-type, road and
|
|
// split-direction codes, so normalising them would produce garbage, not an
|
|
// approximation.
|
|
Assert.NotEqual(GpuVertexFormat.UByte4Normalized, GpuVertexFormat.UByte4UInt);
|
|
Assert.Contains(GpuVertexFormat.UByte4UInt, Enum.GetValues<GpuVertexFormat>());
|
|
}
|
|
|
|
[Fact]
|
|
public void AVertexLayoutCanDeclareAPerInstanceBinding()
|
|
{
|
|
// Campaign V slice V6l. Both particle pipelines draw with per-instance
|
|
// VERTEX attributes, and the V0 contract could express instanced DRAWING
|
|
// (Draw/DrawIndexed both take firstInstance) but not instanced vertex
|
|
// INPUT — one stride, no divisor, one buffer at vertex rate. Plan
|
|
// §5.5.16 recorded that as what blocked V4e and named three ways out;
|
|
// this is option (i), the second binding with a per-instance rate, which
|
|
// both backends carry natively (VK_VERTEX_INPUT_RATE_INSTANCE,
|
|
// glVertexAttribDivisor) and which needs no shader edit.
|
|
var layout = new GpuVertexLayout(
|
|
[
|
|
new GpuVertexBinding(0, 16, GpuVertexInputRate.Vertex),
|
|
new GpuVertexBinding(1, 68, GpuVertexInputRate.Instance),
|
|
],
|
|
[
|
|
new GpuVertexAttribute(0, GpuVertexFormat.Float2, 0),
|
|
new GpuVertexAttribute(2, GpuVertexFormat.Float4, 0, Binding: 1),
|
|
]);
|
|
|
|
Assert.Equal(16u, layout.StrideOf(0));
|
|
Assert.Equal(68u, layout.StrideOf(1));
|
|
Assert.Equal(GpuVertexInputRate.Vertex, layout.InputRateOf(0));
|
|
Assert.Equal(GpuVertexInputRate.Instance, layout.InputRateOf(1));
|
|
|
|
// An attribute that names no declared binding is a composition error,
|
|
// not a silent bind against binding 0.
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => layout.StrideOf(2));
|
|
}
|
|
|
|
[Fact]
|
|
public void ASingleBindingLayoutStillMeansOneInterleavedVertexRateBuffer()
|
|
{
|
|
// Every layout written before V6l has to keep its meaning exactly. The
|
|
// default attribute binding is 0 and the Interleaved factory's one
|
|
// binding is vertex-rate, so no existing declaration changed behaviour.
|
|
Assert.Equal(0u, new GpuVertexAttribute(3, GpuVertexFormat.Float3, 12).Binding);
|
|
|
|
GpuVertexLayout world = GpuVertexLayout.WorldMesh;
|
|
GpuVertexBinding only = Assert.Single(world.Bindings);
|
|
Assert.Equal(0u, only.Binding);
|
|
Assert.Equal(GpuVertexInputRate.Vertex, only.InputRate);
|
|
Assert.Equal(world.StrideBytes, only.StrideBytes);
|
|
|
|
// And a buffer-fed pipeline declares no binding at all.
|
|
Assert.Empty(GpuVertexLayout.None.Bindings);
|
|
Assert.Empty(GpuVertexLayout.None.Attributes);
|
|
Assert.Equal(0u, GpuVertexLayout.None.StrideBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScalarIntegerVertexAttributesAreRepresentable()
|
|
{
|
|
// particle.vert declares `layout(location = 6) in uint aTextureIndex` —
|
|
// the per-instance texture-table slot. The amendment's premise is that no
|
|
// shader is edited, so the contract has to be able to name a scalar uint:
|
|
// GL requires glVertexAttribIPointer for it and Vulkan requires R32_UINT,
|
|
// and the float path would reinterpret its bits rather than approximate
|
|
// its value. Same kind-distinction UByte4UInt was added for at V4d.
|
|
Assert.Contains(GpuVertexFormat.UInt1, Enum.GetValues<GpuVertexFormat>());
|
|
Assert.NotEqual(GpuVertexFormat.Float1, GpuVertexFormat.UInt1);
|
|
}
|
|
|
|
[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 APipelineCanDeclareThatItUsesTheStencilAspect()
|
|
{
|
|
// Campaign V slice V6l. Issue #117's portal punch is a two-pass
|
|
// stencil operation — mark where the aperture wins a depth test, then
|
|
// write the far-Z punch only on marked pixels and zero the stencil as it
|
|
// goes — and the V0 contract carried no stencil state at all, so
|
|
// PortalDepthMaskRenderer stayed raw GL and V4g's "stencil/depth-mask
|
|
// pipelines" row could not be written (plan §5.5.16 defect 2).
|
|
var description = new GpuPipelineDescription
|
|
{
|
|
Name = "contract-stencil",
|
|
Shaders = new GpuShaderSet("portal_depth"),
|
|
VertexLayout = GpuVertexLayout.None,
|
|
};
|
|
|
|
// Off by default, so no pipeline written before this slice changed.
|
|
Assert.False(description.StencilTest);
|
|
Assert.Equal(GpuStencilState.Default, description.Stencil);
|
|
Assert.Equal(GpuCompareOp.Always, GpuStencilState.Default.Compare);
|
|
Assert.Equal(GpuStencilOp.Keep, GpuStencilState.Default.Pass);
|
|
|
|
GpuPipelineDescription punch = description with
|
|
{
|
|
StencilTest = true,
|
|
Stencil = GpuStencilState.Default with
|
|
{
|
|
Compare = GpuCompareOp.Equal,
|
|
Pass = GpuStencilOp.Zero,
|
|
Reference = 1,
|
|
},
|
|
};
|
|
Assert.True(punch.StencilTest);
|
|
Assert.Equal(GpuCompareOp.Equal, punch.Stencil.Compare);
|
|
Assert.Equal(GpuStencilOp.Zero, punch.Stencil.Pass);
|
|
Assert.False(description.StencilTest);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryStencilOperationThePortalPunchNeedsIsRepresentable()
|
|
{
|
|
// Replace marks, Equal gates, Zero self-cleans. Nothing else in the tree
|
|
// touches stencil, and a fourth value would be a facility with no
|
|
// consumer rather than completeness.
|
|
Assert.Equal(3, Enum.GetValues<GpuStencilOp>().Length);
|
|
Assert.Contains(GpuStencilOp.Keep, Enum.GetValues<GpuStencilOp>());
|
|
Assert.Contains(GpuStencilOp.Zero, Enum.GetValues<GpuStencilOp>());
|
|
Assert.Contains(GpuStencilOp.Replace, Enum.GetValues<GpuStencilOp>());
|
|
}
|
|
|
|
[Fact]
|
|
public void TheDepthStencilAttachmentFormatCarriesAStencilAspect()
|
|
{
|
|
// The punch has nowhere to mark without one. The V5 capability gate
|
|
// prefers D32_SFLOAT_S8_UINT and falls back to D24_UNORM_S8_UINT rather
|
|
// than taking a depth-only format for exactly this reason, and the
|
|
// backbuffer pass clears both aspects through one ClearDepthStencil.
|
|
GpuPassDescription pass = GpuPassDescription.BackbufferClear(
|
|
"world",
|
|
Vector4.Zero,
|
|
sampleCount: 4);
|
|
Assert.Equal(0u, pass.Depth!.Value.ClearStencil);
|
|
Assert.Equal(GpuLoadOp.Clear, pass.Depth!.Value.Load);
|
|
Assert.Equal(GpuTextureFormat.Depth24Stencil8, GpuTextureFormat.Depth24Stencil8);
|
|
}
|
|
|
|
[Fact]
|
|
public void UniformBindingsDoNotCollide()
|
|
{
|
|
// Campaign V slice V6e added the sky block. Vulkan has ONE binding
|
|
// namespace per set, so two uniform buffers sharing a number is not a
|
|
// style problem — it is one of them silently reading the other's bytes.
|
|
uint[] uniformBindings =
|
|
[
|
|
GpuBindingModel.UniformSceneLighting,
|
|
GpuBindingModel.UniformTerrainTiling,
|
|
GpuBindingModel.UniformSkyParams,
|
|
];
|
|
|
|
Assert.Equal(uniformBindings.Length, uniformBindings.Distinct().Count());
|
|
|
|
// Binding 2 is the terrain clip block, which sky.vert also reads and
|
|
// which has no constant here because no CPU writer names it through the
|
|
// binding model. It is spelled as a literal on purpose: a new uniform
|
|
// buffer that took 2 would compile, link, and render the wrong thing.
|
|
Assert.DoesNotContain(2u, uniformBindings);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnassignedTextureSlotIsNeverAValidIndex()
|
|
{
|
|
Assert.False(GpuTextureSlot.Unassigned.IsAssigned);
|
|
Assert.True(new GpuTextureSlot(0).IsAssigned);
|
|
Assert.Equal("slot#unassigned", GpuTextureSlot.Unassigned.ToString());
|
|
Assert.Equal("slot#7", new GpuTextureSlot(7).ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void WorldMeshVertexLayoutMatchesTheMeshShaderInputs()
|
|
{
|
|
GpuVertexLayout layout = GpuVertexLayout.WorldMesh;
|
|
|
|
Assert.Equal(32u, layout.StrideBytes);
|
|
Assert.Equal(3, layout.Attributes.Length);
|
|
Assert.Equal(new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0), layout.Attributes[0]);
|
|
Assert.Equal(new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12), layout.Attributes[1]);
|
|
Assert.Equal(new GpuVertexAttribute(2, GpuVertexFormat.Float2, 24), layout.Attributes[2]);
|
|
}
|
|
|
|
[Fact]
|
|
public void MultisampledBackbufferPassResolvesWhileDepthIsDiscarded()
|
|
{
|
|
GpuPassDescription multisampled = GpuPassDescription.BackbufferClear("world", Vector4.Zero, sampleCount: 4);
|
|
Assert.Equal(GpuStoreOp.Resolve, multisampled.Color.Store);
|
|
Assert.Null(multisampled.Color.Target);
|
|
Assert.Equal(GpuStoreOp.DontCare, multisampled.Depth!.Value.Store);
|
|
Assert.Equal(1f, multisampled.Depth!.Value.ClearDepth);
|
|
|
|
GpuPassDescription single = GpuPassDescription.BackbufferClear("world", Vector4.Zero, sampleCount: 1);
|
|
Assert.Equal(GpuStoreOp.Store, single.Color.Store);
|
|
}
|
|
|
|
[Fact]
|
|
public void CapabilityRecordAcceptsADeviceThatMeetsEveryRequirement()
|
|
{
|
|
GpuCapabilityRecord record = SupportedRecord();
|
|
|
|
Assert.Empty(record.SupportFailures);
|
|
Assert.True(record.IsSupported);
|
|
}
|
|
|
|
[Fact]
|
|
public void CapabilityRecordNamesEveryMissingRequirement()
|
|
{
|
|
GpuCapabilityRecord record = SupportedRecord() with
|
|
{
|
|
SupportsMultiDrawIndirect = false,
|
|
SupportsDrawParameters = false,
|
|
SupportsTextureCompressionBc = false,
|
|
MaxTextureTableSlots = 16,
|
|
MaxStorageBufferBindings = 4,
|
|
MaxPushConstantBytes = 32,
|
|
MaxClipDistances = 0,
|
|
};
|
|
|
|
Assert.False(record.IsSupported);
|
|
Assert.Equal(7, record.SupportFailures.Count);
|
|
Assert.Contains(record.SupportFailures, failure => failure.Contains("Multi-draw-indirect", StringComparison.Ordinal));
|
|
Assert.Contains(record.SupportFailures, failure => failure.Contains("gl_DrawID", StringComparison.Ordinal));
|
|
Assert.Contains(record.SupportFailures, failure => failure.Contains("BC (DXT)", StringComparison.Ordinal));
|
|
Assert.Contains(record.SupportFailures, failure => failure.Contains("clip distances", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void TimestampSupportIsOptional()
|
|
{
|
|
// Losing GPU timing degrades profiling; it must never refuse to start.
|
|
GpuCapabilityRecord record = SupportedRecord() with { SupportsTimestampQueries = false };
|
|
Assert.True(record.IsSupported);
|
|
}
|
|
|
|
private static GpuCapabilityRecord SupportedRecord() => new()
|
|
{
|
|
Backend = GpuBackendKind.Vulkan,
|
|
DeviceName = "test-adapter",
|
|
DriverInfo = "test-driver",
|
|
ApiVersion = "Vulkan 1.3.0",
|
|
MaxTextureTableSlots = GpuBindingModel.TextureTableCapacity,
|
|
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
|
|
MaxPushConstantBytes = GpuBindingModel.MaxPushConstantBytes,
|
|
MinStorageBufferOffsetAlignment = 64,
|
|
MinUniformBufferOffsetAlignment = 256,
|
|
MaxClipDistances = GpuBindingModel.ClipPlanesPerSlot,
|
|
MaxSampleCount = 8,
|
|
SupportsMultiDrawIndirect = true,
|
|
SupportsDrawParameters = true,
|
|
SupportsTextureCompressionBc = true,
|
|
SupportsTimestampQueries = true,
|
|
SupportsPersistentlyMappedRings = true,
|
|
};
|
|
}
|