diff options
| -rw-r--r-- | docs/user-guide/03-convenience-features.md | 60 | ||||
| -rw-r--r-- | docs/user-guide/toc.html | 1 | ||||
| -rw-r--r-- | source/slang/core.meta.slang | 44 | ||||
| -rw-r--r-- | source/slang/slang-check-decl.cpp | 2 | ||||
| -rw-r--r-- | source/slang/slang-check-expr.cpp | 4 | ||||
| -rw-r--r-- | source/slang/slang-diagnostic-defs.h | 2 | ||||
| -rw-r--r-- | source/slang/slang-emit.cpp | 3 | ||||
| -rw-r--r-- | source/slang/slang-ir-inst-defs.h | 27 | ||||
| -rw-r--r-- | source/slang/slang-ir-insts.h | 22 | ||||
| -rw-r--r-- | source/slang/slang-ir-legalize-empty-array.cpp | 196 | ||||
| -rw-r--r-- | source/slang/slang-ir-legalize-empty-array.h | 11 | ||||
| -rw-r--r-- | tests/diagnostics/array-neg-size.slang (renamed from tests/diagnostics/array-zero-size.slang) | 2 | ||||
| -rw-r--r-- | tests/language-feature/0-array-1.slang | 31 | ||||
| -rw-r--r-- | tests/language-feature/0-array.slang | 64 | ||||
| -rw-r--r-- | tests/language-feature/cond-field.slang | 64 |
15 files changed, 511 insertions, 22 deletions
diff --git a/docs/user-guide/03-convenience-features.md b/docs/user-guide/03-convenience-features.md index f49fdce90..3f659015b 100644 --- a/docs/user-guide/03-convenience-features.md +++ b/docs/user-guide/03-convenience-features.md @@ -446,9 +446,65 @@ int caller() } ``` +## `Conditional<T, bool condition>` Type + +A `Conditional` type can be used to define struct fields that can be specialized away. If `condition` is `false`, the field will be removed +by the compiler from the target code. This is useful for scenarios where a developer would like to make sure a field is not defined in a +specialized shader variant when it is not used by the shader. + +For example, a common use case is to define the vertex shader output / fragment shader input: + +```slang +interface IVertex +{ + property float3 position{get;} + property Optional<float3> normal{get;} + property Optional<float3> color{get;} +} + +struct Vertex<bool hasNormal, bool hasColor> : IVertex +{ + private float3 m_position; + private Conditional<float3, hasNormal> m_normal; + private Conditional<float3, hasColor> m_color; + + __init(float3 position, float3 normal, float3 color) + { + m_position = position; + m_normal = normal; + m_color = color; + } + + property float3 position + { + get { return m_position; } + } + property Optional<float3> normal + { + get { return m_normal; } + } + property Optional<float3> color + { + get { return m_color; } + } +} +``` + +In this example, `Vertex` type is parameterized on `hasNormal` and `hasColor`. If `hasNormal` is false, the `m_normal` field will be eliminated in the target code, allowing a specialized vertex shader to declare minimum output fields. For example, a vertex shader +can be defined as follows: + +```slang +[shader("vertex")] +Vertex<hasNormal, hasColor> vertMain<bool hasNormal, bool hasColor>(VertexIn inputVertex) +{ + ... +} +``` + + ## `if_let` syntax -Slang supports `if (let name = expr)` syntax to simplify the code when working with `Optional<T>` value. The syntax is similar to Rust's -`if let` syntax, the value expression must be an `Optional<T>` type, for example: +Slang supports `if (let name = expr)` syntax to simplify the code when working with `Optional<T>` or `Conditional<T, hasValue>` value. The syntax is similar to Rust's +`if let` syntax, the value expression must be an `Optional<T>` or `Conditional<T, hasValue>` type, for example: ```csharp Optional<int> getOptInt() { ... } diff --git a/docs/user-guide/toc.html b/docs/user-guide/toc.html index 5e8ecc207..1621d1da5 100644 --- a/docs/user-guide/toc.html +++ b/docs/user-guide/toc.html @@ -42,6 +42,7 @@ <li data-link="convenience-features#subscript-operator"><span>Subscript Operator</span></li> <li data-link="convenience-features#tuple-types"><span>Tuple Types</span></li> <li data-link="convenience-features#optionalt-type"><span>`Optional<T>` type</span></li> +<li data-link="convenience-features#conditionalt-bool-condition-type"><span>`Conditional<T, bool condition>` Type</span></li> <li data-link="convenience-features#if_let-syntax"><span>`if_let` syntax</span></li> <li data-link="convenience-features#reinterprett-operation"><span>`reinterpret<T>` operation</span></li> <li data-link="convenience-features#pointers-limited"><span>Pointers (limited)</span></li> diff --git a/source/slang/core.meta.slang b/source/slang/core.meta.slang index 484f51bfc..51e2f326e 100644 --- a/source/slang/core.meta.slang +++ b/source/slang/core.meta.slang @@ -1513,6 +1513,50 @@ bool operator!=(__none_t noneVal, Optional<T> val) return val.hasValue; } +struct Conditional<T, bool hasValue> +{ + internal T storage[hasValue]; + + __implicit_conversion($(kConversionCost_ValToOptional)) + [__unsafeForceInlineEarly] + __init(T val) { if (hasValue) storage[0] = val;} + + [__unsafeForceInlineEarly] + public Optional<T> get() + { + if (hasValue) + { + return Optional<T>(storage[0]); + } + else + { + return none; + } + } + + [__unsafeForceInlineEarly] + [mutating] + public void set(T value) + { + if (hasValue) + storage[0] = value; + } +} + +extension<T> Optional<T> +{ + __implicit_conversion($(kConversionCost_ImplicitDereference)) + __generic<bool condHasValue> + [__unsafeForceInlineEarly] + __init(Conditional<T, condHasValue> condVal) + { + if (condHasValue) + this = Optional<T>(condVal.storage[0]); + else + this = none; + } +} + //@public: /// A variadic generic storing the product of several types. diff --git a/source/slang/slang-check-decl.cpp b/source/slang/slang-check-decl.cpp index e3b05ec00..aa84a057f 100644 --- a/source/slang/slang-check-decl.cpp +++ b/source/slang/slang-check-decl.cpp @@ -10120,7 +10120,7 @@ void SemanticsVisitor::validateArraySizeForVariable(VarDeclBase* varDecl) // TODO(tfoley): How to handle the case where bound isn't known? auto elementCount = arrayType->getElementCount(); - if (GetMinBound(elementCount) <= 0) + if (GetMinBound(elementCount) < 0) { getSink()->diagnose(varDecl, Diagnostics::invalidArraySize); return; diff --git a/source/slang/slang-check-expr.cpp b/source/slang/slang-check-expr.cpp index 1e052a553..66c2f9796 100644 --- a/source/slang/slang-check-expr.cpp +++ b/source/slang/slang-check-expr.cpp @@ -2414,10 +2414,10 @@ Expr* SemanticsExprVisitor::visitIndexExpr(IndexExpr* subscriptExpr) nullptr, ConstantFoldingKind::SpecializationConstant); - // Validate that array size is greater than zero + // Validate that array size is non-negative. if (auto constElementCount = as<ConstantIntVal>(elementCount)) { - if (constElementCount->getValue() <= 0) + if (constElementCount->getValue() < 0) { getSink()->diagnose( subscriptExpr->indexExprs[0], diff --git a/source/slang/slang-diagnostic-defs.h b/source/slang/slang-diagnostic-defs.h index 465602a33..3786a0cf3 100644 --- a/source/slang/slang-diagnostic-defs.h +++ b/source/slang/slang-diagnostic-defs.h @@ -657,7 +657,7 @@ DIAGNOSTIC( Error, cannotConvertArrayOfSmallerToLargerSize, "Cannot convert array of size $0 to array of size $1 as this would truncate data") -DIAGNOSTIC(30025, Error, invalidArraySize, "array size must be larger than zero.") +DIAGNOSTIC(30025, Error, invalidArraySize, "array size must be non-negative.") DIAGNOSTIC( 30026, Error, diff --git a/source/slang/slang-emit.cpp b/source/slang/slang-emit.cpp index 20459c722..88533d938 100644 --- a/source/slang/slang-emit.cpp +++ b/source/slang/slang-emit.cpp @@ -56,6 +56,7 @@ #include "slang-ir-layout.h" #include "slang-ir-legalize-array-return-type.h" #include "slang-ir-legalize-binary-operator.h" +#include "slang-ir-legalize-empty-array.h" #include "slang-ir-legalize-global-values.h" #include "slang-ir-legalize-image-subscript.h" #include "slang-ir-legalize-mesh-outputs.h" @@ -1158,6 +1159,8 @@ Result linkAndOptimizeIR( addUserTypeHintDecorations(irModule); } + legalizeEmptyArray(irModule, sink); + // We don't need the legalize pass for C/C++ based types if (options.shouldLegalizeExistentialAndResourceTypes) { diff --git a/source/slang/slang-ir-inst-defs.h b/source/slang/slang-ir-inst-defs.h index 7a281bac4..eac337deb 100644 --- a/source/slang/slang-ir-inst-defs.h +++ b/source/slang/slang-ir-inst-defs.h @@ -428,19 +428,20 @@ INST(Load, load, 1, 0) INST(Store, store, 2, 0) // Atomic Operations -INST(AtomicLoad, atomicLoad, 1, 0) -INST(AtomicStore, atomicStore, 2, 0) -INST(AtomicExchange, atomicExchange, 2, 0) -INST(AtomicCompareExchange, atomicCompareExchange, 3, 0) -INST(AtomicAdd, atomicAdd, 2, 0) -INST(AtomicSub, atomicSub, 2, 0) -INST(AtomicAnd, atomicAnd, 2, 0) -INST(AtomicOr, atomicOr, 2, 0) -INST(AtomicXor, atomicXor, 2, 0) -INST(AtomicMin, atomicMin, 2, 0) -INST(AtomicMax, atomicMax, 2, 0) -INST(AtomicInc, atomicInc, 1, 0) -INST(AtomicDec, atomicDec, 1, 0) + INST(AtomicLoad, atomicLoad, 1, 0) + INST(AtomicStore, atomicStore, 2, 0) + INST(AtomicExchange, atomicExchange, 2, 0) + INST(AtomicCompareExchange, atomicCompareExchange, 3, 0) + INST(AtomicAdd, atomicAdd, 2, 0) + INST(AtomicSub, atomicSub, 2, 0) + INST(AtomicAnd, atomicAnd, 2, 0) + INST(AtomicOr, atomicOr, 2, 0) + INST(AtomicXor, atomicXor, 2, 0) + INST(AtomicMin, atomicMin, 2, 0) + INST(AtomicMax, atomicMax, 2, 0) + INST(AtomicInc, atomicInc, 1, 0) + INST(AtomicDec, atomicDec, 1, 0) +INST_RANGE(AtomicOperation, AtomicLoad, AtomicDec) // Produced and removed during backward auto-diff pass as a temporary placeholder representing the // currently accumulated derivative to pass to some dOut argument in a nested call. diff --git a/source/slang/slang-ir-insts.h b/source/slang/slang-ir-insts.h index b5c1a6475..a1f66d142 100644 --- a/source/slang/slang-ir-insts.h +++ b/source/slang/slang-ir-insts.h @@ -2500,7 +2500,14 @@ struct IRLoad : IRInst IRInst* getPtr() { return ptr.get(); } }; -struct IRAtomicLoad : IRInst +struct IRAtomicOperation : IRInst +{ + IR_PARENT_ISA(AtomicOperation); + + IRInst* getPtr() { return getOperand(0); } +}; + +struct IRAtomicLoad : IRAtomicOperation { IRUse ptr; IR_LEAF_ISA(AtomicLoad) @@ -2521,7 +2528,7 @@ struct IRStore : IRInst IRUse* getValUse() { return &val; } }; -struct IRAtomicStore : IRInst +struct IRAtomicStore : IRAtomicOperation { IRUse ptr; IRUse val; @@ -3074,6 +3081,17 @@ struct IREach : IRInst IRInst* getElement() { return getOperand(0); } }; +struct IRMakeArray : IRInst +{ + IR_LEAF_ISA(MakeArray) +}; + +struct IRMakeArrayFromElement : IRInst +{ + IR_LEAF_ISA(MakeArrayFromElement) +}; + + // An Instruction that creates a tuple value. struct IRMakeTuple : IRInst { diff --git a/source/slang/slang-ir-legalize-empty-array.cpp b/source/slang/slang-ir-legalize-empty-array.cpp new file mode 100644 index 000000000..561327565 --- /dev/null +++ b/source/slang/slang-ir-legalize-empty-array.cpp @@ -0,0 +1,196 @@ +#include "slang-ir-legalize-empty-array.h" + +#include "slang-ir-insts.h" +#include "slang-ir-util.h" +#include "slang-ir.h" + +namespace Slang +{ +struct EmptyArrayLoweringContext +{ + IRModule* module; + DiagnosticSink* sink; + + InstWorkList workList; + InstHashSet workListSet; + + Dictionary<IRInst*, IRInst*> replacements; + + EmptyArrayLoweringContext(IRModule* module) + : module(module), workList(module), workListSet(module) + { + } + + void addToWorkList(IRInst* inst) + { + for (auto ii = inst->getParent(); ii; ii = ii->getParent()) + { + if (as<IRGeneric>(ii)) + return; + } + + if (workListSet.contains(inst)) + return; + + workList.add(inst); + workListSet.add(inst); + } + + bool isEmptyArray(IRType* t) + { + const auto lenLit = composeGetters<IRIntLit>(t, &IRArrayType::getElementCount); + return lenLit ? getIntVal(lenLit) == 0 : false; + }; + + bool hasEmptyArrayType(IRInst* i) { return isEmptyArray(i->getDataType()); } + + bool hasEmptyArrayPtrType(IRInst* i) + { + const auto ptr = as<IRPtrTypeBase>(i->getDataType()); + return ptr && isEmptyArray(ptr->getValueType()); + } + + // Visit each instruction and replace it with legalized instructiosn if necessary. + void processInst(IRInst* inst) + { + IRInst* replacement = nullptr; + + IRBuilder builder(module); + builder.setInsertBefore(inst); + replacement = instMatch<IRInst*>( + inst, + nullptr, + // The following match instructions which take a 0-sized array as an + // operand and produces a result value for the inst. + [&](IRGetElement* getElement) + { + const auto base = getElement->getBase(); + return hasEmptyArrayType(base) ? builder.emitUndefined(getElement->getDataType()) + : nullptr; + }, + [&](IRGetElementPtr* gep) + { + const auto base = gep->getBase(); + return hasEmptyArrayPtrType(base) || base->getOp() == kIROp_undefined + ? builder.emitUndefined(gep->getDataType()) + : nullptr; + }, + [&](IRFieldAddress* gep) + { + const auto base = gep->getBase(); + return base->getOp() == kIROp_undefined ? builder.emitUndefined(gep->getDataType()) + : nullptr; + }, + [&](IRLoad* load) + { + return load->getOperand(0)->getOp() == kIROp_undefined + ? builder.emitUndefined(load->getDataType()) + : nullptr; + }, + [&](IRImageLoad* load) + { + return load->getOperand(0)->getOp() == kIROp_undefined + ? builder.emitUndefined(load->getDataType()) + : nullptr; + }, + [&](IRStore* store) + { + if (store->getPtr()->getOp() == kIROp_undefined) + store->removeAndDeallocate(); + return nullptr; + }, + [&](IRAtomicStore* store) + { + if (store->getPtr()->getOp() == kIROp_undefined) + store->removeAndDeallocate(); + return nullptr; + }, + [&](IRImageStore* store) + { + if (store->getImage()->getOp() == kIROp_undefined) + store->removeAndDeallocate(); + return nullptr; + }, + [&](IRImageSubscript* subscript) + { + return subscript->getImage()->getOp() == kIROp_undefined + ? builder.emitUndefined(subscript->getDataType()) + : nullptr; + }, + [&](IRAtomicOperation* atomic) + { + return atomic->getOperand(0)->getOp() == kIROp_undefined + ? builder.emitUndefined(atomic->getDataType()) + : nullptr; + }, + // The following should match any instruction which can construct a 0-sized array. + [&](IRMakeArray* makeArray) { + return hasEmptyArrayType(makeArray->getDataType()) ? builder.getVoidValue() + : nullptr; + }, + [&](IRMakeArrayFromElement* makeArray) { + return hasEmptyArrayType(makeArray->getDataType()) ? builder.getVoidValue() + : nullptr; + }); + + // If we did get a replacement, add that to our mapping and return + // it, otherwise return the original (to maybe be updated later) + if (replacement) + { + inst->replaceUsesWith(replacement); + inst->removeAndDeallocate(); + addToWorkList(replacement); + for (auto use = replacement->firstUse; use; use = use->nextUse) + { + addToWorkList(use->getUser()); + } + } + else if (isEmptyArray((IRType*)inst)) + { + replacements.add(inst, builder.getVoidType()); + } + } + + void processModule() + { + addToWorkList(module->getModuleInst()); + + while (workList.getCount() != 0) + { + IRInst* inst = workList.getLast(); + + workList.removeLast(); + workListSet.remove(inst); + + // Run this inst through the replacer + processInst(inst); + + for (auto child = inst->getLastChild(); child; child = child->getPrevInst()) + { + addToWorkList(child); + } + } + + // Apply all deferred replacements + // + // It's important to defer this as if we were updating things + // on-the-fly we would be losing information about what was + // actually a 0-array or not. + for (const auto& [old, replacement] : replacements) + { + if (old != replacement) + { + old->replaceUsesWith(replacement); + old->removeAndDeallocate(); + } + } + } +}; + +void legalizeEmptyArray(IRModule* module, DiagnosticSink* sink) +{ + EmptyArrayLoweringContext context(module); + context.sink = sink; + context.processModule(); +} +} // namespace Slang diff --git a/source/slang/slang-ir-legalize-empty-array.h b/source/slang/slang-ir-legalize-empty-array.h new file mode 100644 index 000000000..657c18403 --- /dev/null +++ b/source/slang/slang-ir-legalize-empty-array.h @@ -0,0 +1,11 @@ +#pragma once + +namespace Slang +{ +struct IRModule; +class DiagnosticSink; + +// Legalize 0-sized arrays to `void` types. +void legalizeEmptyArray(IRModule* module, DiagnosticSink* sink); + +} // namespace Slang diff --git a/tests/diagnostics/array-zero-size.slang b/tests/diagnostics/array-neg-size.slang index f67f163fd..660f0b6c5 100644 --- a/tests/diagnostics/array-zero-size.slang +++ b/tests/diagnostics/array-neg-size.slang @@ -10,4 +10,4 @@ void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID) bar(); } //CHECK: ([[# @LINE+1]]): error 30025 -func bar() -> int[0]; +func bar() -> int[-1]; diff --git a/tests/language-feature/0-array-1.slang b/tests/language-feature/0-array-1.slang new file mode 100644 index 000000000..327f71444 --- /dev/null +++ b/tests/language-feature/0-array-1.slang @@ -0,0 +1,31 @@ +//TEST:SIMPLE(filecheck=SPV): -target spirv +//TEST:SIMPLE(filecheck=HLSL): -target hlsl -profile cs_6_0 -entry computeMain + +struct MyData +{ + int a[0][0][0]; +} + +uniform MyData* myData; +uniform int * output; + +[numthreads(1, 1, 1)] +void computeMain() +{ + // These are all ill-formed, but we want to still ensure our backend + // can handle them gracefully without crashing. + // In actual user code, any access to 0-sized arrays should be protected + // by a `if` statement that checks the size before accessing. + // The condition would then evaluate to false and causing all the accessing + // code to be optimized out. + InterlockedAdd(myData.a[0][0][0], 1); + myData.a[0][0][0] += 1; + output[0] = myData.a[0][0][0]; +} + +//SPV: OpEntryPoint +//SPV-NOT: OpAtomic +//SPV-NOT: OpStore +//SPV-NOT: OpLoad + +//HLSL: computeMain diff --git a/tests/language-feature/0-array.slang b/tests/language-feature/0-array.slang new file mode 100644 index 000000000..7379faace --- /dev/null +++ b/tests/language-feature/0-array.slang @@ -0,0 +1,64 @@ +//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK): +//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK): -vk + +interface IVertex +{ + property float3 position{get;} + property Optional<float3> normal{get;} + property Optional<float3> color{get;} +} + +struct Vertex<bool hasNormal, bool hasColor> : IVertex +{ + private float3 m_position; + private float3 m_normal[hasNormal]; + private float3 m_color[hasColor]; + + __init(float3 position, float3 normal, float3 color) + { + m_position = position; + if (hasNormal) m_normal[0] = normal; + if (hasColor) m_color[0] = color; + } + + property float3 position + { + get { return m_position; } + } + property Optional<float3> normal + { + get { if (hasNormal) return m_normal[0]; else return none; } + } + property Optional<float3> color + { + get { if (hasColor) return m_color[0]; else return none; } + } +} + +//TEST_INPUT:ubuffer(data=[0 0 0 0], stride=4):out,name=outputBuffer +RWStructuredBuffer<int> outputBuffer; + +void test<V:IVertex>(V vert) +{ + // CHECK: 0 + // CHECK: 0 + // CHECK: 1 + // CHECK: 3 + if (let normal = vert.normal) + { + outputBuffer[0] = 1; + outputBuffer[1] = (int)normal.x; + } + + if (let color = vert.color) + { + outputBuffer[2] = 1; + outputBuffer[3] = (int)color.x; + } +} + +[numthreads(1,1,1)] +void computeMain() +{ + test<Vertex<false, true>>(Vertex<false, true>(1.0, 2.0, 3.0)); +}
\ No newline at end of file diff --git a/tests/language-feature/cond-field.slang b/tests/language-feature/cond-field.slang new file mode 100644 index 000000000..5e9f4a06e --- /dev/null +++ b/tests/language-feature/cond-field.slang @@ -0,0 +1,64 @@ +//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK): +//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK): -vk + +interface IVertex +{ + property float3 position{get;} + property Optional<float3> normal{get;} + property Optional<float3> color{get;} +} + +struct Vertex<bool hasNormal, bool hasColor> : IVertex +{ + private float3 m_position; + private Conditional<float3, hasNormal> m_normal; + private Conditional<float3, hasColor> m_color; + + __init(float3 position, float3 normal, float3 color) + { + m_position = position; + m_normal = normal; + m_color = color; + } + + property float3 position + { + get { return m_position; } + } + property Optional<float3> normal + { + get { return m_normal; } + } + property Optional<float3> color + { + get { return m_color; } + } +} + +//TEST_INPUT:ubuffer(data=[0 0 0 0], stride=4):out,name=outputBuffer +RWStructuredBuffer<int> outputBuffer; + +void test<V:IVertex>(V vert) +{ + // CHECK: 0 + // CHECK: 0 + // CHECK: 1 + // CHECK: 3 + if (let normal = vert.normal) + { + outputBuffer[0] = 1; + outputBuffer[1] = (int)normal.x; + } + + if (let color = vert.color) + { + outputBuffer[2] = 1; + outputBuffer[3] = (int)color.x; + } +} + +[numthreads(1,1,1)] +void computeMain() +{ + test<Vertex<false, true>>(Vertex<false, true>(1.0, 2.0, 3.0)); +}
\ No newline at end of file |
