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:
parent
c97a3048fb
commit
6e94cc6209
7 changed files with 230 additions and 40 deletions
|
|
@ -59,7 +59,8 @@ shaded through an intersection shader with an any-hit cut-out.
|
||||||
> in a closest-hit, including different descriptors in neighbouring lanes of
|
> in a closest-hit, including different descriptors in neighbouring lanes of
|
||||||
> a wave. The rest of this note applies to the older drivers. A **constant / spec-constant**
|
> a wave. The rest of this note applies to the older drivers. A **constant / spec-constant**
|
||||||
> index is fine (that's why [Sponza](examples/Sponza/README.md)'s
|
> index is fine (that's why [Sponza](examples/Sponza/README.md)'s
|
||||||
> closest-hit reads `albedo[albedoSlot]` through a spec constant), and
|
> closest-hit reads `textures[albedoSlot]` and `samplers[samplerSlot]`
|
||||||
|
> through spec constants), and
|
||||||
> the identical dynamic pattern works in fragment shaders (the UI
|
> the identical dynamic pattern works in fragment shaders (the UI
|
||||||
> renderer indexes `uiTextures[]` by per-item runtime slots) — so this
|
> renderer indexes `uiTextures[]` by per-item runtime slots) — so this
|
||||||
> is **RT-stage-specific**, not a general heap problem. Unlike the
|
> is **RT-stage-specific**, not a general heap problem. Unlike the
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,16 @@ it via ray tracing on both Vulkan (native) and WebGPU (wasm). Same
|
||||||
at the barycentric attribs as UVs — proof-of-binding, not visually
|
at the barycentric attribs as UVs — proof-of-binding, not visually
|
||||||
accurate. Per-vertex UV interpolation is the next step.
|
accurate. Per-vertex UV interpolation is the next step.
|
||||||
|
|
||||||
The closest-hit reads its texture through a **spec constant**
|
The closest-hit reads its texture and its sampler through **spec
|
||||||
(`albedo[albedoSlot]`), not a runtime index. That is deliberate — see
|
constants** (`textures[albedoSlot]`, `samplers[samplerSlot]`), not a
|
||||||
below.
|
runtime index. That is deliberate — see below.
|
||||||
|
|
||||||
|
On Vulkan the texture and the sampler are separate descriptors: a sampled
|
||||||
|
image in the resource heap and a sampler in the sampler heap, joined where
|
||||||
|
the closest-hit samples them,
|
||||||
|
`texture(sampler2D(textures[albedoSlot], samplers[samplerSlot]), uv)`.
|
||||||
|
The descriptor heap has no combined image samplers, and glslang rejects a
|
||||||
|
`descriptor_heap` `sampler2D` (#164).
|
||||||
|
|
||||||
## Native RT limitation: dynamic `descriptor_heap` indexing in hit shaders
|
## Native RT limitation: dynamic `descriptor_heap` indexing in hit shaders
|
||||||
|
|
||||||
|
|
@ -44,7 +51,8 @@ closest-hit:
|
||||||
|
|
||||||
- A closest-hit that reads only `lightHeap[lightSlot]` where `lightSlot`
|
- A closest-hit that reads only `lightHeap[lightSlot]` where `lightSlot`
|
||||||
is a **spec constant** survives indefinitely. ✅ (This example's
|
is a **spec constant** survives indefinitely. ✅ (This example's
|
||||||
`albedo[albedoSlot]` is exactly this case.)
|
`textures[albedoSlot]` and `samplers[samplerSlot]` are exactly this
|
||||||
|
case.)
|
||||||
- Reading `indexHeap[assetIndexStart + gl_InstanceCustomIndexEXT]` /
|
- Reading `indexHeap[assetIndexStart + gl_InstanceCustomIndexEXT]` /
|
||||||
`vertexHeap[...]` — a heap index offset by a **runtime** value —
|
`vertexHeap[...]` — a heap index offset by a **runtime** value —
|
||||||
device-losts on the first geometry hit. ❌
|
device-losts on the first geometry hit. ❌
|
||||||
|
|
@ -116,11 +124,14 @@ indexing the heap:
|
||||||
slot and index by **layer**:
|
slot and index by **layer**:
|
||||||
|
|
||||||
```glsl
|
```glsl
|
||||||
// ✅ one array texture (spec-constant slot), dynamic layer index
|
// ✅ one array texture (spec-constant slots), dynamic layer index
|
||||||
layout(constant_id = 1) const uint16_t materialArraySlot = 0us;
|
layout(constant_id = 1) const uint16_t materialArraySlot = 0us;
|
||||||
layout(descriptor_heap) uniform sampler2DArray materials[];
|
layout(constant_id = 2) const uint16_t samplerSlot = 0us;
|
||||||
|
layout(descriptor_heap) uniform texture2DArray materials[];
|
||||||
|
layout(descriptor_heap) uniform sampler samplers[];
|
||||||
uint layer = materialLayer[gl_InstanceCustomIndexEXT]; // from a bound SSBO
|
uint layer = materialLayer[gl_InstanceCustomIndexEXT]; // from a bound SSBO
|
||||||
vec3 albedo = texture(materials[materialArraySlot], vec3(uv, layer)).rgb;
|
vec3 albedo = texture(sampler2DArray(materials[materialArraySlot], samplers[samplerSlot]),
|
||||||
|
vec3(uv, layer)).rgb;
|
||||||
```
|
```
|
||||||
|
|
||||||
This is precisely what the WebGPU path already does — bucketed texture
|
This is precisely what the WebGPU path already does — bucketed texture
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,24 @@
|
||||||
#extension GL_EXT_descriptor_heap : enable
|
#extension GL_EXT_descriptor_heap : enable
|
||||||
#extension GL_EXT_nonuniform_qualifier : enable
|
#extension GL_EXT_nonuniform_qualifier : enable
|
||||||
|
|
||||||
// Specialization constant: descriptor-heap slot of the albedo texture.
|
// Specialization constants: the albedo's slot in the resource heap and its
|
||||||
// Set from descriptorHeap.bufferStartElement + the slot allocated for
|
// sampler's slot in the sampler heap, as the host allocated them (image and
|
||||||
// the Image2D<RGBA8> on the host side. Sampling uses gl_HitAttributeEXT
|
// sampler slots index their heaps directly; only buffer slots are offset by
|
||||||
// barycentrics as UVs — proof-of-binding rather than UV-correct shading.
|
// bufferStartElement). The descriptor heap has no combined image samplers,
|
||||||
// Per-vertex UV interpolation lands when Mesh on Vulkan exposes the
|
// and glslang rejects a descriptor_heap sampler2D (#164), so the texture
|
||||||
// data-region buffer.
|
// 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(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;
|
hitAttributeEXT vec2 hitAttrs;
|
||||||
layout(location = 0) rayPayloadInEXT vec3 hitValue;
|
layout(location = 0) rayPayloadInEXT vec3 hitValue;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 bary = vec2(hitAttrs.x, hitAttrs.y);
|
vec2 bary = vec2(hitAttrs.x, hitAttrs.y);
|
||||||
hitValue = texture(albedo[albedoSlot], bary).rgb;
|
hitValue = texture(sampler2D(textures[albedoSlot], samplers[samplerSlot]), bary).rgb;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,26 +79,32 @@ int main() {
|
||||||
VkCommandBuffer cmd = window.StartInit();
|
VkCommandBuffer cmd = window.StartInit();
|
||||||
|
|
||||||
DescriptorHeapVulkan descriptorHeap;
|
DescriptorHeapVulkan descriptorHeap;
|
||||||
descriptorHeap.Initialize(/*images*/ 2, /*buffers*/ 1, /*samplers*/ 0);
|
descriptorHeap.Initialize(/*images*/ 2, /*buffers*/ 1, /*samplers*/ 1);
|
||||||
|
|
||||||
// Two specialization constants: the TLAS slot offset (shared with
|
// Specialization constants: the TLAS slot offset for raygen (shared
|
||||||
// VulkanTriangle pattern) and the albedo slot index for closesthit.
|
// with the VulkanTriangle pattern), and the albedo's image and sampler
|
||||||
|
// slots for closesthit.
|
||||||
VkSpecializationMapEntry raygenEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
|
VkSpecializationMapEntry raygenEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
|
||||||
VkSpecializationInfo raygenSpec = {
|
VkSpecializationInfo raygenSpec = {
|
||||||
.mapEntryCount = 1, .pMapEntries = &raygenEntry,
|
.mapEntryCount = 1, .pMapEntries = &raygenEntry,
|
||||||
.dataSize = sizeof(std::uint16_t), .pData = &descriptorHeap.bufferStartElement,
|
.dataSize = sizeof(std::uint16_t), .pData = &descriptorHeap.bufferStartElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Allocate the albedo slot first so its index is known when we
|
// Allocate the albedo's slots first so their indices are known when we
|
||||||
// compile closesthit.spv.
|
// compile closesthit.spv.
|
||||||
auto imgSlots = descriptorHeap.AllocateImageSlots(2);
|
auto imgSlots = descriptorHeap.AllocateImageSlots(2);
|
||||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
||||||
|
auto samplerSlots = descriptorHeap.AllocateSamplerSlots(1);
|
||||||
std::uint16_t albedoHeapSlot = static_cast<std::uint16_t>(imgSlots.firstElement + 1);
|
std::uint16_t albedoHeapSlot = static_cast<std::uint16_t>(imgSlots.firstElement + 1);
|
||||||
|
|
||||||
VkSpecializationMapEntry hitEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
|
const std::array<std::uint16_t, 2> hitConstants { albedoHeapSlot, samplerSlots.firstElement };
|
||||||
|
const std::array<VkSpecializationMapEntry, 2> hitEntries {{
|
||||||
|
{ .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) },
|
||||||
|
{ .constantID = 1, .offset = sizeof(std::uint16_t), .size = sizeof(std::uint16_t) },
|
||||||
|
}};
|
||||||
VkSpecializationInfo hitSpec = {
|
VkSpecializationInfo hitSpec = {
|
||||||
.mapEntryCount = 1, .pMapEntries = &hitEntry,
|
.mapEntryCount = static_cast<std::uint32_t>(hitEntries.size()), .pMapEntries = hitEntries.data(),
|
||||||
.dataSize = sizeof(std::uint16_t), .pData = &albedoHeapSlot,
|
.dataSize = sizeof(hitConstants), .pData = hitConstants.data(),
|
||||||
};
|
};
|
||||||
|
|
||||||
std::array<VulkanShader, 3> shaders {{
|
std::array<VulkanShader, 3> shaders {{
|
||||||
|
|
@ -140,7 +146,6 @@ int main() {
|
||||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
albedo.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
albedo.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
SamplerVulkan<RGBA8> sampler;
|
|
||||||
|
|
||||||
static RenderingElement3D renderer;
|
static RenderingElement3D renderer;
|
||||||
renderer.instance = {
|
renderer.instance = {
|
||||||
|
|
@ -161,13 +166,26 @@ int main() {
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
// Write descriptors: TLAS at bufSlots[0], output image at imgSlots[0],
|
// Write descriptors: TLAS at bufSlots[0], output image at imgSlots[0],
|
||||||
// albedo (combined image+sampler) at imgSlots[1]. Per-frame replicated.
|
// albedo (a sampled image) at imgSlots[1], and its sampler at
|
||||||
|
// samplerSlots[0] in the sampler heap. Per-frame replicated.
|
||||||
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
||||||
VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
|
VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
|
||||||
VkDescriptorImageInfo albedoInfo {
|
// The heap takes an image view's create-info, not a VkImageView.
|
||||||
.sampler = sampler.textureSampler,
|
const VkImageViewCreateInfo albedoView {
|
||||||
.imageView = albedo.imageView,
|
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||||
.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
.image = albedo.image,
|
||||||
|
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||||
|
.format = VK_FORMAT_R8G8B8A8_UNORM,
|
||||||
|
.subresourceRange = {
|
||||||
|
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||||
|
.levelCount = albedo.mipLevels,
|
||||||
|
.layerCount = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const VkImageDescriptorInfoEXT albedoInfo {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
||||||
|
.pView = &albedoView,
|
||||||
|
.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||||
};
|
};
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
|
|
@ -205,8 +223,8 @@ int main() {
|
||||||
});
|
});
|
||||||
resources.push_back({
|
resources.push_back({
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
|
||||||
.data = { .pCombinedImageSampler = &albedoInfo },
|
.data = { .pImage = &albedoInfo },
|
||||||
});
|
});
|
||||||
destinations.push_back({
|
destinations.push_back({
|
||||||
.address = descriptorHeap.resourceHeap[f].value
|
.address = descriptorHeap.resourceHeap[f].value
|
||||||
|
|
@ -221,6 +239,34 @@ int main() {
|
||||||
descriptorHeap.resourceHeap[f].FlushDevice();
|
descriptorHeap.resourceHeap[f].FlushDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The sampler heap takes a sampler's create-info, as
|
||||||
|
// UIRenderer::RegisterSampler writes it.
|
||||||
|
const VkSamplerCreateInfo albedoSamplerInfo {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
||||||
|
.magFilter = VK_FILTER_LINEAR,
|
||||||
|
.minFilter = VK_FILTER_LINEAR,
|
||||||
|
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
|
||||||
|
.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT,
|
||||||
|
.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT,
|
||||||
|
.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT,
|
||||||
|
.maxLod = VK_LOD_CLAMP_NONE,
|
||||||
|
};
|
||||||
|
VkSamplerCreateInfo samplerInfos[Window::numFrames];
|
||||||
|
VkHostAddressRangeEXT samplerDestinations[Window::numFrames];
|
||||||
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
|
samplerInfos[f] = albedoSamplerInfo;
|
||||||
|
samplerDestinations[f] = {
|
||||||
|
.address = descriptorHeap.samplerHeap[f].value
|
||||||
|
+ descriptorHeap.SamplerByteOffset(samplerSlots.firstElement),
|
||||||
|
.size = Device::descriptorHeapProperties.samplerDescriptorSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Device::vkWriteSamplerDescriptorsEXT(Device::device, Window::numFrames,
|
||||||
|
samplerInfos, samplerDestinations);
|
||||||
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
|
descriptorHeap.samplerHeap[f].FlushDevice();
|
||||||
|
}
|
||||||
|
|
||||||
window.descriptorHeap = &descriptorHeap;
|
window.descriptorHeap = &descriptorHeap;
|
||||||
RTPass rtPass(&pipeline);
|
RTPass rtPass(&pipeline);
|
||||||
window.passes.push_back(&rtPass);
|
window.passes.push_back(&rtPass);
|
||||||
|
|
|
||||||
|
|
@ -119,13 +119,6 @@ scene for the dynamic-upload PRs) would be needed.
|
||||||
|
|
||||||
## Caveats / notes
|
## Caveats / notes
|
||||||
|
|
||||||
* The stock `examples/Sponza` **native** path does not currently compile
|
|
||||||
against the (unpinned) Vulkan-Headers `main`: the
|
|
||||||
`VkResourceDescriptorDataEXT` union no longer has the
|
|
||||||
`pCombinedImageSampler` member the example uses. SponzaBench sidesteps
|
|
||||||
this by not binding a combined image+sampler. (Pre-existing, unrelated to
|
|
||||||
#155 — worth a follow-up to port Sponza to the split sampled-image +
|
|
||||||
sampler-heap model the engine's own UI renderer already uses.)
|
|
||||||
* Numbers are CPU/GPU specific. Re-run `run-bench.sh` locally for your
|
* Numbers are CPU/GPU specific. Re-run `run-bench.sh` locally for your
|
||||||
hardware; the *deltas* between revisions are the point, not the absolute
|
hardware; the *deltas* between revisions are the point, not the absolute
|
||||||
figures.
|
figures.
|
||||||
|
|
|
||||||
|
|
@ -549,6 +549,15 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
fusedTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
fusedTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
||||||
cfg.tests.push_back(std::move(fusedTest));
|
cfg.tests.push_back(std::move(fusedTest));
|
||||||
|
|
||||||
|
// Issue #164: every GLSL shader under examples/ compiles. Nothing here
|
||||||
|
// builds the examples, so when glslang began rejecting Sponza's
|
||||||
|
// descriptor_heap sampler2D nothing noticed. Compiles each one as
|
||||||
|
// crafter-build would for its example, the descriptor-set build too
|
||||||
|
// where it has one. No GPU; glslang is a required tool.
|
||||||
|
Test exampleShadersTest = LibraryTest("ExampleShaders");
|
||||||
|
exampleShadersTest.requires_ = { "tool:glslang" };
|
||||||
|
cfg.tests.push_back(std::move(exampleShadersTest));
|
||||||
|
|
||||||
// Issue #60: VulkanBuffer::FlushDevice / FlushHost now record the chosen
|
// Issue #60: VulkanBuffer::FlushDevice / FlushHost now record the chosen
|
||||||
// memory type's propertyFlags at Create time and skip the
|
// memory type's propertyFlags at Create time and skip the
|
||||||
// flush/invalidate when the memory is HOST_COHERENT. The gate is pure
|
// flush/invalidate when the memory is HOST_COHERENT. The gate is pure
|
||||||
|
|
|
||||||
125
tests/ExampleShaders/main.cpp
Normal file
125
tests/ExampleShaders/main.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
// Every GLSL shader the examples ship still compiles. The examples are
|
||||||
|
// projects of their own that nothing in this one builds, and crafter-build
|
||||||
|
// compiles their shaders with whichever glslang it links, unpinned. When
|
||||||
|
// glslang began rejecting a descriptor_heap combined image sampler (upstream
|
||||||
|
// c8253623), examples/Sponza stopped building and nothing noticed until it
|
||||||
|
// was built by hand (#164). This test compiles each examples/<Name>/*.glsl as
|
||||||
|
// crafter-build does (Vulkan rules, SPIR-V 1.4, shaders/ on the include
|
||||||
|
// path), with the stage taken from the file name, and a shader that includes
|
||||||
|
// ui-shared.glsl a second time for the descriptor-set build its example also
|
||||||
|
// registers. A file whose name gives no stage fails rather than being
|
||||||
|
// skipped, so a new shader is never silently left out.
|
||||||
|
//
|
||||||
|
// Needs glslang on the PATH (a required tool in project.cpp), which need not
|
||||||
|
// be the glslang crafter-build links, and the source tree, which it finds
|
||||||
|
// from the build output directory. No GPU.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
import std;
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
void Check(bool ok, std::string_view what) {
|
||||||
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||||
|
if (!ok) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
int RunCommand(const std::string& cmd) {
|
||||||
|
int status = std::system(cmd.c_str());
|
||||||
|
if (status == -1) return -1;
|
||||||
|
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ReadFile(const fs::path& path) {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
// glslang's -S name for the stage, from the file names the examples use:
|
||||||
|
// raygen.glsl, closesthit.glsl, ..., <name>.comp.glsl. Empty when unknown.
|
||||||
|
std::string_view StageOf(const fs::path& shader) {
|
||||||
|
const std::string stem = shader.stem().string();
|
||||||
|
if (stem.ends_with(".comp")) return "comp";
|
||||||
|
constexpr std::pair<std::string_view, std::string_view> stages[] = {
|
||||||
|
{"raygen", "rgen"}, {"intersection", "rint"}, {"anyhit", "rahit"},
|
||||||
|
{"closesthit", "rchit"}, {"miss", "rmiss"},
|
||||||
|
};
|
||||||
|
for (const auto& [name, stage] : stages) {
|
||||||
|
if (stem == name) return stage;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The directory holding examples/ and shaders/, whether the test runs from
|
||||||
|
// its build output directory or the project root, as UIFusedShader finds its
|
||||||
|
// shader.
|
||||||
|
fs::path LocateRoot() {
|
||||||
|
for (const fs::path candidate : {".", "..", "../..", "../../.."}) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (fs::is_directory(candidate / "examples", ec)
|
||||||
|
&& fs::is_regular_file(candidate / "shaders" / "ui-shared.glsl", ec)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const fs::path root = LocateRoot();
|
||||||
|
if (root.empty()) {
|
||||||
|
std::println(std::cerr, "could not find examples/ and shaders/ from {}", fs::current_path().string());
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
const fs::path out = fs::temp_directory_path() / "crafter-example-shaders-test";
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(out, ec);
|
||||||
|
|
||||||
|
// Each example's own directory only: its bin/ and build/ hold outputs.
|
||||||
|
std::vector<fs::path> shaders;
|
||||||
|
for (const fs::directory_entry& example : fs::directory_iterator(root / "examples")) {
|
||||||
|
if (!example.is_directory()) continue;
|
||||||
|
for (const fs::directory_entry& file : fs::directory_iterator(example.path())) {
|
||||||
|
if (file.is_regular_file() && file.path().extension() == ".glsl") shaders.push_back(file.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::ranges::sort(shaders);
|
||||||
|
Check(!shaders.empty(), std::format("found the examples' shaders ({})", shaders.size()));
|
||||||
|
|
||||||
|
for (const fs::path& shader : shaders) {
|
||||||
|
const std::string name = (shader.parent_path().filename() / shader.filename()).generic_string();
|
||||||
|
const std::string_view stage = StageOf(shader);
|
||||||
|
if (stage.empty()) {
|
||||||
|
Check(false, std::format("{}: no stage known for this file name; name it after its stage or teach StageOf", name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A shader on ui-shared.glsl's UI_HEAP_* macros is registered twice
|
||||||
|
// by its example, the second build for drivers without the heap.
|
||||||
|
const bool setsBuild = ReadFile(shader).contains("ui-shared.glsl");
|
||||||
|
for (const bool sets : {false, true}) {
|
||||||
|
if (sets && !setsBuild) continue;
|
||||||
|
const std::string base = std::format("{}-{}{}", shader.parent_path().filename().string(),
|
||||||
|
shader.stem().string(), sets ? ".sets" : "");
|
||||||
|
const fs::path log = out / (base + ".log");
|
||||||
|
const std::string compile = std::format(
|
||||||
|
"glslang -V --target-env spirv1.4 -S {} {}-I\"{}\" -o \"{}\" \"{}\" > \"{}\" 2>&1",
|
||||||
|
stage, sets ? "-DCRAFTER_GRAPHICS_DESCRIPTOR_SETS " : "", (root / "shaders").string(),
|
||||||
|
(out / (base + ".spv")).string(), shader.string(), log.string());
|
||||||
|
const bool compiled = RunCommand(compile) == 0;
|
||||||
|
if (!compiled) std::print("{}", ReadFile(log));
|
||||||
|
Check(compiled, std::format("{} compiles{}", name, sets ? " (descriptor-set build)" : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||||
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue