// Copyright 2023 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/Assets/MaterialAsset.h" #include #include #include #include "Common/JsonUtil.h" #include "Common/Logging/Log.h" #include "Common/StringUtil.h" #include "Common/VariantUtil.h" #include "VideoCommon/Assets/CustomAssetLibrary.h" #include "VideoCommon/ShaderGenCommon.h" namespace VideoCommon { namespace { // While not optimal, we pad our data to match std140 shader requirements // this memory constant indicates the memory stride for a single uniform // regardless of data type constexpr std::size_t MemorySize = sizeof(float) * 4; template bool ParseNumeric(const CustomAssetLibrary::AssetID& asset_id, const picojson::value& json_value, std::string_view code_name, MaterialProperty::Value* value) { static_assert(ElementCount <= 4, "Numeric data expected to be four elements or less"); if constexpr (ElementCount == 1) { if (!json_value.is()) { ERROR_LOG_FMT(VIDEO, "Asset id '{}' material has attribute '{}' where " "a double was expected but not provided.", asset_id, code_name); return false; } *value = static_cast(json_value.get()); } else { if (!json_value.is()) { ERROR_LOG_FMT(VIDEO, "Asset id '{}' material has attribute '{}' where " "an array was expected but not provided.", asset_id, code_name); return false; } const auto json_data = json_value.get(); if (json_data.size() != ElementCount) { ERROR_LOG_FMT(VIDEO, "Asset id '{}' material has attribute '{}' with incorrect number " "of elements, expected {}", asset_id, code_name, ElementCount); return false; } if (!std::all_of(json_data.begin(), json_data.end(), [](const picojson::value& v) { return v.is(); })) { ERROR_LOG_FMT(VIDEO, "Asset id '{}' material has attribute '{}' where " "all elements are not of type double.", asset_id, code_name); return false; } std::array data; for (std::size_t i = 0; i < ElementCount; i++) { data[i] = static_cast(json_data[i].get()); } *value = std::move(data); } return true; } bool ParsePropertyValue(const CustomAssetLibrary::AssetID& asset_id, const picojson::value& json_value, std::string_view code_name, std::string_view type, MaterialProperty::Value* value) { if (type == "int") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "int2") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "int3") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "int4") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "float") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "float2") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "float3") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "float4") { return ParseNumeric(asset_id, json_value, code_name, value); } else if (type == "bool") { if (json_value.is()) { *value = json_value.get(); return true; } } else if (type == "texture_asset") { if (json_value.is()) { *value = json_value.get(); return true; } } ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value is not valid for type '{}'", asset_id, type); return false; } bool ParseMaterialProperties(const CustomAssetLibrary::AssetID& asset_id, const picojson::array& values_data, std::vector* material_property) { for (const auto& value_data : values_data) { VideoCommon::MaterialProperty property; if (!value_data.is()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value is not the right json type", asset_id); return false; } const auto& value_data_obj = value_data.get(); const auto type_iter = value_data_obj.find("type"); if (type_iter == value_data_obj.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value entry 'type' not found", asset_id); return false; } if (!type_iter->second.is()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value entry 'type' is not " "the right json type", asset_id); return false; } std::string type = type_iter->second.to_str(); Common::ToLower(&type); const auto code_name_iter = value_data_obj.find("code_name"); if (code_name_iter == value_data_obj.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value entry " "'code_name' not found", asset_id); return false; } if (!code_name_iter->second.is()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value entry 'code_name' is not " "the right json type", asset_id); return false; } property.m_code_name = code_name_iter->second.to_str(); const auto value_iter = value_data_obj.find("value"); if (value_iter != value_data_obj.end()) { if (!ParsePropertyValue(asset_id, value_iter->second, property.m_code_name, type, &property.m_value)) { return false; } } material_property->push_back(std::move(property)); } return true; } } // namespace void MaterialProperty::WriteToMemory(u8*& buffer, const MaterialProperty& property) { const auto write_memory = [&](const void* raw_value, std::size_t data_size) { std::memcpy(buffer, raw_value, data_size); std::memset(buffer + data_size, 0, MemorySize - data_size); buffer += MemorySize; }; std::visit( overloaded{ [&](const CustomAssetLibrary::AssetID&) {}, [&](s32 value) { write_memory(&value, sizeof(s32)); }, [&](const std::array& value) { write_memory(value.data(), sizeof(s32) * 2); }, [&](const std::array& value) { write_memory(value.data(), sizeof(s32) * 3); }, [&](const std::array& value) { write_memory(value.data(), sizeof(s32) * 4); }, [&](float value) { write_memory(&value, sizeof(float)); }, [&](const std::array& value) { write_memory(value.data(), sizeof(float) * 2); }, [&](const std::array& value) { write_memory(value.data(), sizeof(float) * 3); }, [&](const std::array& value) { write_memory(value.data(), sizeof(float) * 4); }, [&](bool value) { write_memory(&value, sizeof(bool)); }}, property.m_value); } std::size_t MaterialProperty::GetMemorySize(const MaterialProperty& property) { std::size_t result = 0; std::visit(overloaded{[&](const CustomAssetLibrary::AssetID&) {}, [&](s32) { result = MemorySize; }, [&](const std::array&) { result = MemorySize; }, [&](const std::array&) { result = MemorySize; }, [&](const std::array&) { result = MemorySize; }, [&](float) { result = MemorySize; }, [&](const std::array&) { result = MemorySize; }, [&](const std::array&) { result = MemorySize; }, [&](const std::array&) { result = MemorySize; }, [&](bool) { result = MemorySize; }}, property.m_value); return result; } void MaterialProperty::WriteAsShaderCode(ShaderCode& shader_source, const MaterialProperty& property) { const auto write_shader = [&](std::string_view type, u32 element_count) { if (element_count == 1) { shader_source.Write("{} {};\n", type, property.m_code_name); } else { shader_source.Write("{}{} {};\n", type, element_count, property.m_code_name); } for (std::size_t i = element_count; i < 4; i++) { shader_source.Write("{} {}_padding_{};\n", type, property.m_code_name, i + 1); } }; std::visit(overloaded{[&](const CustomAssetLibrary::AssetID&) {}, [&](s32 value) { write_shader("int", 1); }, [&](const std::array& value) { write_shader("int", 2); }, [&](const std::array& value) { write_shader("int", 3); }, [&](const std::array& value) { write_shader("int", 4); }, [&](float value) { write_shader("float", 1); }, [&](const std::array& value) { write_shader("float", 2); }, [&](const std::array& value) { write_shader("float", 3); }, [&](const std::array& value) { write_shader("float", 4); }, [&](bool value) { write_shader("bool", 1); }}, property.m_value); } bool MaterialData::FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json, MaterialData* data) { const auto values_iter = json.find("values"); if (values_iter == json.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'values' not found", asset_id); return false; } if (!values_iter->second.is()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'values' is not the right json type", asset_id); return false; } const auto& values_array = values_iter->second.get(); if (!ParseMaterialProperties(asset_id, values_array, &data->properties)) return false; const auto shader_asset_iter = json.find("shader_asset"); if (shader_asset_iter == json.end()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'shader_asset' not found", asset_id); return false; } if (!shader_asset_iter->second.is()) { ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'shader_asset' is not the right json type", asset_id); return false; } data->shader_asset = shader_asset_iter->second.to_str(); return true; } void MaterialData::ToJson(picojson::object* obj, const MaterialData& data) { if (!obj) [[unlikely]] return; auto& json_obj = *obj; picojson::array json_properties; for (const auto& property : data.properties) { picojson::object json_property; json_property["code_name"] = picojson::value{property.m_code_name}; std::visit(overloaded{[&](const CustomAssetLibrary::AssetID& value) { json_property["type"] = picojson::value{"texture_asset"}; json_property["value"] = picojson::value{value}; }, [&](s32 value) { json_property["type"] = picojson::value{"int"}; json_property["value"] = picojson::value{static_cast(value)}; }, [&](const std::array& value) { json_property["type"] = picojson::value{"int2"}; json_property["value"] = picojson::value{ToJsonArray(value)}; }, [&](const std::array& value) { json_property["type"] = picojson::value{"int3"}; json_property["value"] = picojson::value{ToJsonArray(value)}; }, [&](const std::array& value) { json_property["type"] = picojson::value{"int4"}; json_property["value"] = picojson::value{ToJsonArray(value)}; }, [&](float value) { json_property["type"] = picojson::value{"float"}; json_property["value"] = picojson::value{static_cast(value)}; }, [&](const std::array& value) { json_property["type"] = picojson::value{"float2"}; json_property["value"] = picojson::value{ToJsonArray(value)}; }, [&](const std::array& value) { json_property["type"] = picojson::value{"float3"}; json_property["value"] = picojson::value{ToJsonArray(value)}; }, [&](const std::array& value) { json_property["type"] = picojson::value{"float4"}; json_property["value"] = picojson::value{ToJsonArray(value)}; }, [&](bool value) { json_property["type"] = picojson::value{"bool"}; json_property["value"] = picojson::value{value}; }}, property.m_value); json_properties.emplace_back(std::move(json_property)); } json_obj["values"] = picojson::value{std::move(json_properties)}; json_obj["shader_asset"] = picojson::value{data.shader_asset}; } CustomAssetLibrary::LoadInfo MaterialAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id) { auto potential_data = std::make_shared(); const auto loaded_info = m_owning_library->LoadMaterial(asset_id, potential_data.get()); if (loaded_info.m_bytes_loaded == 0) return {}; { std::lock_guard lk(m_data_lock); m_loaded = true; m_data = std::move(potential_data); } return loaded_info; } } // namespace VideoCommon