Skip to content

Commit 0f6765b

Browse files
author
Tim Foley
authored
Refactor the flow of type legalization (shader-slang#1594)
The existing type legalization logic worked as a single preorder pass over the IR tree. This could create problems in cases where an instruction might be processed before one of its operands (e.g., a function that references a global shader parameter is processed before that parameter). This change makes it so that type legalization uses a work list, and only adds instructions to the work list once their parent, type, and operands have been processed. As a result, we should be able to guarantee that an instruction will only be processed once all of its operands have been. One wrinkle here is that in the current IR it is possible to end up with a cycle of uses for global-scope instructions, specifically around interface types and their list of requirements. This change includes a short-term kludge to break those cycles and allow the pass to complete. As it stands, this is simply a refactoring pass and no new functionality is introduced. The changes are necessary to unblock work in a feature branch that depends on type legalization being more robust against IR that might use an unexpected ordering.
1 parent c985f5f commit 0f6765b

File tree

1 file changed

+192
-30
lines changed

1 file changed

+192
-30
lines changed

source/slang/slang-ir-legalize-types.cpp

+192-30
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,9 @@ static LegalVal legalizeInst(
14021402
case kIROp_GlobalParam:
14031403
return legalizeGlobalParam(context, cast<IRGlobalParam>(inst));
14041404

1405+
case kIROp_Block:
1406+
return LegalVal::simple(inst);
1407+
14051408
default:
14061409
break;
14071410
}
@@ -1515,27 +1518,6 @@ static void addParamType(List<IRType*>& ioParamTypes, LegalType t)
15151518
}
15161519
}
15171520

1518-
static void legalizeInstsInParent(
1519-
IRTypeLegalizationContext* context,
1520-
IRInst* parent)
1521-
{
1522-
IRInst* nextChild = nullptr;
1523-
for(auto child = parent->getFirstDecorationOrChild(); child; child = nextChild)
1524-
{
1525-
nextChild = child->getNextInst();
1526-
1527-
if (auto block = as<IRBlock>(child))
1528-
{
1529-
legalizeInstsInParent(context, block);
1530-
}
1531-
else
1532-
{
1533-
LegalVal legalVal = legalizeInst(context, child);
1534-
registerLegalizedValue(context, child, legalVal);
1535-
}
1536-
}
1537-
}
1538-
15391521
static LegalVal legalizeFunc(
15401522
IRTypeLegalizationContext* context,
15411523
IRFunc* irFunc)
@@ -1575,7 +1557,6 @@ static LegalVal legalizeFunc(
15751557

15761558
context->builder->setDataType(irFunc, newFuncType);
15771559

1578-
legalizeInstsInParent(context, irFunc);
15791560
return LegalVal::simple(irFunc);
15801561
}
15811562

@@ -2470,19 +2451,200 @@ static LegalVal legalizeGlobalParam(
24702451
}
24712452
}
24722453

