fix(rt): split Sponza's combined image sampler into a heap texture and sampler

examples/Sponza no longer built natively (#164). Its closest-hit declared a
descriptor_heap sampler2D, which glslang has rejected since c8253623
("Reject descriptor_heap combined image samplers"), and main.cpp wrote it
through VkResourceDescriptorDataEXT::pCombinedImageSampler, which that union
no longer has: the descriptor heap has no combined image samplers. Both went
unnoticed because nothing builds the examples.

Sponza
- closesthit.glsl declares texture2D and sampler heap arrays and joins them
  where it samples, as ui-shared.glsl and HeapDynamicIndexHit do. The
  sampler slot is a second spec constant beside the image slot, so the
  shader assumes nothing about where the host put it, and a constant index
  keeps the example running on NVIDIA 610.43.02 (see its README). The
  comment no longer says the image slot is offset by bufferStartElement;
  only buffer slots are.
- main.cpp gives the heap a sampler slot, writes the albedo as a
  SAMPLED_IMAGE from a view create-info, and writes the sampler into the
  sampler heap from a VkSamplerCreateInfo with vkWriteSamplerDescriptorsEXT,
  as UIRenderer::RegisterSampler does. SamplerVulkan's VkSampler had no use
  left. The sampler keeps its linear filtering and repeat addressing and
  drops anisotropy, which a hit shader cannot use: it samples at LOD 0,
  without derivatives.

Docs
- README.md and examples/Sponza/README.md quote the new declarations, and
  the Sponza README says why the texture and the sampler are two
  descriptors. Its texture2DArray materials pattern declared a
  descriptor_heap sampler2DArray, which glslang rejects as well; it now
  joins a texture2DArray and a sampler.
- examples/SponzaBench/README.md drops its caveat that Sponza does not
  compile.

Tests
- ExampleShaders compiles every GLSL shader under examples/ with glslang as
  crafter-build does for the example (Vulkan rules, SPIR-V 1.4, shaders/ on
  the include path, and the descriptor-set build where the example
  registers one), so a glslang that starts rejecting one fails a test
  rather than an example build nobody runs. It fails on Sponza's old
  closest-hit and needs glslang only.

Closes #164.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 08:25:52 +01:00
commit 6e94cc6209
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
7 changed files with 230 additions and 40 deletions

View file

@ -8,19 +8,24 @@
#extension GL_EXT_descriptor_heap : enable
#extension GL_EXT_nonuniform_qualifier : enable
// Specialization constant: descriptor-heap slot of the albedo texture.
// Set from descriptorHeap.bufferStartElement + the slot allocated for
// the Image2D<RGBA8> on the host side. Sampling uses gl_HitAttributeEXT
// barycentrics as UVs — proof-of-binding rather than UV-correct shading.
// Per-vertex UV interpolation lands when Mesh on Vulkan exposes the
// data-region buffer.
// Specialization constants: the albedo's slot in the resource heap and its
// sampler's slot in the sampler heap, as the host allocated them (image and
// sampler slots index their heaps directly; only buffer slots are offset by
// bufferStartElement). The descriptor heap has no combined image samplers,
// and glslang rejects a descriptor_heap sampler2D (#164), so the texture
// and the sampler are separate descriptors, joined where they are sampled.
// Sampling uses gl_HitAttributeEXT barycentrics as UVs — proof-of-binding
// rather than UV-correct shading. Per-vertex UV interpolation lands when
// Mesh on Vulkan exposes the data-region buffer.
layout(constant_id = 0) const uint16_t albedoSlot = 0us;
layout(descriptor_heap) uniform sampler2D albedo[];
layout(constant_id = 1) const uint16_t samplerSlot = 0us;
layout(descriptor_heap) uniform texture2D textures[];
layout(descriptor_heap) uniform sampler samplers[];
hitAttributeEXT vec2 hitAttrs;
layout(location = 0) rayPayloadInEXT vec3 hitValue;
void main() {
vec2 bary = vec2(hitAttrs.x, hitAttrs.y);
hitValue = texture(albedo[albedoSlot], bary).rgb;
hitValue = texture(sampler2D(textures[albedoSlot], samplers[samplerSlot]), bary).rgb;
}