Files
EVEngine/test/animation_binary.cpp
T

181 lines
6.1 KiB
C++

#include <bit>
#include <cmath>
#include <cstdint>
#include <limits>
#include <vector>
#include "animation/AnimClip.h"
#include "animation/AnimClipBinary.h"
#include "animation/AnimParallelInternal.h"
#include "animation/AnimPose.h"
#include "animation/AnimSkeleton.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
using namespace eve::animation;
namespace {
using Bytes = std::vector<std::byte>;
void integer(Bytes& b, std::uint32_t v) {
for (int i = 0; i < 4; ++i) b.push_back(static_cast<std::byte>((v >> (8 * i)) & 255));
}
void number(Bytes& b, float v) { integer(b, std::bit_cast<std::uint32_t>(v)); }
void string(Bytes& b, const std::string& v) {
integer(b, static_cast<std::uint32_t>(v.size()));
for (auto c : v) b.push_back(static_cast<std::byte>(c));
}
Bytes fixture() {
Bytes b;
integer(b, 0x43415645);
integer(b, 1);
integer(b, 1);
number(b, 2.f);
number(b, 30.f);
integer(b, 1);
string(b, "test");
string(b, "root");
integer(b, 2);
for (float t : {0.f, 2.f}) {
number(b, t);
number(b, 0.f);
number(b, 0.f);
number(b, 3.f * t);
}
integer(b, 1);
number(b, 0.f);
number(b, 0.f);
number(b, 0.f);
number(b, 0.f);
number(b, 1.f);
integer(b, 1);
number(b, 0.f);
number(b, 1.f);
number(b, 1.f);
number(b, 1.f);
return b;
}
} // namespace
TEST_CASE("animation.binary.parallelBatchMatchesSerialAndRejectsAtomically") {
AnimSkeleton sk;
sk.addBone("root");
const auto bytes = fixture();
for (int workers : {1, 2, 8}) {
std::vector<AnimClip> clips(32);
std::vector<AnimationTrackInput> inputs;
for (auto& clip : clips) inputs.push_back({clip, bytes});
auto loaded = loadAnimationTrackBatch(inputs, sk, workers);
REQUIRE(loaded.ok());
CHECK(loaded.value() >= 1);
CHECK(loaded.value() <= workers);
for (auto& clip : clips) {
AnimPose pose;
clip.sample(0.5f, &pose, &sk);
CHECK(pose.getLocalPositionZ(0) == 1.5f);
clip.setName("unchanged");
}
inputs[17].bytes = std::span<const std::byte>(bytes).first(12);
auto rejected = loadAnimationTrackBatch(inputs, sk, workers);
CHECK(!rejected.ok());
for (auto& clip : clips) CHECK(clip.getName() == "unchanged");
inputs[17].bytes = bytes;
inputs[31].destination = clips[0];
auto duplicate = loadAnimationTrackBatch(inputs, sk, workers);
CHECK(!duplicate.ok());
for (auto& clip : clips) CHECK(clip.getName() == "unchanged");
}
auto empty = loadAnimationTrackBatch({}, sk);
REQUIRE(empty.ok());
CHECK(empty.value() == 0);
auto invalid = loadAnimationTrackBatch({}, sk, -1);
CHECK(!invalid.ok());
}
TEST_CASE("animation.binary.parallelWorkersJoinBeforeFailureReturns") {
std::vector<int> visited(64, 0);
bool caught = false;
try {
eve::animation::detail::parallelAnimationItems(visited.size(), 8, [&](std::size_t i) {
visited[i] = 1;
if (i == 3 || i == 30) throw std::runtime_error(std::to_string(i));
});
} catch (const std::runtime_error& error) {
caught = true;
CHECK(std::string(error.what()) == "3");
}
CHECK(caught);
for (int item : visited) CHECK(item == 1);
}
TEST_CASE("animation.binary.loadsTracksAndPreservesSampling") {
AnimSkeleton sk;
sk.addBone("root");
sk.addBone("unanimated", 0);
sk.setBindPosition(1, 0.f, 1.f, 0.f);
AnimClip clip("old");
auto loaded = loadAnimationTracks(clip, fixture(), sk);
REQUIRE(loaded.ok());
CHECK(clip.getName() == "test");
CHECK(clip.getLoop());
CHECK(clip.getDuration() == 2.f);
AnimPose p;
clip.sample(0.5f, &p, &sk);
p.computeWorld(&sk);
CHECK(std::abs(p.getWorldPositionZ(1) - 1.5f) < 1e-6f);
CHECK(p.getWorldPositionY(1) == 1.f);
clip.sample(2.5f, &p, &sk);
CHECK(std::abs(p.getLocalPositionZ(0) - 1.5f) < 1e-6f);
}
TEST_CASE("animation.binary.rejectsEveryTruncationAtomically") {
AnimSkeleton sk;
sk.addBone("root");
AnimClip clip("unchanged");
clip.setDuration(4.f);
const auto bytes = fixture();
for (std::size_t size = 0; size < bytes.size(); ++size) {
auto r = loadAnimationTracks(clip, {bytes.data(), size}, sk);
CHECK(!r.ok());
CHECK(clip.getName() == "unchanged");
CHECK(clip.getDuration() == 4.f);
}
}
TEST_CASE("animation.binary.rejectsVersionTrailingNamesFlagsAndNonfinite") {
AnimSkeleton sk;
sk.addBone("root");
AnimClip clip("old");
for (int bad : {0, 1, 2, 3, 4, 5, 6, 7}) {
auto bytes = fixture();
if (bad == 0) bytes[4] = std::byte{2};
if (bad == 1) bytes.push_back(std::byte{0});
if (bad == 2) bytes[36] = std::byte{'X'};
if (bad == 3) bytes[20] = std::byte{2};
if (bad == 4) {
Bytes nan;
number(nan, std::numeric_limits<float>::quiet_NaN());
for (int i = 0; i < 4; ++i) bytes[12 + i] = nan[i];
}
if (bad == 5) bytes[36] = std::byte{0xc0};
if (bad == 6)
for (int i = 0; i < 4; ++i) bytes[60 + i] = std::byte{0};
if (bad == 7)
for (int i = 0; i < 4; ++i) bytes[96 + i] = std::byte{0};
auto r = loadAnimationTracks(clip, bytes, sk);
CHECK(!r.ok());
CHECK(clip.getName() == "old");
}
}
TEST_CASE("animation.binary.stepBoundaryAtAdjacentFloatTimes") {
AnimClip clip("step");
clip.setLoop(false);
AnimSkeleton sk;
sk.addBone("root");
const float before = std::nextafter(1.f, 0.f);
clip.addPositionKey(0, 0.f, 0.f, 0.f, 0.f);
clip.addPositionKey(0, before, 0.f, 0.f, 0.f);
clip.addPositionKey(0, 1.f, 0.f, 0.f, 2.f);
clip.addPositionKey(0, 2.f, 0.f, 0.f, 2.f);
AnimPose p;
for (float t : {0.3f, before}) {
clip.sample(t, &p, &sk);
CHECK(p.getLocalPositionZ(0) == 0.f);
}
for (float t : {1.f, std::nextafter(1.f, 2.f), 1.8f}) {
clip.sample(t, &p, &sk);
CHECK(p.getLocalPositionZ(0) == 2.f);
}
}