I'd like to update the ML on control-flow progress and
demonstrate some problems.
The WIP repo is here: https://github.com/HansKristian-Work/DXIL2SPIRV. I'm developing it as a standalone module for time being.
Control flow in DXIL is a complicated beast as it's a soup of gotos, as it is LLVM. The only saving grace is that it must be reducible, i.e. no branching straight into a loop, or arbitrary backward gotos.
The main problem with emitting SPIR-V is:
- For every conditional branch we need a selection merge
construct with a unique merge block which header dominates.
- For every loop header, we need a loop merge with designated
continue block and unique merge block which header dominates.
- Cannot break out of more than one loop construct at a time
(guess what DXIL does!).
The main complication currently is that we need ladder breaking. Here's a concrete example:
cbuffer Buff : register(b10, space1)
{
int count1;
int count2;
int data[1024];
};
float get_r()
{
float r = 0.0;
[loop]
for (int i = 0; i < count1; i++)
{
[loop]
for (int j = 0; j < count2; j++)
{
if (data[i ^ j] == 40)
return r; // <-- goto end;
when inlined
r += float(data[i ^ j]);
}
}
return r;
}
float4 main(float4 pos : POSITION, float4 pos2 : COLOR) :
SV_Position
{
float r = get_r();
return r.xxxx;
}
This gets compiled into:
...
define void @main() {
%Buff_cbuffer = call %dx.types.Handle @dx.op.createHandle(i32
57, i8 2, i32 0, i32 10, i1 false) ;
CreateHandle(resourceClass,rangeId,index,nonUniformIndex)
%1 = call %dx.types.CBufRet.i32
@dx.op.cbufferLoadLegacy.i32(i32 59, %dx.types.Handle
%Buff_cbuffer, i32 0) ; CBufferLoadLegacy(handle,regIndex)
%2 = extractvalue %dx.types.CBufRet.i32 %1, 0
%3 = icmp sgt i32 %2, 0
br i1 %3, label %.lr.ph2.preheader, label
%"\01?get_r@@YAMXZ.exit"
.lr.ph2.preheader: ; preds = %0
br label %.lr.ph2
.lr.ph2: ; preds =
%._crit_edge, %.lr.ph2.preheader
%i.i.0 = phi i32 [ %19, %._crit_edge ], [ 0,
%.lr.ph2.preheader ]
%r.i.0 = phi float [ %r.i.2, %._crit_edge ], [ 0.000000e+00,
%.lr.ph2.preheader ]
%4 = call %dx.types.CBufRet.i32
@dx.op.cbufferLoadLegacy.i32(i32 59, %dx.types.Handle
%Buff_cbuffer, i32 0) ; CBufferLoadLegacy(handle,regIndex)
%5 = extractvalue %dx.types.CBufRet.i32 %4, 1
%6 = icmp sgt i32 %5, 0
br i1 %6, label %.lr.ph.preheader, label %._crit_edge
.lr.ph.preheader: ; preds =
%.lr.ph2
br label %.lr.ph
.lr.ph: ; preds = %12,
%.lr.ph.preheader
%j.i.0 = phi i32 [ %15, %12 ], [ 0, %.lr.ph.preheader ]
%r.i.1 = phi float [ %14, %12 ], [ %r.i.0, %.lr.ph.preheader ]
%7 = xor i32 %j.i.0, %i.i.0
%8 = add i32 %7, 1
%9 = call %dx.types.CBufRet.i32
@dx.op.cbufferLoadLegacy.i32(i32 59, %dx.types.Handle
%Buff_cbuffer, i32 %8) ; CBufferLoadLegacy(handle,regIndex)
%10 = extractvalue %dx.types.CBufRet.i32 %9, 0
%11 = icmp eq i32 %10, 40
br i1 %11, label %"\01?get_r@@YAMXZ.exit.loopexit", label %12
; <label>:12 ; preds
= %.lr.ph
%13 = sitofp i32 %10 to float
%14 = fadd fast float %13, %r.i.1
%15 = add nuw nsw i32 %j.i.0, 1
%16 = call %dx.types.CBufRet.i32
@dx.op.cbufferLoadLegacy.i32(i32 59, %dx.types.Handle
%Buff_cbuffer, i32 0) ; CBufferLoadLegacy(handle,regIndex)
%17 = extractvalue %dx.types.CBufRet.i32 %16, 1
%18 = icmp slt i32 %15, %17
br i1 %18, label %.lr.ph, label %._crit_edge.loopexit,
!llvm.loop !25
._crit_edge.loopexit: ; preds = %12
br label %._crit_edge
._crit_edge: ; preds =
%._crit_edge.loopexit, %.lr.ph2
%r.i.2 = phi float [ %r.i.0, %.lr.ph2 ], [ %14,
%._crit_edge.loopexit ]
%19 = add nuw nsw i32 %i.i.0, 1
%20 = call %dx.types.CBufRet.i32
@dx.op.cbufferLoadLegacy.i32(i32 59, %dx.types.Handle
%Buff_cbuffer, i32 0) ; CBufferLoadLegacy(handle,regIndex)
%21 = extractvalue %dx.types.CBufRet.i32 %20, 0
%22 = icmp slt i32 %19, %21
br i1 %22, label %.lr.ph2, label
%"\01?get_r@@YAMXZ.exit.loopexit.11", !llvm.loop !27
"\01?get_r@@YAMXZ.exit.loopexit": ; preds =
%.lr.ph
br label %"\01?get_r@@YAMXZ.exit"
"\01?get_r@@YAMXZ.exit.loopexit.11": ; preds =
%._crit_edge
br label %"\01?get_r@@YAMXZ.exit"
"\01?get_r@@YAMXZ.exit": ; preds =
%"\01?get_r@@YAMXZ.exit.loopexit.11",
%"\01?get_r@@YAMXZ.exit.loopexit", %0
%.0 = phi float [ 0.000000e+00, %0 ], [ %r.i.1,
%"\01?get_r@@YAMXZ.exit.loopexit" ], [ %r.i.2,
%"\01?get_r@@YAMXZ.exit.loopexit.11" ]
call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 0,
float %.0) ; StoreOutput(outputSigId,rowIndex,colIndex,value)
call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 1,
float %.0) ; StoreOutput(outputSigId,rowIndex,colIndex,value)
call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 2,
float %.0) ; StoreOutput(outputSigId,rowIndex,colIndex,value)
call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 3,
float %.0) ; StoreOutput(outputSigId,rowIndex,colIndex,value)
ret void
}
...
The "return r;" inside the inner loop turns into a full "exit". This is a very common pattern where inlined leaf functions return. For a normal HLSL -> SPIR-V path, spirv-opt carefully introduces ladders to convert a return into a chain of breaks. We have no such luxury in DXIL. Basically, the return becomes a forward goto.
After a lot of implementation weirdness, I can turn this into SPIR-V which validates. Only control flow is emitted since I haven't looked at actual codegen. The resulting GLSL from SPIRV-Cross ends up looking something like:
#version 450
void main()
{
bool COND1;
if (COND1)
{
bool _lr_ph2_preheader;
bool _lr_ph2;
bool _lr_ph2_succ;
bool _lr_ph_preheader;
bool _lr_ph;
bool COND11;
bool _get_r_YAMXZ_exit_loopexit;
bool _crit_edge_loopexit_pred;
bool _crit_edge_loopexit;
bool _crit_edge;
do
{
if (_lr_ph2_succ)
{
do
{
if (_lr_ph)
{
break; // <-- goto end;
}
else
{
}
} while (!COND11);
if (!_crit_edge_loopexit_pred) // <-- Ladder
to handle goto
{
break;
}
}
} while (!_crit_edge);
bool _get_r_YAMXZ_exit_loopexit_11_pred;
if (_get_r_YAMXZ_exit_loopexit_11_pred) // <-- Ladder
to handle goto
{
bool _get_r_YAMXZ_exit_loopexit_11;
}
bool _get_r_YAMXZ_exit_loopexit_11_succ;
}
// end:
bool _get_r_YAMXZ_exit;
}
Some other complications left to consider will be to deal with Phi nodes properly, and switch blocks.
Cheers,
Hans-Kristian