using Ryujinx.Graphics.Shader.IntermediateRepresentation; using Ryujinx.Graphics.Shader.Translation; using System; using System.Collections.Generic; using System.Numerics; namespace Ryujinx.Graphics.Shader.StructuredIr { static class StructuredProgram { // TODO: Eventually it should be possible to specify the parameter types for the function instead of using S32 for everything. private const AggregateType FuncParameterType = AggregateType.S32; public static StructuredProgramInfo MakeStructuredProgram( IReadOnlyList functions, AttributeUsage attributeUsage, ShaderDefinitions definitions, ResourceManager resourceManager, TargetLanguage targetLanguage, bool debugMode) { StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, debugMode); for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++) { Function function = functions[funcIndex]; BasicBlock[] blocks = function.Blocks; AggregateType returnType = function.ReturnsValue ? FuncParameterType : AggregateType.Void; AggregateType[] inArguments = new AggregateType[function.InArgumentsCount]; AggregateType[] outArguments = new AggregateType[function.OutArgumentsCount]; for (int i = 0; i < inArguments.Length; i++) { inArguments[i] = FuncParameterType; } for (int i = 0; i < outArguments.Length; i++) { outArguments[i] = FuncParameterType; } context.EnterFunction(blocks.Length, function.Name, returnType, inArguments, outArguments); PhiFunctions.Remove(blocks); for (int blkIndex = 0; blkIndex < blocks.Length; blkIndex++) { BasicBlock block = blocks[blkIndex]; context.EnterBlock(block); for (LinkedListNode opNode = block.Operations.First; opNode != null; opNode = opNode.Next) { Operation operation = (Operation)opNode.Value; if (IsBranchInst(operation.Inst)) { context.LeaveBlock(block, operation); } else { AddOperation(context, operation, targetLanguage); } } } GotoElimination.Eliminate(context.GetGotos()); AstOptimizer.Optimize(context); context.LeaveFunction(); } return context.Info; } private static void AddOperation(StructuredProgramContext context, Operation operation, TargetLanguage targetLanguage) { Instruction inst = operation.Inst; StorageKind storageKind = operation.StorageKind; if (inst == Instruction.Load || inst == Instruction.Store) { if (storageKind.IsInputOrOutput()) { IoVariable ioVariable = (IoVariable)operation.GetSource(0).Value; bool isOutput = storageKind.IsOutput(); int location = 0; int component = 0; if (context.Definitions.HasPerLocationInputOrOutput(ioVariable, isOutput)) { location = operation.GetSource(1).Value; if (operation.SourcesCount > 2 && operation.GetSource(2).Type == OperandType.Constant && context.Definitions.HasPerLocationInputOrOutputComponent(ioVariable, location, operation.GetSource(2).Value, isOutput)) { component = operation.GetSource(2).Value; } } context.Info.IoDefinitions.Add(new IoDefinition(storageKind, ioVariable, location, component)); } else if (storageKind == StorageKind.ConstantBuffer && operation.GetSource(0).Type == OperandType.Constant) { context.ResourceManager.SetUsedConstantBufferBinding(operation.GetSource(0).Value); } } bool vectorDest = IsVectorDestInst(inst); int sourcesCount = operation.SourcesCount; int outDestsCount = operation.DestsCount != 0 && !vectorDest ? operation.DestsCount - 1 : 0; IAstNode[] sources = new IAstNode[sourcesCount + outDestsCount]; if (inst == Instruction.Call && targetLanguage == TargetLanguage.Spirv) { // SPIR-V requires that all function parameters are copied to a local variable before the call // (or at least that's what the Khronos compiler does). // First one is the function index. sources[0] = context.GetOperandOrCbLoad(operation.GetSource(0)); // Remaining ones are parameters, copy them to a temp local variable. for (int index = 1; index < operation.SourcesCount; index++) { AstOperand argTemp = context.NewTemp(FuncParameterType); context.AddNode(new AstAssignment(argTemp, context.GetOperandOrCbLoad(operation.GetSource(index)))); sources[index] = argTemp; } } else { for (int index = 0; index < operation.SourcesCount; index++) { sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index)); } } for (int index = 0; index < outDestsCount; index++) { AstOperand oper = context.GetOperand(operation.GetDest(1 + index)); oper.VarType = InstructionInfo.GetSrcVarType(inst, sourcesCount + index); sources[sourcesCount + index] = oper; } AstTextureOperation GetAstTextureOperation(TextureOperation texOp) { return new AstTextureOperation(inst, texOp.Type, texOp.Format, texOp.Flags, texOp.Binding, texOp.Index, sources); } int componentsCount = BitOperations.PopCount((uint)operation.Index); if (vectorDest && componentsCount > 1) { AggregateType destType = InstructionInfo.GetDestVarType(inst); IAstNode source; if (operation is TextureOperation texOp) { if (texOp.Inst == Instruction.ImageLoad) { destType = texOp.Format.GetComponentType(); } source = GetAstTextureOperation(texOp); } else { source = new AstOperation( inst, operation.StorageKind, operation.ForcePrecise, operation.Index, sources, operation.SourcesCount); } AggregateType destElemType = destType; switch (componentsCount) { case 2: destType |= AggregateType.Vector2; break; case 3: destType |= AggregateType.Vector3; break; case 4: destType |= AggregateType.Vector4; break; } AstOperand destVec = context.NewTemp(destType); context.AddNode(new AstAssignment(destVec, source)); for (int i = 0; i < operation.DestsCount; i++) { AstOperand dest = context.GetOperand(operation.GetDest(i)); AstOperand index = new(OperandType.Constant, i); dest.VarType = destElemType; context.AddNode(new AstAssignment(dest, new AstOperation(Instruction.VectorExtract, StorageKind.None, false, new[] { destVec, index }, 2))); } } else if (operation.Dest != null) { AstOperand dest = context.GetOperand(operation.Dest); // If all the sources are bool, it's better to use short-circuiting // logical operations, rather than forcing a cast to int and doing // a bitwise operation with the value, as it is likely to be used as // a bool in the end. if (IsBitwiseInst(inst) && AreAllSourceTypesEqual(sources, AggregateType.Bool)) { inst = GetLogicalFromBitwiseInst(inst); } bool isCondSel = inst == Instruction.ConditionalSelect; bool isCopy = inst == Instruction.Copy; if (isCondSel || isCopy) { AggregateType type = GetVarTypeFromUses(operation.Dest); if (isCondSel && type == AggregateType.FP32) { inst |= Instruction.FP32; } dest.VarType = type; } else { dest.VarType = InstructionInfo.GetDestVarType(inst); } IAstNode source; if (operation is TextureOperation texOp) { if (texOp.Inst == Instruction.ImageLoad) { dest.VarType = texOp.Format.GetComponentType(); } source = GetAstTextureOperation(texOp); } else if (!isCopy) { source = new AstOperation( inst, operation.StorageKind, operation.ForcePrecise, operation.Index, sources, operation.SourcesCount); } else { source = sources[0]; } context.AddNode(new AstAssignment(dest, source)); } else if (operation.Inst == Instruction.Comment) { context.AddNode(new AstComment(((CommentNode)operation).Comment)); } else if (operation is TextureOperation texOp) { AstTextureOperation astTexOp = GetAstTextureOperation(texOp); context.AddNode(astTexOp); } else { context.AddNode(new AstOperation( inst, operation.StorageKind, operation.ForcePrecise, operation.Index, sources, operation.SourcesCount)); } // Those instructions needs to be emulated by using helper functions, // because they are NVIDIA specific. Those flags helps the backend to // decide which helper functions are needed on the final generated code. switch (operation.Inst) { case Instruction.MultiplyHighS32: context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighS32; break; case Instruction.MultiplyHighU32: context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighU32; break; case Instruction.SwizzleAdd: context.Info.HelperFunctionsMask |= HelperFunctionsMask.SwizzleAdd; break; case Instruction.FSIBegin: case Instruction.FSIEnd: context.Info.HelperFunctionsMask |= HelperFunctionsMask.FSI; break; } } private static AggregateType GetVarTypeFromUses(Operand dest) { HashSet visited = new(); Queue pending = new(); bool Enqueue(Operand operand) { if (visited.Add(operand)) { pending.Enqueue(operand); return true; } return false; } Enqueue(dest); while (pending.TryDequeue(out Operand operand)) { foreach (INode useNode in operand.UseOps) { if (useNode is not Operation operation) { continue; } if (operation.Inst == Instruction.Copy) { if (operation.Dest.Type == OperandType.LocalVariable) { if (Enqueue(operation.Dest)) { break; } } else { return OperandInfo.GetVarType(operation.Dest.Type); } } else { for (int index = 0; index < operation.SourcesCount; index++) { if (operation.GetSource(index) == operand) { return InstructionInfo.GetSrcVarType(operation.Inst, index); } } } } } return AggregateType.S32; } private static bool AreAllSourceTypesEqual(IAstNode[] sources, AggregateType type) { foreach (IAstNode node in sources) { if (node is not AstOperand operand) { return false; } if (operand.VarType != type) { return false; } } return true; } private static bool IsVectorDestInst(Instruction inst) { return inst switch { Instruction.ImageLoad or Instruction.TextureSample => true, _ => false, }; } private static bool IsBranchInst(Instruction inst) { return inst switch { Instruction.Branch or Instruction.BranchIfFalse or Instruction.BranchIfTrue => true, _ => false, }; } private static bool IsBitwiseInst(Instruction inst) { return inst switch { Instruction.BitwiseAnd or Instruction.BitwiseExclusiveOr or Instruction.BitwiseNot or Instruction.BitwiseOr => true, _ => false, }; } private static Instruction GetLogicalFromBitwiseInst(Instruction inst) { return inst switch { Instruction.BitwiseAnd => Instruction.LogicalAnd, Instruction.BitwiseExclusiveOr => Instruction.LogicalExclusiveOr, Instruction.BitwiseNot => Instruction.LogicalNot, Instruction.BitwiseOr => Instruction.LogicalOr, _ => throw new ArgumentException($"Unexpected instruction \"{inst}\"."), }; } } }