// Copyright Epic Games, Inc. All Rights Reserved. #include "MetaHumanParameterMappingTable.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformProperties.h" #include "Engine/DataTable.h" #include "Engine/Texture2D.h" #include "Misc/ConfigCacheIni.h" #include "PerQualityLevelProperties.h" #include "UObject/UnrealType.h" #if WITH_EDITOR #include "Interfaces/ITargetPlatform.h" #endif bool FMetaHumanParameterValue::operator==(const FMetaHumanParameterValue& Other) const { if (Type != Other.Type) { return false; } // Data for inactive types is ignored switch (Type) { case EMetaHumanParameterValueType::Invalid: // No data to compare return true; case EMetaHumanParameterValueType::Texture: return TextureValue == Other.TextureValue; case EMetaHumanParameterValueType::Name: return NameValue == Other.NameValue; case EMetaHumanParameterValueType::Color: return ColorValue == Other.ColorValue; case EMetaHumanParameterValueType::Float: return FloatValue == Other.FloatValue; case EMetaHumanParameterValueType::Bool: return bBoolValue == Other.bBoolValue; default: // Unhandled type checkNoEntry(); return false; } } bool FMetaHumanParameterValue::Matches(const FMetaHumanParameterMappingInput& MappingInput) const { if (!ensureMsgf(MappingInput.Type == EMetaHumanParameterMappingInputSourceType::Parameter, TEXT("Comparing a parameter value to a mapping input that's not a parameter. Mapping input name is %s"), *MappingInput.Name.ToString())) { return false; } // Data for inactive types is ignored switch (Type) { case EMetaHumanParameterValueType::Invalid: // Can't match with an invalid value return false; case EMetaHumanParameterValueType::Texture: // Textures are not (yet?) supported as an input type return false; case EMetaHumanParameterValueType::Name: return NameValue == MappingInput.NameValue; case EMetaHumanParameterValueType::Color: // Colors are not (yet?) supported as an input type return false; case EMetaHumanParameterValueType::Float: return FloatValue == MappingInput.FloatValue; case EMetaHumanParameterValueType::Bool: return bBoolValue == MappingInput.bBoolValue; default: // Unhandled type checkNoEntry(); return false; } } FMetaHumanCompiledParameterMappingTable::FMetaHumanCompiledParameterMappingTable( TArray&& InMappings, TMap&& InReachableScalabilityValues) : Mappings(InMappings) , ReachableScalabilityValues(InReachableScalabilityValues) { } void FMetaHumanCompiledParameterMappingTable::Evaluate( const TMap& TableInputParameters, const TArray& ConsoleVariableOverrides, const FOutputParameterDelegate& OutputParameterDelegate) const { #if WITH_EDITOR // Use the running platform (i.e. the editor platform) if no target platform was set when this // table was compiled. const FName ParameterMappingPlatformName = TargetPlatformName != NAME_None ? TargetPlatformName : FPlatformProperties::PlatformName(); #endif for (const FMetaHumanParameterMapping& Mapping : Mappings) { int32 MatchingRowIndex = INDEX_NONE; TSet OutOfRangeScalabilityVariables; for (int32 RowIndex = 0; RowIndex < Mapping.Rows.Num(); RowIndex++) { const FMetaHumanParameterMappingRow& Row = Mapping.Rows[RowIndex]; bool bRowMatches = true; for (const FMetaHumanParameterMappingInput& RowInput : Row.InputParameters) { bool bInputMatches = true; switch (RowInput.Type) { case EMetaHumanParameterMappingInputSourceType::Parameter: { const FMetaHumanParameterValue* TableInputValue = TableInputParameters.Find(RowInput.Name); if (!TableInputValue) { // TODO: Throw error: A required parameter has not been passed in bInputMatches = false; break; } if (!TableInputValue->Matches(RowInput)) { bInputMatches = false; } } break; case EMetaHumanParameterMappingInputSourceType::Scalability: { const IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*RowInput.Name.ToString()); if (!CVar) { // TODO: Throw error: Couldn't find cvar bInputMatches = false; break; } if (!CVar->TestFlags(static_cast(ECVF_Scalability | ECVF_ScalabilityGroup))) { // TODO: Throw error: Not a scalability cvar. Use ConsoleVariable type instead. bInputMatches = false; break; } const FMetaHumanParameterMappingInput* Override = Algo::FindBy(ConsoleVariableOverrides, RowInput.Name, &FMetaHumanParameterMappingInput::Name); int32 ValueToUse; if (CVar->IsVariableInt()) { if (Override) { ValueToUse = FMath::RoundToInt32(Override->FloatValue); } else { ValueToUse = CVar->GetInt(); } } else if (CVar->IsVariableFloat()) { if (Override) { ValueToUse = FMath::RoundToInt32(Override->FloatValue); } else { ValueToUse = FMath::RoundToInt32(CVar->GetFloat()); } } else { // TODO: Throw error: Unsupported cvar type bInputMatches = false; break; } if (!OutOfRangeScalabilityVariables.Contains(RowInput.Name)) { if (const FMetaHumanScalabilityValueSet* ReachableValues = ReachableScalabilityValues.Find(RowInput.Name)) { if (!ReachableValues->Values.Contains(ValueToUse)) { // Don't warn about this variable again OutOfRangeScalabilityVariables.Add(RowInput.Name); // TODO: Improve this message. Note that this error may show during PIE if the CO was compiled for a specific target platform. This is not necessarily a problem. UE_LOG(LogTemp, Error, TEXT("Scalability variable %s is set to %i, which is outside the range of expected values"), *RowInput.Name.ToString(), ValueToUse); } } } if (!FMath::IsNearlyEqual(static_cast(ValueToUse), RowInput.FloatValue)) { bInputMatches = false; } } break; case EMetaHumanParameterMappingInputSourceType::Platform: { // On cooked platforms, platform inputs should be stripped from the mapping at cook time check(!FPlatformProperties::RequiresCookedData()); #if WITH_EDITOR const FMetaHumanParameterMappingInput* Override = Algo::FindBy(ConsoleVariableOverrides, TEXT("Platform"), &FMetaHumanParameterMappingInput::Name); if (Override) { if (Override->NameValue != RowInput.Name) { bInputMatches = false; } } else { if (ParameterMappingPlatformName != RowInput.Name) { bInputMatches = false; } } #endif // WITH_EDITOR } break; default: // TODO: Throw error: Unsupported source type bInputMatches = false; } if (!bInputMatches) { bRowMatches = false; break; } } if (bRowMatches) { MatchingRowIndex = RowIndex; break; } } // The compiler should generate a set of rows where at least one is guaranteed to match, // but if it doesn't we handle it as gracefully as possible here. if (MatchingRowIndex == INDEX_NONE) { // The compiler shouldn't allow Rows to be empty. // // It may be impossible to recover gracefully at this point if Rows is empty, so we // can't really continue. // // For example, if we just leave the output parameter set to its default value, that // may not be a valid value for the Pipeline, so it will just cause problems downstream // that will be harder to debug. check(Mapping.Rows.Num() > 0); MatchingRowIndex = 0; // TODO: Log an error: Failed to find a matching row UE_LOG(LogTemp, Error, TEXT("Failed to find matching row for parameter %s"), *Mapping.ParameterName.ToString()); } // Output parameter from matching row const FMetaHumanParameterMappingRow& MatchingRow = Mapping.Rows[MatchingRowIndex]; OutputParameterDelegate.ExecuteIfBound(Mapping.ParameterName, MatchingRow.Value); } } #if WITH_EDITOR bool FMetaHumanParameterMappingTable::TryCompile( const TMap& ConstantParameters, const ITargetPlatform* TargetPlatform, FMetaHumanCompiledParameterMappingTable& OutCompiledTable, TMap>& OutPossibleParameterValues) const { if (!Table) { return false; } struct FMetaHumanParameterMappingGenerationData { FMetaHumanParameterMapping Mapping; TArray PossibleValues; }; // Key is parameter name TMap ParameterMappingData; // Key is scalability cvar name TMap ReachableScalabilityValues; TSet NewRowPopulatedValues; TSet ReadParameters; const FName ParameterMappingPlatformName = TargetPlatform ? FName(*TargetPlatform->PlatformName()) : NAME_None; // Gather all scalability cvars that are read from the table and build a lookup with // their possible values for this platform, so that we can cull any rows that would be // unreachable. { for (const TPair& RowPair : Table->GetRowMap()) { for (const FMetaHumanParameterMappingInputColumnSet& InputColumnSet : InputColumnSets) { FMetaHumanParameterMappingInput Input; // TODO: The type and name reading code is copied from below. It should be factored out into a function. // Type { const FProperty* TypeProperty = Table->FindTableProperty(InputColumnSet.TypeColumn); if (!TypeProperty) { // TODO: Compile error continue; } const FEnumProperty* TypeEnumProperty = CastField(TypeProperty); if (!TypeEnumProperty || TypeEnumProperty->GetEnum() != StaticEnum()) { // TODO: Compile error: Column must be of correct enum type continue; } check(TypeEnumProperty->GetElementSize() == sizeof(EMetaHumanParameterMappingInputSourceType)); TypeEnumProperty->GetSingleValue_InContainer(RowPair.Value, &Input.Type, 0); } if (Input.Type != EMetaHumanParameterMappingInputSourceType::Scalability) { break; } // Name { const FProperty* NameProperty = Table->FindTableProperty(InputColumnSet.NameColumn); if (!NameProperty) { // TODO: Compile error continue; } if (NameProperty->IsA()) { check(NameProperty->GetElementSize() == sizeof(FName)); NameProperty->GetSingleValue_InContainer(RowPair.Value, &Input.Name, 0); } else if (NameProperty->IsA()) { FString TempString; check(NameProperty->GetElementSize() == sizeof(FString)); NameProperty->GetSingleValue_InContainer(RowPair.Value, &TempString, 0); Input.Name = FName(*TempString); } else { // TODO: Compile error: Unsupported column type for parameter name continue; } } if (Input.Name != NAME_None) { ReachableScalabilityValues.FindOrAdd(Input.Name); } } } for (TPair& PossibleValuesEntry : ReachableScalabilityValues) { // If this variable could be set from a section in *Scalability.ini, we need to // find out which section it could be set from. FString ScalabilitySection; if (!PossibleValuesEntry.Key.ToString().StartsWith(TEXT("sg."))) { FConfigCacheIni* ConfigSystemPlatform = FConfigCacheIni::ForPlatform(ParameterMappingPlatformName); // Load the scalability platform file if (const FConfigFile* PlatformScalability = ConfigSystemPlatform->FindConfigFile(GScalabilityIni)) { for (const TPair& Section : *PlatformScalability) { int32 IndexOfDelimiter = INDEX_NONE; if (Section.Value.Contains(PossibleValuesEntry.Key) && Section.Key.FindChar(TEXT('@'), IndexOfDelimiter)) { ScalabilitySection = Section.Key.Left(IndexOfDelimiter); // It should be safe to assume that a cvar is only set from one // scalability group, otherwise the two groups are going to // conflict. // // Therefore, we only need to find the first section that sets // this cvar in order to determine the scalability group that // owns it. break; } } } } FPerQualityLevelInt TempProperty; TempProperty.SetQualityLevelCVarForCooking(*PossibleValuesEntry.Key.ToString(), *ScalabilitySection); // GetSupportedQualityLevels returns all the possible int values this cvar // could be set to by device profile or scalability group on this platform. // // The intention is that they represent scalability quality values, but // technically any int value will work. For example, if this cvar is set to 100 // by a device profile for the current target platform, 100 will be in the list // of values returned even though quality levels are usually in the range 0-4. PossibleValuesEntry.Value.Values = TempProperty.GetSupportedQualityLevels(*ParameterMappingPlatformName.ToString()).Array(); // Sort the entries, so that they display nicely in the editor UI PossibleValuesEntry.Value.Values.Sort(); if (PossibleValuesEntry.Value.Values.Num() == 0) { // TODO: Compile error: No reachable values detected for cvar } } } for (const TPair& RowPair : Table->GetRowMap()) { for (const FMetaHumanParameterMappingOutputColumnSet& OutputColumnSet : OutputColumnSets) { FName WrittenParameterName; { const FProperty* NameProperty = Table->FindTableProperty(OutputColumnSet.NameColumn); if (!NameProperty) { // TODO: Compile error continue; } if (NameProperty->IsA()) { check(NameProperty->GetElementSize() == sizeof(FName)); NameProperty->GetSingleValue_InContainer(RowPair.Value, &WrittenParameterName, 0); } else if (NameProperty->IsA()) { FString TempString; check(NameProperty->GetElementSize() == sizeof(FString)); NameProperty->GetSingleValue_InContainer(RowPair.Value, &TempString, 0); WrittenParameterName = FName(*TempString); } else { // TODO: Compile error: Unsupported column type for parameter name continue; } } // TODO: Set default value (or remove this and make it a row instead) FMetaHumanParameterMappingGenerationData& MappingGenerationData = ParameterMappingData.FindOrAdd(WrittenParameterName); if (MappingGenerationData.Mapping.ParameterName == NAME_None) { MappingGenerationData.Mapping.ParameterName = WrittenParameterName; } FMetaHumanParameterMappingRow NewRow; NewRowPopulatedValues.Reset(); bool bIsNewRowValid = true; for (const FName ValueColumnName : OutputColumnSet.ValueColumns) { const FProperty* ValueProperty = Table->FindTableProperty(ValueColumnName); if (!ValueProperty) { // TODO: Compile error continue; } const void* PopulatedValuePtr; if (ValueProperty->IsA()) { check(ValueProperty->GetElementSize() == sizeof(FName)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &NewRow.Value.NameValue, 0); PopulatedValuePtr = &NewRow.Value.NameValue; } else if (ValueProperty->IsA()) { FString TempString; check(ValueProperty->GetElementSize() == sizeof(FString)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &TempString, 0); NewRow.Value.NameValue = FName(*TempString); PopulatedValuePtr = &NewRow.Value.NameValue; } else if (ValueProperty->IsA()) { // We coerce any numerical property to a float here, as floats are the only // numerical type supported by FMetaHumanParameterValue. // // This parameter value representation is still WIP, so we could support other // numerical types in future depending on the eventual implementation. const void* ValuePointer = ValueProperty->ContainerPtrToValuePtr(RowPair.Value); const FNumericProperty* NumericValueProperty = CastFieldChecked(ValueProperty); if (NumericValueProperty->IsFloatingPoint()) { const double TempValue = NumericValueProperty->GetFloatingPointPropertyValue(ValuePointer); NewRow.Value.FloatValue = static_cast(TempValue); } else { const int64 TempValue = NumericValueProperty->GetSignedIntPropertyValue(ValuePointer); NewRow.Value.FloatValue = static_cast(TempValue); } PopulatedValuePtr = &NewRow.Value.FloatValue; } else if (ValueProperty->IsA()) { check(ValueProperty->GetElementSize() == sizeof(bool)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &NewRow.Value.bBoolValue, 0); PopulatedValuePtr = &NewRow.Value.bBoolValue; } else if (ValueProperty->IsA()) { check(ValueProperty->GetElementSize() == sizeof(TSoftObjectPtr)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &NewRow.Value.TextureValue, 0); PopulatedValuePtr = &NewRow.Value.TextureValue; } else if (ValueProperty->IsA()) { UObject* TempObject = CastFieldChecked(ValueProperty)->LoadObjectPropertyValue_InContainer(RowPair.Value); if (!TempObject) { continue; } UTexture2D* Texture = Cast(TempObject); if (!Texture) { // TODO: Compile error: Object not a texture continue; } NewRow.Value.TextureValue = TSoftObjectPtr(Texture); PopulatedValuePtr = &NewRow.Value.TextureValue; } else if (ValueProperty->IsA()) { const FStructProperty* StructProperty = CastFieldChecked(ValueProperty); if (StructProperty->Struct == TBaseStructure::Get()) { check(ValueProperty->GetElementSize() == sizeof(FLinearColor)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &NewRow.Value.ColorValue, 0); PopulatedValuePtr = &NewRow.Value.ColorValue; } else { // TODO: Compile error: Unsupported column type for parameter value continue; } } else { // TODO: Compile error: Unsupported column type for parameter value continue; } bool bIsAlreadyInSet = false; NewRowPopulatedValues.FindOrAdd(PopulatedValuePtr, &bIsAlreadyInSet); if (bIsAlreadyInSet) { // TODO: Compile error: Two value columns of same type } } NewRowPopulatedValues.Reset(); for (const FMetaHumanParameterMappingInputColumnSet& InputColumnSet : InputColumnSets) { FMetaHumanParameterMappingInput Input; // Type { const FProperty* TypeProperty = Table->FindTableProperty(InputColumnSet.TypeColumn); if (!TypeProperty) { // TODO: Compile error continue; } const FEnumProperty* TypeEnumProperty = CastField(TypeProperty); if (!TypeEnumProperty || TypeEnumProperty->GetEnum() != StaticEnum()) { // TODO: Compile error: Column must be of correct enum type continue; } check(TypeEnumProperty->GetElementSize() == sizeof(EMetaHumanParameterMappingInputSourceType)); TypeEnumProperty->GetSingleValue_InContainer(RowPair.Value, &Input.Type, 0); } // Name { const FProperty* NameProperty = Table->FindTableProperty(InputColumnSet.NameColumn); if (!NameProperty) { // TODO: Compile error continue; } if (NameProperty->IsA()) { check(NameProperty->GetElementSize() == sizeof(FName)); NameProperty->GetSingleValue_InContainer(RowPair.Value, &Input.Name, 0); } else if (NameProperty->IsA()) { FString TempString; check(NameProperty->GetElementSize() == sizeof(FString)); NameProperty->GetSingleValue_InContainer(RowPair.Value, &TempString, 0); Input.Name = FName(*TempString); } else { // TODO: Compile error: Unsupported column type for parameter name continue; } } if (Input.Name == NAME_None) { // An empty name means this input column set is a wildcard continue; } if (Input.Type == EMetaHumanParameterMappingInputSourceType::Parameter) { ReadParameters.Add(Input.Name); } bool bFoundNumericValueColumn = false; for (const FName ValueColumnName : InputColumnSet.ValueColumns) { const FProperty* ValueProperty = Table->FindTableProperty(ValueColumnName); if (!ValueProperty) { // TODO: Compile error continue; } const void* PopulatedValuePtr; if (ValueProperty->IsA()) { check(ValueProperty->GetElementSize() == sizeof(FName)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &Input.NameValue, 0); PopulatedValuePtr = &Input.NameValue; } else if (ValueProperty->IsA()) { FString TempString; check(ValueProperty->GetElementSize() == sizeof(FString)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &TempString, 0); Input.NameValue = FName(*TempString); PopulatedValuePtr = &Input.NameValue; } else if (ValueProperty->IsA()) { // We coerce any numerical property to a float here, as floats are the only // numerical type supported by FMetaHumanParameterValue. // // This parameter value representation is still WIP, so we could support other // numerical types in future depending on the eventual implementation. const void* ValuePointer = ValueProperty->ContainerPtrToValuePtr(RowPair.Value); const FNumericProperty* NumericValueProperty = CastFieldChecked(ValueProperty); if (NumericValueProperty->IsFloatingPoint()) { const double TempValue = NumericValueProperty->GetFloatingPointPropertyValue(ValuePointer); Input.FloatValue = static_cast(TempValue); } else { const int64 TempValue = NumericValueProperty->GetSignedIntPropertyValue(ValuePointer); Input.FloatValue = static_cast(TempValue); } PopulatedValuePtr = &Input.FloatValue; bFoundNumericValueColumn = true; } else if (ValueProperty->IsA()) { check(ValueProperty->GetElementSize() == sizeof(bool)); ValueProperty->GetSingleValue_InContainer(RowPair.Value, &Input.bBoolValue, 0); PopulatedValuePtr = &Input.bBoolValue; } else { // TODO: Compile error: Unsupported column type for parameter value continue; } bool bIsAlreadyInSet = false; NewRowPopulatedValues.FindOrAdd(PopulatedValuePtr, &bIsAlreadyInSet); if (bIsAlreadyInSet) { // TODO: Compile error: Two value columns of same type } } if (Input.Type == EMetaHumanParameterMappingInputSourceType::Scalability && !bFoundNumericValueColumn) { // TODO: Compile error: Scalability values are numeric, so there must be a numeric value to compare them against } // If this row is unreachable for this platform, it can be culled from the // compiled mapping, which reduces the set of possible values that a mapped // parameter can be set to. // // This should allow us to produce more optimal built data in future. bool bShouldIncludeThisInput = true; { // If Type is platform and we're cooking, cull this row if it doesn't match // the current target platform. if (Input.Type == EMetaHumanParameterMappingInputSourceType::Platform) { if (ParameterMappingPlatformName == Input.Name) { // All non-matching rows will be culled, so no need to evaluate // platform at runtime. bShouldIncludeThisInput = false; } else { bIsNewRowValid = false; break; } } else if (Input.Type == EMetaHumanParameterMappingInputSourceType::Scalability) { // The name should always be found, as this map is populated by // scanning the table const FMetaHumanScalabilityValueSet& PossibleValues = ReachableScalabilityValues[Input.Name]; if (!PossibleValues.Values.Contains(FMath::RoundToInt32(Input.FloatValue))) { // This scalability variable can never be set to the value // specified by this row on this platform, so the row is // redundant. bIsNewRowValid = false; break; } } else if (Input.Type == EMetaHumanParameterMappingInputSourceType::Parameter) { const FMetaHumanParameterValue* ConstantParameterValue = ConstantParameters.Find(Input.Name); if (ConstantParameterValue) { // This row refers to a parameter that has been made constant at // compile time. if (ConstantParameterValue->Matches(Input)) { // Keep this row and skip the evaluation of this parameter at runtime bShouldIncludeThisInput = false; } else { // The parameter value for this row doesn't match the constant that // the parameter is being locked to, so this row can never be // activated at runtime. bIsNewRowValid = false; break; } } } } if (bShouldIncludeThisInput) { NewRow.InputParameters.Add(Input); } } if (bIsNewRowValid) { MappingGenerationData.Mapping.Rows.Add(NewRow); } } } for (const FName ReadParameter : ReadParameters) { if (ParameterMappingData.Contains(ReadParameter)) { // TODO: Compile error: Can't read and write to the same parameter } } TArray CompiledMapping; CompiledMapping.Reset(ParameterMappingData.Num()); OutPossibleParameterValues.Empty(ParameterMappingData.Num()); for (TPair& Pair : ParameterMappingData) { check(Pair.Value.PossibleValues.Num() == 0); for (const FMetaHumanParameterMappingRow& Row : Pair.Value.Mapping.Rows) { Pair.Value.PossibleValues.AddUnique(Row.Value); } if (Pair.Value.PossibleValues.Num() == 0) { // TODO: Compile error: All rows were culled. This can happen if e.g. a parameter is only set on a certain platform and we're compiling for a different platform } CompiledMapping.Add(MoveTemp(Pair.Value.Mapping)); OutPossibleParameterValues.Add(Pair.Key, Pair.Value.PossibleValues); } OutCompiledTable = FMetaHumanCompiledParameterMappingTable(MoveTemp(CompiledMapping), MoveTemp(ReachableScalabilityValues)); return true; } #endif // WITH_EDITOR bool FMetaHumanParameterMappingTable::IsValid() const { return Table != nullptr; }