@@ 1,19 @@
+Copyright (c) 2023 Joshua Goins
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
@@ 1,601 @@
+#include "structcompiler.hpp"
+#include "utility.hpp"
+
+#include <array>
+#include <fmt/format.h>
+#include <fstream>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <string_utils.hpp>
+#include <unordered_map>
+
+const std::unordered_map<std::string, FieldType> typeNameToType = {
+ {"mat4", FieldType::Matrix4x4},
+ {"vec4", FieldType::Float4},
+ {"vec3", FieldType::Float3},
+ {"vec2", FieldType::Float2},
+ {"ivec3", FieldType::Int3},
+ {"ivec4", FieldType::Int4},
+ {"ivec2", FieldType::Int2},
+ {"float", FieldType::Float},
+ {"bool", FieldType::Bool},
+ {"int", FieldType::Int}};
+
+const std::unordered_map<FieldType, std::string> typeToCpp = {
+ {FieldType::Matrix4x4, "Matrix4x4"},
+ {FieldType::Float4, "prism::float4"},
+ {FieldType::Float3, "prism::float3"},
+ {FieldType::Float2, "prism::float2"},
+ {FieldType::Int4, "prism::int4"},
+ {FieldType::Int3, "prism::int3"},
+ {FieldType::Int2, "prism::int2"},
+ {FieldType::Float, "float"},
+ {FieldType::Bool, "bool"},
+ {FieldType::Int, "int"}};
+
+const std::unordered_map<FieldType, std::string> typeToGlsl = {
+ {FieldType::Matrix4x4, "mat4"},
+ {FieldType::Float4, "vec4"},
+ {FieldType::Float3, "vec3"},
+ {FieldType::Float2, "vec2"},
+ {FieldType::Int4, "ivec4"},
+ {FieldType::Int3, "ivec3"},
+ {FieldType::Int2, "ivec2"},
+ {FieldType::Float, "float"},
+ {FieldType::Bool, "bool"},
+ {FieldType::Int, "int"}};
+
+UnderlyingType underlying_type(FieldType type) {
+ switch (type) {
+ case FieldType::Int4:
+ case FieldType::Int3:
+ case FieldType::Int2:
+ case FieldType::Int:
+ case FieldType::Bool:
+ return UnderlyingType::Int;
+ case FieldType::Float4:
+ case FieldType::Float3:
+ case FieldType::Float2:
+ case FieldType::Float:
+ return UnderlyingType::Float;
+ }
+}
+
+int num_components(FieldType type) {
+ switch (type) {
+ case FieldType::Matrix4x4:
+ return 16;
+ case FieldType::Int4:
+ case FieldType::Float4:
+ return 4;
+ case FieldType::Int3:
+ case FieldType::Float3:
+ return 3;
+ case FieldType::Int2:
+ case FieldType::Float2:
+ return 2;
+ case FieldType::Bool:
+ case FieldType::Int:
+ case FieldType::Float:
+ return 1;
+ default:
+ break;
+ }
+
+ return -1;
+}
+
+std::optional<StructCompilerOutput> StructCompiler::compile(const std::string_view path) {
+ std::ifstream t(path.data());
+ if (!t.is_open()) {
+ return {};
+ }
+
+ std::stringstream buffer;
+ buffer << t.rdbuf();
+
+ StructCompilerOutput output;
+
+ std::optional<StructDefinition> currentDefinition;
+
+ auto lines = tokenize(buffer.view(), "\n");
+ for (const auto& line : lines) {
+ std::string currentBuffer;
+
+ std::optional<std::string> structName;
+ std::optional<std::string> variableName;
+ std::optional<std::string> variableType;
+
+ auto outputContents = [¤tBuffer]() -> std::string {
+ auto buf = currentBuffer;
+ currentBuffer.clear();
+
+ return buf;
+ };
+
+ bool next_characters_is_struct_name = false;
+ bool is_primary = false;
+ for (auto character : line) {
+ if (character == ' ') {
+ if (next_characters_is_struct_name) {
+ structName = outputContents();
+ }
+
+ if (currentBuffer == "struct") {
+ next_characters_is_struct_name = true;
+ currentBuffer.clear();
+ } else if (currentBuffer == "primary") {
+ next_characters_is_struct_name = true;
+ is_primary = true;
+ currentBuffer.clear();
+ }
+
+ continue;
+ }
+
+ if (character == ':') {
+ variableName = outputContents();
+ continue;
+ }
+
+ currentBuffer += character;
+ }
+
+ if (variableName) {
+ variableType = outputContents();
+ }
+
+ if (variableName && variableType) {
+ StructField field;
+ field.name = *variableName;
+
+ if (typeNameToType.contains(*variableType)) {
+ field.type = typeNameToType.at(*variableType);
+ } else if (string_contains(*variableType, "[") && string_contains(*variableType, "]")) {
+ const auto arrayTokens = tokenize((*variableType).substr(1, variableType->length() - 1), ";");
+
+ const auto& arrayTypeName = arrayTokens[0];
+
+ if (typeNameToType.contains(arrayTypeName)) {
+ field.array_type = typeNameToType.at(arrayTypeName);
+ } else {
+ field.array_type = FieldType::CustomStruct;
+ field.custom_array_type_name = arrayTypeName;
+ }
+
+ const auto& arraySize = arrayTokens[1].substr(0, arrayTokens[1].length() - 1);
+ field.array_size = atoi(arraySize.c_str());
+
+ field.type = FieldType::Array;
+ }
+
+ currentDefinition->fields.push_back(field);
+ }
+
+ if (structName) {
+ if (currentDefinition) {
+ output.definitions.push_back(*currentDefinition);
+ }
+
+ currentDefinition = StructDefinition();
+ currentDefinition->name = *structName;
+ currentDefinition->primary = is_primary;
+ }
+ }
+
+ if (currentDefinition) {
+ output.definitions.push_back(*currentDefinition);
+ }
+
+ return output;
+}
+
+std::string StructCompiler::translate_into_cpp(const StructCompilerOutput& output) {
+ std::string result = "#pragma once\n\n";
+
+ for (const auto& definition : output.definitions) {
+ result += "struct " + definition.name + " {\n";
+ for (const auto& field : definition.fields) {
+ std::string typeName;
+ std::string extraInfo;
+
+ if (field.type == FieldType::Array) {
+ extraInfo = fmt::format("[{}]", field.array_size);
+ }
+
+ if (field.type == FieldType::Array && field.array_type == FieldType::CustomStruct) {
+ typeName = field.custom_array_type_name;
+
+ extraInfo = fmt::format("[{}]", field.array_size);
+ } else {
+ FieldType realType = field.type;
+ if (field.type == FieldType::Optimized) {
+ realType = field.optimized_type;
+ } else if (field.type == FieldType::Array) {
+ realType = field.array_type;
+ }
+
+ typeName = typeToCpp.at(realType);
+ }
+
+ result += " " + typeName + " " + field.name + extraInfo + ";\n";
+ }
+
+ for (const auto& field : definition.fields) {
+ if (field.type == FieldType::Array) {
+ continue;
+ }
+
+ if (field.type == FieldType::Optimized) {
+ int all_length = 0;
+ for (auto optimized_field : field.optimized_fields) {
+ std::array<char, 4> accessors = {'x', 'y', 'z', 'w'};
+
+ const int length = num_components(optimized_field.type);
+
+ std::string accessor;
+ int accessorOffset = all_length;
+ int accessorEnd = accessorOffset + length;
+
+ for (int i = accessorOffset; i < accessorEnd; i++) {
+ accessor += accessors[i];
+ }
+
+ all_length += length;
+
+ result += fmt::format(
+ " void set_{}({} value) {{\n", optimized_field.name, typeToCpp.at(optimized_field.type));
+ result += fmt::format(" {}.{} = value;\n", field.name, accessor);
+ result += fmt::format(" }}\n\n");
+ }
+ } else {
+ result += fmt::format(" void set_{}({} value) {{\n", field.name, typeToCpp.at(field.type));
+ result += fmt::format(" {} = value;\n", field.name);
+ result += fmt::format(" }}\n\n");
+ }
+ }
+
+ result += "};\n";
+ }
+
+ return result;
+}
+
+// Convert lowerCamelCase and UpperCamelCase strings to lower_with_underscore.
+// from https://gist.github.com/rodamber/2558e25d4d8f6b9f2ffdf7bd49471340
+std::string convert(std::string&& camelCase) {
+ std::string str(1, tolower(camelCase[0]));
+
+ // First place underscores between contiguous lower and upper case letters.
+ // For example, `_LowerCamelCase` becomes `_Lower_Camel_Case`.
+ for (auto it = camelCase.begin() + 1; it != camelCase.end(); ++it) {
+ if (isupper(*it) && *(it - 1) != '_' && islower(*(it - 1))) {
+ str += "_";
+ }
+ str += *it;
+ }
+
+ // Then convert it to lower case.
+ std::transform(str.begin(), str.end(), str.begin(), ::tolower);
+
+ return str;
+}
+
+std::string
+StructCompiler::translate_into_glsl(const StructCompilerOutput& output, std::string_view usage, std::string_view name) {
+ std::string result;
+
+ for (const auto& definition : output.definitions) {
+ if (definition.primary) {
+ if (string_contains(usage, "binding")) {
+ result += fmt::format("layout(std430, {}) buffer readonly {} {{\n", usage, definition.name);
+ } else {
+ result += fmt::format("layout({}) uniform {} {{\n", usage, definition.name);
+ }
+ } else {
+ result += fmt::format("struct {} {{\n", definition.name);
+ }
+ for (const auto& field : definition.fields) {
+ std::string typeName;
+ std::string extraInfo;
+
+ if (field.type == FieldType::Array) {
+ extraInfo = fmt::format("[{}]", field.array_size);
+ }
+
+ if (field.type == FieldType::Array && field.array_type == FieldType::CustomStruct) {
+ typeName = field.custom_array_type_name;
+
+ extraInfo = fmt::format("[{}]", field.array_size);
+ } else {
+ FieldType realType = field.type;
+ if (field.type == FieldType::Optimized) {
+ realType = field.optimized_type;
+ } else if (field.type == FieldType::Array) {
+ realType = field.array_type;
+ }
+
+ typeName = typeToGlsl.at(realType);
+ }
+
+ result += " " + typeName + " " + field.name + extraInfo + ";\n";
+ }
+ if (definition.primary) {
+ result += fmt::format("}} {};\n\n", name);
+ } else {
+ result += "};\n\n";
+ }
+ }
+
+ // now write accessors
+ for (const auto& definition : output.definitions) {
+ for (const auto& field : definition.fields) {
+ if (field.type == FieldType::Array) {
+ continue;
+ }
+
+ std::string objectToAccess;
+ bool is_param = true;
+ if (definition.primary) {
+ objectToAccess = name;
+ is_param = false;
+ } else {
+ objectToAccess = "val";
+ }
+
+ std::string accessorName;
+ if (is_param) {
+ accessorName = definition.name + " " + objectToAccess;
+ }
+
+ std::string prefix;
+ if (!definition.primary) {
+ prefix = convert(definition.name.data()) + "_";
+ }
+
+ if (field.type == FieldType::Optimized) {
+ int all_length = 0;
+ for (auto optimized_field : field.optimized_fields) {
+ std::array<char, 4> accessors = {'x', 'y', 'z', 'w'};
+
+ const int length = num_components(optimized_field.type);
+
+ std::string accessor;
+ int accessorOffset = all_length;
+ int accessorEnd = accessorOffset + length;
+
+ for (int i = accessorOffset; i < accessorEnd; i++) {
+ accessor += accessors[i];
+ }
+
+ all_length += length;
+
+ result += fmt::format(
+ "{} {}{}({}) {{\n",
+ typeToGlsl.at(optimized_field.type),
+ prefix,
+ optimized_field.name,
+ accessorName);
+ if (optimized_field.type == FieldType::Bool) {
+ result += fmt::format(" return {}.{}.{} == 1;\n", objectToAccess, field.name, accessor);
+ } else {
+ result += fmt::format(" return {}.{}.{};\n", objectToAccess, field.name, accessor);
+ }
+ result += fmt::format("}}\n\n");
+ }
+ } else {
+ result += fmt::format("{} {}{}({}) {{\n", typeToGlsl.at(field.type), prefix, field.name, accessorName);
+ result += fmt::format(" return {}.{};\n", objectToAccess, field.name);
+ result += fmt::format("}}\n\n");
+ }
+ }
+ }
+
+ return result;
+}
+
+StructCompilerOutput StructCompiler::optimize_structs(const StructCompilerOutput& output) {
+ StructCompilerOutput optimized;
+
+ for (const auto& definition : output.definitions) {
+ StructDefinition newDefinition;
+ newDefinition.name = definition.name;
+ newDefinition.primary = definition.primary;
+
+ struct OptimizedField {
+ std::vector<StructField> contained_fields;
+
+ bool skip = false;
+
+ std::string get_contained_names() {
+ std::string com;
+ for (auto field : contained_fields) {
+ com += field.name + ", ";
+ }
+
+ return com;
+ }
+
+ int number_components() const {
+ int sum = 0;
+ for (const auto& field : contained_fields) {
+ sum += num_components(field.type);
+ }
+
+ return sum;
+ }
+
+ int best_match() const {
+ return 4 - number_components();
+ }
+
+ UnderlyingType get_underlying_type() {
+ return underlying_type(contained_fields[0].type);
+ }
+ };
+
+ std::vector<OptimizedField> optimized_fields;
+
+ // we want to iterate through all of the fields, and collect as many "small" fields as we can
+ // these include vec2, vec3, float for example.
+ for (const auto& field : definition.fields) {
+ if (field.type == FieldType::Matrix4x4 || field.type == FieldType::Float4 ||
+ field.type == FieldType::Int4 || field.type == FieldType::Array) {
+ newDefinition.fields.push_back(field);
+ continue;
+ }
+
+ OptimizedField opt;
+ opt.contained_fields = {field};
+
+ optimized_fields.push_back(opt);
+ }
+
+ // first iterate through best matches first, and then brute-force the rest
+ bool should_do_best_match_first = true;
+
+ // what is the magic number of rounds required?
+ bool any_matches = true;
+ while (any_matches) {
+ bool should_match = false;
+ for (auto& a : optimized_fields) {
+ if (a.skip) {
+ continue;
+ }
+
+ if (a.number_components() >= 4) {
+ continue;
+ }
+
+ for (auto& b : optimized_fields) {
+ if (a.contained_fields == b.contained_fields) {
+ continue;
+ }
+
+ if (b.skip) {
+ continue;
+ }
+
+ if (b.get_underlying_type() != a.get_underlying_type()) {
+ continue;
+ }
+
+ bool should_combine = false;
+ if (should_do_best_match_first) {
+ should_combine = a.best_match() == b.number_components();
+ } else {
+ should_combine = (a.number_components() + b.number_components()) <= 4;
+ }
+
+ should_match |= should_combine;
+ }
+ }
+
+ any_matches = should_match;
+
+ for (auto& a : optimized_fields) {
+ if (a.skip) {
+ continue;
+ }
+
+ if (a.number_components() >= 4) {
+ continue;
+ }
+
+ for (auto& b : optimized_fields) {
+ if (a.contained_fields == b.contained_fields) {
+ continue;
+ }
+
+ if (b.skip) {
+ continue;
+ }
+
+ if (b.number_components() == 4) {
+ continue;
+ }
+
+ if (b.get_underlying_type() != a.get_underlying_type()) {
+ continue;
+ }
+
+ bool should_combine = false;
+ if (should_do_best_match_first) {
+ should_combine = a.best_match() == b.number_components();
+ } else {
+ should_combine = a.number_components() + b.number_components() <= 4;
+ }
+
+ if (should_combine) {
+ b.skip = true;
+ a.contained_fields.insert(
+ a.contained_fields.end(), b.contained_fields.begin(), b.contained_fields.end());
+ }
+ }
+ }
+
+ if (should_do_best_match_first) {
+ any_matches = true;
+ }
+
+ should_do_best_match_first = false;
+ }
+
+ optimized_fields.erase(
+ std::remove_if(
+ optimized_fields.begin(),
+ optimized_fields.end(),
+ [](OptimizedField& field) {
+ return field.skip;
+ }),
+ optimized_fields.end());
+
+ for (auto field : optimized_fields) {
+ if (field.contained_fields.size() == 1) {
+ newDefinition.fields.push_back(field.contained_fields[0]);
+ continue;
+ }
+
+ StructField output;
+ output.type = FieldType::Optimized;
+
+ for (auto contained_field : field.contained_fields) {
+ output.name += contained_field.name + "_";
+ output.optimized_fields.push_back(contained_field);
+ }
+
+ if (field.get_underlying_type() == UnderlyingType::Float) {
+ output.optimized_type = FieldType::Float4;
+ } else {
+ output.optimized_type = FieldType::Int4;
+ }
+
+ newDefinition.fields.push_back(output);
+ }
+
+ const auto get_size = [](const StructField field) -> int {
+ if (field.type == FieldType::Optimized) {
+ int sum = 0;
+ for (auto field : field.optimized_fields) {
+ sum += num_components(field.type);
+ }
+
+ return sum;
+ } else {
+ return num_components(field.type);
+ }
+ };
+
+ std::sort(
+ newDefinition.fields.begin(),
+ newDefinition.fields.end(),
+ [get_size](const StructField& a, const StructField& b) {
+ return get_size(a) > get_size(b);
+ });
+
+ optimized.definitions.push_back(newDefinition);
+ }
+
+ return optimized;
+}