248 lines
12 KiB
C++
248 lines
12 KiB
C++
#include <bit>
|
|
#include "asset/AssetCooker.h"
|
|
#include "asset/CanonicalMesh.h"
|
|
#include "asset/EvpackResourceReader.h"
|
|
#include "asset/import/AssetImporter.h"
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
#ifdef EVE_TEST_MESH_UPLOAD
|
|
#include "asset/graphics/EvpackGraphicsLoader.h"
|
|
#endif
|
|
using namespace eve;
|
|
namespace {
|
|
asset_import::GltfImportRequest uvMesh() {
|
|
asset_import::GltfImportRequest r;
|
|
r.package = {*PersistentId::parse("11111111-2222-4333-8444-555555555555"), "uv.test", "1.0.0", {}};
|
|
r.sourceName = "uv.gltf";
|
|
auto& b = r.externalResources["data.bin"];
|
|
b.resize(78);
|
|
// Three distinct UV representations, including sparse channel numbers.
|
|
for (size_t i = 0; i < 6; ++i) {
|
|
auto bits = std::bit_cast<uint32_t>(float(i) / 2.f);
|
|
for (size_t k = 0; k < 4; ++k) b[36 + i * 4 + k] = uint8_t(bits >> (k * 8));
|
|
uint16_t value = (i % 2) ? 65535 : 0;
|
|
b[60 + i * 2] = uint8_t(value);
|
|
b[61 + i * 2] = uint8_t(value >> 8);
|
|
b[72 + i] = uint8_t(i * 51);
|
|
}
|
|
std::string json =
|
|
R"({"asset":{"version":"2.0"},"buffers":[{"uri":"data.bin","byteLength":78}],"bufferViews":[{"buffer":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":24},{"buffer":0,"byteOffset":60,"byteLength":12},{"buffer":0,"byteOffset":72,"byteLength":6}],"accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":3,"type":"VEC2"},{"bufferView":2,"componentType":5123,"normalized":true,"count":3,"type":"VEC2"},{"bufferView":3,"componentType":5121,"normalized":true,"count":3,"type":"VEC2"}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1,"TEXCOORD_2":2,"TEXCOORD_7":3}}]}]})";
|
|
r.documentBytes.assign(json.begin(), json.end());
|
|
return r;
|
|
}
|
|
std::vector<uint8_t> meshBytes(const asset_import::PreparedAssetImport& imported) {
|
|
for (const auto& e : imported.entries)
|
|
if (e.path.ends_with("mesh.bin")) return e.bytes;
|
|
return {};
|
|
}
|
|
} // namespace
|
|
TEST_CASE("asset.mesh.multipleUvSetsSurviveImportAndCook") {
|
|
auto imported = asset_import::prepareGltfImport(uvMesh());
|
|
REQUIRE(imported.ok());
|
|
REQUIRE_EQ(imported.value().manifest.assets.front().schemaVersion, SchemaVersion(2));
|
|
auto decoded = asset::decodeCanonicalMesh(meshBytes(imported.value()));
|
|
REQUIRE(decoded.ok());
|
|
REQUIRE_EQ(decoded.value().texcoords.size(), size_t(3));
|
|
REQUIRE_EQ(decoded.value().texcoords.at(0)[5], 2.5f);
|
|
REQUIRE_EQ(decoded.value().texcoords.at(2)[1], 1.f);
|
|
REQUIRE_EQ(decoded.value().texcoords.at(7)[5], 1.f);
|
|
auto archiveBytes = asset::buildEvaArchive(imported.value().manifest, imported.value().entries);
|
|
REQUIRE(archiveBytes.ok());
|
|
auto archive = asset::parseEvaArchive(archiveBytes.value());
|
|
REQUIRE(archive.ok());
|
|
auto profile = asset::assetCookProfileForTarget("windows-x86_64-vulkan");
|
|
REQUIRE(profile.ok());
|
|
auto cooked = asset::cookEvaToEvpack(archive.value(), profile.value());
|
|
REQUIRE(cooked.ok());
|
|
auto pack = asset::parseEvpack(cooked.value().bytes);
|
|
REQUIRE(pack.ok());
|
|
asset::EvpackResourceReader reader(std::make_shared<const asset::Evpack>(std::move(pack).takeValue()));
|
|
asset::EvpackCapabilities caps{"windows", "x86_64", "vulkan", {"rgba8"}, {"spirv-1.6"}, {"high"}, {}};
|
|
auto payload = reader.read(imported.value().manifest.entrypoints.at("default"), "eve.mesh/3", caps, 1024 * 1024);
|
|
REQUIRE(payload.ok());
|
|
bool checked = false;
|
|
for (const auto& c : payload.value().chunks)
|
|
if (c.kind == asset::EvpackChunkKind::Bulk) {
|
|
auto actual = asset::decodeCanonicalMesh(c.bytes);
|
|
REQUIRE(actual.ok());
|
|
REQUIRE_EQ(actual.value().texcoords, decoded.value().texcoords);
|
|
checked = true;
|
|
}
|
|
REQUIRE(checked);
|
|
}
|
|
TEST_CASE("asset.mesh.rejectsMalformedUvStreamsAndBudgets") {
|
|
auto imported = asset_import::prepareGltfImport(uvMesh());
|
|
REQUIRE(imported.ok());
|
|
const auto original = meshBytes(imported.value());
|
|
auto bad = original;
|
|
bad[28] = 0; // Duplicate UV descriptor (0 instead of 2).
|
|
REQUIRE(!asset::decodeCanonicalMesh(bad).ok());
|
|
bad = original;
|
|
bad.pop_back();
|
|
REQUIRE(!asset::decodeCanonicalMesh(bad).ok());
|
|
bad = original;
|
|
bad[20] = 255;
|
|
REQUIRE(!asset::decodeCanonicalMesh(bad).ok());
|
|
bad = original;
|
|
bad[48] = 0;
|
|
bad[49] = 0;
|
|
bad[50] = 128;
|
|
bad[51] = 127;
|
|
REQUIRE(!asset::decodeCanonicalMesh(bad).ok());
|
|
asset::CanonicalMeshLimits limits;
|
|
limits.maximumDecodedBytes = 32;
|
|
REQUIRE(!asset::decodeCanonicalMesh(original, limits).ok());
|
|
auto request = uvMesh();
|
|
request.limits.maximumDecodedBytes = 32;
|
|
REQUIRE(!asset_import::prepareGltfImport(request).ok());
|
|
}
|
|
#ifdef EVE_TEST_MESH_UPLOAD
|
|
TEST_CASE("asset.mesh.graphicsSelectsUvSetWithoutSilentRemap") {
|
|
auto imported = asset_import::prepareGltfImport(uvMesh());
|
|
REQUIRE(imported.ok());
|
|
auto bytes = asset::buildEvaArchive(imported.value().manifest, imported.value().entries);
|
|
REQUIRE(bytes.ok());
|
|
auto archive = asset::parseEvaArchive(bytes.value());
|
|
REQUIRE(archive.ok());
|
|
auto profile = asset::assetCookProfileForTarget("windows-x86_64-vulkan");
|
|
REQUIRE(profile.ok());
|
|
auto cooked = asset::cookEvaToEvpack(archive.value(), profile.value());
|
|
REQUIRE(cooked.ok());
|
|
auto pack = asset::parseEvpack(cooked.value().bytes);
|
|
REQUIRE(pack.ok());
|
|
asset::EvpackResourceReader reader(std::make_shared<const asset::Evpack>(std::move(pack).takeValue()));
|
|
asset::EvpackCapabilities caps{"windows", "x86_64", "vulkan", {"rgba8"}, {"spirv-1.6"}, {"high"}, {}};
|
|
class Factory final : public graphics::IMeshResourceFactory {
|
|
public:
|
|
int calls = 0, token = 0, releases = 0;
|
|
std::vector<float> uv;
|
|
bool multiUv = false;
|
|
std::map<uint32_t, std::vector<float>> channels;
|
|
Result<void> setMeshTexcoords(graphics::Mesh* mesh, uint32_t set, std::span<const float> values) override {
|
|
if (!multiUv) return IMeshResourceFactory::setMeshTexcoords(mesh, set, values);
|
|
channels[set] = {values.begin(), values.end()};
|
|
return Result<void>::success();
|
|
}
|
|
Result<graphics::Mesh*> uploadMesh(const float*, const float*, const float* t, int n, const uint32_t*,
|
|
int) override {
|
|
++calls;
|
|
uv.assign(t, t + n * 2);
|
|
return Result<graphics::Mesh*>::success(reinterpret_cast<graphics::Mesh*>(&token));
|
|
}
|
|
Result<void> releaseMesh(graphics::Mesh*) override {
|
|
++releases;
|
|
return Result<void>::success();
|
|
}
|
|
} factory;
|
|
asset_graphics::EvpackGraphicsLoader loader(reader, factory);
|
|
const auto ref = imported.value().manifest.entrypoints.at("default");
|
|
auto mesh = loader.loadMesh(ref, caps, {}, 2);
|
|
REQUIRE(mesh.ok());
|
|
REQUIRE_EQ(factory.uv, std::vector<float>({0, 1, 0, 1, 0, 1}));
|
|
REQUIRE_EQ(mesh.value().texcoordSet, uint32_t(2));
|
|
REQUIRE(factory.releaseMesh(mesh.value().mesh).ok());
|
|
auto missing = loader.loadMesh(ref, caps, {}, 1);
|
|
REQUIRE(!missing.ok());
|
|
REQUIRE_EQ(factory.calls, 1);
|
|
auto absent = loader.loadMesh(ref, caps, {}, 0, true);
|
|
REQUIRE(!absent.ok());
|
|
REQUIRE_EQ(factory.releases, 2);
|
|
factory.multiUv = true;
|
|
auto complete = loader.loadMesh(ref, caps, {}, 0, true);
|
|
REQUIRE(complete.ok());
|
|
REQUIRE_EQ(factory.channels.size(), size_t(2));
|
|
REQUIRE_EQ(factory.channels.at(2), std::vector<float>({0, 1, 0, 1, 0, 1}));
|
|
REQUIRE(factory.releaseMesh(complete.value().mesh).ok());
|
|
}
|
|
#endif
|
|
TEST_CASE("asset.mesh.legacyBinaryDecodesToCanonicalUvSets") {
|
|
auto request = uvMesh();
|
|
auto doc = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(doc.ok());
|
|
auto& attrs = *doc.value()
|
|
.getIf<Value::Object>()
|
|
->at("meshes")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>()
|
|
->at("primitives")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>()
|
|
->at("attributes")
|
|
.getIf<Value::Object>();
|
|
attrs.erase("TEXCOORD_2");
|
|
attrs.erase("TEXCOORD_7");
|
|
auto json = doc.value().toJson();
|
|
REQUIRE(json.ok());
|
|
request.documentBytes.assign(json.value().begin(), json.value().end());
|
|
auto imported = asset_import::prepareGltfImport(request);
|
|
REQUIRE(imported.ok());
|
|
auto bytes = meshBytes(imported.value());
|
|
bytes[7] = 1;
|
|
bytes[16] = 2;
|
|
bytes[20] = 0;
|
|
bytes.erase(bytes.begin() + 24, bytes.begin() + 28);
|
|
auto legacy = asset::decodeCanonicalMesh(bytes);
|
|
REQUIRE(legacy.ok());
|
|
REQUIRE_EQ(legacy.value().texcoords.size(), size_t(1));
|
|
REQUIRE_EQ(legacy.value().texcoords.at(0)[5], 2.5f);
|
|
bytes[7] = 3;
|
|
REQUIRE(!asset::decodeCanonicalMesh(bytes).ok());
|
|
}
|
|
TEST_CASE("asset.mesh.rejectsInvalidSourceUvAccessors") {
|
|
for (int kind = 0; kind < 3; ++kind) {
|
|
auto request = uvMesh();
|
|
auto document = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(document.ok());
|
|
auto& root = *document.value().getIf<Value::Object>();
|
|
auto& uv = *root.at("accessors").getIf<Value::Array>()->at(2).getIf<Value::Object>();
|
|
if (kind == 0) uv["normalized"] = Value(false);
|
|
if (kind == 1) uv["count"] = Value(int64_t(2));
|
|
if (kind == 2) {
|
|
auto& attrs = *root.at("meshes")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>()
|
|
->at("primitives")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>()
|
|
->at("attributes")
|
|
.getIf<Value::Object>();
|
|
attrs["TEXCOORD_4294967296"] = Value(int64_t(2));
|
|
}
|
|
auto json = document.value().toJson();
|
|
REQUIRE(json.ok());
|
|
request.documentBytes.assign(json.value().begin(), json.value().end());
|
|
REQUIRE(!asset_import::prepareGltfImport(request).ok());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("asset.mesh.encoderRoundTripsCanonicalVersion3WithVertexColors") {
|
|
asset::CanonicalMeshData source;
|
|
source.positions = {0, 0, 0, 1, 0, 0, 0, 1, 0};
|
|
source.normals = {0, 0, 1, 0, 0, 1, 0, 0, 1};
|
|
source.texcoords[0] = {0, 0, 1, 0, 0, 1};
|
|
source.colors = {1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5f};
|
|
source.indices = {0, 1, 2};
|
|
auto encoded = asset::encodeCanonicalMesh(source);
|
|
REQUIRE(encoded.ok());
|
|
auto decoded = asset::decodeCanonicalMesh(encoded.value());
|
|
REQUIRE(decoded.ok());
|
|
CHECK_EQ(decoded.value().positions, source.positions);
|
|
CHECK_EQ(decoded.value().normals, source.normals);
|
|
CHECK_EQ(encoded.value()[7], std::uint8_t(3));
|
|
CHECK_EQ(decoded.value().texcoords, source.texcoords);
|
|
CHECK_EQ(decoded.value().colors, source.colors);
|
|
CHECK_EQ(decoded.value().indices, source.indices);
|
|
auto invalid = source;
|
|
invalid.texcoords[0].pop_back();
|
|
REQUIRE(!asset::encodeCanonicalMesh(invalid).ok());
|
|
invalid = source;
|
|
invalid.colors.pop_back();
|
|
REQUIRE(!asset::encodeCanonicalMesh(invalid).ok());
|
|
invalid = source;
|
|
invalid.indices[2] = 3;
|
|
REQUIRE(!asset::encodeCanonicalMesh(invalid).ok());
|
|
} |