Files
EVEngine/test/physics_core_boundary.cpp
T

300 lines
13 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "physics/Body.h"
#include "physics/Body3D.h"
#include "physics/Physics.h"
#include "physics/PhysicsLink.h"
#include "physics/Shape3D.h"
#include "physics/World3D.h"
#include "physics/backend/SimulationBackend.h"
#include "physics/World.h"
#include "common/Capability.h"
#include <Box2D/Box2D.h>
#include <box3d/box3d.h>
#include <cmath>
#include <cstdint>
#include <memory>
#include <stdexcept>
using namespace eve::physics;
namespace {
class CapabilityReset {
public:
CapabilityReset() { eve::cap::detail::clearAllRaw(); }
~CapabilityReset() { eve::cap::detail::clearAllRaw(); }
};
class MockAcceleratorProvider final : public IAcceleratorBackendProvider {
public:
[[nodiscard]] bool supports(SimulationBackendDomain domain) const noexcept override {
return domain == SimulationBackendDomain::World2D;
}
[[nodiscard]] eve::Result<std::unique_ptr<ISimulationBackend>> create(SimulationBackendDomain domain,
void * /*state*/) override {
if (!supports(domain)) {
return eve::Result<std::unique_ptr<ISimulationBackend>>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::Unsupported, "Mock accelerator does not implement this domain"));
}
return eve::Result<std::unique_ptr<ISimulationBackend>>::success(detail::makeMockAcceleratorBackend());
}
};
class FailingProvider final : public IAcceleratorBackendProvider {
public:
explicit FailingProvider(bool throws) : throws_(throws) {}
[[nodiscard]] bool supports(SimulationBackendDomain) const noexcept override { return true; }
[[nodiscard]] eve::Result<std::unique_ptr<ISimulationBackend>> create(SimulationBackendDomain,
void*) override {
if (throws_) throw std::runtime_error("accelerator setup exploded");
return eve::Result<std::unique_ptr<ISimulationBackend>>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::Conflict, "accelerator rejected shared state", "provider.state"));
}
private:
bool throws_ = false;
};
class FailSecondStepBackend final : public ISimulationBackend {
public:
explicit FailSecondStepBackend(b3WorldId* world)
: delegate_(detail::makeCallbackSimulationBackend(world, &stepBox3D, SimulationBackendKind::MockAccelerator,
SimulationDeterminism::ToleranceBounded)) {}
[[nodiscard]] eve::Result<void> step(const eve::SimulationStep& step,
const SimulationSettings& settings) override {
if (delegate_->observation().stepCount != 0) {
return eve::Result<void>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::Failed, "injected second-step failure", "physics.test.step"));
}
return delegate_->step(step, settings);
}
[[nodiscard]] SimulationObservation observation() const noexcept override { return delegate_->observation(); }
[[nodiscard]] SimulationBackendKind kind() const noexcept override { return delegate_->kind(); }
[[nodiscard]] SimulationDeterminism determinism() const noexcept override { return delegate_->determinism(); }
[[nodiscard]] eve::Result<void> restoreObservation(const SimulationObservation& value) override {
return delegate_->restoreObservation(value);
}
private:
static void stepBox3D(void* context, const eve::SimulationStep& step,
const SimulationSettings& settings) noexcept {
b3World_Step(*static_cast<b3WorldId*>(context), static_cast<float>(step.delta.seconds()),
settings.subStepCount);
}
std::unique_ptr<ISimulationBackend> delegate_;
};
class FailSecondStepProvider final : public IAcceleratorBackendProvider {
public:
[[nodiscard]] bool supports(SimulationBackendDomain domain) const noexcept override {
return domain == SimulationBackendDomain::World3D;
}
[[nodiscard]] eve::Result<std::unique_ptr<ISimulationBackend>> create(SimulationBackendDomain,
void* state) override {
return eve::Result<std::unique_ptr<ISimulationBackend>>::success(
std::make_unique<FailSecondStepBackend>(static_cast<b3WorldId*>(state)));
}
};
void checkObservableContract(ISimulationBackend &backend) {
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)};
auto applied = backend.step(step, SimulationSettings{});
const bool appliedOk = applied.ok();
CHECK(appliedOk);
const SimulationObservation observation = backend.observation();
CHECK_EQ(observation.stepCount, std::uint64_t(1));
CHECK(observation.lastTick == step.tick);
CHECK_EQ(observation.simulatedDuration, step.delta);
auto duplicate = backend.step(step, SimulationSettings{});
const bool duplicateOk = duplicate.ok();
CHECK(!duplicateOk);
CHECK_EQ(duplicate.code(), eve::StatusCode::Rejected);
}
} // namespace
TEST_CASE("physics.core.backendContract.isHeadlessAndObservable") {
auto *physics = Physics::create();
std::unique_ptr<World> world(physics->newWorld(0.f, 900.f));
REQUIRE(world.get() != nullptr);
Body *body = world->newBody("dynamic", 0.f, 0.f);
body->newCircleFixture(10.f);
const float y0 = body->getY();
auto backend = detail::makeBox2DSimulationBackend(world->raw());
REQUIRE(backend.get() != nullptr);
CHECK_EQ(backend->kind(), SimulationBackendKind::Cpu);
CHECK_EQ(backend->determinism(), SimulationDeterminism::ToleranceBounded);
CHECK_EQ(backend->observation().stepCount, std::uint64_t(0));
auto stepped = backend->step(SimulationStep{eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)},
SimulationSettings{});
const bool steppedOk = stepped.ok();
REQUIRE(steppedOk);
const SimulationObservation observation = backend->observation();
CHECK_EQ(observation.stepCount, std::uint64_t(1));
CHECK(observation.lastTick == eve::SimulationTick{1});
CHECK(std::fabs(observation.simulatedSeconds - 0.016666667) < 1e-9);
CHECK(std::fabs(observation.lastDeltaSeconds - 0.016666667f) < 1e-6f);
CHECK_GT(body->getY(), y0);
}
TEST_CASE("physics.core.backendContract.cpuAndMockShareObservableRules") {
CapabilityReset reset;
b2World rawWorld(b2Vec2_zero);
auto cpu = detail::makeBox2DSimulationBackend(&rawWorld);
REQUIRE(cpu.get() != nullptr);
checkObservableContract(*cpu);
auto mock = detail::makeMockAcceleratorBackend();
REQUIRE(mock.get() != nullptr);
CHECK_EQ(mock->kind(), SimulationBackendKind::MockAccelerator);
checkObservableContract(*mock);
}
TEST_CASE("physics.core.backendFallbackIsStructuredAndObservable") {
CapabilityReset reset;
b2World rawWorld(b2Vec2_zero);
auto absent = detail::selectSimulationBackend(SimulationBackendDomain::World2D,
detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true);
const bool absentOk = absent.ok();
REQUIRE(absentOk);
CHECK_EQ(absent.status().code(), eve::StatusCode::Applied);
REQUIRE_EQ(absent.diagnostics().size(), std::size_t(1));
CHECK_EQ(absent.diagnostics().front().code(), eve::DiagnosticCode::Unsupported);
auto absentSelection = std::move(absent).takeValue();
CHECK(absentSelection.usedFallback);
CHECK_EQ(absentSelection.actualKind, SimulationBackendKind::Cpu);
MockAcceleratorProvider provider;
eve::cap::provide<IAcceleratorBackendProvider>(&provider);
auto present = detail::selectSimulationBackend(SimulationBackendDomain::World2D,
detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true);
const bool presentOk = present.ok();
REQUIRE(presentOk);
CHECK(present.diagnostics().empty());
auto presentSelection = std::move(present).takeValue();
CHECK(!presentSelection.usedFallback);
CHECK_EQ(presentSelection.actualKind, SimulationBackendKind::MockAccelerator);
}
TEST_CASE("physics.core.backendFallbackPreservesProviderFailureDiagnostics") {
CapabilityReset reset;
b2World rawWorld(b2Vec2_zero);
FailingProvider provider(false);
eve::cap::provide<IAcceleratorBackendProvider>(&provider);
auto selected = detail::selectSimulationBackend(SimulationBackendDomain::World2D,
detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true);
REQUIRE(selected.ok());
REQUIRE_EQ(selected.diagnostics().size(), std::size_t(2));
CHECK_EQ(selected.diagnostics()[1].code(), eve::DiagnosticCode::Conflict);
CHECK_EQ(selected.diagnostics()[1].message(), std::string("accelerator rejected shared state"));
CHECK_EQ(selected.diagnostics()[1].path(), std::string("provider.state"));
}
TEST_CASE("physics.core.backendFallbackPreservesProviderExceptionMessage") {
CapabilityReset reset;
b2World rawWorld(b2Vec2_zero);
FailingProvider provider(true);
eve::cap::provide<IAcceleratorBackendProvider>(&provider);
auto selected = detail::selectSimulationBackend(SimulationBackendDomain::World2D,
detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true);
REQUIRE(selected.ok());
REQUIRE_EQ(selected.diagnostics().size(), std::size_t(2));
CHECK_EQ(selected.diagnostics()[1].code(), eve::DiagnosticCode::CallbackFailure);
CHECK_EQ(selected.diagnostics()[1].message(), std::string("accelerator setup exploded"));
}
TEST_CASE("physics.core.world3dFailedStepPreservesPreviousContactEvents") {
CapabilityReset reset;
FailSecondStepProvider provider;
eve::cap::provide<IAcceleratorBackendProvider>(&provider);
World3D world(0.f, -9.8f, 0.f, false);
std::unique_ptr<Body3D> bodyA(world.newBody("dynamic", 0.f, 0.f, 0.f));
std::unique_ptr<Body3D> bodyB(world.newBody("dynamic", 0.f, 0.f, 0.f));
std::unique_ptr<Shape3D> shapeA(bodyA->newSphereShape(1.f));
std::unique_ptr<Shape3D> shapeB(bodyB->newSphereShape(1.f));
auto first = world.step({eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)});
REQUIRE(first.ok());
REQUIRE_GT(world.getBeginContactCount(), 0);
const int previousCount = world.getBeginContactCount();
auto failed = world.step({eve::SimulationTick{2}, eve::Duration::fromNanoseconds(16666667)});
CHECK(!failed.ok());
CHECK_EQ(world.getBeginContactCount(), previousCount);
CHECK_EQ(world.simulationTick(), eve::SimulationTick{1});
}
TEST_CASE("physics.core.worldUpdateNeedsNoGraphics") {
auto *physics = Physics::create();
std::unique_ptr<World> world(physics->newWorld(0.f, 0.f));
REQUIRE(world.get() != nullptr);
Body *body = world->newBody("dynamic", 20.f, 30.f);
body->newCircleFixture(5.f);
body->setLinearVelocity(60.f, 0.f);
world->update(1.f / 60.f);
CHECK(std::fabs(body->getX() - 21.f) < 0.05f);
CHECK(std::fabs(body->getY() - 30.f) < 0.05f);
}
TEST_CASE("physics.core.worldAcceptsInjectedTickAndPhysicsLinkResolvesStale") {
auto *physics = Physics::create();
std::unique_ptr<World> world(physics->newWorld(0.f, 0.f));
REQUIRE(world.get() != nullptr);
Body *body = world->newBody("dynamic", 0.f, 0.f);
body->newCircleFixture(5.f);
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)};
auto stepped = world->step(step);
const bool steppedOk = stepped.ok();
REQUIRE(steppedOk);
CHECK(world->simulationTick() == step.tick);
CHECK_EQ(world->simulationObservation().lastTick, step.tick);
CHECK(world->backendSelectionStatus().code() == eve::StatusCode::Applied);
auto linkResult = PhysicsLink::fromBody(*body);
const bool linkOk = linkResult.ok();
REQUIRE(linkOk);
const PhysicsLink link = std::move(linkResult).takeValue();
auto resolved = link.resolve(*world);
const bool resolvedOk = resolved.ok();
REQUIRE(resolvedOk);
CHECK(std::move(resolved).takeValue() == body);
body->destroy();
auto stale = link.resolve(*world);
const bool staleOk = stale.ok();
CHECK(!staleOk);
CHECK_EQ(stale.code(), eve::StatusCode::Rejected);
}
TEST_CASE("physics.core.backendContract.rejectsNullWorld") {
bool threw = false;
try {
[[maybe_unused]] auto backend = detail::makeBox2DSimulationBackend(nullptr);
} catch (...) {
threw = true;
}
CHECK(threw);
}