Files
EVEngine/test/asset_graphics_loader.cpp
T

249 lines
10 KiB
C++

#include "asset/CanonicalMesh.h"
#include "asset/graphics/EvpackGraphicsLoader.h"
#include "asset/graphics/EvpackImageLoader.h"
#include "graphics/IMeshResourceFactory.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <bit>
using namespace eve;
using namespace eve::asset;
using namespace eve::asset_graphics;
namespace {
PersistentId persistentId(std::string_view text) {
auto parsed = PersistentId::parse(text);
REQUIRE(parsed);
return *parsed;
}
AssetRef assetRef(std::string_view text) {
auto parsed = AssetRef::parse(text);
REQUIRE(parsed.ok());
return std::move(parsed).takeValue();
}
void put32(std::vector<std::uint8_t>& bytes, std::uint32_t value) {
for (unsigned shift = 0; shift != 32; shift += 8)
bytes.push_back(static_cast<std::uint8_t>(value >> shift));
}
void putFloat(std::vector<std::uint8_t>& bytes, float value) {
put32(bytes, std::bit_cast<std::uint32_t>(value));
}
std::vector<std::uint8_t> triangleBlob() {
std::vector<std::uint8_t> bytes = {'E', 'V', 'M', 'E', 'S', 'H', 0, 1};
put32(bytes, 3);
put32(bytes, 3);
put32(bytes, 3);
put32(bytes, 0);
const float vertices[][8] = {{0, 0, 0, 0, 0, 1, 0, 0},
{1, 0, 0, 0, 0, 1, 1, 0},
{0, 1, 0, 0, 0, 1, 0, 1}};
for (const auto& vertex : vertices)
for (float value : vertex) putFloat(bytes, value);
put32(bytes, 0);
put32(bytes, 1);
put32(bytes, 2);
return bytes;
}
std::shared_ptr<const Evpack> makePack(std::vector<std::uint8_t> mesh) {
const auto id = persistentId("550e8400-e29b-41d4-a716-446655440000");
EvpackBuild build;
build.packageId = persistentId("018f6f22-2490-7ad2-bf58-4f1dbca31040");
build.buildId = persistentId("018f6f22-2490-7ad2-bf58-4f1dbca31041");
build.variants = {{"windows", "x86_64", "vulkan", {"bc"}, "spirv-1.6", "high", {}}};
const auto schema = mesh.size() > 7 && mesh[7] == 3 ? SchemaVersion(3) : SchemaVersion(1);
build.chunks = {{id, "eve.mesh", schema, 0, EvpackChunkKind::Definition, 0,
EvpackCodec::None, 8, {}, {'{', '}' }},
{id, "eve.mesh", schema, 0, EvpackChunkKind::Bulk, 1,
EvpackCodec::None, 16, {}, std::move(mesh)}};
auto bytes = buildEvpack(std::move(build));
REQUIRE(bytes.ok());
auto parsed = parseEvpack(bytes.value());
REQUIRE(parsed.ok());
return std::make_shared<const Evpack>(std::move(parsed).takeValue());
}
class RecordingMeshFactory final : public eve::graphics::IMeshResourceFactory {
public:
Result<eve::graphics::Mesh*> uploadMesh(const float* positions, const float* normals,
const float* texcoords, int vertexCount,
const std::uint32_t* indices, int indexCount) override {
++uploads;
observedPositions.assign(positions, positions + vertexCount * 3);
observedNormals.assign(normals, normals + vertexCount * 3);
observedTexcoords.assign(texcoords, texcoords + vertexCount * 2);
observedIndices.assign(indices, indices + indexCount);
if (reject)
return Result<eve::graphics::Mesh*>::failure(
Diagnostic::error(DiagnosticCode::Failed, "injected upload rejection"));
return Result<eve::graphics::Mesh*>::success(
reinterpret_cast<eve::graphics::Mesh*>(&token));
}
Result<eve::graphics::Mesh*> uploadMeshColored(
const float* positions, const float* normals, const float* texcoords, const float* colors,
int vertexCount, const std::uint32_t* indices, int indexCount) override {
auto result = uploadMesh(positions, normals, texcoords, vertexCount, indices, indexCount);
if (result) observedColors.assign(colors, colors + vertexCount * 4);
return result;
}
Result<void> releaseMesh(eve::graphics::Mesh*) override { return Result<void>::success(); }
bool reject = false;
int uploads = 0;
std::vector<float> observedPositions;
std::vector<float> observedNormals;
std::vector<float> observedTexcoords;
std::vector<float> observedColors;
std::vector<std::uint32_t> observedIndices;
private:
std::uint64_t token = 0;
};
class RecordingImageFactory final : public eve::graphics::IImageResourceFactory {
public:
Result<eve::graphics::Texture*> uploadRgba8(std::uint32_t, std::uint32_t,
const std::uint8_t*, bool) override {
++uploads;
if (reject)
return Result<eve::graphics::Texture*>::failure(
Diagnostic::error(DiagnosticCode::Failed, "injected image upload rejection"));
return Result<eve::graphics::Texture*>::success(
reinterpret_cast<eve::graphics::Texture*>(&token));
}
Result<void> releaseImage(eve::graphics::Texture*) override {
return Result<void>::success();
}
bool reject = false;
int uploads = 0;
private:
std::uint64_t token = 0;
};
std::shared_ptr<const Evpack> makeImagePack(std::vector<std::uint8_t> image) {
const auto id = persistentId("550e8400-e29b-41d4-a716-446655440000");
EvpackBuild build;
build.packageId = persistentId("018f6f22-2490-7ad2-bf58-4f1dbca31040");
build.buildId = persistentId("018f6f22-2490-7ad2-bf58-4f1dbca31041");
build.variants = {{"windows", "x86_64", "vulkan", {"rgba8"}, "spirv-1.6", "high", {}}};
build.chunks = {{id, "eve.image", SchemaVersion(2), 0, EvpackChunkKind::Definition, 0,
EvpackCodec::None, 8, {}, {'{', '}' }},
{id, "eve.image", SchemaVersion(2), 0, EvpackChunkKind::Bulk, 1,
EvpackCodec::None, 8, {}, std::move(image)}};
auto bytes = buildEvpack(std::move(build));
REQUIRE(bytes.ok());
auto parsed = parseEvpack(bytes.value());
REQUIRE(parsed.ok());
return std::make_shared<const Evpack>(std::move(parsed).takeValue());
}
std::vector<std::uint8_t> onePixelEvimg() {
std::vector<std::uint8_t> bytes = {'E', 'V', 'I', 'M', 'G', 0, 1, 0};
put32(bytes, 1);
put32(bytes, 1);
put32(bytes, 0);
put32(bytes, 0);
bytes.insert(bytes.end(), {10, 20, 30, 255});
return bytes;
}
const EvpackCapabilities capabilities = {"windows", "x86_64", "vulkan", {"bc"},
{"spirv-1.6"}, {"high"}, {}};
} // namespace
TEST_CASE("asset.graphics.validatesCanonicalMeshBeforeBackendUpload") {
EvpackResourceReader reader(makePack(triangleBlob()));
RecordingMeshFactory factory;
EvpackGraphicsLoader loader(reader, factory);
auto loaded = loader.loadMesh(assetRef("asset://550e8400-e29b-41d4-a716-446655440000"),
capabilities);
REQUIRE(loaded.ok());
CHECK(loaded.value().mesh != nullptr);
CHECK_EQ(factory.uploads, 1);
CHECK_EQ(factory.observedPositions.size(), std::size_t(9));
CHECK_EQ(factory.observedNormals.size(), std::size_t(9));
CHECK_EQ(factory.observedTexcoords.size(), std::size_t(6));
CHECK_EQ(factory.observedIndices, std::vector<std::uint32_t>({0, 1, 2}));
}
TEST_CASE("asset.graphics.uploadsCanonicalVertexColorsWithoutDroppingThem") {
asset::CanonicalMeshData mesh;
mesh.positions = {0, 0, 0, 1, 0, 0, 0, 1, 0};
mesh.normals = {0, 0, 1, 0, 0, 1, 0, 0, 1};
mesh.texcoords[0] = {0, 0, 1, 0, 0, 1};
mesh.colors = {1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1};
mesh.indices = {0, 1, 2};
auto encoded = encodeCanonicalMesh(mesh);
REQUIRE(encoded.ok());
EvpackResourceReader reader(makePack(std::move(encoded).takeValue()));
RecordingMeshFactory factory;
EvpackGraphicsLoader loader(reader, factory);
auto loaded = loader.loadMesh(assetRef("asset://550e8400-e29b-41d4-a716-446655440000"),
capabilities);
REQUIRE(loaded.ok());
CHECK_EQ(factory.observedColors, mesh.colors);
}
TEST_CASE("asset.graphics.invalidMeshNeverReachesBackend") {
auto bytes = triangleBlob();
bytes.back() = 9;
EvpackResourceReader reader(makePack(std::move(bytes)));
RecordingMeshFactory factory;
EvpackGraphicsLoader loader(reader, factory);
auto loaded = loader.loadMesh(assetRef("asset://550e8400-e29b-41d4-a716-446655440000"),
capabilities);
REQUIRE(!loaded.ok());
CHECK_EQ(loaded.error()->code(), DiagnosticCode::ParseError);
CHECK_EQ(factory.uploads, 0);
}
TEST_CASE("asset.graphics.backendRejectionIsStructuredFailure") {
EvpackResourceReader reader(makePack(triangleBlob()));
RecordingMeshFactory factory;
factory.reject = true;
EvpackGraphicsLoader loader(reader, factory);
auto loaded = loader.loadMesh(assetRef("asset://550e8400-e29b-41d4-a716-446655440000"),
capabilities);
REQUIRE(!loaded.ok());
CHECK_EQ(loaded.error()->code(), DiagnosticCode::Failed);
CHECK_EQ(factory.uploads, 1);
}
TEST_CASE("asset.graphics.imageValidationPrecedesTextureUpload") {
auto invalid = onePixelEvimg();
invalid.pop_back();
EvpackResourceReader reader(makeImagePack(std::move(invalid)));
RecordingImageFactory factory;
EvpackImageLoader loader(reader, factory);
auto loaded = loader.load(assetRef("asset://550e8400-e29b-41d4-a716-446655440000"),
{"windows", "x86_64", "vulkan", {"rgba8"},
{"spirv-1.6"}, {"high"}, {}});
REQUIRE(!loaded.ok());
CHECK_EQ(loaded.error()->code(), DiagnosticCode::InvalidArgument);
CHECK_EQ(factory.uploads, 0);
}
TEST_CASE("asset.graphics.imageBackendRejectionIsStructuredFailure") {
EvpackResourceReader reader(makeImagePack(onePixelEvimg()));
RecordingImageFactory factory;
factory.reject = true;
EvpackImageLoader loader(reader, factory);
auto loaded = loader.load(assetRef("asset://550e8400-e29b-41d4-a716-446655440000"),
{"windows", "x86_64", "vulkan", {"rgba8"},
{"spirv-1.6"}, {"high"}, {}});
REQUIRE(!loaded.ok());
CHECK_EQ(loaded.error()->code(), DiagnosticCode::Failed);
CHECK_EQ(factory.uploads, 1);
}