Repository: shader-slang/slang
Pull request #6725: Metal remove void field
Author: login: kaizhangNV
Labels: name: pr: non-breaking
description: PRs without breaking changes
Created: 2025-04-01T20:10:12Z | Merged: 2025-04-02T17:34:58Z

Remove argument in make_struct call corresponding to void field

    
- This is a follow-up of #6543, where we leave the VoidType field as it in
  make_struct call during legalization pass.
    
  So during cleaning_void IR pass, when we remove "VoidType" from struct,
  we will have to also clean up the argument corresponding to the
  "VoidType" field.

- Reapply "Eliminate empty struct on metal target (#6603)" which is reverted in (#6711)
  Because #6603 is not the root casuse. #6543 is.


Diff:
diff --git a/source/slang/slang-emit.cpp b/source/slang/slang-emit.cpp
index 841f44a80a8..8d7577b520b 100644
--- a/source/slang/slang-emit.cpp
+++ b/source/slang/slang-emit.cpp
@@ -1278,6 +1278,15 @@ Result linkAndOptimizeIR(
         //
         legalizeResourceTypes(targetProgram, irModule, sink);
 
+        // We also need to legalize empty types for Metal targets.
+        switch (target)
+        {
+        case CodeGenTarget::Metal:
+        case CodeGenTarget::MetalLib:
+        case CodeGenTarget::MetalLibAssembly:
+            legalizeEmptyTypes(targetProgram, irModule, sink);
+            break;
+        }
         //  Debugging output of legalization
 #if 0
         dumpIRIfEnabled(codeGenContext, irModule, "LEGALIZED");
diff --git a/source/slang/slang-ir-cleanup-void.cpp b/source/slang/slang-ir-cleanup-void.cpp
index 3a776fc4fb2..84532d0ec9c 100644
--- a/source/slang/slang-ir-cleanup-void.cpp
+++ b/source/slang/slang-ir-cleanup-void.cpp
@@ -122,6 +122,8 @@ struct CleanUpVoidContext
         case kIROp_StructType:
             {
                 List<IRInst*> toRemove;
+                UInt fieldCount = 0;
+                ShortList<UInt> voidFieldIndex;
                 for (auto child : inst->getChildren())
                 {
                     if (auto field = as<IRStructField>(child))
@@ -129,11 +131,33 @@ struct CleanUpVoidContext
                         if (field->getFieldType()->getOp() == kIROp_VoidType)
                         {
                             toRemove.add(field);
+                            voidFieldIndex.add(fieldCount);
                         }
                     }
+                    fieldCount++;
                 }
                 for (auto ii : toRemove)
                     ii->removeAndDeallocate();
+
+                // Once we remove the void fields in the struct, we also need update the make_struct
+                // call sites to remove the arguments corresponding to the void fields.
+                if (inst->hasUses() && voidFieldIndex.getCount())
+                {
+                    UInt currentFieldCount = fieldCount - toRemove.getCount();
+                    for (auto use = inst->firstUse; use; use = use->nextUse)
+                    {
+                        if (auto makeStructInst = as<IRMakeStruct>(use->user))
+                        {
+                            if (makeStructInst->getOperandCount() != currentFieldCount)
+                            {
+                                for (Int i = 0; i < voidFieldIndex.getCount(); i++)
+                                {
+                                    makeStructInst->removeOperand(voidFieldIndex[i]);
+                                }
+                            }
+                        }
+                    }
+                }
             }
             break;
         default:
diff --git a/source/slang/slang-ir-legalize-types.cpp b/source/slang/slang-ir-legalize-types.cpp
index 197cd93c53d..837193abcc0 100644
--- a/source/slang/slang-ir-legalize-types.cpp
+++ b/source/slang/slang-ir-legalize-types.cpp
@@ -4123,6 +4123,11 @@ struct IREmptyTypeLegalizationContext : IRTypeLegalizationContext
 
     bool isSimpleType(IRType* type) override
     {
+        if (isMetalTarget(targetProgram->getTargetReq()))
+        {
+            return false;
+        }
+
         // If type is used as public interface, then treat it as simple.
         for (auto decor : type->getDecorations())
         {
@@ -4145,6 +4150,11 @@ struct IREmptyTypeLegalizationContext : IRTypeLegalizationContext
     {
         return LegalType();
     }
+
+    virtual bool shouldLegalizeParameterBlockElementType() override
+    {
+        return isMetalTarget(targetProgram->getTargetReq());
+    }
 };
 
 // The main entry points that are used when transforming IR code
diff --git a/source/slang/slang-legalize-types.cpp b/source/slang/slang-legalize-types.cpp
index e26475522de..cc2c12d4202 100644
--- a/source/slang/slang-legalize-types.cpp
+++ b/source/slang/slang-legalize-types.cpp
@@ -1211,11 +1211,15 @@ LegalType legalizeTypeImpl(TypeLegalizationContext* context, IRType* type)
         LegalType legalElementType;
 
         if (isMetalTarget(context->targetProgram->getTargetReq()) &&
-            as<IRParameterBlockType>(uniformBufferType))
+            as<IRParameterBlockType>(uniformBufferType) &&
+            !context->shouldLegalizeParameterBlockElementType())
         {
             // On Metal, we do not need to legalize the element type of
             // a parameter block because we can translate it directly into
             // an argument buffer.
+            //
+            // But we do need empty type legalized for Metal, because Metal doesn't
+            // allow empty struct in argument buffer.
             legalElementType = LegalType::simple(originalElementType);
         }
         else
diff --git a/source/slang/slang-legalize-types.h b/source/slang/slang-legalize-types.h
index ae76cbd39d0..0c705c4fae5 100644
--- a/source/slang/slang-legalize-types.h
+++ b/source/slang/slang-legalize-types.h
@@ -659,6 +659,12 @@ struct IRTypeLegalizationContext
         IROp op,
         LegalType legalElementType,
         IRInst* layoutOperand) = 0;
