Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/internals/physics-and-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ One system per World, `rawframe.physics2d.step` or `rawframe.physics3d.step`, st
5. Contacts are written from the step's contact and overlap streams, and every body's pose and velocity are written back.
6. Attached entities are put at their parent's pose and offset, parents before children.

The 2D step takes ticks of one microsecond to ten seconds, the lengths Maul2D steps; at any other tick rate it fails before it changes anything. A kinematic target too far to reach within Maul2D's speed limit in one tick is cleared and not carried out, and the statistics count only the targets carried out.

Gameplay that pushes bodies runs `before` the step; gameplay that reads where they went runs `after`. Nothing in the step depends on addresses, hash order or time, so a predicting client that makes its player's body again where the server says steps exactly as the server did ([prediction.md](prediction.md)).

## Collision
Expand Down
2 changes: 1 addition & 1 deletion docs/operations/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Every third-party library is vendored under `third_party/` at an exact revision
| Library | Upstream | Revision | License | What is vendored |
|---|---|---|---|---|
| Kest | `Rawframe-Project/kest` | `fa092f540ffe2cb94f75199842792fb61e68451f` | MIT | `include/`, `src/` except `main.c`, `lib/`, `LICENSE` |
| Maul2D 0.6.0 | `Rawframe-Project/maul2d` release archive `maul2d-0.6.0.tar.gz`, SHA-256 `f97262cd98a47e2f52ed7a811ce144bff1c3f51931323457d1313484c9a845ae` | `501ac5c191658435cbdd4f70a0abd6cdd9e37f03` (`v0.6.0`) | MIT | `include/`, `src/`, `LICENSE`; the build file is the engine's |
| Maul2D 1.3.1 | `Rawframe-Project/maul2d` release archive `maul2d-1.3.1.tar.gz`, SHA-256 `eb4066d15463f45aef7b55cd9331abe110cf4e177daea881c44bcdc31c4a0995` | `cf08f1f2d994e40707ad44bc63f42e6f76f70891` (`v1.3.1`) | MIT | `include/`, `src/`, `LICENSE`; the build file is the engine's |
| Maul3D 0.7.0 | `Rawframe-Project/maul3d` release archive `maul3d-0.7.0.tar.gz`, SHA-256 `b4e42736c2c8413f8218846b9ce777bab79506d75c620707d571f24cb0632d4f` | `bf11f1fc3976ffb8b2ab4e4604252e760aea221c` (`v0.7.0`) | MIT | `include/`, `src/`, `LICENSE`; the build file is the engine's |
| Maul Unicode 1.1.2 | `Rawframe-Project/maul-unicode` release archive `maul-unicode-1.1.2.tar.gz`, SHA-256 `a0e8ed3f04d7316763c7f4658a466eaf7438bd6c9bc26a68bc7e90d7a706991e` | `b026e91fa336eddce422ea6181857417f4e1ec04` (`v1.1.2`) | MIT; its Unicode tables Unicode-3.0 | `include/`, `src/`, `cmake/`, `CMakeLists.txt`, `LICENSE` |
| Maul Window 0.12.0 | `Rawframe-Project/maul-window` release archive `maul-window-0.12.0.tar.gz`, SHA-256 `389b522f338f9584c2d96e682ef60a0c0dd31a5939ef2770b52d7df2247e1764` | `f904519e6c8f86071862080bdbca6f14065f7ea1` (`v0.12.0`) | MIT | `include/`, `src/`, `cmake/`, `protocols/`, `java/`, `tools/gen_web_glue.py`, `CMakeLists.txt`, `LICENSE` |
Expand Down
2 changes: 1 addition & 1 deletion docs/user/dedicated-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The configuration is `key = value` lines; `#` starts a comment, and relative pat
| `kest.game` | The game description, in development. |
| `content.root`, `content.manifest` | Cooked content to read the game from. |
| `content.library` | An installed library, whose active Composition the server plays. |
| `world.tick_rate` | Ticks a second. |
| `world.tick_rate` | Ticks a second. A game with 2D physics takes 1 to 1,000,000; its physics step fails at any other rate. |
| `world.maximum_ticks_per_iteration`, `world.degraded_ms`, `world.debt_limit_ms`, `world.overload_ms` | How far behind the World may fall: it catches up at most so many ticks an iteration, is degraded (admission closed) so far behind, and overloaded past the debt limit or after degrading so long. |
| `replication.endpoint` | Where players connect; replication is off without it. |
| `replication.maximum_connections`, `replication.state_rate`, `replication.egress_bytes_per_second` | How many players, how often each is sent state, and how many bytes a second. |
Expand Down
4 changes: 2 additions & 2 deletions modules/physics2d/include/rawframe/physics2d/errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ inline constexpr result::ErrorDomain kPhysics2DDomain{base::parseBits128Hex("b6e
enum class Physics2DError : std::uint32_t {
/// This processor cannot run the physics build's kernels.
Unsupported = 1,
/// Settings out of range, or a World whose physics components are not
/// the engine's.
/// Settings out of range, a tick rate the physics cannot step, or a
/// World whose physics components are not the engine's.
InvalidSettings = 2,
/// No more physics worlds in this process, or a full world.
Capacity = 3,
Expand Down
6 changes: 3 additions & 3 deletions modules/physics2d/src/characters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ CharacterMove moveCharacter(const m2World* world,
const m2Capsule kShape{.point1 = {0, -halfHeight}, .point2 = {0, halfHeight}, .radius = radius};
const m2Capsule kThinner{
.point1 = {0, -halfHeight}, .point2 = {0, halfHeight}, .radius = std::max(radius - kSkin, radius / 2)};
std::array<m2MoverPlane, M2_MOVER_PLANES> planes{};
std::array<m2MoverPlane, M2_MAX_MOVER_PLANES> planes{};
const auto kCollide = [&](m2Transform at) {
return std::min(m2World_CollideMover(world, &kShape, at, planes.data(), M2_MOVER_PLANES, filter),
std::int32_t{M2_MOVER_PLANES});
return std::min(m2World_CollideMover(world, &kShape, at, filter, planes.data(), M2_MAX_MOVER_PLANES),
std::int32_t{M2_MAX_MOVER_PLANES});
};
const auto kShift = [](m2Transform& at, m2Vec2 by) {
at.p.x += static_cast<double>(by.x);
Expand Down
4 changes: 2 additions & 2 deletions modules/physics2d/src/joints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ namespace rawframe::physics2d {
definition.upperAngle = joint.angularUpper;
definition.enableMotor = joint.motor == 3;
definition.motorSpeed = joint.motorSpeed;
definition.maxMotorTorque = joint.motorEffort;
definition.maxMotorEffort = joint.motorEffort;
definition.enableConnectedCollision = joint.collideConnected;
return m2CreateRevoluteJoint(physics, &definition, &made) == m2_success ? made : m2JointId{};
}
Expand All @@ -95,7 +95,7 @@ namespace rawframe::physics2d {
definition.upperTranslation = kAlongX ? joint.linearUpperX : joint.linearUpperY;
definition.enableMotor = joint.motor != 0;
definition.motorSpeed = joint.motorSpeed;
definition.maxMotorForce = joint.motorEffort;
definition.maxMotorEffort = joint.motorEffort;
definition.enableConnectedCollision = joint.collideConnected;
return m2CreatePrismaticJoint(physics, &definition, &made) == m2_success ? made : m2JointId{};
}
Expand Down
35 changes: 25 additions & 10 deletions modules/physics2d/src/physics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "attachments.h"
#include "characters.h"
#include "joints.h"
#include "rawframe/base/assert.h"
#include "rawframe/physics/filters.h"
#include "rawframe/physics/rewind.h"
#include "rawframe/physics2d/components.h"
Expand Down Expand Up @@ -203,7 +204,7 @@ struct Physics2D::State {
definition.linearDamping = body.linearDamping;
definition.angularDamping = body.angularDamping;
definition.motionLocks.angularZ = body.fixedRotation;
definition.isBullet = body.bullet;
definition.enableBullet = body.bullet;
definition.userData = (std::uint64_t{row.entity.slot} << 32U) | row.entity.generation;
m2BodyId kBody{};
if (m2CreateBody(physics, &definition, &kBody) != m2_success) {
Expand Down Expand Up @@ -607,6 +608,15 @@ struct Physics2D::State {
}

result::Status step(world::World& world, world::TickRate rate, world::TickIndex tick) {
// The tick's length, which each step and kinematic target is taken
// over. Maul2D steps 1 microsecond to 10 seconds, so a rate outside
// that is refused before anything changes.
const auto kSeconds = static_cast<float>(static_cast<double>(rate.seconds) / static_cast<double>(rate.ticks));
if (!(kSeconds >= M2_MIN_STEP && kSeconds <= M2_STEP_LIMIT)) {
return refuse(result::ErrorClass::InvalidArgument,
Physics2DError::InvalidSettings,
"physics step: a tick of 1 microsecond to 10 seconds");
}
rows.clear();
bodies->forEach(world,
[this](world::EntityHandle entity, const Body2D& body, Pose2D& pose, Velocity2D& velocity) {
Expand Down Expand Up @@ -634,8 +644,6 @@ struct Physics2D::State {
remove(entry->second);
entry = mapped.erase(entry);
}
// The tick's length, which a kinematic target is reached over.
const auto kSeconds = static_cast<float>(static_cast<double>(rate.seconds) / static_cast<double>(rate.ticks));
for (const Row& row : rows) {
const auto [kEntry, kNew] = mapped.try_emplace(row.entity);
Mapped& entry = kEntry->second;
Expand Down Expand Up @@ -690,9 +698,13 @@ struct Physics2D::State {
const Pose2D kGoal{.x = kTarget->x, .y = kTarget->y, .c = kTarget->c, .s = kTarget->s};
if (entry.made.motion == static_cast<std::uint8_t>(physics::Motion::Kinematic) &&
std::isfinite(kGoal.x) && std::isfinite(kGoal.y) && finite(kGoal.c) && finite(kGoal.s)) {
m2Body_SetTargetTransform(
physics, entry.body, m2Pos2{kGoal.x, kGoal.y}, rotationOf(kGoal), kSeconds);
++statistics.targets;
// A pose too far to reach within Maul2D's speed
// limit in one tick is refused, and not counted.
if (m2Body_SetTargetTransform(
physics, entry.body, m2Pos2{kGoal.x, kGoal.y}, rotationOf(kGoal), kSeconds) ==
m2_success) {
++statistics.targets;
}
}
kTarget->set = false;
}
Expand All @@ -709,7 +721,10 @@ struct Physics2D::State {
moveCharacter(row, mapped.find(row.entity)->second, kSeconds);
}
}
m2World_Step(physics, kSeconds, static_cast<std::int32_t>(settings.substeps));
// The tick's length and the substeps were checked, and no host code
// of the world's own is running: nothing is left to refuse.
const m2Result kStepped = m2World_Step(physics, kSeconds, static_cast<std::int32_t>(settings.substeps));
RAWFRAME_CHECK(kStepped == m2_success, "Maul2D refused a step whose length and substeps were checked");
++statistics.steps;

report(world);
Expand Down Expand Up @@ -784,7 +799,7 @@ result::Result<std::unique_ptr<Physics2D>> Physics2D::create(const Physics2DSett
Physics2DError::InvalidSettings,
"physics settings: finite gravity, 1 to 64 substeps, and capacities of 1 to 2^20");
}
if (m2CpuSupportsBackend() == 0) {
if (!m2IsCpuBackendSupported()) {
return refuse(result::ErrorClass::Unsupported,
Physics2DError::Unsupported,
"this processor cannot run the physics build's kernels");
Expand Down Expand Up @@ -898,10 +913,10 @@ void Physics2D::overlapCircle(
std::vector<m2ShapeId>& shapes = state_->overlapShapes;
shapes.resize(std::max<std::size_t>(shapes.size(), 16));
std::int32_t total = m2World_OverlapCircle(
state_->physics, &kCircle, kAt, shapes.data(), static_cast<std::int32_t>(shapes.size()), *kFilter);
state_->physics, &kCircle, kAt, *kFilter, shapes.data(), static_cast<std::int32_t>(shapes.size()));
if (static_cast<std::size_t>(total) > shapes.size()) {
shapes.resize(static_cast<std::size_t>(total));
total = m2World_OverlapCircle(state_->physics, &kCircle, kAt, shapes.data(), total, *kFilter);
total = m2World_OverlapCircle(state_->physics, &kCircle, kAt, *kFilter, shapes.data(), total);
}
for (std::int32_t index = 0; index < total; ++index) {
const world::EntityHandle kOwner = state_->ownerOf(shapes[static_cast<std::size_t>(index)]);
Expand Down
22 changes: 21 additions & 1 deletion modules/physics2d/tests/physics_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ RAWFRAME_TEST(AKinematicBodyIsCarriedToItsTargetPushingWhatItMeets) {
kTarget(kBallEntity) = Target2D{.x = -5, .c = 1, .set = true};
scene.run(1);
RAWFRAME_EXPECT(scene.pose(kBallEntity).x > 2 && !kTarget(kBallEntity).set);
// A target too far to reach in one tick within Maul2D's speed limit,
// 100 km/s, is cleared and not carried out: the paddle stays.
kTarget(kPaddle) = Target2D{.x = 3000, .c = 0, .s = 1, .set = true};
scene.run(1);
RAWFRAME_EXPECT(std::abs(scene.pose(kPaddle).x - 3) < 1e-4 && !kTarget(kPaddle).set);
const Physics2DStatistics kStatistics = scene.physics->statistics();
RAWFRAME_EXPECT(kStatistics.targets == 31 && kStatistics.teleports == 0);
}
Expand Down Expand Up @@ -276,7 +281,7 @@ RAWFRAME_TEST(TheSameWorldAndWritesGiveTheSameBits) {
RAWFRAME_EXPECT(first.physics->digest() == second.physics->digest());
// The same bits on every compiler and target the check builds, x86-64
// and wasm32 alike: a moved digest says why in its commit.
RAWFRAME_EXPECT(first.physics->digest() == 0xc42bbb4bf908bd7fULL);
RAWFRAME_EXPECT(first.physics->digest() == 0x57dd106d48ba3973ULL);
RAWFRAME_EXPECT(kFirst.size() == kSecond.size() &&
std::memcmp(kFirst.data(), kSecond.data(), kFirst.size() * sizeof(Pose2D)) == 0);
// And something did happen: the balls moved and bounced apart.
Expand Down Expand Up @@ -531,6 +536,21 @@ RAWFRAME_TEST(SettingsAndWorldsOutOfRangeAreRefused) {
RAWFRAME_EXPECT(!kMade.has_value() && kMade.error().code() == code(Physics2DError::InvalidSettings));
}
RAWFRAME_EXPECT(Physics2D::create({.bodyCapacity = 1024, .historyTicks = 1024}).has_value());
// A tick Maul2D cannot step, a minute or a two-millionth of a second
// long, is refused before anything changes; a tick of ten seconds steps.
{
Scene scene;
const world::EntityHandle kFalling = scene.body(kCrate, {.y = 1, .c = 1});
for (const world::TickRate kRate : {*world::TickRate::of(1, 60), *world::TickRate::of(2'000'000)}) {
const auto kRan = scene.schedule->runTick(scene.world, scene.tick, kRate);
RAWFRAME_EXPECT(kRan.has_value() && kRan->failures.size() == 1 && kRan->failures[0].system == kStepSystem &&
kRan->failures[0].error.code() == code(Physics2DError::InvalidSettings));
}
RAWFRAME_EXPECT(scene.physics->statistics().steps == 0 && scene.physics->statistics().bodiesMade == 0);
const auto kRan = scene.schedule->runTick(scene.world, scene.tick, *world::TickRate::of(1, 10));
RAWFRAME_EXPECT(kRan.has_value() && kRan->failures.empty());
RAWFRAME_EXPECT(scene.physics->statistics().steps == 1 && scene.pose(kFalling).y < 1);
}
// A World without the physics components.
schema::RegistryBuilder builder;
builder.add<Pose2D>();
Expand Down
1 change: 1 addition & 0 deletions third_party/maul2d/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ add_library(rawframe_maul2d_vendor STATIC
src/shape_read.c
src/snapshot.c
src/snapshot_check.c
src/snapshot_pairs.c
src/state_checks.c
src/solver.c
src/world.c
Expand Down
31 changes: 19 additions & 12 deletions third_party/maul2d/include/maul2d/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,22 @@
// M2_NODISCARD marks a function whose result must be read: every
// function that returns a status. The attribute is standard in C23 and
// C++17 and left out for older dialects. MSVC keeps __cplusplus at
// 199711L unless /Zc:__cplusplus is given, so _MSVC_LANG is read too.
// 199711L unless /Zc:__cplusplus is given, so _MSVC_LANG is read too. In
// C, any dialect after C17 whose compiler has the attribute takes it:
// GCC 14 reports 202000L in its C23 mode.
#if (defined(__cplusplus) && __cplusplus >= 201703L) || \
(defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
#define M2_NODISCARD [[nodiscard]]
#elif !defined(__cplusplus) && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
#elif !defined(__cplusplus) && defined(__STDC_VERSION__) && __STDC_VERSION__ > 201710L && \
defined(__has_c_attribute)
#if __has_c_attribute(nodiscard)
#define M2_NODISCARD [[nodiscard]]
#else
#define M2_NODISCARD
#endif
#else
#define M2_NODISCARD
#endif

// M2_ALIGN8 gives a 64-bit member (a double or a 64-bit integer) 8-byte
// alignment on every target. 32-bit x86 aligns them to 4 inside a struct,
Expand All @@ -58,13 +65,13 @@ extern "C"
// The library version. CMake reads it from here, and release-please
// moves it between the markers.
// x-release-please-start-major
#define M2_VERSION_MAJOR 0
#define M2_VERSION_MAJOR 1
// x-release-please-end
// x-release-please-start-minor
#define M2_VERSION_MINOR 6
#define M2_VERSION_MINOR 3
// x-release-please-end
// x-release-please-start-patch
#define M2_VERSION_PATCH 0
#define M2_VERSION_PATCH 1
// x-release-please-end

// The binary interface a program is built for: the whole version before
Expand Down Expand Up @@ -106,8 +113,8 @@ extern "C"
// A journal call in the wrong recording state: a start while
// recording, a stop or a replay that does not fit it.
m2_errorState = -6,
// A def, a snapshot or a journal tape comes from another binary
// interface or release than the library's (M2_ABI_VERSION).
// A def, a snapshot or a journal tape comes from another minor
// version (M2_ABI_VERSION) or build than the library's.
m2_errorVersion = -7,
// A snapshot's or a journal tape's bytes fail their checks:
// damaged, truncated, from another world shape, or a record the
Expand Down Expand Up @@ -155,7 +162,7 @@ extern "C"
/// @return A static, NUL-terminated string such as "m2_errorCapacity".
/// @par Thread safety
/// Safe from any thread.
M2_API const char* m2ResultName(m2Result result);
M2_API const char* m2GetResultName(m2Result result);

/// Returns the SIMD backend the library was compiled for: "avx2",
/// "neon" or "scalar". Every backend produces the same bits; this only
Expand All @@ -173,10 +180,10 @@ extern "C"
/// backend with m2_errorUnsupported, so a program can call this first to
/// choose another path, or build with -DMAUL2D_SIMD=scalar.
///
/// @return 1 when the CPU can run this library, 0 when it cannot.
/// @return Whether the CPU can run this library.
/// @par Thread safety
/// Safe from any thread.
M2_API int32_t m2CpuSupportsBackend(void);
M2_API bool m2IsCpuBackendSupported(void);

/// The 64-bit hash every determinism check is built on: eight bytes a
/// round, xored in, multiplied by an odd constant and folded, the
Expand All @@ -188,7 +195,7 @@ extern "C"
/// @return The new running hash.
/// @par Thread safety
/// Safe from any thread.
M2_API uint64_t m2Hash64(uint64_t seed, const void* data, int32_t byteCount);
M2_API uint64_t m2Hash64(uint64_t seed, const void* data, int64_t byteCount);

// The seed of an m2Hash64 chain.
#define M2_HASH_INIT 0xCBF29CE484222325ull
Expand Down Expand Up @@ -231,7 +238,7 @@ extern "C"
// particle strength.
#define M2_COEFFICIENT_LIMIT 1.0e3f

// The largest density, in kg/m^2, of a shape, a fluid volume or the
// The largest density, in kg/m^2, of a shape, a water volume or the
// particles. Lead is about 1.1e4.
#define M2_DENSITY_LIMIT 1.0e6f

Expand Down
Loading
Loading