forked from BilalY/Rasagar
537 lines
26 KiB
C#
537 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Burst.Compiler.IL.Syntax;
|
|
using Mono.Cecil;
|
|
using Mono.Cecil.Cil;
|
|
using Mono.Cecil.Rocks;
|
|
|
|
namespace zzzUnity.Burst.CodeGen
|
|
{
|
|
/// <summary>
|
|
/// Transforms a direct invoke on a burst function pointer into an calli, avoiding the need to marshal the delegate back.
|
|
/// </summary>
|
|
internal class FunctionPointerInvokeTransform
|
|
{
|
|
private struct CaptureInformation
|
|
{
|
|
public MethodReference Operand;
|
|
|
|
public List<Instruction> Captured;
|
|
}
|
|
|
|
private Dictionary<TypeReference, (MethodDefinition method, Instruction instruction)> _needsNativeFunctionPointer;
|
|
private Dictionary<MethodDefinition, TypeReference> _needsIl2cppInvoke;
|
|
private Dictionary<MethodDefinition, List<CaptureInformation>> _capturedSets;
|
|
private MethodDefinition _monoPInvokeAttributeCtorDef;
|
|
private MethodDefinition _nativePInvokeAttributeCtorDef;
|
|
private MethodDefinition _unmanagedFunctionPointerAttributeCtorDef;
|
|
private TypeReference _burstFunctionPointerType;
|
|
private TypeReference _burstCompilerType;
|
|
private TypeReference _systemType;
|
|
private TypeReference _callingConventionType;
|
|
|
|
private LogDelegate _debugLog;
|
|
private int _logLevel;
|
|
|
|
private AssemblyResolver _loader;
|
|
|
|
private ErrorDiagnosticDelegate _errorReport;
|
|
|
|
public readonly static bool enableInvokeAttribute = true;
|
|
public readonly static bool enableCalliOptimisation = false; // For now only run the pass on dots player/tiny
|
|
public readonly static bool enableUnmangedFunctionPointerInject = true;
|
|
|
|
public FunctionPointerInvokeTransform(AssemblyResolver loader,ErrorDiagnosticDelegate error, LogDelegate log = null, int logLevel = 0)
|
|
{
|
|
_loader = loader;
|
|
|
|
_needsNativeFunctionPointer = new Dictionary<TypeReference, (MethodDefinition, Instruction)>();
|
|
_needsIl2cppInvoke = new Dictionary<MethodDefinition, TypeReference>();
|
|
_capturedSets = new Dictionary<MethodDefinition, List<CaptureInformation>>();
|
|
_monoPInvokeAttributeCtorDef = null;
|
|
_unmanagedFunctionPointerAttributeCtorDef = null;
|
|
_nativePInvokeAttributeCtorDef = null; // Only present on DOTS_PLAYER
|
|
_burstFunctionPointerType = null;
|
|
_burstCompilerType = null;
|
|
_systemType = null;
|
|
_callingConventionType = null;
|
|
_debugLog = log;
|
|
_logLevel = logLevel;
|
|
_errorReport = error;
|
|
}
|
|
|
|
private AssemblyDefinition GetAsmDefinitionFromFile(AssemblyResolver loader, string assemblyName)
|
|
{
|
|
if (loader.TryResolve(AssemblyNameReference.Parse(assemblyName), out var result))
|
|
{
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void Initialize(AssemblyResolver loader, AssemblyDefinition assemblyDefinition, TypeSystem typeSystem)
|
|
{
|
|
if (_monoPInvokeAttributeCtorDef == null)
|
|
{
|
|
var burstAssembly = GetAsmDefinitionFromFile(loader, "Unity.Burst");
|
|
|
|
_burstFunctionPointerType = burstAssembly.MainModule.GetType("Unity.Burst.FunctionPointer`1");
|
|
_burstCompilerType = burstAssembly.MainModule.GetType("Unity.Burst.BurstCompiler");
|
|
|
|
var corLibrary = loader.Resolve(typeSystem.CoreLibrary as AssemblyNameReference);
|
|
// If the corLibrary is a redirecting assembly, then the type isn't present in Types
|
|
// and GetType() will therefore not find it, so instead we'll have to look it up in ExportedTypes
|
|
Func<string, TypeDefinition> getCorLibTy = (name) =>
|
|
{
|
|
return corLibrary.MainModule.GetType(name) ??
|
|
corLibrary.MainModule.ExportedTypes.FirstOrDefault(x => x.FullName == name)?.Resolve();
|
|
};
|
|
_systemType = getCorLibTy("System.Type"); // Only needed for MonoPInvokeCallback constructor in Unity
|
|
|
|
if (enableUnmangedFunctionPointerInject)
|
|
{
|
|
var unmanagedFunctionPointerAttribute = getCorLibTy("System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute");
|
|
_callingConventionType = getCorLibTy("System.Runtime.InteropServices.CallingConvention");
|
|
_unmanagedFunctionPointerAttributeCtorDef = unmanagedFunctionPointerAttribute.GetConstructors().Single(c => c.Parameters.Count == 1 && c.Parameters[0].ParameterType.MetadataType == _callingConventionType.MetadataType);
|
|
}
|
|
|
|
var asmDef = GetAsmDefinitionFromFile(loader, "UnityEngine.CoreModule");
|
|
// bail if we can't find a reference, handled gracefully later
|
|
if (asmDef == null)
|
|
return;
|
|
|
|
var monoPInvokeAttribute = asmDef.MainModule.GetType("AOT.MonoPInvokeCallbackAttribute");
|
|
_monoPInvokeAttributeCtorDef = monoPInvokeAttribute.GetConstructors().First();
|
|
}
|
|
|
|
}
|
|
|
|
public bool Run(AssemblyDefinition assemblyDefinition)
|
|
{
|
|
Initialize(_loader, assemblyDefinition, assemblyDefinition.MainModule.TypeSystem);
|
|
|
|
var types = assemblyDefinition.MainModule.GetTypes().ToArray();
|
|
foreach (var type in types)
|
|
{
|
|
CollectDelegateInvokesFromType(type);
|
|
}
|
|
|
|
return Finish();
|
|
}
|
|
|
|
public void CollectDelegateInvokesFromType(TypeDefinition type)
|
|
{
|
|
foreach (var m in type.Methods)
|
|
{
|
|
if (m.HasBody)
|
|
{
|
|
CollectDelegateInvokes(m);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool ProcessUnmanagedAttributeFixups()
|
|
{
|
|
if (_unmanagedFunctionPointerAttributeCtorDef == null)
|
|
return false;
|
|
|
|
bool modified = false;
|
|
|
|
foreach (var kp in _needsNativeFunctionPointer)
|
|
{
|
|
var delegateType = kp.Key;
|
|
var instruction = kp.Value.instruction;
|
|
var method = kp.Value.method;
|
|
var delegateDef = delegateType.Resolve();
|
|
|
|
var hasAttributeAlready = delegateDef.CustomAttributes.FirstOrDefault(x => x.AttributeType.FullName == _unmanagedFunctionPointerAttributeCtorDef.DeclaringType.FullName);
|
|
|
|
// If there is already an an attribute present
|
|
if (hasAttributeAlready!=null)
|
|
{
|
|
if (hasAttributeAlready.ConstructorArguments.Count==1)
|
|
{
|
|
var cc = (System.Runtime.InteropServices.CallingConvention)hasAttributeAlready.ConstructorArguments[0].Value;
|
|
if (cc == System.Runtime.InteropServices.CallingConvention.Cdecl)
|
|
{
|
|
if (_logLevel > 2) _debugLog?.Invoke($"UnmanagedAttributeFixups Skipping appending unmanagedFunctionPointerAttribute as already present aand calling convention matches");
|
|
}
|
|
else
|
|
{
|
|
// constructor with non cdecl calling convention
|
|
_errorReport(method, instruction, $"BurstCompiler.CompileFunctionPointer is only compatible with cdecl calling convention, this delegate type already has `[UnmanagedFunctionPointer(CallingConvention.{ Enum.GetName(typeof(System.Runtime.InteropServices.CallingConvention), cc) })]` please remove the attribute if you wish to use this function with Burst.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Empty constructor which defaults to Winapi which is incompatable
|
|
_errorReport(method, instruction, $"BurstCompiler.CompileFunctionPointer is only compatible with cdecl calling convention, this delegate type already has `[UnmanagedFunctionPointer]` please remove the attribute if you wish to use this function with Burst.");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var attribute = new CustomAttribute(delegateType.Module.ImportReference(_unmanagedFunctionPointerAttributeCtorDef));
|
|
attribute.ConstructorArguments.Add(new CustomAttributeArgument(delegateType.Module.ImportReference(_callingConventionType), System.Runtime.InteropServices.CallingConvention.Cdecl));
|
|
delegateDef.CustomAttributes.Add(attribute);
|
|
modified = true;
|
|
}
|
|
|
|
return modified;
|
|
}
|
|
|
|
private bool ProcessIl2cppInvokeFixups()
|
|
{
|
|
if (_monoPInvokeAttributeCtorDef == null)
|
|
return false;
|
|
|
|
bool modified = false;
|
|
foreach (var invokeNeeded in _needsIl2cppInvoke)
|
|
{
|
|
var declaringType = invokeNeeded.Value;
|
|
var implementationMethod = invokeNeeded.Key;
|
|
|
|
// Unity requires a type parameter for the attributecallback
|
|
if (declaringType == null)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Unable to automatically append CallbackAttribute due to missing declaringType for {implementationMethod}");
|
|
continue;
|
|
}
|
|
|
|
var attribute = new CustomAttribute(implementationMethod.Module.ImportReference(_monoPInvokeAttributeCtorDef));
|
|
attribute.ConstructorArguments.Add(new CustomAttributeArgument(implementationMethod.Module.ImportReference(_systemType), implementationMethod.Module.ImportReference(declaringType)));
|
|
implementationMethod.CustomAttributes.Add(attribute);
|
|
modified = true;
|
|
|
|
if (_logLevel > 1) _debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Added InvokeCallbackAttribute to {implementationMethod}");
|
|
}
|
|
|
|
return modified;
|
|
}
|
|
|
|
private bool ProcessFunctionPointerInvokes()
|
|
{
|
|
var madeChange = false;
|
|
foreach (var capturedData in _capturedSets)
|
|
{
|
|
var latePatchMethod = capturedData.Key;
|
|
var capturedList = capturedData.Value;
|
|
|
|
latePatchMethod.Body.SimplifyMacros(); // De-optimise short branches, since we will end up inserting instructions
|
|
|
|
foreach(var capturedInfo in capturedList)
|
|
{
|
|
var captured = capturedInfo.Captured;
|
|
var operand = capturedInfo.Operand;
|
|
|
|
if (captured.Count!=2)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.Finish: expected 2 instructions - Unable to optimise this reference");
|
|
continue;
|
|
}
|
|
|
|
if (_logLevel > 1) _debugLog?.Invoke($"FunctionPtrInvoke.Finish:{Environment.NewLine} latePatchMethod:{latePatchMethod}{Environment.NewLine} captureList:{capturedList}{Environment.NewLine} capture0:{captured[0]}{Environment.NewLine} operand:{operand}");
|
|
|
|
var processor = latePatchMethod.Body.GetILProcessor();
|
|
|
|
var genericContext = GenericContext.From(operand, operand.DeclaringType);
|
|
CallSite callsite;
|
|
try
|
|
{
|
|
callsite = new CallSite(genericContext.Resolve(operand.ReturnType))
|
|
{
|
|
CallingConvention = MethodCallingConvention.C
|
|
};
|
|
|
|
for (int oo = 0; oo < operand.Parameters.Count; oo++)
|
|
{
|
|
var param = operand.Parameters[oo];
|
|
var ty = genericContext.Resolve(param.ParameterType);
|
|
callsite.Parameters.Add(new ParameterDefinition(param.Name, param.Attributes, ty));
|
|
}
|
|
}
|
|
catch (NullReferenceException)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.Finish: Failed to resolve the generic context of `{operand}`");
|
|
continue;
|
|
}
|
|
|
|
// Make sure everything is in order before we make a change
|
|
|
|
var originalGetInvoke = captured[0];
|
|
|
|
if (originalGetInvoke.Operand is MethodReference mmr)
|
|
{
|
|
var genericMethodDef = mmr.Resolve();
|
|
|
|
var genericInstanceType = mmr.DeclaringType as GenericInstanceType;
|
|
var genericInstanceDef = genericInstanceType.Resolve();
|
|
|
|
// Locate the correct instance method - we know already at this point we have an instance of Function
|
|
MethodReference mr = default;
|
|
bool failed = true;
|
|
foreach (var m in genericInstanceDef.Methods)
|
|
{
|
|
if (m.FullName.Contains("get_Value"))
|
|
{
|
|
mr = m;
|
|
failed = false;
|
|
break;
|
|
}
|
|
}
|
|
if (failed)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.Finish: failed to locate get_Value method on {genericInstanceDef} - Unable to optimise this reference");
|
|
continue;
|
|
}
|
|
|
|
var newGenericRef = new MethodReference(mr.Name, mr.ReturnType, genericInstanceType)
|
|
{
|
|
HasThis = mr.HasThis,
|
|
ExplicitThis = mr.ExplicitThis,
|
|
CallingConvention = mr.CallingConvention
|
|
};
|
|
foreach (var param in mr.Parameters)
|
|
newGenericRef.Parameters.Add(new ParameterDefinition(param.ParameterType));
|
|
foreach (var gparam in mr.GenericParameters)
|
|
newGenericRef.GenericParameters.Add(new GenericParameter(gparam.Name, newGenericRef));
|
|
var importRef = latePatchMethod.Module.ImportReference(newGenericRef);
|
|
var newMethodCall = processor.Create(OpCodes.Call, importRef);
|
|
|
|
// Replace get_invoke with get_Value - Don't use replace though as if the original call is target of a branch
|
|
//the branch doesn't get updated.
|
|
originalGetInvoke.OpCode = newMethodCall.OpCode;
|
|
originalGetInvoke.Operand = newMethodCall.Operand;
|
|
|
|
// Add local to capture result
|
|
var newLocal = new VariableDefinition(mr.ReturnType);
|
|
latePatchMethod.Body.Variables.Add(newLocal);
|
|
|
|
// Store result of get_Value
|
|
var storeInst = processor.Create(OpCodes.Stloc, newLocal);
|
|
processor.InsertAfter(originalGetInvoke, storeInst);
|
|
|
|
// Swap invoke with calli
|
|
var calli = processor.Create(OpCodes.Calli, callsite);
|
|
// We can use replace here, since we already checked this is in the same Basic Block, and thus can't be target of a branch
|
|
processor.Replace(captured[1], calli);
|
|
|
|
// Insert load local prior to calli
|
|
var loadValue = processor.Create(OpCodes.Ldloc, newLocal);
|
|
processor.InsertBefore(calli, loadValue);
|
|
|
|
if (_logLevel > 1) _debugLog?.Invoke($"FunctionPtrInvoke.Finish: Optimised {originalGetInvoke} with {newMethodCall}");
|
|
|
|
madeChange = true;
|
|
}
|
|
}
|
|
|
|
latePatchMethod.Body.OptimizeMacros(); // Re-optimise branches
|
|
}
|
|
return madeChange;
|
|
}
|
|
|
|
public bool Finish()
|
|
{
|
|
bool madeChange = false;
|
|
|
|
if (enableInvokeAttribute)
|
|
{
|
|
madeChange |= ProcessIl2cppInvokeFixups();
|
|
}
|
|
|
|
if (enableUnmangedFunctionPointerInject)
|
|
{
|
|
madeChange |= ProcessUnmanagedAttributeFixups();
|
|
}
|
|
|
|
if (enableCalliOptimisation)
|
|
{
|
|
madeChange |= ProcessFunctionPointerInvokes();
|
|
}
|
|
|
|
return madeChange;
|
|
}
|
|
|
|
private bool IsBurstFunctionPointerMethod(MethodReference methodRef, string method, out GenericInstanceType methodInstance)
|
|
{
|
|
methodInstance = methodRef?.DeclaringType as GenericInstanceType;
|
|
return (methodInstance != null && methodInstance.ElementType.FullName == _burstFunctionPointerType.FullName && methodRef.Name == method);
|
|
}
|
|
|
|
private bool IsBurstCompilerMethod(GenericInstanceMethod methodRef, string method)
|
|
{
|
|
var methodInstance = methodRef?.DeclaringType as TypeReference;
|
|
return (methodInstance != null && methodInstance.FullName == _burstCompilerType.FullName && methodRef.Name == method);
|
|
}
|
|
|
|
private void LocateFunctionPointerTCreation(MethodDefinition m, Instruction i)
|
|
{
|
|
if (i.OpCode == OpCodes.Call)
|
|
{
|
|
var genInstMethod = i.Operand as GenericInstanceMethod;
|
|
|
|
var isBurstCompilerCompileFunctionPointer = IsBurstCompilerMethod(genInstMethod, "CompileFunctionPointer");
|
|
var isBurstFunctionPointerGetInvoke = IsBurstFunctionPointerMethod(i.Operand as MethodReference, "get_Invoke", out var instanceType);
|
|
if (!(isBurstCompilerCompileFunctionPointer || isBurstFunctionPointerGetInvoke)) return;
|
|
|
|
if (enableUnmangedFunctionPointerInject)
|
|
{
|
|
var delegateType = isBurstCompilerCompileFunctionPointer ? genInstMethod.GenericArguments[0].Resolve() : instanceType.GenericArguments[0].Resolve();
|
|
// We check for null, since unfortunately it is possible that the call is wrapped inside
|
|
//another open delegate and we cannot determine the delegate type
|
|
if (delegateType != null && !_needsNativeFunctionPointer.ContainsKey(delegateType))
|
|
{
|
|
_needsNativeFunctionPointer.Add(delegateType, (m, i));
|
|
}
|
|
}
|
|
|
|
// No need to process further if its not a CompileFunctionPointer method
|
|
if (!isBurstCompilerCompileFunctionPointer) return;
|
|
|
|
if (enableInvokeAttribute)
|
|
{
|
|
// Currently only handles the following pre-pattern (which should cover most common uses)
|
|
// ldftn ...
|
|
// newobj ...
|
|
|
|
if (i.Previous?.OpCode != OpCodes.Newobj)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Unable to automatically append CallbackAttribute due to not finding NewObj {i.Previous}");
|
|
return;
|
|
}
|
|
|
|
var newObj = i.Previous;
|
|
if (newObj.Previous?.OpCode != OpCodes.Ldftn)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Unable to automatically append CallbackAttribute due to not finding LdFtn {newObj.Previous}");
|
|
return;
|
|
}
|
|
|
|
var ldFtn = newObj.Previous;
|
|
|
|
// Determine the delegate type
|
|
var methodDefinition = newObj.Operand as MethodDefinition;
|
|
var declaringType = methodDefinition?.DeclaringType;
|
|
|
|
// Fetch the implementation method
|
|
var implementationMethod = ldFtn.Operand as MethodDefinition;
|
|
|
|
var hasInvokeAlready = implementationMethod?.CustomAttributes.FirstOrDefault(x =>
|
|
(x.AttributeType.FullName == _monoPInvokeAttributeCtorDef.DeclaringType.FullName)
|
|
|| (_nativePInvokeAttributeCtorDef != null && x.AttributeType.FullName == _nativePInvokeAttributeCtorDef.DeclaringType.FullName));
|
|
|
|
if (hasInvokeAlready != null)
|
|
{
|
|
if (_logLevel > 2) _debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Skipping appending Callback Attribute as already present {hasInvokeAlready}");
|
|
return;
|
|
}
|
|
|
|
if (implementationMethod == null)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Unable to automatically append CallbackAttribute due to missing method from {ldFtn} {ldFtn.Operand}");
|
|
return;
|
|
}
|
|
|
|
if (implementationMethod.CustomAttributes.FirstOrDefault(x => x.Constructor.DeclaringType.Name == "BurstCompileAttribute") == null)
|
|
{
|
|
_debugLog?.Invoke($"FunctionPtrInvoke.LocateFunctionPointerTCreation: Unable to automatically append CallbackAttribute due to missing burst attribute from {implementationMethod}");
|
|
return;
|
|
}
|
|
|
|
// Need to add the custom attribute
|
|
if (!_needsIl2cppInvoke.ContainsKey(implementationMethod))
|
|
{
|
|
_needsIl2cppInvoke.Add(implementationMethod, declaringType);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Obsolete("Will be removed in a future Burst verison")]
|
|
public bool IsInstructionForFunctionPointerInvoke(MethodDefinition m, Instruction i)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
private void CollectDelegateInvokes(MethodDefinition m)
|
|
{
|
|
if (!(enableCalliOptimisation || enableInvokeAttribute || enableUnmangedFunctionPointerInject))
|
|
return;
|
|
|
|
bool hitGetInvoke = false;
|
|
TypeDefinition delegateType = null;
|
|
List<Instruction> captured = null;
|
|
|
|
foreach (var inst in m.Body.Instructions)
|
|
{
|
|
if (_logLevel > 2) _debugLog?.Invoke($"FunctionPtrInvoke.CollectDelegateInvokes: CurrentInstruction {inst} {inst.Operand}");
|
|
|
|
// Check for a FunctionPointerT creation
|
|
if (enableUnmangedFunctionPointerInject || enableInvokeAttribute)
|
|
{
|
|
LocateFunctionPointerTCreation(m, inst);
|
|
}
|
|
|
|
if (enableCalliOptimisation)
|
|
{
|
|
if (!hitGetInvoke)
|
|
{
|
|
if (inst.OpCode != OpCodes.Call) continue;
|
|
if (!IsBurstFunctionPointerMethod(inst.Operand as MethodReference, "get_Invoke", out var methodInstance)) continue;
|
|
|
|
// At this point we have a call to a FunctionPointer.Invoke
|
|
hitGetInvoke = true;
|
|
|
|
delegateType = methodInstance.GenericArguments[0].Resolve();
|
|
|
|
captured = new List<Instruction>();
|
|
|
|
captured.Add(inst); // Capture the get_invoke, we will swap this for get_value and a store to local
|
|
}
|
|
else
|
|
{
|
|
if (!(inst.OpCode.FlowControl == FlowControl.Next || inst.OpCode.FlowControl == FlowControl.Call))
|
|
{
|
|
// Don't perform transform across blocks
|
|
hitGetInvoke = false;
|
|
}
|
|
else
|
|
{
|
|
if (inst.OpCode == OpCodes.Callvirt)
|
|
{
|
|
if (inst.Operand is MethodReference mref)
|
|
{
|
|
var method = mref.Resolve();
|
|
|
|
if (method.DeclaringType == delegateType)
|
|
{
|
|
hitGetInvoke = false;
|
|
|
|
List<CaptureInformation> storage = null;
|
|
if (!_capturedSets.TryGetValue(m, out storage))
|
|
{
|
|
storage = new List<CaptureInformation>();
|
|
_capturedSets.Add(m, storage);
|
|
}
|
|
|
|
// Capture the invoke - which we will swap for a load local (stored from the get_value) and a calli
|
|
captured.Add(inst);
|
|
var captureInfo = new CaptureInformation { Captured = captured, Operand = mref };
|
|
if (_logLevel > 1) _debugLog?.Invoke($"FunctionPtrInvoke.CollectDelegateInvokes: captureInfo:{captureInfo}{Environment.NewLine}capture0{captured[0]}");
|
|
storage.Add(captureInfo);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hitGetInvoke = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |