Repository: shader-slang/slang (2026-02-15T09:54:42Z)

Issue:
Off-by-2x in `isParameterLocationUsed` in param blocks with uniforms
# Issue Description
Hullo! `isParameterLocationUsed` seems to have an indexing bug when used with parameter blocks that contain automatically-introduced constant buffers (at least when targeting SPIR-V / Vulkan)

Given this shader:

```
struct Foo {
    float a;
    Texture2D<float> b;
};

struct Bar {
    Texture2D<float4> c;
};

[[vk::binding(0, 5)]]
ParameterBlock<Foo> foo;

[[vk::binding(0, 10)]]
ParameterBlock<Bar> bar;

[shader("fragment")]
float main(): SV_Target {
    return foo.a;
}
```

Slang reports that the binding in param block `bar` is used, even though it's not referenced at all. It also claims that _no_ binding in `foo` is used.

The way the binding spaces `5` and `10` are significant here, is just that a uniform inside a param block in space `N` seems to be reported through param block `N*2` instead.

If the `float a;` in `Foo` is replaced with a resource, say, a texture, then the reporting is correct.

Note that the explicit `vk::binding` attribs here are purely for illustrating the issue - the repro comes from a larger program without explicit `vk::binding`s.

# Reproducer Code
Here's a small C++ app that reproduces the issue

```
#include <slang.h>
#include <slang-com-ptr.h>

#include <iostream>
#include <string_view>
#include <vector>

using Slang::ComPtr;

namespace
{
    void dumpDiagnostics(const char *context, slang::IBlob *diagnostics)
    {
        if (!diagnostics)
            return;

        const auto *buffer = static_cast<const char *>(diagnostics->getBufferPointer());
        const size_t size = diagnostics->getBufferSize();
        if (size)
        {
            std::cerr << "[" << context << "] " << std::string_view(buffer, size) << std::endl;
        }
    }

    bool reportIfFailed(SlangResult result, const char *action, slang::IBlob *diagnostics)
    {
        if (SLANG_FAILED(result))
        {
            dumpDiagnostics(action, diagnostics);
            std::cerr << action << " failed with SlangResult=" << result << std::endl;
            return true;
        }
        return false;
    }
} // namespace

int main()
{
    const char *shaderSource = R"(
        struct Foo {
            float a;
            Texture2D<float> b;
        };

        struct Bar {
            Texture2D<float4> c;
        };

        [[vk::binding(0, 5)]]
        ParameterBlock<Foo> foo;

        [[vk::binding(0, 10)]]
        ParameterBlock<Bar> bar;

        [shader("fragment")]
        float main(): SV_Target {
            return foo.a;
        }
    )";

    ComPtr<slang::IGlobalSession> globalSession;
    if (SLANG_FAILED(slang_createGlobalSession(SLANG_API_VERSION, globalSession.writeRef())))
    {
        std::cerr << "Unable to create global Slang session." << std::endl;
        return 1;
    }

    slang::TargetDesc targetDesc = {};
    targetDesc.format = SLANG_SPIRV;
    targetDesc.profile = globalSession->findProfile("spirv_1_3");
    if (!targetDesc.profile)
    {
        std::cerr << "Profile spirv_1_3 not found." << std::endl;
        return 1;
    }

    slang::SessionDesc sessionDesc = {};
    sessionDesc.targetCount = 1;
    sessionDesc.targets = &targetDesc;

    ComPtr<slang::ISession> session;
    if (SLANG_FAILED(globalSession->createSession(sessionDesc, session.writeRef())))
    {
        std::cerr << "Unable to create Slang session." << std::endl;
        return 1;
    }

    ComPtr<slang::IBlob> diagnostics;
    diagnostics.setNull();
    auto module = session->loadModuleFromSourceString(
        "moduleG",               // module name
        "moduleG.slang",         // virtual path
        shaderSource,            // source
        diagnostics.writeRef()); // diagnostics
    if (!module)
    {
        dumpDiagnostics("loadModuleFromSourceString", diagnostics);
        return 1;
    }

    ComPtr<slang::IEntryPoint> entryPoint;
    diagnostics.setNull();
    if (reportIfFailed(
            module->findAndCheckEntryPoint(
                "main",
                SLANG_STAGE_FRAGMENT,
                entryPoint.writeRef(),
                diagnostics.writeRef()),
            "findAndCheckEntryPoint",
            diagnostics))
    {
        return 1;
    }

    ComPtr<slang::IComponentType> compositeProgram;
    slang::IComponentType *components[] = {module, entryPoint.get()};
    diagnostics.setNull();
    if (reportIfFailed(
            session->createCompositeComponentType(
                components,
                SLANG_COUNT_OF(components),
                compositeProgram.writeRef(),
                diagnostics.writeRef()),
            "createCompositeComponentType",
            diagnostics))
    {
        return 1;
    }

    ComPtr<slang::IComponentType> linkedProgram;
    diagnostics.setNull();
    if (reportIfFailed(
            compositeProgram->link(linkedProgram.writeRef(), diagnostics.writeRef()),
            "link",
            diagnostics))
    {
        return 1;
    }

    ComPtr<slang::IMetadata> metadata;
    diagnostics.setNull();
    if (reportIfFailed(
            linkedProgram->getTargetMetadata(0, metadata.writeRef(), diagnostics.writeRef()),
            "getTargetMetadata",
            diagnostics))
    {
        return 1;
    }

    struct Expectation
    {
        SlangParameterCategory category;
        SlangUInt spaceIndex;
        SlangUInt registerIndex;
        bool expectedUsed;
    };

    const std::vector<Expectation> expectations = {
        // The automatically-introduced constant buffer in Foo, is used
        {SLANG_PARAMETER_CATEGORY_DESCRIPTOR_TABLE_SLOT, 5, 0, true},
        // Foo::b, a texture, is unused.
        {SLANG_PARAMETER_CATEGORY_DESCRIPTOR_TABLE_SLOT, 5, 1, false},
        // Bar::c, a texture, is unused.
        {SLANG_PARAMETER_CATEGORY_DESCRIPTOR_TABLE_SLOT, 10, 0, false},
    };

    bool allOk = true;
    for (const auto &expectation : expectations)
    {
        bool isUsed = false;
        if (reportIfFailed(
                metadata->isParameterLocationUsed(
                    expectation.category,
                    expectation.spaceIndex,
                    expectation.registerIndex,
                    isUsed),
                "isParameterLocationUsed",
                diagnostics))
        {
            return 1;
        }

        if (isUsed != expectation.expectedUsed)
        {
            std::cerr << "Unexpected usage for category=" << expectation.category << ", space="
                      << expectation.spaceIndex << ", register=" << expectation.registerIndex
                      << "; expected " << (expectation.expectedUsed ? "used" : "unused")
                      << " but got " << (isUsed ? "used" : "unused") << std::endl;
            allOk = false;
        }
    }

    if (allOk)
    {
        std::cout << "All parameter usage checks passed." << std::endl;
    }

    return 0;
}
```

