rendering: formalize shader keyword metadata contract
This commit is contained in:
@@ -95,6 +95,21 @@ bool IsTextureMaterialPropertyType(MaterialPropertyType type) {
|
||||
return type == MaterialPropertyType::Texture || type == MaterialPropertyType::Cubemap;
|
||||
}
|
||||
|
||||
Containers::String NormalizeShaderKeyword(const Containers::String& keyword) {
|
||||
const Containers::String normalized = keyword.Trim();
|
||||
if (normalized.Empty() ||
|
||||
normalized == Containers::String("_") ||
|
||||
normalized == Containers::String("__")) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool CompareShaderKeywords(const Containers::String& left, const Containers::String& right) {
|
||||
return std::strcmp(left.CStr(), right.CStr()) < 0;
|
||||
}
|
||||
|
||||
MaterialPropertyType GetMaterialPropertyTypeForShaderProperty(ShaderPropertyType type) {
|
||||
switch (type) {
|
||||
case ShaderPropertyType::Float:
|
||||
@@ -312,6 +327,7 @@ Material::~Material() {
|
||||
// resetting them here avoids teardown-order issues during destruction.
|
||||
m_shader.Reset();
|
||||
m_tags = Containers::Array<MaterialTagEntry>();
|
||||
m_keywordSet = ShaderKeywordSet();
|
||||
m_properties = Containers::HashMap<Containers::String, MaterialProperty>();
|
||||
m_constantLayout = Containers::Array<MaterialConstantFieldDesc>();
|
||||
m_constantBufferData = Containers::Array<Core::uint8>();
|
||||
@@ -324,6 +340,7 @@ void Material::Release() {
|
||||
m_renderState = MaterialRenderState();
|
||||
m_shaderPass.Clear();
|
||||
m_tags.Clear();
|
||||
m_keywordSet.enabledKeywords.Clear();
|
||||
m_properties.Clear();
|
||||
m_constantLayout.Clear();
|
||||
m_textureBindings.Clear();
|
||||
@@ -336,6 +353,7 @@ void Material::Release() {
|
||||
void Material::SetShader(const ResourceHandle<Shader>& shader) {
|
||||
m_shader = shader;
|
||||
SyncShaderSchemaProperties(true);
|
||||
SyncShaderSchemaKeywords(true);
|
||||
MarkChanged(true);
|
||||
}
|
||||
|
||||
@@ -420,6 +438,90 @@ Containers::String Material::GetTagValue(Core::uint32 index) const {
|
||||
return index < m_tags.Size() ? m_tags[index].value : Containers::String();
|
||||
}
|
||||
|
||||
void Material::EnableKeyword(const Containers::String& keyword) {
|
||||
const Containers::String normalizedKeyword = NormalizeShaderKeyword(keyword);
|
||||
if (normalizedKeyword.Empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_shader.Get() != nullptr && !m_shader->DeclaresKeyword(normalizedKeyword)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const Containers::String& existingKeyword : m_keywordSet.enabledKeywords) {
|
||||
if (existingKeyword == normalizedKeyword) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_keywordSet.enabledKeywords.PushBack(normalizedKeyword);
|
||||
std::sort(
|
||||
m_keywordSet.enabledKeywords.begin(),
|
||||
m_keywordSet.enabledKeywords.end(),
|
||||
CompareShaderKeywords);
|
||||
MarkChanged(false);
|
||||
}
|
||||
|
||||
void Material::DisableKeyword(const Containers::String& keyword) {
|
||||
const Containers::String normalizedKeyword = NormalizeShaderKeyword(keyword);
|
||||
if (normalizedKeyword.Empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t keywordIndex = 0; keywordIndex < m_keywordSet.enabledKeywords.Size(); ++keywordIndex) {
|
||||
if (m_keywordSet.enabledKeywords[keywordIndex] == normalizedKeyword) {
|
||||
if (keywordIndex != m_keywordSet.enabledKeywords.Size() - 1) {
|
||||
m_keywordSet.enabledKeywords[keywordIndex] = std::move(m_keywordSet.enabledKeywords.Back());
|
||||
}
|
||||
m_keywordSet.enabledKeywords.PopBack();
|
||||
std::sort(
|
||||
m_keywordSet.enabledKeywords.begin(),
|
||||
m_keywordSet.enabledKeywords.end(),
|
||||
CompareShaderKeywords);
|
||||
MarkChanged(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Material::SetKeywordEnabled(const Containers::String& keyword, bool enabled) {
|
||||
if (enabled) {
|
||||
EnableKeyword(keyword);
|
||||
} else {
|
||||
DisableKeyword(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
bool Material::IsKeywordEnabled(const Containers::String& keyword) const {
|
||||
const Containers::String normalizedKeyword = NormalizeShaderKeyword(keyword);
|
||||
if (normalizedKeyword.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const Containers::String& existingKeyword : m_keywordSet.enabledKeywords) {
|
||||
if (existingKeyword == normalizedKeyword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Material::ClearKeywords() {
|
||||
if (m_keywordSet.enabledKeywords.Empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_keywordSet.enabledKeywords.Clear();
|
||||
MarkChanged(false);
|
||||
}
|
||||
|
||||
Containers::String Material::GetKeyword(Core::uint32 index) const {
|
||||
return index < m_keywordSet.enabledKeywords.Size()
|
||||
? m_keywordSet.enabledKeywords[index]
|
||||
: Containers::String();
|
||||
}
|
||||
|
||||
void Material::SetFloat(const Containers::String& name, float value) {
|
||||
if (!CanAssignPropertyType(name, MaterialPropertyType::Float)) {
|
||||
return;
|
||||
@@ -976,6 +1078,29 @@ void Material::SyncShaderSchemaProperties(bool removeUnknownProperties) {
|
||||
}
|
||||
}
|
||||
|
||||
void Material::SyncShaderSchemaKeywords(bool removeUnknownKeywords) {
|
||||
if (m_shader.Get() == nullptr || !removeUnknownKeywords) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t keywordIndex = 0; keywordIndex < m_keywordSet.enabledKeywords.Size();) {
|
||||
if (m_shader->DeclaresKeyword(m_keywordSet.enabledKeywords[keywordIndex])) {
|
||||
++keywordIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keywordIndex != m_keywordSet.enabledKeywords.Size() - 1) {
|
||||
m_keywordSet.enabledKeywords[keywordIndex] = std::move(m_keywordSet.enabledKeywords.Back());
|
||||
}
|
||||
m_keywordSet.enabledKeywords.PopBack();
|
||||
}
|
||||
|
||||
std::sort(
|
||||
m_keywordSet.enabledKeywords.begin(),
|
||||
m_keywordSet.enabledKeywords.end(),
|
||||
CompareShaderKeywords);
|
||||
}
|
||||
|
||||
void Material::MarkChanged(bool updateConstantBuffer) {
|
||||
if (updateConstantBuffer) {
|
||||
UpdateConstantBuffer();
|
||||
@@ -992,6 +1117,7 @@ void Material::UpdateMemorySize() {
|
||||
sizeof(MaterialRenderState) +
|
||||
m_shaderPass.Length() +
|
||||
m_tags.Size() * sizeof(MaterialTagEntry) +
|
||||
m_keywordSet.enabledKeywords.Size() * sizeof(Containers::String) +
|
||||
m_textureBindings.Size() * sizeof(MaterialTextureBinding) +
|
||||
m_properties.Size() * sizeof(MaterialProperty) +
|
||||
m_name.Length() +
|
||||
@@ -1002,6 +1128,10 @@ void Material::UpdateMemorySize() {
|
||||
m_memorySize += tag.value.Length();
|
||||
}
|
||||
|
||||
for (const Containers::String& keyword : m_keywordSet.enabledKeywords) {
|
||||
m_memorySize += keyword.Length();
|
||||
}
|
||||
|
||||
for (const MaterialConstantFieldDesc& field : m_constantLayout) {
|
||||
m_memorySize += field.name.Length();
|
||||
}
|
||||
|
||||
@@ -300,6 +300,13 @@ bool TryDecodeAssetRef(const Containers::String& value, AssetRef& outRef) {
|
||||
return outRef.IsValid();
|
||||
}
|
||||
|
||||
bool TryExtractDelimitedText(const std::string& text,
|
||||
size_t valuePos,
|
||||
char openChar,
|
||||
char closeChar,
|
||||
std::string& outValue,
|
||||
size_t* nextPos);
|
||||
|
||||
bool TryExtractObject(const std::string& json, const char* key, std::string& outObject) {
|
||||
size_t valuePos = 0;
|
||||
if (!FindValueStart(json, key, valuePos) || valuePos >= json.size() || json[valuePos] != '{') {
|
||||
@@ -345,6 +352,15 @@ bool TryExtractObject(const std::string& json, const char* key, std::string& out
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryExtractArray(const std::string& json, const char* key, std::string& outArray) {
|
||||
size_t valuePos = 0;
|
||||
if (!FindValueStart(json, key, valuePos) || valuePos >= json.size() || json[valuePos] != '[') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryExtractDelimitedText(json, valuePos, '[', ']', outArray, nullptr);
|
||||
}
|
||||
|
||||
std::string TrimCopy(const std::string& text) {
|
||||
const size_t first = SkipWhitespace(text, 0);
|
||||
if (first >= text.size()) {
|
||||
@@ -359,6 +375,17 @@ std::string TrimCopy(const std::string& text) {
|
||||
return text.substr(first, last - first);
|
||||
}
|
||||
|
||||
Containers::String NormalizeMaterialKeywordToken(const Containers::String& keyword) {
|
||||
const Containers::String normalized = keyword.Trim();
|
||||
if (normalized.Empty() ||
|
||||
normalized == Containers::String("_") ||
|
||||
normalized == Containers::String("__")) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool IsJsonValueTerminator(char ch) {
|
||||
return std::isspace(static_cast<unsigned char>(ch)) != 0 ||
|
||||
ch == ',' ||
|
||||
@@ -887,6 +914,58 @@ bool TryParseMaterialPropertiesObject(const std::string& objectText, Material* m
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseMaterialKeywordsArray(const std::string& arrayText, Material* material) {
|
||||
if (material == nullptr || arrayText.empty() || arrayText.front() != '[' || arrayText.back() != ']') {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pos = 1;
|
||||
while (pos < arrayText.size()) {
|
||||
pos = SkipWhitespace(arrayText, pos);
|
||||
if (pos >= arrayText.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arrayText[pos] == ']') {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string rawValue;
|
||||
JsonRawValueType rawType = JsonRawValueType::Invalid;
|
||||
if (!TryExtractRawValue(arrayText, pos, rawValue, rawType, &pos) ||
|
||||
rawType != JsonRawValueType::String) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Containers::String normalizedKeyword =
|
||||
NormalizeMaterialKeywordToken(Containers::String(rawValue.c_str()));
|
||||
if (!normalizedKeyword.Empty()) {
|
||||
material->EnableKeyword(normalizedKeyword);
|
||||
if (!material->IsKeywordEnabled(normalizedKeyword)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
pos = SkipWhitespace(arrayText, pos);
|
||||
if (pos >= arrayText.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arrayText[pos] == ',') {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arrayText[pos] == ']') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseRenderQueueName(const Containers::String& queueName, Core::int32& outQueue) {
|
||||
const Containers::String normalized = queueName.ToLower();
|
||||
if (normalized == "background") {
|
||||
@@ -1487,7 +1566,10 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) {
|
||||
}
|
||||
|
||||
const std::string magic(fileHeader.magic, fileHeader.magic + 7);
|
||||
if (magic != "XCMAT02") {
|
||||
const bool isLegacySchema = magic == "XCMAT02" && fileHeader.schemaVersion == 2u;
|
||||
const bool isCurrentSchema =
|
||||
magic == "XCMAT03" && fileHeader.schemaVersion == kMaterialArtifactSchemaVersion;
|
||||
if (!isLegacySchema && !isCurrentSchema) {
|
||||
return LoadResult("Invalid material artifact magic: " + path);
|
||||
}
|
||||
|
||||
@@ -1523,9 +1605,22 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) {
|
||||
material->SetShaderPass(shaderPass);
|
||||
}
|
||||
|
||||
MaterialArtifactHeader header;
|
||||
if (!ReadMaterialArtifactValue(data, offset, header)) {
|
||||
return LoadResult("Failed to parse material artifact body: " + path);
|
||||
MaterialArtifactHeader header = {};
|
||||
if (isLegacySchema) {
|
||||
MaterialArtifactHeaderV2 legacyHeader = {};
|
||||
if (!ReadMaterialArtifactValue(data, offset, legacyHeader)) {
|
||||
return LoadResult("Failed to parse material artifact body: " + path);
|
||||
}
|
||||
|
||||
header.renderQueue = legacyHeader.renderQueue;
|
||||
header.renderState = legacyHeader.renderState;
|
||||
header.tagCount = legacyHeader.tagCount;
|
||||
header.propertyCount = legacyHeader.propertyCount;
|
||||
header.textureBindingCount = legacyHeader.textureBindingCount;
|
||||
} else {
|
||||
if (!ReadMaterialArtifactValue(data, offset, header)) {
|
||||
return LoadResult("Failed to parse material artifact body: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
material->SetRenderQueue(header.renderQueue);
|
||||
@@ -1542,6 +1637,21 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) {
|
||||
material->SetTag(tagName, tagValue);
|
||||
}
|
||||
|
||||
for (Core::uint32 keywordIndex = 0; keywordIndex < header.keywordCount; ++keywordIndex) {
|
||||
Containers::String keyword;
|
||||
if (!ReadMaterialArtifactString(data, offset, keyword)) {
|
||||
return LoadResult("Failed to read material artifact keywords: " + path);
|
||||
}
|
||||
|
||||
const Containers::String normalizedKeyword = NormalizeMaterialKeywordToken(keyword);
|
||||
if (!normalizedKeyword.Empty()) {
|
||||
material->EnableKeyword(normalizedKeyword);
|
||||
if (!material->IsKeywordEnabled(normalizedKeyword)) {
|
||||
return LoadResult("Material artifact references undeclared shader keyword: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Core::uint32 propertyIndex = 0; propertyIndex < header.propertyCount; ++propertyIndex) {
|
||||
Containers::String propertyName;
|
||||
MaterialPropertyArtifact propertyArtifact;
|
||||
@@ -1692,6 +1802,14 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array<Core::uint8>& dat
|
||||
}
|
||||
}
|
||||
|
||||
if (HasKey(jsonText, "keywords")) {
|
||||
std::string keywordsArray;
|
||||
if (!TryExtractArray(jsonText, "keywords", keywordsArray) ||
|
||||
!TryParseMaterialKeywordsArray(keywordsArray, material)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (HasKey(jsonText, "tags")) {
|
||||
std::string tagObject;
|
||||
if (!TryExtractObject(jsonText, "tags", tagObject) || !TryParseTagMap(tagObject, material)) {
|
||||
|
||||
Reference in New Issue
Block a user