+
+    /// Customization point to decide whether a parameter block type should be legalized.
+    ///
+    /// This function is called in `legalizeTypeImpl` to decide whether a parameter block
+    /// type should be legalized. Not all legalization passes need to legalize parameter block.
+    virtual bool shouldLegalizeParameterBlockElementType() { return false; }
 };
 
 // This typedef exists to support pre-existing code from when
diff --git a/tests/metal/empty-struct-remove.slang b/tests/metal/empty-struct-remove.slang
new file mode 100644
index 00000000000..5ac1ccb693a
--- /dev/null
+++ b/tests/metal/empty-struct-remove.slang
@@ -0,0 +1,33 @@
+
+//TEST:SIMPLE(filecheck=LIB):-target metallib -entry computeMain -stage compute
+//TEST:SIMPLE(filecheck=METAL):-target metal -entry computeMain -stage compute
+
+// METAL-NOT: struct emptyStruct
+struct emptyStruct
+{
+    void set(RWStructuredBuffer<int> buffer, int value) {buffer[0] = value;}
+}
+
+
+struct MyStruct
+{
+    RWStructuredBuffer<int> buffer;
+    int value;
+    void set()
+    {
+        e.set(buffer, value);
+    }
+    emptyStruct e;
+}
+
+ParameterBlock<MyStruct> param;
+
+// LIB: @computeMain
+[shader("compute")]
+[numthreads(1, 1, 1)]
+void computeMain(
+    uint tid: SV_DispatchThreadID,
+)
+{
+    param.set();
+}


Reviews:
user: login: jkwak-work
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T04:30:47Z
user: login: kaizhangNV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T05:16:41Z
user: login: kaizhangNV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T05:21:56Z
user: login: kaizhangNV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T05:22:05Z
user: login: jkwak-work
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T06:26:12Z
user: login: kaizhangNV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:40:10Z
user: login: jkwak-work
body: Looks good to me
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T16:15:45Z

Review comments:
diff_hunk: @@ -0,0 +1,33 @@
+
+//TEST:SIMPLE(filecheck=LIB):-target metallib -entry computeMain -stage compute -DMETAL
+//TEST:SIMPLE(filecheck=METAL):-target metal -entry computeMain -stage compute -DMETAL
path: tests/metal/empty-struct-remove.slang
user: login: jkwak-work
body: I don't think `-DMETAL` is needed here, because this file doesn't use the macro.
I guess this lines are copied from another test case where the test uses "METAL" macro in a way like `#ifdef METAL`
created_at: 2025-04-02T04:24:04Z
author_association: COLLABORATOR
diff_hunk: @@ -659,6 +659,8 @@ struct IRTypeLegalizationContext
         IROp op,
         LegalType legalElementType,
         IRInst* layoutOperand) = 0;