# Expected Behavior
I'd expect the app to say "All parameter usage checks passed" - confirming that the first of the two bindings in `foo` is used, and that `bar`'s one binding is unused.

# Actual Behavior
The app says:
```
Unexpected usage for category=9, space=5, register=0; expected used but got unused
Unexpected usage for category=9, space=10, register=0; expected unused but got used
```
thus illustrating the mismatch.

# Environment
 - Slang Version: 2026.1
 - OS: Linux 6.18.2-arch2-1 x86_64


Discussion:
_(I don't know much about Slang's internals but)_ this Codex-authored change seems to fix it: https://github.com/shader-slang/slang/compare/master...h3r2tic:slang:fix/is-param-used-auto-cb, so I could submit it as a PR.
@h3r2tic Sure, submit it as a PR and we can review.
@tangent-vector This looks like a pretty simple fix, can you review it?
Apologies for the delay; here's the PR :)
https://github.com/shader-slang/slang/pull/10029



Patch:
diff --git a/source/slang/slang-ir-metadata.cpp b/source/slang/slang-ir-metadata.cpp
index d789f52ef39..a056d3683e5 100644
--- a/source/slang/slang-ir-metadata.cpp
+++ b/source/slang/slang-ir-metadata.cpp
@@ -92,7 +92,10 @@ void collectMetadataFromInst(IRInst* param, ArtifactPostEmitMetadata& outMetadat
         varLayout->findOffsetAttr(LayoutResourceKind::SubElementRegisterSpace);
     if (!containerSpaceOffset)
         return;
-    spaceOffset += containerSpaceOffset->getOffset();
+    // The sub-element register space offset already includes the descriptor set allocated for the
+    // parameter block. Do not accumulate the parent register-space offset here, or we will double
+    // count the descriptor set index for the automatically introduced constant buffer.
+    UInt containerSpaceOffsetValue = containerSpaceOffset->getOffset();
     for (auto sizeAttr : containerVarLayout->getTypeLayout()->getSizeAttrs())
     {
         auto kind = sizeAttr->getResourceKind();
@@ -102,7 +105,7 @@ void collectMetadataFromInst(IRInst* param, ArtifactPostEmitMetadata& outMetadat
 
         if (auto offsetAttr = containerVarLayout->findOffsetAttr(kind))
         {
-            auto spaceIndex = spaceOffset + offsetAttr->getSpace();
+            auto spaceIndex = containerSpaceOffsetValue + offsetAttr->getSpace();
             auto registerIndex = offsetAttr->getOffset();
             auto size = sizeAttr->getSize();
             auto count = size.getFiniteValueOr(0);


Test patch:
diff --git a/tools/slang-unit-test/unit-test-parameter-usage-reflection.cpp b/tools/slang-unit-test/unit-test-parameter-usage-reflection.cpp
index 05acfc9c24f..050bb757a6a 100644
--- a/tools/slang-unit-test/unit-test-parameter-usage-reflection.cpp
+++ b/tools/slang-unit-test/unit-test-parameter-usage-reflection.cpp
@@ -106,3 +106,99 @@ SLANG_UNIT_TEST(isParameterLocationUsedReflection)
     metadata->isParameterLocationUsed(SLANG_PARAMETER_CATEGORY_VARYING_INPUT, 0, 1, isUsed);
     SLANG_CHECK(isUsed);
 }
+
+SLANG_UNIT_TEST(isParameterLocationUsedParameterBlockConstantBuffer)
+{
+    const char* shaderSource = R"(
+        struct Foo {
+            float a;
+            Texture2D<float> b;
+        };
+
+        struct Bar {
+            Texture2D<float4> c;
+        };
+
+        [[vk::binding(0, 5)]]
+        ParameterBlock<Foo> foo;
+
+        [[vk::binding(0, 10)]]
+        ParameterBlock<Bar> bar;
+
+        [shader("fragment")]
+        float main(): SV_Target {
+            return foo.a;
+        }
+    )";
+
+    ComPtr<slang::IGlobalSession> globalSession;
+    SLANG_CHECK(slang_createGlobalSession(SLANG_API_VERSION, globalSession.writeRef()) == SLANG_OK);
+
+    slang::TargetDesc targetDesc = {};
+    targetDesc.format = SLANG_SPIRV;
+    targetDesc.profile = globalSession->findProfile("spirv_1_3");
+
+    slang::SessionDesc sessionDesc = {};
+    sessionDesc.targetCount = 1;
+    sessionDesc.targets = &targetDesc;
+
+    ComPtr<slang::ISession> session;
+    SLANG_CHECK(globalSession->createSession(sessionDesc, session.writeRef()) == SLANG_OK);
+
+    ComPtr<slang::IBlob> diagnosticBlob;
+    auto module = session->loadModuleFromSourceString(
+        "isParameterLocationUsedParameterBlockConstantBuffer",
+        "pb.slang",
+        shaderSource,
+        diagnosticBlob.writeRef());
+    SLANG_CHECK(module != nullptr);
+
+    ComPtr<slang::IEntryPoint> entryPoint;
+    SLANG_CHECK(
+        module->findAndCheckEntryPoint(
+            "main",
+            SLANG_STAGE_FRAGMENT,
+            entryPoint.writeRef(),
+            diagnosticBlob.writeRef()) == SLANG_OK);
+
+    ComPtr<slang::IComponentType> compositeProgram;
+    slang::IComponentType* components[] = {module, entryPoint.get()};
+    SLANG_CHECK(
+        session->createCompositeComponentType(
+            components,
+            2,
+            compositeProgram.writeRef(),
+            diagnosticBlob.writeRef()) == SLANG_OK);
+
+    ComPtr<slang::IComponentType> linkedProgram;
+    SLANG_CHECK(compositeProgram->link(linkedProgram.writeRef(), nullptr) == SLANG_OK);
+
+    ComPtr<slang::IMetadata> metadata;
+    SLANG_CHECK(linkedProgram->getTargetMetadata(0, metadata.writeRef(), nullptr) == SLANG_OK);
+
+    struct Expectation
+    {
+        SlangParameterCategory category;
+        SlangUInt spaceIndex;
+        SlangUInt registerIndex;
+        bool expectedUsed;
+    };
+
+    Expectation expectations[] = {
+        {SLANG_PARAMETER_CATEGORY_DESCRIPTOR_TABLE_SLOT, 5, 0, true},
+        {SLANG_PARAMETER_CATEGORY_DESCRIPTOR_TABLE_SLOT, 5, 1, false},
+        {SLANG_PARAMETER_CATEGORY_DESCRIPTOR_TABLE_SLOT, 10, 0, false},
+    };
+
+    for (auto expectation : expectations)
+    {
+        bool isUsed = false;
+        SLANG_CHECK(
+            metadata->isParameterLocationUsed(
+                expectation.category,
+                expectation.spaceIndex,
+                expectation.registerIndex,
+                isUsed) == SLANG_OK);
+        SLANG_CHECK(isUsed == expectation.expectedUsed);
+    }
+}