Vixen
02b45cc4
csharp
public sealed class BindlessTable

One unbounded descriptor array, and the TextureViewHandle → uint table that names its slots.

No guide page documents this yet — the page shows what the code says about itself.

Remarks

What bindless actually is, once the extension names are set aside. A descriptor set stops being a thing a draw binds and becomes a thing a shader indexes: one set, bound once for the frame, holding every texture the frame could possibly sample, and a draw carries a number rather than a set. That is the whole mechanism, and every consequence follows from it — draws that differ only in their textures stop differing at all, so they can be sorted together, merged into one indirect command, and issued from a list the device wrote for itself.

An index, once handed out, does not move. Not because moving would be hard but because the index is written into data the host has already given away — a material's record, a per-object block, a buffer the device filled last frame — and none of those can be found again to be renumbered. So the table is an allocator with a free list and never a compactor, and Capacity is a real ceiling rather than a hint.

The same view twice is the same index. Forty materials over one albedo atlas is one descriptor, and the reference count is what lets the fortieth material be released without the other thirty-nine losing their texture. A table that deduplicated without counting would be worse than one that did not deduplicate at all: the failure is not a wasted slot, it is a live material sampling whatever arrives at the index it was given.

A freed index is retired, not reused. This is DescriptorAllocator's hazard in its sharpest form. A material record written for frame f holds an index the GPU reads while the CPU records f + 1; handing that index straight back means the next texture to want a slot takes one an unfinished frame is still sampling, and the picture is one frame of the wrong texture on an object nobody touched. So a released index goes into a ring FramesInFlight deep and only becomes available again once the frame that could have named it has retired — which makes BeginFrame mandatory rather than an optimisation.

Writes go to the device immediately, and that is only safe because of the flag. Updating a descriptor set that a recorded-but-unsubmitted command buffer binds is invalid usage on Vulkan — unless the set was created with update-after-bind, which is exactly what an unbounded binding is created with. It is also why this can write one descriptor at a time without buffering: a distinct view is written once for the life of the table, not once per frame, so the steady state of a settled scene is no writes at all.

It does not exist without the capability. There is no emulated path here and there should not be one: a table faked as a bounded array of the largest size the device allows is a different shader, a different limit and a different failure, and pretending otherwise would put the discovery of it in a driver's error rather than in a host's if. Ask IsSupportedBy and take the per-material set instead.

Fields and properties (7)

  • public DescriptorSetLayoutHandle Layout

    The layout a pipeline declares to be able to index this table.

  • public DescriptorSetHandle Set

    The one set, bound once a frame and never rebound.

  • public int Capacity

    How many slots it has.

  • public int FramesInFlight

    How deep the retirement ring is, which is the device's frames in flight.

  • public int Count

    How many distinct views currently hold a slot.

  • public int HighWaterMark

    The highest slot ever handed out, which is what the free list works below.

  • public int WriteCount

    How many descriptor writes have gone to the device over this table's life.

Methods (8)

  • public BindlessTable(IGraphicsDevice device, ShaderStage stages = Fragment, int capacity = 0, uint binding = 0, DescriptorSetSlot slot = Bindless, DescriptorKind kind = SampledTexture, TextureViewHandle fallback = default(TextureViewHandle), string name = "Bindless")

    Creates a table, its set layout and its one set.

  • public static bool IsSupportedBy(IGraphicsDevice device)

    Whether a device can hold one of these at all.

  • public void BeginFrame()

    Starts a frame, making available the slots released FramesInFlight frames ago.

  • public uint Add(TextureViewHandle view)

    Gives a view a slot, or returns the one it already has.

  • public bool TryGetIndex(TextureViewHandle view, out uint index)

    The slot a view already holds, if it holds one.

  • public bool Remove(TextureViewHandle view)

    Gives back one reference to a view's slot.

  • public void Reset()

    Empties the table, high-water mark included.

  • public void Dispose()

    Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.

Used by (9)

  • BindlessFrameTestsVixen.Rendering.Tests
  • BindlessSamplingDeviceTestsVixen.Graphics.Golden.Tests
  • BindlessTableDeviceTestsVixen.Graphics.Golden.Tests
  • BindlessTableTestsVixen.Graphics.Tests
  • HarnessVixen.Rendering.Tests
  • MaterialRenderFeatureVixen.Rendering
  • MaterialTextureIndexTestsVixen.Rendering.Tests
  • MeshRenderFeatureVixen.Rendering
  • WorldRendererVixen.Engine.Renderer