2454+
struct IRTypeLegalizationPass
2455+
{
2456+
IRTypeLegalizationContext* context;
2457+
2458+
// The goal of this pass is to ensure that legalization has been
2459+
// applied to each instruction in a module. We also want to
2460+
// ensure that an insturction doesn't get processed until after
2461+
// all of its operands have been processed.
2462+
//
2463+
// The basic idea will be to maintain a work list of instructions
2464+
// that are able to be processed, and also a set to track which
2465+
// instructions have ever been added to the work list.
2466+
2467+
List<IRInst*> workList;
2468+
HashSet<IRInst*> addedToWorkListSet;
2469+
2470+
// We will add a simple query to check whether an instruciton
2471+
// has been put on the work list before (or if it should be
2472+
// treated *as if* it has been placed on the work list).
2473+
//
2474+
bool hasBeenAddedToWorkList(IRInst* inst)
2475+
{
2476+
// Sometimes we end up with instructions that have a null
2477+
// operand (mostly as the type field of key instructions
2478+
// like the module itself).
2479+
//
2480+
// We want to treat such null pointers like we would an
2481+
// already-processed instruction.
2482+
//
2483+
if(!inst) return true;
2484+
2485+
// HACK(tfoley): In most cases it is structurally invalid for our
2486+
// IR to have a cycle where following the operands (or type) of
2487+
// instructions can lead back to the original instruction.
2488+
//
2489+
// (Note that circular dependencies are still possible, but they
2490+
// must generally be from *children* of an instruction back
2491+
// to the instruction itself. E.g., an instruction in the body
2492+
// of a function can directly or indirectly depend on that function.)
2493+
//
2494+
// The one key counterexample is with interface types, because their
2495+
// requirements and the expected types of those requirements are
2496+
// encoded as operands. A typical method on inteface `I` will have a type
2497+
// that involves a `ThisType<I>` parameter for `this`, and that creates
2498+
// a cycle back to `I`.
2499+
//
2500+
// In our type legalization pass we are going to manually break that
2501+
// cycle by treating all `IRInterfaceRequirementEntry` instructions
2502+
// as having already been processed, since there is no particular
2503+
// need for us to handle them as part of legalization.
2504+
//
2505+
if(inst->op == kIROp_InterfaceRequirementEntry) return true;
2506+
2507+
return addedToWorkListSet.Contains(inst);
2508+
}
2509+
2510+
// Next we define a convenience routine for adding something to the work list.
2511+
//
2512+
void addToWorkList(IRInst* inst)
2513+
{
2514+
// We want to avoid adding anything we've already added or processed.
2515+
//
2516+
if(addedToWorkListSet.Contains(inst))
2517+
return;
2518+
2519+
workList.add(inst);
2520+
addedToWorkListSet.Add(inst);
2521+
}
2522+
2523+
void processModule(IRModule* module)
2524+
{
2525+
// In order to process an entire module, we start by adding the
2526+
// root module insturction to our work list, and then we will
2527+
// proceed to process instructions until the work list goes dry.
2528+
//
2529+
addToWorkList(module->getModuleInst());
2530+
while( workList.getCount() != 0 )
2531+
{
2532+
// The order of items in the work list is signficiant;
2533+
// later entries could depend on earlier ones. As such, we
2534+
// cannot just do something like the `fastRemoveAt(...)`
2535+
// operation that could potentially lead to instructions
2536+
// being processed in a different order than they were added.
2537+
//
2538+
// Instead, we will make a copy of the current work list
2539+
// at each step, and swap in an empty work list to be added
2540+
// to with any new instructions.
2541+
//
2542+
List<IRInst*> workListCopy;
2543+
Swap(workListCopy, workList);
2544+
2545+
// Now we simply process each instruction on the copy of
2546+
// the work list, knowing that `processInst` may add additional
2547+
// instructions to the original work list.
2548+
//
2549+
for( auto inst : workListCopy )
2550+
{
2551+
processInst(inst);
2552+
}
2553+
}
2554+
2555+
// After we are done, there might be various instructions that
2556+
// were marked for deletion, but have not yet been cleaned up.
2557+
//
2558+
// We will clean up all those unnecessary instructions now.
2559+
//
2560+
for (auto& lv : context->replacedInstructions)
2561+
{
2562+
lv->removeAndDeallocate();
2563+
}
2564+
}
2565+
2566+
void processInst(IRInst* inst)
2567+
{
2568+
// The main logic for legalizing an instruction is defined
2569+
// earlier in this file.
2570+
//
2571+
// Our primary task here is to legalize the instruction, and then
2572+
// register the result of legalization as the proper value
2573+
// for that instruction.
2574+
//
2575+
LegalVal legalVal = legalizeInst(context, inst);
2576+
registerLegalizedValue(context, inst, legalVal);
2577+
2578+
// Once the instruction has been processed, we need to consider
2579+
// whether any other instructions might now be ready to process.
2580+
//
2581+
// An instruction `i` might have been blocked by `inst` if:
2582+
//
2583+
// * `inst` was an operand (including the type operand) of `i`, or
2584+
// * `inst` was the parent of `i`.
2585+
//
2586+
// To turn those relationships around, we want to check instructions
2587+
// `i` where:
2588+
//
2589+
// * `i` is a user of `inst`, or
2590+
// * `i` is a child of `inst`.
2591+
//
2592+
for( auto use = inst->firstUse; use; use = use->nextUse )
2593+
{
2594+
auto user = use->getUser();
2595+
maybeAddToWorkList(user);
2596+
}
2597+
for( auto child : inst->getDecorationsAndChildren() )
2598+
{
2599+
maybeAddToWorkList(child);
2600+
}
2601+
}
2602+
2603+
void maybeAddToWorkList(IRInst* inst)
2604+
{
2605+
// Here we have an `inst` that might be ready to go on
2606+
// the work list, but we need to check that it would
2607+
// be valid to put it on there.
2608+
//
2609+
// First, we don't want to add something if it has
2610+
// already been added.
2611+
//
2612+
if(hasBeenAddedToWorkList(inst))
2613+
return;
2614+
2615+
// Next, we don't want to add something if its parent
2616+
// hasn't been added already.
2617+
//
2618+
if(!hasBeenAddedToWorkList(inst->getParent()))
2619+
return;
2620+
2621+
// Finally, we don't want to add something if its
2622+
// type and/or operands haven't all been added.
2623+
//
2624+
if(!hasBeenAddedToWorkList(inst->getFullType()))
2625+
return;
2626+
Index operandCount = (Index) inst->getOperandCount();
2627+
for( Index i = 0; i < operandCount; ++i )
2628+
{
2629+
auto operand = inst->getOperand(i);
2630+
if(!hasBeenAddedToWorkList(operand))
2631+
return;
2632+
}
2633+
2634+
// If all those checks pass, then we are ready to
2635+
// process `inst`, and we will add it to our work list.
2636+
//
2637+
addToWorkList(inst);
2638+
}
2639+
};
24732640

24742641
static void legalizeTypes(
24752642
IRTypeLegalizationContext* context)
24762643
{
2477-
// Legalize all the top-level instructions in the module
2478-
auto module = context->module;
2479-
legalizeInstsInParent(context, module->moduleInst);
2644+
IRTypeLegalizationPass pass;
2645+
pass.context = context;
24802646

2481-
// Clean up after any instructions we replaced along the way.
2482-
for (auto& lv : context->replacedInstructions)
2483-
{
2484-
lv->removeAndDeallocate();
2485-
}
2647+
pass.processModule(context->module);
24862648
}
24872649

24882650
// We use the same basic type legalization machinery for both simplifying

0 commit comments

Comments
 (0)