+
+    virtual bool shouldLegalizeParameterBlockElementType() { return false; }
path: source/slang/slang-legalize-types.h
user: login: jkwak-work
body: I think this function deserves a comment that describes what it is for.
The other functions have explanations.
created_at: 2025-04-02T04:26:17Z
author_association: COLLABORATOR
diff_hunk: @@ -1211,11 +1211,15 @@ LegalType legalizeTypeImpl(TypeLegalizationContext* context, IRType* type)
         LegalType legalElementType;
 
         if (isMetalTarget(context->targetProgram->getTargetReq()) &&
-            as<IRParameterBlockType>(uniformBufferType))
+            as<IRParameterBlockType>(uniformBufferType) &&
+            !context->shouldLegalizeParameterBlockElementType())
         {
path: source/slang/slang-legalize-types.cpp
user: login: jkwak-work
body: This seems little confusing to me.
It reads as "isMetalTarget && .. && ! isMetalTarget".
It looks like it has two conflicting conditions: is it metal target and is it not metal target.

In what situation this condition is expected to be true??
created_at: 2025-04-02T04:30:31Z
author_association: COLLABORATOR
diff_hunk: @@ -1211,11 +1211,15 @@ LegalType legalizeTypeImpl(TypeLegalizationContext* context, IRType* type)
         LegalType legalElementType;
 
         if (isMetalTarget(context->targetProgram->getTargetReq()) &&
-            as<IRParameterBlockType>(uniformBufferType))
+            as<IRParameterBlockType>(uniformBufferType) &&
+            !context->shouldLegalizeParameterBlockElementType())
         {
path: source/slang/slang-legalize-types.cpp
user: login: kaizhangNV
body: why do you read as this ? 
"isMetalTarget && .. && ! isMetalTarget"


it's 

```
isMetalTarget &&
!context->shouldLegalizeParameterBlockElementType()
```
created_at: 2025-04-02T05:16:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+
+//TEST:SIMPLE(filecheck=LIB):-target metallib -entry computeMain -stage compute -DMETAL
+//TEST:SIMPLE(filecheck=METAL):-target metal -entry computeMain -stage compute -DMETAL
path: tests/metal/empty-struct-remove.slang
user: login: kaizhangNV
body: updated.
created_at: 2025-04-02T05:21:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -659,6 +659,8 @@ struct IRTypeLegalizationContext
         IROp op,
         LegalType legalElementType,
         IRInst* layoutOperand) = 0;
+
+    virtual bool shouldLegalizeParameterBlockElementType() { return false; }
path: source/slang/slang-legalize-types.h
user: login: kaizhangNV
body: done
created_at: 2025-04-02T05:22:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1211,11 +1211,15 @@ LegalType legalizeTypeImpl(TypeLegalizationContext* context, IRType* type)
         LegalType legalElementType;
 
         if (isMetalTarget(context->targetProgram->getTargetReq()) &&
-            as<IRParameterBlockType>(uniformBufferType))
+            as<IRParameterBlockType>(uniformBufferType) &&
+            !context->shouldLegalizeParameterBlockElementType())
         {
path: source/slang/slang-legalize-types.cpp
user: login: jkwak-work
body: That part is what's little confusing to me.
I see that
```
    virtual bool shouldLegalizeParameterBlockElementType() override
    {
        return isMetalTarget(targetProgram->getTargetReq());
    }
```
So it reads as "isMetalTarget && !isMetalTarget", but probably that's not the case.
created_at: 2025-04-02T06:26:12Z
author_association: COLLABORATOR
diff_hunk: @@ -1211,11 +1211,15 @@ LegalType legalizeTypeImpl(TypeLegalizationContext* context, IRType* type)
         LegalType legalElementType;
 
         if (isMetalTarget(context->targetProgram->getTargetReq()) &&
-            as<IRParameterBlockType>(uniformBufferType))
+            as<IRParameterBlockType>(uniformBufferType) &&
+            !context->shouldLegalizeParameterBlockElementType())
         {
path: source/slang/slang-legalize-types.cpp
user: login: kaizhangNV
body: This is only for one pass, but the other passes, this does not necessarily depends on metal.
The `TypeLegalizationContext` is an abstract class, that all the type legalization passes inherit from it. So only when it's the empty type legalization pass, we will return true for metal, and we do want to legalize parameter block for metal, so it will not hit this if condition (Note this if condition means that we don't want to legalize a parameter block as it simple return a `simple` legal vale). But for other type legalization passes, e.g. resource/existantial, we always return `false` for parameter block.

This is the pattern that how to legalize different aspects of a type for different targets.


created_at: 2025-04-02T15:40:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: kaizhangNV
created_at: 2025-04-02T04:23:36Z
body: The only actual change in this PR is in this file: source/slang/slang-ir-cleanup-void.cpp
author_association: CONTRIBUTOR
user: login: kaizhangNV
created_at: 2025-04-02T16:20:09Z
body: I Just rebase my PR to re-run the CI because I merged a new change to enable MacOS CI test https://github.com/shader-slang/slang/pull/6731
author_association: CONTRIBUTOR